Last active
April 19, 2021 19:06
-
-
Save uchidev/1b6c48353aa42f45f43d4cbac2ee07ce to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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