-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Open
Description
I use this sample https://github.com/android/camera-samples/blob/main/Camera2Video/app/src/main/java/com/example/android/camera2/video/fragments/CameraFragment.kt
But I changed Fragment to Activity, but everything is the same, the logic is exactly the same, I just changed fragment related stuff to activity's stuff.
And when the activity stops sometimes I receive the next errors which I don't receive when using a fragment and it happens only for Android 6 (so weird), and it happens all the time when I start some other activity from this camera activity (for example the list of videos screen):
2022-04-29 23:01:35.234 21439-21528/com.android.example.camera2.video E/Legacy-CameraDevice-JNI: LegacyCameraDevice_nativeGetSurfaceId: Could not retrieve native Surface from surface.
2022-04-29 23:01:35.234 21439-21528/com.android.example.camera2.video E/CameraDeviceGLThread-0: Received exception on GL render thread:
java.lang.IllegalArgumentException: Surface had no valid native Surface.
at android.hardware.camera2.legacy.LegacyCameraDevice.nativeGetSurfaceId(Native Method)
at android.hardware.camera2.legacy.LegacyCameraDevice.getSurfaceId(LegacyCameraDevice.java:658)
at android.hardware.camera2.legacy.LegacyCameraDevice.getSurfaceIds(LegacyCameraDevice.java:667)
at android.hardware.camera2.legacy.SurfaceTextureRenderer.drawIntoSurfaces(SurfaceTextureRenderer.java:715)
at android.hardware.camera2.legacy.GLThreadManager$1.handleMessage(GLThreadManager.java:105)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:148)
at android.os.HandlerThread.run(HandlerThread.java:61)
2022-04-29 23:01:35.234 21439-21528/com.android.example.camera2.video I/CameraDeviceState: Legacy camera service transitioning to state ERROR
2022-04-29 23:01:35.235 21439-21507/com.android.example.camera2.video E/CameraActivity: Camera 0 error: (1) Camera in use
java.lang.RuntimeException: Camera 0 error: (1) Camera in use
at com.example.android.camera2.video.fragments.CameraActivity$openCamera$2$1.onError(CameraActivity.kt:318)
at android.hardware.camera2.impl.CameraDeviceImpl$CameraDeviceCallbacks$1.run(CameraDeviceImpl.java:1670)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.os.HandlerThread.run(HandlerThread.java:61)
CameraActivity:
class CameraActivity : AppCompatActivity() {
/** Android ViewBinding */
private var _activityCameraBinding: ActivityCameraBinding? = null
private val activityCameraBinding get() = _activityCameraBinding!!
/** Detects, characterizes, and connects to a CameraDevice (used for all camera operations) */
private val cameraManager: CameraManager by lazy {
val context = applicationContext
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
/** [CameraCharacteristics] corresponding to the provided Camera ID */
private val characteristics: CameraCharacteristics by lazy {
cameraManager.getCameraCharacteristics("0")
}
/** File where the recording will be saved */
private val outputFile: File by lazy { createFile(this, "mp4") }
/**
* 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 val recorderSurface: Surface by lazy {
// 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).apply {
prepare()
release()
}
surface
}
/** Saves the video recording */
private val recorder: MediaRecorder by lazy { createRecorder(recorderSurface) }
/** [HandlerThread] where all camera operations run */
private val cameraThread = HandlerThread("CameraThread").apply { start() }
/** [Handler] corresponding to [cameraThread] */
private val cameraHandler = Handler(cameraThread.looper)
/** Performs recording animation of flashing screen */
private val animationTask: Runnable by lazy {
Runnable {
// Flash white animation
activityCameraBinding.overlay.foreground = Color.argb(150, 255, 255, 255).toDrawable()
// Wait for ANIMATION_FAST_MILLIS
activityCameraBinding.overlay.postDelayed({
// Remove white flash animation
activityCameraBinding.overlay.foreground = null
// Restart animation recursively
activityCameraBinding.overlay.postDelayed(
animationTask,
ANIMATION_FAST_MILLIS
)
}, ANIMATION_FAST_MILLIS)
}
}
/** Captures frames from a [CameraDevice] for our video recording */
private lateinit var session: CameraCaptureSession
/** The [CameraDevice] that will be opened in this activity */
private lateinit var camera: CameraDevice
/** Requests used for preview only in the [CameraCaptureSession] */
private val previewRequest: CaptureRequest by lazy {
// Capture request holds references to target surfaces
session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
// Add the preview surface target
addTarget(activityCameraBinding.viewFinder.holder.surface)
}.build()
}
/** Requests used for preview and recording in the [CameraCaptureSession] */
private val recordRequest: CaptureRequest by lazy {
// Capture request holds references to target surfaces
session.device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
// Add the preview and recording surface targets
addTarget(activityCameraBinding.viewFinder.holder.surface)
addTarget(recorderSurface)
// Sets user requested FPS for all targets
// set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(args.fps, args.fps))
}.build()
}
private var recordingStartMillis: Long = 0L
/** Live data listener for changes in the device orientation relative to the camera */
private lateinit var relativeOrientation: OrientationLiveData
@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_activityCameraBinding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(activityCameraBinding.root)
activityCameraBinding.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 = getPreviewOutputSize(
activityCameraBinding.viewFinder.display,
characteristics,
SurfaceHolder::class.java
)
Log.d(
TAG,
"View finder size: ${activityCameraBinding.viewFinder.width} x ${activityCameraBinding.viewFinder.height}"
)
Log.d(TAG, "Selected preview size: $previewSize")
activityCameraBinding.viewFinder.setAspectRatio(
previewSize.width,
previewSize.height
)
// To ensure that size is set, initialize camera in the view's thread
activityCameraBinding.viewFinder.post { initializeCamera() }
}
})
// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(this, characteristics).apply {
observe(this@CameraActivity, Observer { orientation ->
Log.d(TAG, "Orientation changed: $orientation")
})
}
}
/** Creates a [MediaRecorder] instance using the provided [Surface] as input */
private fun createRecorder(surface: Surface) = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(outputFile.absolutePath)
setVideoEncodingBitRate(RECORDER_VIDEO_BITRATE)
setVideoFrameRate(24)
setVideoSize(1280, 720)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setInputSurface(surface)
}
/**
* 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
*/
@SuppressLint("ClickableViewAccessibility")
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
// Open the selected camera
camera = openCamera(cameraManager, "0", cameraHandler)
// Creates list of Surfaces where the camera will output frames
val targets = listOf(activityCameraBinding.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
session.setRepeatingRequest(previewRequest, null, cameraHandler)
// React to user touching the capture button
activityCameraBinding.captureButton.setOnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> lifecycleScope.launch(Dispatchers.IO) {
// Prevents screen rotation during the video recording
this@CameraActivity.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LOCKED
// Start recording repeating requests, which will stop the ongoing preview
// repeating requests without having to explicitly call `session.stopRepeating`
session.setRepeatingRequest(recordRequest, null, cameraHandler)
// Finalizes recorder setup and starts recording
recorder.apply {
// Sets output orientation based on current sensor value at start time
relativeOrientation.value?.let { setOrientationHint(it) }
prepare()
start()
}
recordingStartMillis = System.currentTimeMillis()
Log.d(TAG, "Recording started")
// Starts recording animation
activityCameraBinding.overlay.post(animationTask)
}
MotionEvent.ACTION_UP -> lifecycleScope.launch(Dispatchers.IO) {
// Unlocks screen rotation after recording finished
this@CameraActivity.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
// Requires recording of at least MIN_REQUIRED_RECORDING_TIME_MILLIS
val elapsedTimeMillis = System.currentTimeMillis() - recordingStartMillis
if (elapsedTimeMillis < MIN_REQUIRED_RECORDING_TIME_MILLIS) {
delay(MIN_REQUIRED_RECORDING_TIME_MILLIS - elapsedTimeMillis)
}
Log.d(TAG, "Recording stopped. Output file: $outputFile")
recorder.stop()
// Removes recording animation
activityCameraBinding.overlay.removeCallbacks(animationTask)
// Broadcasts the media file to the rest of the system
MediaScannerConnection.scanFile(
view.context, arrayOf(outputFile.absolutePath), null, null
)
// Launch external activity via intent to play video recorded using our provider
startActivity(Intent().apply {
action = Intent.ACTION_VIEW
type = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(outputFile.extension)
val authority = "${BuildConfig.APPLICATION_ID}.provider"
data = FileProvider.getUriForFile(view.context, authority, outputFile)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_ACTIVITY_CLEAR_TOP
})
// Finishes our current camera screen
delay(ANIMATION_SLOW_MILLIS)
finish()
}
}
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) {
Log.w(TAG, "Camera $cameraId has been disconnected")
this@CameraActivity.finish()
}
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 = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e(TAG, exc.message, exc)
if (cont.isActive) cont.resumeWithException(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 = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}, handler)
}
override fun onStop() {
super.onStop()
try {
camera.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}
override fun onDestroy() {
super.onDestroy()
cameraThread.quitSafely()
recorder.release()
recorderSurface.release()
_activityCameraBinding = null
}
companion object {
private val TAG = CameraActivity::class.java.simpleName
private const val RECORDER_VIDEO_BITRATE: Int = 10_000_000
private const val MIN_REQUIRED_RECORDING_TIME_MILLIS: Long = 1000L
/** Milliseconds used for UI animations */
private const val ANIMATION_FAST_MILLIS = 50L
private const val ANIMATION_SLOW_MILLIS = 100L
/** Creates a [File] named with the current date and time */
private fun createFile(context: Context, extension: String): File {
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
return File(context.filesDir, "VID_${sdf.format(Date())}.$extension")
}
}
}
Metadata
Metadata
Assignees
Labels
No labels