如何解决使用camera2录制视频时三星S6上的绿色闪烁
我在录制视频时遇到了三星 Galaxy s6 的问题,预览效果很好,但是当我完成录制视频时,结果是一个带有小故障和绿色闪烁的视频。我在这个模型上遇到了这个问题,它所有的手机都应该是...tbh...我已经尝试了很多改变,但我不知道该怎么做
这里是 SurfaceView:
class AutoFitSurfaceView @JvmOverloads constructor(
context: Context,attrs: AttributeSet? = null,defStyle: Int = 0
) : SurfaceView(context,attrs,defStyle) {
private var aspectRatio = 0f
/**
* Sets the aspect ratio for this view. The size of the view will be
* measured based on the ratio calculated from the parameters.
*
* @param width Camera resolution horizontal size
* @param height Camera resolution vertical size
*/
fun setAspectRatio(width: Int,height: Int) {
require(width > 0 && height > 0) { "Size cannot be negative" }
aspectRatio = width.toFloat() / height.toFloat()
holder.setFixedSize(width,height)
requestLayout()
}
override fun onMeasure(widthMeasureSpec: Int,heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (aspectRatio == 0f) {
setMeasuredDimension(width,height)
} else {
// Performs height-crop transformation of the camera frames if necessary
val newHeight: Int
val actualRatio = 1f / aspectRatio
newHeight = (width / actualRatio).roundToInt()
setMeasuredDimension(width,newHeight)
}
}
}
这是实现:
class VideoCameraImplCamera2(
private val context: Context,private val lifecycleOwner: LifecycleOwner,private val viewFinder: AutoFitSurfaceView,private val videoDirectory: String,private val resolution: Resolution,override val listener: VideoCamera.VideoCameraListener
) : VideoCamera,LifecycleObserver {
enum class CameraLens(val value: Int) {
FRONT(1),BACK(0)
}
companion object {
private const val RECORDER_VIDEO_BITRATE: Int = 10_000_000
private const val FRAME_PER_SECOND: Int = 30
private const val RECORDER_AUDIO_BITRATE: Int = 192_000
private const val RECORDER_AUDIO_SAMPLING_RATE: Int = 44_100
private const val DEVICE_ROTATION = Surface.ROTATION_0
private const val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90
private const val SENSOR_ORIENTATION_INVERSE_DEGREES = 270
private val DEFAULT_ORIENTATIONS = SparseIntArray().apply {
append(Surface.ROTATION_0,90)
append(Surface.ROTATION_90,0)
append(Surface.ROTATION_180,270)
append(Surface.ROTATION_270,180)
}
private val INVERSE_ORIENTATIONS = SparseIntArray().apply {
append(Surface.ROTATION_0,270)
append(Surface.ROTATION_90,180)
append(Surface.ROTATION_180,90)
append(Surface.ROTATION_270,0)
}
}
/**
* Orientation of the camera sensor
*/
private var sensorOrientation = 0
/**
* Camera lens active
*/
private var cameraLens = CameraLens.FRONT
/**
* Whether the flash is active
*/
private var cameraFlash = VideoCamera.FlashMode.OFF
/**
* Min recording time for each video
*/
private var minRecordingTimeCompleted = true
/**
* Whether the video recording stopped before min time
*/
private var stoppedBeforeMinTimeCompleted = false
/**
* Whether the app is showing that is recording
*/
private var isRecording = false
/**
* Whether the app is recording video now
*/
private var isRecordingVideo = false
/**
* Whether the camera is available
*/
private var isAvailable = false
/**
* Minimum recording duration countdown
*/
private val minDurationCountDownTimer =
object : CountDownTimer(VideoCamera.MIN_RECORD_DURATION,100) {
init {
minRecordingTimeCompleted = false
}
override fun onFinish() {
if (stoppedBeforeMinTimeCompleted && isRecording) {
Timber.d("Minimum recording time completed")
stopInternalRecording()
}
minRecordingTimeCompleted = true
}
override fun onTick(millisUntilFinished: Long) {
}
}
/**
* Camera id based in [cameraLens] value
*/
private val cameraId: String
get() = cameraManager.cameraIdList[cameraLens.value]
/** Detects,characterizes,and connects to a CameraDevice (used for all camera operations) */
private val cameraManager: CameraManager by lazy {
val context = context.applicationContext
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
/** [CameraCharacteristics] corresponding to the provided Camera ID */
private val characteristics: CameraCharacteristics
get() = cameraManager.getCameraCharacteristics(cameraId)
/**
* Setup a persistent [Surface] for the recorder so we can use it as an output target for the
* camera session without preparing the recorder
*/
private lateinit var recorderSurface: Surface
/** Saves the video recording */
private lateinit var recorder: MediaRecorder
/** [HandlerThread] where all camera operations run */
private val cameraThread = HandlerThread("CameraThread").apply { start() }
/** [Handler] corresponding to [cameraThread] */
private val cameraHandler = Handler(cameraThread.looper)
/** Captures frames from a [CameraDevice] for our video recording */
private lateinit var session: CameraCaptureSession
/** The [CameraDevice] that will be opened in this fragment */
private lateinit var camera: CameraDevice
/** Requests used for preview only in the [CameraCaptureSession] */
private lateinit var previewRequest: CaptureRequest.Builder
/** Requests used for preview and recording in the [CameraCaptureSession] */
private lateinit var recordRequest: CaptureRequest.Builder
private var recordingStartMillis: Long = 0L
/** Number or requested recording this session*/
private var recordingRequested = 0
/** Last clip duration */
private var lastRecordingDuration = 0F
/** Indicate whether the camera is initialized */
private var initialized = false
/** Whether the camera is in pause */
private var onPause = true
/** The [android.util.Size] of video recording. */
private lateinit var videoSize: Size
/** Absolute path of recording video file. It will be create a new one in each recording */
private lateinit var videoFilePath: String
init {
viewFinder.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
override fun surfaceChanged(
holder: SurfaceHolder,format: Int,width: Int,height: Int
) = Unit
override fun surfaceCreated(holder: SurfaceHolder) {
// Selects appropriate preview size and configures view finder
val previewSize = chooseVideoSize()
Timber.d("View finder size: ${viewFinder.width} x ${viewFinder.height}")
Timber.d("Selected preview size: $previewSize")
viewFinder.setAspectRatio(previewSize.width,previewSize.height)
}
})
}
/**
* Initialize video camera
*/
override fun initialize() {
initialized = true
onPause = false
viewFinder.post { initializeCamera() }
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
if (onPause) {
initialize()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
onPause = true
try {
camera.close()
} catch (exc: Throwable) {
Timber.e(exc,"Error closing camera")
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
recorderSurface.release()
cameraThread.quitSafely()
recorderSurface.release()
}
private fun createRecorderSurface(width: Int,height: Int): Surface {
// Get a persistent Surface from MediaCodec,don't forget to release when done
val surface = MediaCodec.createPersistentInputSurface()
// Prepare and release a dummy MediaRecorder with our new surface
// Required to allocate an appropriately sized buffer before passing the Surface as the
// output target to the capture session
createRecorder(
surface,File(context.cacheDir,"dummy.mp4").absolutePath,width,height
).apply {
prepare()
release()
}
return surface
}
/** Creates a [MediaRecorder] instance using the provided [Surface] as input */
private fun createRecorder(
surface: Surface,absoluteFilePath: String,height: Int
) = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(absoluteFilePath)
setVideoEncodingBitRate(RECORDER_VIDEO_BITRATE)
if (FRAME_PER_SECOND > 0) setVideoFrameRate(FRAME_PER_SECOND)
setVideoSize(width,height)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setAudioEncodingBitRate(RECORDER_AUDIO_BITRATE)
setAudioSamplingRate(RECORDER_AUDIO_SAMPLING_RATE)
setInputSurface(surface)
}
/** Creates a [CaptureRequest.Builder] instance for preview using [session] variable*/
private fun createPreviewRequest(): CaptureRequest.Builder {
return session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
// Add the preview surface target
addTarget(viewFinder.holder.surface)
}
}
/** Creates a [CaptureRequest.Builder] instance for recording using [session] variable*/
private fun createRecordRequest(): CaptureRequest.Builder {
// Capture request holds references to target surfaces
return session.device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
// Add the preview and recording surface targets
addTarget(viewFinder.holder.surface)
addTarget(recorderSurface)
// Sets user requested FPS for all targets
set(
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,Range(FRAME_PER_SECOND,FRAME_PER_SECOND)
)
}
}
/**
* Begin all camera operations in a coroutine in the main thread. This function:
* - Opens the camera
* - Configures the camera session
* - Starts the preview by dispatching a repeating request
* - Creates the record request
* - Gets camera orientation
*/
@SuppressLint("ClickableViewAccessibility")
private fun initializeCamera() = lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
Timber.d("initializeCamera() called")
// Camera is unavailable to record or other operation till is initialized
isAvailable = false
videoSize = chooseVideoSize()
recorderSurface = createRecorderSurface(videoSize.width,videoSize.height)
// Open the selected camera
camera = openCamera(cameraManager,cameraId,cameraHandler)
// Creates list of Surfaces where the camera will output frames
val targets = listOf(viewFinder.holder.surface,recorderSurface)
// Start a capture session using our open camera and list of Surfaces where frames will go
session = createCaptureSession(camera,targets,cameraHandler)
// Sends the capture request as frequently as possible until the session is torn down or
// session.stopRepeating() is called
previewRequest = createPreviewRequest()
session.setRepeatingRequest(previewRequest.build(),null,cameraHandler)
recordRequest = createRecordRequest()
sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
isAvailable = true
}
/** Opens the camera and returns the opened device (as the result of the suspend coroutine) */
@SuppressLint("MissingPermission")
private suspend fun openCamera(
manager: CameraManager,cameraId: String,handler: Handler? = null
): CameraDevice = suspendCancellableCoroutine { cont ->
manager.openCamera(cameraId,object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = cont.resume(device)
override fun onDisconnected(device: CameraDevice) {
Timber.w("Camera $cameraId has been disconnected")
}
override fun onError(device: CameraDevice,error: Int) {
val msg = when (error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = AnalyticsException(
RuntimeException("Camera $cameraId error: ($error) $msg"),getRecordingPropertiesMap()
)
if (cont.isActive) {
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
listener.onVideoCameraError(exc)
}
}
}
},handler)
}
/**
* Creates a [CameraCaptureSession] and returns the configured session (as the result of the
* suspend coroutine)
*/
private suspend fun createCaptureSession(
device: CameraDevice,targets: List<Surface>,handler: Handler? = null
): CameraCaptureSession = suspendCoroutine { cont ->
// Creates a capture session using the predefined targets,and defines a session state
// callback which resumes the coroutine once the session is configured
device.createCaptureSession(targets,object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = AnalyticsException(
RuntimeException("Camera ${device.id} session configuration failed"),getRecordingPropertiesMap()
)
listener.onVideoCameraError(exc)
// cont.resumeWithException(exc)
}
},handler)
}
override fun startRecordingVideo() {
Timber.d("startRecordingVideo() called")
if (isRecordingVideo || !isAvailable) return
isAvailable = false
recordingRequested++
// creating file to write in
videoFilePath = createFile()
recorder = createRecorder(recorderSurface,videoFilePath,videoSize.width,videoSize.height)
// Start recording repeating requests,which will stop the ongoing preview
// repeating requests without having to explicitly call `session.stopRepeating`
session.setRepeatingRequest(recordRequest.build(),cameraHandler)
isRecording = true
minRecordingTimeCompleted = false
// Start a capture session
// Once the session starts,we can update the UI and start recording
listener.onVideoCameraStartRecording()
// Finalizes recorder setup and starts recording
recorder.apply {
// Sets output orientation based on current sensor value at start time
Timber.d("setUpMediaRecorder: orientation= $sensorOrientation")
when (sensorOrientation) {
SENSOR_ORIENTATION_DEFAULT_DEGREES ->
setOrientationHint(
DEFAULT_ORIENTATIONS.get(
DEVICE_ROTATION
)
)
SENSOR_ORIENTATION_INVERSE_DEGREES ->
setOrientationHint(
INVERSE_ORIENTATIONS.get(
DEVICE_ROTATION
)
)
}
prepare()
isRecordingVideo = true
start()
}
minDurationCountDownTimer.cancel()
minDurationCountDownTimer.start()
listener.onVideoCameraStartRecording()
recordingStartMillis = System.currentTimeMillis()
Timber.d("Recording started")
}
override fun stopRecordingVideo() {
Timber.d("stopRecordingVideo() called")
if (minRecordingTimeCompleted && isRecording && !isAvailable) {
Timber.d("stopRecordingVideo: video reached min duration to stop,stopping recording")
stopInternalRecording()
} else if (isRecording && !stoppedBeforeMinTimeCompleted && !isAvailable) {
Timber.d("stopRecordingVideo: video didn't reach min duration to stop,we'll wait")
stoppedBeforeMinTimeCompleted = true
} else {
Timber.d("stopRecordingVideo: device is not recording anything,not action taken")
}
}
private fun stopInternalRecording() {
if (isRecordingVideo) {
isRecordingVideo = false
listener.onVideoCameraStopRecording(videoFilePath)
try {
lastRecordingDuration =
(System.currentTimeMillis() - recordingStartMillis).toFloat() / 1000
recorder.stop()
} catch (e: Exception) {
listener.onVideoCameraError(
AnalyticsException(
e,getRecordingPropertiesMap()
)
)
}
recorder.release()
isRecording = false
stoppedBeforeMinTimeCompleted = false
object : CountDownTimer(VideoCamera.MIN_SAFE_TIME,100) {
override fun onFinish() {
Timber.d("Minimum waiting time after recording completed,device available again")
isAvailable = true
}
override fun onTick(millisUntilFinished: Long) {
}
}.start()
}
}
override fun switchCamera() {
if (isAvailable) {
isAvailable = false
cameraLens = if (cameraLens == CameraLens.FRONT) {
CameraLens.BACK
} else {
cameraFlash = VideoCamera.FlashMode.OFF
CameraLens.FRONT
}
camera.close()
// We hide and show the preview surface to recreate it and get proper size and ratio
viewFinder.visibility = View.INVISIBLE
viewFinder.visibility = View.VISIBLE
initializeCamera()
listener.onVideoCameraSwitchCamera(getCameraFace())
}
}
override fun switchFlashMode() {
if (!isRecording) {
cameraFlash = when (cameraFlash) {
VideoCamera.FlashMode.ON -> {
VideoCamera.FlashMode.OFF
}
VideoCamera.FlashMode.OFF -> {
VideoCamera.FlashMode.ON
}
}
setupFlash()
listener.onVideoCameraFlashChanged(getFlashMode())
}
}
private fun setupFlash() {
val flashMode = when (cameraFlash) {
VideoCamera.FlashMode.ON -> CaptureRequest.FLASH_MODE_TORCH
VideoCamera.FlashMode.OFF -> CaptureRequest.FLASH_MODE_OFF
}
recordRequest.set(CaptureRequest.FLASH_MODE,flashMode)
session.setRepeatingRequest(recordRequest.build(),null)
}
override fun getCameraFace(): VideoCamera.CameraFace {
return if (cameraLens == CameraLens.BACK) {
VideoCamera.CameraFace.BACK
} else {
VideoCamera.CameraFace.FRONT
}
}
override fun getFlashMode(): VideoCamera.FlashMode {
return cameraFlash
}
/**
* We choose a video size according with [resolution] value,if camera does not have this resolution
* we will get a lower one of [Resolution] values
*
* @return The video size
*/
@Throws(RuntimeException::class)
private fun chooseVideoSize(): Size {
// Correcting height and width in the right order in case it came in wrong one
val choices = getVideoSizes().map { size ->
if (size.width >= size.height) size else Size(size.height,size.width)
}
val resolutionSizes =
Resolution.values()
.flatMap { listOf(it.get169Size(),it.get43BigSize(),it.get43SmallSize()) }
// Selecting optimal video size depending on [resolution]
val videoSize =
choices.firstOrNull { size -> size == resolution.get43BigSize() }
?: choices.firstOrNull { size -> size == resolution.get43SmallSize() }
?: choices.firstOrNull { size -> size == resolution.get169Size() }
?: choices.filter { size -> resolutionSizes.contains(size) }
.maxByOrNull { size -> size.height }
?: choices.minByOrNull { it ->
listOf(
abs(it.width - resolution.get169Size().width),abs(it.width - resolution.get43BigSize().width),abs(it.width - resolution.get43SmallSize().width)
).minByOrNull { it }!! + listOf(
abs(it.height - resolution.get169Size().height),abs(it.height - resolution.get43BigSize().height)
).minByOrNull { it }!!
}
?: throw RuntimeException("Cannot find a suitable resolution size")
Timber.d("chooseVideoSize() returned: $videoSize")
return videoSize
}
private fun getVideoSizes(): List<Size> {
return characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
?.getOutputSizes(MediaRecorder::class.java)?.toList()
?: throw RuntimeException("Cannot get available preview/video sizes")
}
/** Creates a [File] named with the current date and time */
private fun createFile(): String {
Timber.d("createFile() called")
val filename = "${System.currentTimeMillis()}.mp4"
val absolutePath = "$videoDirectory/$filename"
File(absolutePath)
return absolutePath
}
private fun getRecordingPropertiesMap() = linkedMapOf(
"resolution" to resolution.toString(),"videoSize" to videoSize.toString(),"cameraLens" to cameraLens.name,"flashMode" to cameraFlash.name,"recordingRequested" to recordingRequested.toString(),"recording time" to "$lastRecordingDuration seconds","video sizes" to getVideoSizes().toString()
)
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。