client-android/app/src/main/java/com/urkob/wittrail_android/CameraXRecordingService.kt

251 lines
9.4 KiB
Kotlin

package com.urkob.wittrail_android
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ContentValues
import android.content.Intent
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleService
import com.google.common.util.concurrent.ListenableFuture
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraXRecordingService : LifecycleService() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private val tag = "RecordingService"
private val fileNameFormat = "yyyy-MM-dd-HH-mm-ss-SSS"
private var useFrontCamera = false
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null
private lateinit var cameraExecutor: ExecutorService
private var isCameraInitialized = false
private var cameraInitializedCallback: CameraInitializedCallback? = null
companion object {
const val ACTION_START_RECORDING = "com.urkob.wittrail_android.action.START_RECORDING"
const val ACTION_STOP_RECORDING = "com.urkob.wittrail_android.action.STOP_RECORDING"
private const val CHANNEL_ID = "ForegroundServiceChannel"
private const val NOTIFICATION_ID = 1
}
private fun setCameraInitializedCallback(callback: CameraInitializedCallback) {
this.cameraInitializedCallback = callback
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
startForeground(NOTIFICATION_ID, createNotification())
cameraExecutor = Executors.newSingleThreadExecutor()
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
Log.e(tag, "onCreate")
cameraProviderFuture.addListener({
// Initialize videoCapture here
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
Log.e(tag, "videoCapture initialized")
// Now we can start the camera
startCamera()
isCameraInitialized = true
isCameraInitialized = true
cameraInitializedCallback?.onCameraInitialized()
cameraInitializedCallback = null
}, ContextCompat.getMainExecutor(this))
Log.e(tag, "END onCreate")
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
}
private fun createNotification(): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Recording Service")
.setContentText("Recording is running in the background")
.setSmallIcon(R.drawable.ic_launcher_foreground) // Replace with your own drawable resource
.setContentIntent(pendingIntent)
.setTicker("Ticker text")
.build()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
useFrontCamera = intent?.getBooleanExtra("useFrontCamera", false) ?: false
when (intent?.action) {
ACTION_START_RECORDING -> {
if (isCameraInitialized) {
captureVideo()
} else {
setCameraInitializedCallback(object : CameraInitializedCallback {
override fun onCameraInitialized() {
captureVideo()
}
})
}
}
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
Log.e(tag, "ON DESTROY IS CALLED SHOUlD STOP CAMERA")
// Release resources
stopRecording()
cameraExecutor.shutdown()
Log.e(tag, "Resources released")
super.onDestroy()
}
private fun startCamera() {
Log.e(tag, "startCamera")
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val cameraSelector = if (useFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(this, cameraSelector, videoCapture)
} catch(exc: Exception) {
Log.e(tag, "Use case binding failed", exc)
}
}
// Implements VideoCapture use case, including start and stop capturing.
@SuppressLint("MissingPermission")
private fun captureVideo() {
if (!isCameraInitialized) {
Log.e(tag, "Camera not initialized yet.")
// Optionally, queue this request or set a flag to try again later
return
}
Log.e(tag, "START CAPTURE VIDEO")
Log.e(tag, "Attempting to start video capture")
val videoCapture = this.videoCapture ?: run {
Log.e(tag, "VideoCapture not initialized")
return
}
val curRecording = recording
if (curRecording != null) {
Log.e(tag, "curRecording != null")
// Stop the current recording session.
curRecording.stop()
recording = null
return
}
// create and start a new recording session
val name = SimpleDateFormat(fileNameFormat, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/wittrail")
}
}
Log.e(tag, "contentValues")
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
Log.e(tag, "mediaStoreOutputOptions")
recording = videoCapture.output
.prepareRecording(this, mediaStoreOutputOptions)
.apply {
withAudioEnabled()
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
when(recordEvent) {
is VideoRecordEvent.Start -> {
// TODO: should only display stop button
Log.e(tag, "START RECORDING")
}
is VideoRecordEvent.Finalize -> {
Log.e(tag, "ON FINALIZE")
if (!recordEvent.hasError()) {
Log.d(tag, "Saved URI: " + recordEvent.outputResults.outputUri)
val msg = "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
Log.d(tag, msg)
} else {
Log.e(tag, "Video capture ends with error: " +
"${recordEvent.error} $recordEvent")
Log.d(tag, "Saved URI: " + recordEvent.outputResults.outputUri)
recording?.close()
recording = null
}
Log.e(tag, "RECORDING STOPPED")
}
}
}
}
private fun stopRecording() {
Log.e(tag, "Attempting to stop recording")
val curRecording = recording
if (curRecording != null) {
curRecording.stop()
recording = null
Log.e(tag, "Recording stopped. Now stopping service.")
stopSelf() // This will stop the service.
} else {
Log.e(tag, "No active recording to stop")
}
}
}
interface CameraInitializedCallback {
fun onCameraInitialized()
}