Skip to content

Instantly share code, notes, and snippets.

@uchidev
Last active April 19, 2021 19:06
Show Gist options
  • Save uchidev/1b6c48353aa42f45f43d4cbac2ee07ce to your computer and use it in GitHub Desktop.
Save uchidev/1b6c48353aa42f45f43d4cbac2ee07ce to your computer and use it in GitHub Desktop.
class RecordService : LifecycleService() {
private val notificationBuilder by lazy { NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) }
private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) }
private val recordThread = HandlerThread("RecorderThread").apply { start() }
private val recordHandler = Handler(recordThread.looper)
private lateinit var recordParam: RecordServiceParam
// private lateinit var cameraCharacteristics: CameraCharacteristics
private lateinit var cameraDevice: CameraDevice
private lateinit var inputCaptureSession: CameraCaptureSession
private lateinit var inputSurface: Surface
private lateinit var cameraRecorder: MediaRecorder
private val cameraManager: CameraManager by lazy {
applicationContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
private val outFile: File by lazy { File(filesDir, "recorded.mp4") }
private fun createInputSurface() = MediaCodec.createPersistentInputSurface().also { surface ->
createRecorder(surface).apply {
prepare()
release()
}
}
private fun createRecorder(surface: Surface) = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(outFile.absolutePath)
setVideoEncodingBitRate(VIDEO_BITRATE)
if (recordParam.fps > 0) setVideoFrameRate(recordParam.fps)
setVideoSize(recordParam.width, recordParam.height)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setInputSurface(surface)
}
private fun initializeCamera(): Boolean {
if (! ::recordParam.isInitialized) {
Log.e(TAG, "RecordServiceParam can not get")
return false
}
lifecycleScope.launch(Dispatchers.Main) {
// cameraCharacteristics = cameraManager.getCameraCharacteristics(recordParam.cameraId)
Log.d(TAG, "Create camera device")
cameraDevice = openCamera(cameraManager, recordParam.cameraId, recordHandler)
Log.d(TAG, "Create surface")
inputSurface = createInputSurface()
Log.d(TAG, "Create session")
inputCaptureSession = createCaptureSession(cameraDevice, listOf(inputSurface), recordHandler)
inputCaptureSession.device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
addTarget(inputSurface)
set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(recordParam.fps, recordParam.fps))
}.build().also { request ->
inputCaptureSession.setRepeatingRequest(request, null, recordHandler)
}
Log.d(TAG, "Create recorder")
cameraRecorder = createRecorder(inputSurface)
Log.d(TAG, "Record starting")
startRecorder(cameraRecorder)
ensureChannelId()
notificationManager.notify(999, notificationBuilder.setContentText("Record started").build())
Log.d(TAG, "Record started")
delay(5000)
// MediaScannerConnection.scanFile(this@RecordService, arrayOf(outFile.absolutePath), null, null)
Log.d(TAG, "output ${outFile.absolutePath}")
stopRecorder(cameraRecorder)
Log.d(TAG, "Record stopped")
ensureChannelId()
notificationManager.notify(999, notificationBuilder.setContentText("Record stopped").build())
delay(5000)
stopSelf()
}
return true
}
private fun releaseCamera() {
if (::cameraRecorder.isInitialized) {
Log.d(TAG, "Record release")
cameraRecorder.release()
}
if (::inputSurface.isInitialized) {
Log.d(TAG, "Surface release")
inputSurface.release()
}
if (::cameraDevice.isInitialized) {
Log.d(TAG, "Camera device close")
cameraDevice.close()
}
recordThread.quitSafely()
}
private fun startRecorder(recorder: MediaRecorder) {
recorder.setOrientationHint(0)
recorder.prepare()
recorder.start()
}
private fun stopRecorder(recorder: MediaRecorder) {
recorder.stop()
}
@SuppressLint("MissingPermission")
private suspend fun openCamera(
manager: CameraManager, cameraId: String, handler: Handler? = null
): CameraDevice = suspendCoroutine { cont ->
manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = cont.resume(device)
override fun onDisconnected(device: CameraDevice) {
Log.e(TAG, "Camera $cameraId has been disconnected")
stopSelf()
}
override fun onError(device: CameraDevice, error: Int) {
val msg = when(error) {
ERROR_CAMERA_DEVICE -> "ERROR_CAMERA_DEVICE"
ERROR_CAMERA_DISABLED -> "ERROR_CAMERA_DISABLED"
ERROR_CAMERA_IN_USE -> "ERROR_CAMERA_IN_USE"
ERROR_CAMERA_SERVICE -> "ERROR_CAMERA_SERVICE"
ERROR_MAX_CAMERAS_IN_USE -> "ERROR_MAX_CAMERAS_IN_USE"
else -> "UNKNOWN"
}
Log.e(TAG, "Camera $cameraId error: ($error) $msg")
stopSelf()
}
}, handler)
}
@Suppress("DEPRECATION")
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null
): CameraCaptureSession = suspendCoroutine { cont ->
device.createCaptureSession(targets, object: CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "Camera ${device.id} session configuration failed")
stopSelf()
}
}, handler)
}
private fun ensureChannelId() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT).apply {
description = NOTIFICATION_DESCRIPTION
}
)
}
}
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Log.d(TAG, "onStartCommand")
intent?.also {
val param = it.getSerializableExtra(INTENT_NAME)
if (param is RecordServiceParam) {
Log.d(TAG, "id=${param.cameraId}, width=${param.width}, height=${param.height}, fps=${param.fps}")
recordParam = param
}
}
ensureChannelId()
notificationBuilder.apply {
setContentTitle("Cam2Service")
setContentText("Record preparing")
setSmallIcon(R.mipmap.ic_launcher)
}.build().also {
startForeground(999, it)
}
if (!initializeCamera()) return START_NOT_STICKY
Log.d(TAG, "onStartCommand end")
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy")
releaseCamera()
}
private data class RecordServiceParam(
val cameraId: String,
val width: Int,
val height: Int,
val fps: Int
): Serializable
companion object {
private const val INTENT_NAME = "RecordServiceParam"
private const val NOTIFICATION_CHANNEL_ID = "Cam2Service"
private const val NOTIFICATION_CHANNEL_NAME = "Cam2 Service"
private const val NOTIFICATION_DESCRIPTION = "Cam2 Service notification desc"
private const val TAG = "RecordService"
private const val VIDEO_BITRATE: Int = 10_000_000
fun createIntent(context: Context, cameraId: String, width: Int, height: Int, fps: Int) : Intent {
return Intent(context, RecordService::class.java).also {
it.putExtra(INTENT_NAME, RecordServiceParam(cameraId, width, height, fps))
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment