237 lines
8.4 KiB
Plaintext
237 lines
8.4 KiB
Plaintext
package com.urkob.wittrail_android
|
|
|
|
import android.app.Notification
|
|
import android.app.NotificationChannel
|
|
import android.app.NotificationManager
|
|
import android.app.PendingIntent
|
|
import android.app.Service
|
|
import android.content.Intent
|
|
import android.media.MediaScannerConnection
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.IBinder
|
|
import android.provider.MediaStore
|
|
import android.util.Log
|
|
import android.widget.Toast
|
|
import androidx.camera.core.CameraSelector
|
|
import androidx.camera.core.Preview
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
import androidx.camera.video.MediaStoreOutputOptions
|
|
import androidx.camera.video.Recorder
|
|
import androidx.camera.video.Recording
|
|
import androidx.camera.video.VideoCapture
|
|
import androidx.core.app.NotificationCompat
|
|
import androidx.core.content.ContextCompat
|
|
import com.google.common.util.concurrent.ListenableFuture
|
|
import java.io.File
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Locale
|
|
import java.util.concurrent.ExecutorService
|
|
import java.util.concurrent.Executors
|
|
|
|
|
|
class RecordingServices : Service() {
|
|
private lateinit var cameraExecutor: ExecutorService
|
|
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
|
private var recording: Recording? = null
|
|
private lateinit var videoCapture: VideoCapture<Recorder>
|
|
|
|
private val notificationChannelId = "RECORDING_CHANNEL"
|
|
private val notificationId = 1
|
|
private var videoFile: File? = null
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
|
}
|
|
|
|
override fun onBind(intent: Intent): IBinder? {
|
|
return null
|
|
}
|
|
|
|
private fun bindCamera() {
|
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
|
|
// You need to build the use cases. For example:
|
|
val previewUseCase = Preview.Builder().build()
|
|
val videoCaptureUseCase = VideoCapture.withOutput(Recorder.Builder().build())
|
|
|
|
try {
|
|
cameraProvider.unbindAll()
|
|
cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase, videoCaptureUseCase)
|
|
} catch (exc: Exception) {
|
|
Log.e(TAG, "Use case binding failed", exc)
|
|
}
|
|
}
|
|
|
|
private fun startRecording() {
|
|
val mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
|
|
.build()
|
|
|
|
val outputOptions = VideoCapture.OutputFileOptions.Builder(videoFile).build()
|
|
// Start recording
|
|
videoCapture.startRecording(outputOptions, cameraExecutor, object : VideoCapture.OnVideoSavedCallback {
|
|
fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
|
|
val savedUri = outputFileResults.savedUri ?: Uri.fromFile(videoFile)
|
|
Log.d(TAG, "Video File Saved at $savedUri")
|
|
// Here you can handle the saved video file
|
|
}
|
|
|
|
fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
|
Log.e(TAG, "Video capture error: $message", cause)
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
private val captureListener = object : VideoCapture.OnVideoSavedCallback {
|
|
fun onVideoSaved(file: File) {
|
|
Log.d(TAG, "Video File: $file")
|
|
// Here you can handle the saved video file
|
|
}
|
|
fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
|
Log.e(TAG, "Video capture error: $message", cause)
|
|
}
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
cameraExecutor.shutdown()
|
|
recording?.stop() // Stop the recording
|
|
}
|
|
|
|
// Helper function to create a file
|
|
private fun createFile(baseFolder: File, format: String, extension: String) =
|
|
File(baseFolder, SimpleDateFormat(format, Locale.US)
|
|
.format(System.currentTimeMillis()) + extension)
|
|
|
|
companion object {
|
|
private const val TAG = "RecordingService"
|
|
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
|
|
private const val VIDEO_EXTENSION = ".mp4"
|
|
private val outputDirectory: File by lazy {
|
|
getOutputDirectory()
|
|
}
|
|
}
|
|
|
|
// Method to get the output directory
|
|
private fun getOutputDirectory(): File {
|
|
val mediaDir = externalMediaDirs.firstOrNull()?.let {
|
|
File(it, resources.getString(R.string.app_name)).apply { mkdirs() }
|
|
}
|
|
return if (mediaDir != null && mediaDir.exists())
|
|
mediaDir else filesDir
|
|
}
|
|
|
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
createNotificationChannel()
|
|
startForeground(notificationId, createNotification())
|
|
|
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
|
|
val previewUseCase = Preview.Builder().build()
|
|
val videoCaptureUseCase = VideoCapture.withOutput(Recorder.Builder().build())
|
|
|
|
try {
|
|
cameraProvider.unbindAll()
|
|
cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase, videoCaptureUseCase)
|
|
} catch (exc: Exception) {
|
|
Log.e(TAG, "Use case binding failed", exc)
|
|
}
|
|
|
|
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
|
cameraProviderFuture.addListener(Runnable {
|
|
val cameraProvider = cameraProviderFuture.get()
|
|
bindCamera()
|
|
}, ContextCompat.getMainExecutor(this))
|
|
|
|
return START_STICKY
|
|
}
|
|
|
|
private fun startCamera() {
|
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
|
|
|
val preview = Preview.Builder()
|
|
.build()
|
|
.also {
|
|
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
|
|
}
|
|
|
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
|
|
val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
|
|
|
|
|
|
try {
|
|
cameraProvider.unbindAll()
|
|
cameraProvider.bindToLifecycle(
|
|
this, cameraSelector, preview)
|
|
|
|
cameraProvider.bindToLifecycle(
|
|
this, cameraSelector, preview, imageCapture, videoCapture)
|
|
} catch (exc: Exception) {
|
|
Log.e(TAG, "Use case binding failed", exc)
|
|
}
|
|
}
|
|
|
|
|
|
private fun stopRecording() {
|
|
try {
|
|
Log.d("RecordingService", "Recording stopped. File saved at: ${videoFile?.absolutePath}")
|
|
// Now scan the file
|
|
videoFile?.let { file ->
|
|
Log.d("RecordingService", "Recording stopped. File saved at: ${file.absolutePath}")
|
|
scanFile(file) // Use 'let' to ensure videoFile is not null when calling scanFile
|
|
}
|
|
} catch (e: RuntimeException) {
|
|
e.printStackTrace()
|
|
Toast.makeText(this, "Recording stop failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
|
|
|
|
private fun scanFile(file: File) {
|
|
MediaScannerConnection.scanFile(
|
|
this@`RecordingService.txt`,
|
|
arrayOf(file.absolutePath),
|
|
null
|
|
) { path: String?, uri: Uri? ->
|
|
Log.d("RecordingService", "Scanned $path:")
|
|
Log.d("RecordingService", "-> Uri = $uri")
|
|
}
|
|
}
|
|
|
|
|
|
private fun createNotification(): Notification {
|
|
val notificationIntent = Intent(this, MainActivity::class.java)
|
|
// val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
|
|
|
|
val pendingIntent = PendingIntent.getActivity(
|
|
this, 0, notificationIntent,
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
|
)
|
|
|
|
return NotificationCompat.Builder(this, notificationChannelId)
|
|
.setContentTitle("Recording Service")
|
|
.setContentText("Recording in progress...")
|
|
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
.setContentIntent(pendingIntent)
|
|
.build()
|
|
}
|
|
|
|
private fun createNotificationChannel() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
val serviceChannel = NotificationChannel(
|
|
notificationChannelId,
|
|
"Foreground Service Channel",
|
|
NotificationManager.IMPORTANCE_DEFAULT
|
|
)
|
|
|
|
val manager = getSystemService(NotificationManager::class.java)
|
|
manager.createNotificationChannel(serviceChannel)
|
|
}
|
|
}
|
|
}
|