WIP: was storing first time but issues while stopping video

This commit is contained in:
Urko. 2024-01-30 20:03:20 +01:00
parent 9f663db12a
commit 4b2eb8def9
17 changed files with 748 additions and 43 deletions

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
Wittrail-Android

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

19
.idea/gradle.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,41 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
</component>
</project>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -48,17 +48,22 @@ android {
}
}
}
val cameraxVersion = "1.3.1"
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.camera:camera-camera2:$cameraxVersion")
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
implementation("androidx.camera:camera-video:$cameraxVersion")
implementation("androidx.camera:camera-view:$cameraxVersion")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-service:2.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View File

@ -2,6 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_CAMERA" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_MICROPHONE" tools:ignore="ProtectedPermissions" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -12,6 +22,12 @@
android:supportsRtl="true"
android:theme="@style/Theme.WittrailAndroid"
tools:targetApi="31">
<service
android:name="RecordingService"
android:foregroundServiceType="camera|microphone"
android:exported="false"
tools:ignore="ForegroundServicePermission">
</service>
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -1,46 +1,138 @@
package com.urkob.wittrail_android
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.urkob.wittrail_android.ui.theme.WittrailAndroidTheme
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
companion object {
private const val REQUEST_CAMERA_PERMISSION = 101
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
private var useFrontCamera: Boolean = false
private lateinit var startFrontCameraButton: Button
private lateinit var startBackCameraButton: Button
private lateinit var stopCameraButton: Button
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WittrailAndroidTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
setContentView(R.layout.activity_main)
startFrontCameraButton = findViewById(R.id.startFrontCameraButton)
startBackCameraButton = findViewById(R.id.startBackCameraButton)
stopCameraButton = findViewById(R.id.stopCameraButton)
if (!allPermissionsGranted()) {
requestPermissions()
} else {
Log.e("main", "INIT START CAMERA")
initStartCamera()
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
WittrailAndroidTheme {
Greeting("Android")
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun requestPermissions() {
activityResultLauncher.launch(REQUIRED_PERMISSIONS)
}
@RequiresApi(Build.VERSION_CODES.O)
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions())
{ permissions ->
// Handle Permission granted/rejected
var permissionGranted = true
permissions.entries.forEach {
if (it.key in REQUIRED_PERMISSIONS && !it.value)
permissionGranted = false
}
if (!permissionGranted) {
Toast.makeText(baseContext,
"Permission request denied",
Toast.LENGTH_SHORT).show()
} else {
Log.e("main", "INIT START CAMERA")
initStartCamera()
}
}
private fun hasCameraPermission() =
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
private fun requestCameraPermission() {
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION
)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun initStartCamera(){
startFrontCameraButton.setOnClickListener {
useFrontCamera = true
if (hasCameraPermission()) {
startRecordingService(useFrontCamera)
} else {
requestCameraPermission()
}
}
startBackCameraButton.setOnClickListener {
useFrontCamera = false
if (hasCameraPermission()) {
startRecordingService(useFrontCamera)
} else {
requestCameraPermission()
}
}
stopCameraButton.setOnClickListener {
stopRecordingService()
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun startRecordingService(useFrontCamera: Boolean) {
Log.e("main", "startRecordingService")
val serviceIntent = Intent(this, RecordingService::class.java).apply {
action = RecordingService.ACTION_START_RECORDING
putExtra("useFrontCamera", useFrontCamera)
}
startForegroundService(serviceIntent)
// startService(serviceIntent)
}
private fun stopRecordingService() {
Log.e("main", "stopRecordingService")
val serviceIntent = Intent(this, RecordingService::class.java).apply {
action = RecordingService.ACTION_STOP_RECORDING
}
stopForeground(STOP_FOREGROUND_DETACH)
stopService(serviceIntent)
}
}

View File

@ -0,0 +1,213 @@
package com.urkob.wittrail_android
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 RecordingService : 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
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)
// Now we can start the camera
startCamera()
}, 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 -> {
Log.e(tag, ACTION_START_RECORDING)
captureVideo()
}
ACTION_STOP_RECORDING -> {
Log.e(tag, ACTION_STOP_RECORDING)
stopRecording()
// return START_NOT_STICKY
}
}
// return START_STICKY
return super.onStartCommand(intent, flags, startId)
}
// override fun onDestroy() {
// Log.e(tag, "ON DESTROY IS CALLED SHOUlD STOP CAMERA")
// 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.
private fun captureVideo() {
Log.e(tag, "START CAPTURE VIDEO")
val videoCapture = this.videoCapture ?: 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)
.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")
}
}
}
}
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 stopRecording() {
Log.e(tag, "stopRecording")
val curRecording = recording
if (curRecording != null) {
Log.e(tag, "stopRecording curRecording != null Stop the current recording session")
curRecording.stop()
recording = null
return
}
}
}

View File

@ -0,0 +1,236 @@
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)
}
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center_horizontal">
<Button
android:id="@+id/startFrontCameraButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Front Camera" />
<Button
android:id="@+id/startBackCameraButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Back Camera"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/stopCameraButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop Recording"
android:layout_marginTop="16dp" />
</LinearLayout>

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WittrailAndroid" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
<!-- Base application theme. -->
<style name="Theme.WittrailAndroid" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>