WIP: was storing first time but issues while stopping video
This commit is contained in:
parent
9f663db12a
commit
4b2eb8def9
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
|
@ -0,0 +1 @@
|
|||
Wittrail-Android
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue