@@ -14,7 +14,11 @@ import androidx.lifecycle.LifecycleOwner
1414import androidx.lifecycle.coroutineScope
1515import com.darkrockstudios.app.securecamera.obfuscation.FacialDetection
1616import com.darkrockstudios.app.securecamera.preferences.AppSettingsDataSource
17+ import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
18+ import com.darkrockstudios.app.securecamera.security.streaming.SecvFileFormat
19+ import com.darkrockstudios.app.securecamera.security.streaming.VideoEncryptionHelper
1720import kotlinx.coroutines.*
21+ import kotlinx.coroutines.flow.collectLatest
1822import org.koin.core.component.KoinComponent
1923import org.koin.core.component.inject
2024import timber.log.Timber
@@ -43,8 +47,10 @@ class CameraState internal constructor(
4347 private val clock: Clock by inject()
4448 private val facialDetection: FacialDetection by inject()
4549 private val preferences: AppSettingsDataSource by inject()
50+ private val encryptionScheme: EncryptionScheme by inject()
4651
4752 private var faceAnalyzer: FacialDetectionAnalyzer ? = null
53+ private var videoEncryptionHelper: VideoEncryptionHelper ? = null
4854 private val cameraExecutor: ExecutorService = Executors .newSingleThreadExecutor()
4955 private val analysisExecutor: ExecutorService = Executors .newSingleThreadExecutor()
5056 private val analysisScope: CoroutineScope = CoroutineScope (SupervisorJob () + Dispatchers .Default )
@@ -92,11 +98,66 @@ class CameraState internal constructor(
9298 var recordingDurationMs by mutableLongStateOf(0L )
9399 private set
94100
101+ var isEncryptingVideo by mutableStateOf(false )
102+ private set
103+
104+ var encryptionProgress by mutableFloatStateOf(0f )
105+ private set
106+
95107 private var activeRecording: Recording ? = null
96108 private var videoCapture: VideoCapture <Recorder >? = null
109+ private var pendingTempFile: File ? = null
110+ private var pendingOutputFile: File ? = null
97111
98112 init {
99113 observePreferences()
114+ initializeEncryptionHelper()
115+ }
116+
117+ private fun initializeEncryptionHelper () {
118+ val streamingScheme = encryptionScheme.getStreamingCapability()
119+ if (streamingScheme != null ) {
120+ videoEncryptionHelper = VideoEncryptionHelper (streamingScheme)
121+ observeEncryptionProgress()
122+ } else {
123+ Timber .w(" Streaming encryption not available" )
124+ }
125+ }
126+
127+ private fun observeEncryptionProgress () {
128+ lifecycleOwner.lifecycle.coroutineScope.launch {
129+ videoEncryptionHelper?.encryptionProgress?.collectLatest { progress ->
130+ when (progress) {
131+ is VideoEncryptionHelper .EncryptionProgress .Idle -> {
132+ isEncryptingVideo = false
133+ encryptionProgress = 0f
134+ }
135+
136+ is VideoEncryptionHelper .EncryptionProgress .Starting -> {
137+ isEncryptingVideo = true
138+ encryptionProgress = 0f
139+ }
140+
141+ is VideoEncryptionHelper .EncryptionProgress .InProgress -> {
142+ isEncryptingVideo = true
143+ encryptionProgress = progress.progress
144+ }
145+
146+ is VideoEncryptionHelper .EncryptionProgress .Completed -> {
147+ isEncryptingVideo = false
148+ encryptionProgress = 0f
149+ videoEncryptionHelper?.resetProgress()
150+ }
151+
152+ is VideoEncryptionHelper .EncryptionProgress .Error -> {
153+ isEncryptingVideo = false
154+ encryptionProgress = 0f
155+ Timber .e(" Encryption error: ${progress.message} " )
156+ videoEncryptionHelper?.resetProgress()
157+ }
158+ }
159+ }
160+ }
100161 }
101162
102163 private fun observePreferences () {
@@ -247,8 +308,13 @@ class CameraState internal constructor(
247308 }
248309
249310 /* *
250- * Start video recording. Returns the output file path.
251- * The video will be saved to the app's internal files directory.
311+ * Start video recording. Returns the final encrypted output file path (.secv).
312+ * The video will be saved to the app's internal files directory after encryption.
313+ *
314+ * Recording flow:
315+ * 1. CameraX writes to a temporary .mp4 file
316+ * 2. On recording complete, the temp file is encrypted to .secv format
317+ * 3. The temp file is securely deleted after encryption
252318 */
253319 @SuppressLint(" MissingPermission" )
254320 fun startRecording (context : Context ): File ? {
@@ -257,12 +323,12 @@ class CameraState internal constructor(
257323 return null
258324 }
259325
260- if (isRecording) {
261- Timber .w(" Recording already in progress" )
326+ if (isRecording || isEncryptingVideo ) {
327+ Timber .w(" Recording or encryption already in progress" )
262328 return null
263329 }
264330
265- // Create output file in app's internal storage
331+ // Create output directories
266332 val videosDir = File (context.filesDir, " videos" )
267333 if (! videosDir.exists()) {
268334 videosDir.mkdirs()
@@ -272,9 +338,16 @@ class CameraState internal constructor(
272338 " yyyyMMdd_HHmmss" ,
273339 java.util.Locale .US
274340 ).format(System .currentTimeMillis())
275- val outputFile = File (videosDir, " video_$timestamp .mp4" )
276341
277- val fileOutputOptions = FileOutputOptions .Builder (outputFile).build()
342+ // Temp file for CameraX recording (unencrypted)
343+ val tempFile = File (videosDir, " temp_$timestamp .mp4" )
344+ // Final encrypted output file
345+ val outputFile = File (videosDir, " video_$timestamp .${SecvFileFormat .FILE_EXTENSION } " )
346+
347+ pendingTempFile = tempFile
348+ pendingOutputFile = outputFile
349+
350+ val fileOutputOptions = FileOutputOptions .Builder (tempFile).build()
278351
279352 activeRecording = videoCapture.output
280353 .prepareRecording(context, fileOutputOptions)
@@ -293,21 +366,55 @@ class CameraState internal constructor(
293366
294367 is VideoRecordEvent .Finalize -> {
295368 isRecording = false
369+ activeRecording = null
370+
296371 if (event.hasError()) {
297372 Timber .e(" Recording error: ${event.error} , cause: ${event.cause?.message} " )
298- // Delete the failed file
299- outputFile.delete()
373+ // Delete the failed temp file
374+ tempFile.delete()
375+ pendingTempFile = null
376+ pendingOutputFile = null
300377 } else {
301- Timber .i(" Video saved to: ${event.outputResults.outputUri} " )
378+ Timber .i(" Recording complete, starting encryption..." )
379+ // Start encryption in background
380+ lifecycleOwner.lifecycle.coroutineScope.launch {
381+ encryptRecordedVideo(tempFile, outputFile)
382+ }
302383 }
303- activeRecording = null
304384 }
305385 }
306386 }
307387
308388 return outputFile
309389 }
310390
391+ /* *
392+ * Encrypts the recorded video from temp file to final encrypted file.
393+ */
394+ private suspend fun encryptRecordedVideo (tempFile : File , outputFile : File ) {
395+ val helper = videoEncryptionHelper
396+ if (helper == null ) {
397+ Timber .e(" Video encryption helper not available" )
398+ // Fall back to keeping the unencrypted file (rename to .mp4)
399+ val fallbackFile = File (outputFile.parent, outputFile.nameWithoutExtension + " .mp4" )
400+ tempFile.renameTo(fallbackFile)
401+ return
402+ }
403+
404+ val success = helper.encryptVideoFile(tempFile, outputFile)
405+ if (success) {
406+ Timber .i(" Video encrypted successfully: ${outputFile.absolutePath} " )
407+ } else {
408+ Timber .e(" Video encryption failed" )
409+ // Keep the temp file as fallback (rename to .mp4)
410+ val fallbackFile = File (outputFile.parent, outputFile.nameWithoutExtension + " .mp4" )
411+ tempFile.renameTo(fallbackFile)
412+ }
413+
414+ pendingTempFile = null
415+ pendingOutputFile = null
416+ }
417+
311418 /* *
312419 * Stop the current video recording.
313420 */
@@ -437,6 +544,16 @@ class CameraState internal constructor(
437544 if (isRecording) {
438545 stopRecording()
439546 }
547+
548+ // Clean up any pending temp files
549+ pendingTempFile?.let { file ->
550+ if (file.exists()) {
551+ file.delete()
552+ }
553+ }
554+ pendingTempFile = null
555+ pendingOutputFile = null
556+
440557 faceAnalyzer = null
441558 imageAnalysis?.clearAnalyzer()
442559 analysisScope.cancel()
0 commit comments