From adc126a7b755bf1ffbbf252e54298d782072814f Mon Sep 17 00:00:00 2001 From: tuuhin Date: Mon, 3 Nov 2025 08:30:17 +0530 Subject: [PATCH 01/10] Fixed issue with sharing audiofilemodel In the intent in ShareRecordingsUtilImpl.kt the intent stream extra was not mentioned,thus file content was unable to be shared. --- .../java/com/eva/interactions/data/ShareRecordingsUtilImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/data/interactions/src/main/java/com/eva/interactions/data/ShareRecordingsUtilImpl.kt b/data/interactions/src/main/java/com/eva/interactions/data/ShareRecordingsUtilImpl.kt index 7f87b0f..d272983 100644 --- a/data/interactions/src/main/java/com/eva/interactions/data/ShareRecordingsUtilImpl.kt +++ b/data/interactions/src/main/java/com/eva/interactions/data/ShareRecordingsUtilImpl.kt @@ -52,6 +52,7 @@ internal class ShareRecordingsUtilImpl( val intent = Intent(Intent.ACTION_SEND).apply { setDataAndType(uri, "audio/*") + putExtra(Intent.EXTRA_STREAM, uri) putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_audio_extra_subject)) } From 6a39914464e8e47542bd8265f0a73eaa09341417 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Tue, 4 Nov 2025 20:39:42 +0530 Subject: [PATCH 02/10] Changed the signature of AudioVisualizer.kt Included a lifecycle owner as we are creating threads they need to cleared off CoroutineLifecycleOwner.kt mimics the lifecycle of viewmodel coroutine scope thus ensures when the scope cancels the threads are also stopped Corrected name exceptions --- .../com/eva/player/domain/AudioVisualizer.kt | 18 ++++++++----- .../exceptions/DecoderExistsException.kt | 2 +- .../ExtractorNoTrackFoundException.kt | 3 +++ .../PlayerVisualizerViewmodel.kt | 15 ++++++----- .../util/CoroutineLifecycleOwner.kt | 27 +++++++++++++++++++ 5 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 data/player/src/main/java/com/eva/player/domain/exceptions/ExtractorNoTrackFoundException.kt create mode 100644 feature/player-shared/src/main/java/com/eva/player_shared/util/CoroutineLifecycleOwner.kt diff --git a/data/player/src/main/java/com/eva/player/domain/AudioVisualizer.kt b/data/player/src/main/java/com/eva/player/domain/AudioVisualizer.kt index 92242d3..73f1184 100644 --- a/data/player/src/main/java/com/eva/player/domain/AudioVisualizer.kt +++ b/data/player/src/main/java/com/eva/player/domain/AudioVisualizer.kt @@ -1,5 +1,6 @@ package com.eva.player.domain +import androidx.lifecycle.LifecycleOwner import com.eva.recordings.domain.models.AudioFileModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -10,12 +11,17 @@ interface AudioVisualizer { val normalizedVisualization: Flow - suspend fun prepareVisualization(model: AudioFileModel, timePerPointInMs: Int = 10) - : Result - - suspend fun prepareVisualization(fileUri: String, timePerPointInMs: Int): Result - - suspend fun prepareVisualization(fileId: Long, timePerPointInMs: Int): Result + suspend fun prepareVisualization( + model: AudioFileModel, + lifecycleOwner: LifecycleOwner, + timePerPointInMs: Int = 10 + ): Result + + suspend fun prepareVisualization( + fileUri: String, + lifecycleOwner: LifecycleOwner, + timePerPointInMs: Int + ): Result fun cleanUp() } \ No newline at end of file diff --git a/data/player/src/main/java/com/eva/player/domain/exceptions/DecoderExistsException.kt b/data/player/src/main/java/com/eva/player/domain/exceptions/DecoderExistsException.kt index 8c75437..e889db7 100644 --- a/data/player/src/main/java/com/eva/player/domain/exceptions/DecoderExistsException.kt +++ b/data/player/src/main/java/com/eva/player/domain/exceptions/DecoderExistsException.kt @@ -1,3 +1,3 @@ package com.eva.player.domain.exceptions -class DecoderExistsException : Exception("Decoder already ready clean it to continue") \ No newline at end of file +class DecoderExistsException : Exception("Decoder is holding resources, clean it to run again") \ No newline at end of file diff --git a/data/player/src/main/java/com/eva/player/domain/exceptions/ExtractorNoTrackFoundException.kt b/data/player/src/main/java/com/eva/player/domain/exceptions/ExtractorNoTrackFoundException.kt new file mode 100644 index 0000000..0dd4f50 --- /dev/null +++ b/data/player/src/main/java/com/eva/player/domain/exceptions/ExtractorNoTrackFoundException.kt @@ -0,0 +1,3 @@ +package com.eva.player.domain.exceptions + +class ExtractorNoTrackFoundException : Exception("No tracks found in the associated URI") \ No newline at end of file diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt b/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt index e5ddea3..62f1d23 100644 --- a/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt +++ b/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt @@ -7,7 +7,9 @@ import com.eva.editor.domain.AudioConfigToActionList import com.eva.player.data.reader.compressFloatArray import com.eva.player.domain.AudioVisualizer import com.eva.player.domain.exceptions.DecoderExistsException +import com.eva.player_shared.util.CoroutineLifecycleOwner import com.eva.player_shared.util.updateArrayViaConfigs +import com.eva.recordings.domain.provider.PlayerFileProvider import com.eva.ui.navigation.PlayerSubGraph import com.eva.ui.viewmodel.AppViewModel import com.eva.ui.viewmodel.UIEvents @@ -31,24 +33,22 @@ import javax.inject.Inject @HiltViewModel class PlayerVisualizerViewmodel @Inject constructor( private val visualizer: AudioVisualizer, + private val playerFileProvider: PlayerFileProvider, private val savedStateHandle: SavedStateHandle, ) : AppViewModel() { + private val _lifecycleOwner by lazy { CoroutineLifecycleOwner(viewModelScope.coroutineContext) } + private val route: PlayerSubGraph.NavGraph get() = savedStateHandle.toRoute() - val audioId: Long - get() = route.audioId - private val _compressedVisualization = MutableStateFlow(floatArrayOf()) + private val _clipConfigs = MutableStateFlow(emptyList()) private val _uiEvents = MutableSharedFlow() override val uiEvent: SharedFlow get() = _uiEvents - - private val _clipConfigs = MutableStateFlow(emptyList()) - val isVisualsReady = visualizer.isVisualReady.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), @@ -79,7 +79,8 @@ class PlayerVisualizerViewmodel @Inject constructor( private fun prepareVisuals() = viewModelScope.launch { val result = visualizer.prepareVisualization( - fileId = audioId, + lifecycleOwner = _lifecycleOwner, + fileUri = playerFileProvider.providesAudioFileUri(route.audioId), timePerPointInMs = RecorderConstants.RECORDER_AMPLITUDES_BUFFER_SIZE ) result.onFailure { err -> diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/util/CoroutineLifecycleOwner.kt b/feature/player-shared/src/main/java/com/eva/player_shared/util/CoroutineLifecycleOwner.kt new file mode 100644 index 0000000..48cdf28 --- /dev/null +++ b/feature/player-shared/src/main/java/com/eva/player_shared/util/CoroutineLifecycleOwner.kt @@ -0,0 +1,27 @@ +package com.eva.player_shared.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + +internal class CoroutineLifecycleOwner(context: CoroutineContext) : LifecycleOwner { + + private val lifecycleRegistry = LifecycleRegistry(this) + .apply { currentState = Lifecycle.State.INITIALIZED } + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + init { + if (context[Job]?.isActive == true) { + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + context[Job]?.invokeOnCompletion { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + } else { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + } +} \ No newline at end of file From 296315100e3cdab02852e42425935dcda607e2b9 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Tue, 4 Nov 2025 20:45:15 +0530 Subject: [PATCH 03/10] AudioVisualizerImpl.kt implementation The core is quite same as earlier, but MediaCodecPCMDataDecoder.kt not managing any handlerThreads here initCodec takes up a handler that can be still run on the main thread Thus codec is only responsible for decoding audio buffers Corrected names for MediaCodecState.kt The ThreadLifecycleHandler.kt manages creation of thread, for each instance only a single thread instance is managed Though still some worked required as its crashing sometime , we create the thread on start and on destroy we stop it Also when the MediaCodecPCMDataDecoder.kt work is done ie the end of stream is received we clean the native objects --- .../player/data/reader/AudioVisualizerImpl.kt | 80 +++++--- .../data/reader/MediaCodecPCMDataDecoder.kt | 173 +++++++++++------- .../eva/player/data/reader/MediaCodecState.kt | 11 +- .../data/reader/ThreadLifecycleHandler.kt | 141 ++++++++++++++ 4 files changed, 304 insertions(+), 101 deletions(-) create mode 100644 data/player/src/main/java/com/eva/player/data/reader/ThreadLifecycleHandler.kt diff --git a/data/player/src/main/java/com/eva/player/data/reader/AudioVisualizerImpl.kt b/data/player/src/main/java/com/eva/player/data/reader/AudioVisualizerImpl.kt index 441c55d..f66da30 100644 --- a/data/player/src/main/java/com/eva/player/data/reader/AudioVisualizerImpl.kt +++ b/data/player/src/main/java/com/eva/player/data/reader/AudioVisualizerImpl.kt @@ -1,13 +1,13 @@ package com.eva.player.data.reader -import android.content.ContentUris import android.content.Context import android.media.MediaExtractor -import android.provider.MediaStore import android.util.Log import androidx.core.net.toUri +import androidx.lifecycle.LifecycleOwner import com.eva.player.domain.AudioVisualizer import com.eva.player.domain.exceptions.DecoderExistsException +import com.eva.player.domain.exceptions.ExtractorNoTrackFoundException import com.eva.player.domain.exceptions.InvalidMimeTypeException import com.eva.recordings.domain.models.AudioFileModel import kotlinx.coroutines.Dispatchers @@ -18,14 +18,18 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext private const val TAG = "PLAIN_VISUALIZER" internal class AudioVisualizerImpl(private val context: Context) : AudioVisualizer { + @Volatile private var _extractor: MediaExtractor? = null - private var _decoder: MediaCodecPCMDataDecoder? = null + + private val _threadHandler by lazy { ThreadLifecycleHandler("ComputeThread") } private val _isReady = MutableStateFlow(false) override val isVisualReady: StateFlow @@ -34,43 +38,51 @@ internal class AudioVisualizerImpl(private val context: Context) : AudioVisualiz private val _visualization = MutableStateFlow(floatArrayOf()) override val normalizedVisualization: Flow get() = _visualization.map { array -> array.normalize().smoothen(.4f) } - .catch { err -> Log.d(TAG, "SOME ERROR", err) } .flowOn(Dispatchers.Default) + .catch { err -> Log.d(TAG, "SOME ERROR", err) } - override suspend fun prepareVisualization(model: AudioFileModel, timePerPointInMs: Int) - : Result = prepareVisualization(fileUri = model.fileUri, timePerPointInMs) + private var _decoder: MediaCodecPCMDataDecoder? = null + private val _lock = Mutex() - override suspend fun prepareVisualization(fileId: Long, timePerPointInMs: Int): Result { - val uri = ContentUris.withAppendedId( - MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), - fileId - ) - return prepareVisualization(fileUri = uri.toString(), timePerPointInMs) - } + override suspend fun prepareVisualization( + model: AudioFileModel, + lifecycleOwner: LifecycleOwner, + timePerPointInMs: Int + ): Result = prepareVisualization( + fileUri = model.fileUri, + lifecycleOwner = lifecycleOwner, + timePerPointInMs + ) - override suspend fun prepareVisualization(fileUri: String, timePerPointInMs: Int) - : Result { + override suspend fun prepareVisualization( + fileUri: String, + lifecycleOwner: LifecycleOwner, + timePerPointInMs: Int + ): Result = _lock.withLock { if (_decoder != null) { Log.d(TAG, "CLEAN DECODER TO PREPARE IT AGAIN") return Result.failure(DecoderExistsException()) } - return withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { try { _extractor = MediaExtractor().apply { setDataSource(context, fileUri.toUri(), null) } val format = _extractor?.getTrackFormat(0) - val mimetype = format?.mimeType - if (mimetype?.startsWith("audio") != true) { - Log.e(TAG, "WRONG MIME TYPE") + val mimeType = format?.mimeType + + if (mimeType == null || !mimeType.startsWith("audio")) return@withContext Result.failure(InvalidMimeTypeException()) - } + + if (_extractor?.trackCount == 0) + return@withContext Result.failure(ExtractorNoTrackFoundException()) + _extractor?.selectTrack(0) _decoder = MediaCodecPCMDataDecoder( - extractor = _extractor, + extractor = _extractor!!, totalTime = format.duration, seekDurationMillis = timePerPointInMs, ).apply { @@ -79,25 +91,35 @@ internal class AudioVisualizerImpl(private val context: Context) : AudioVisualiz _visualization.update { it + array } } } - Log.d(TAG, "MEDIA CODEC SET FOR MIME TYPE:$mimetype") - _decoder?.initiateCodec(format, mimetype) + + val handler = _threadHandler.bindToLifecycle(lifecycleOwner) + _decoder?.setOnComplete(::releaseObjects) + _decoder?.initiateCodec(format, mimeType, handler) + Result.success(Unit) } catch (e: Exception) { - Log.e(TAG, "Error decoding or processing audio", e) + Log.e(TAG, "CANNOT DECODE THIS URI", e) Result.failure(e) } } } - override fun cleanUp() { - _isReady.update { false } - - Log.d(TAG, "MEDIA CODEC IS RELEASED") + private fun releaseObjects() { + Log.d(TAG, "CLEARING UP OBJECTS") _decoder?.cleanUp() _decoder = null - Log.d(TAG, "MEDIA EXTRACTOR IS RELEASED") _extractor?.release() _extractor = null } + + override fun cleanUp() { + // reset values + _isReady.update { false } + _visualization.update { floatArrayOf() } + + // cleanup + releaseObjects() + } + } \ No newline at end of file diff --git a/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt b/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt index 5363193..7bd7e3a 100644 --- a/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt +++ b/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt @@ -4,7 +4,6 @@ import android.media.MediaCodec import android.media.MediaExtractor import android.media.MediaFormat import android.os.Handler -import android.os.HandlerThread import android.util.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -29,26 +28,24 @@ import kotlin.math.sqrt import kotlin.time.Duration private const val TAG = "CODEC_CALLBACK" +private const val PROCESSING_TAG = "CODEC_PROCESSING" @OptIn(ExperimentalAtomicApi::class) internal class MediaCodecPCMDataDecoder( private val seekDurationMillis: Int, private val totalTime: Duration, - private val extractor: MediaExtractor? = null, + private val extractor: MediaExtractor, ) : MediaCodec.Callback() { - private val threadName = "MediaCodecComputeThread" - private var _handlerThread: HandlerThread? = null - private var _handler: Handler? = null + @Volatile private var _mediaCodec: MediaCodec? = null - private val _scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - private val _operations = ConcurrentLinkedQueue>() - @Volatile - private var _codecState = MediaCodecState.EXEC + private var _codecState = MediaCodecState.STOPPED private val _mutex = Mutex() + private val _scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private val _operations = ConcurrentLinkedQueue>() // callbacks private var _onBufferDecoded: ((FloatArray) -> Unit)? = null @@ -59,37 +56,44 @@ internal class MediaCodecPCMDataDecoder( private val currentTimeInMs = AtomicLong(0L) private val _isBatchProcessing = AtomicBoolean(false) + private var _isCleaningUp = AtomicBoolean(false) override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { - - // codec state is non exec means work is done return END_OF_STREAM - if (_codecState != MediaCodecState.EXEC) { - Log.d(TAG, "CANNOT RUN IN WRONG STATE OR END OF BUFFER REACHED") - codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + // clean up is called thus no more processing + if (_isCleaningUp.load()) { + Log.i(TAG, "IGNORING READING INPUT CALLBACK") return } - val currentTime = currentTimeInMs.load() - // if timeInMs is greater than totalTime+extra return END_OF_STREAM - if (currentTime >= totalTime.inWholeMilliseconds + seekDurationMillis) { - Log.d(TAG, "TOTAL TIME IS ALREADY REACHED") - codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) - return + try { + // codec state is non exec means work is done return END_OF_STREAM + if (_codecState != MediaCodecState.EXEC) { + Log.d(TAG, "WRONG CODEC STATE OR END OF BUFFER") + codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + return + } + val currentTime = currentTimeInMs.load() + // if timeInMs is greater than totalTime+extra return END_OF_STREAM + if (currentTime >= totalTime.inWholeMilliseconds + seekDurationMillis) { + Log.d(TAG, "TOTAL TIME HAS REACHED SENDING END OF STREAM") + codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + return + } + } catch (e: Exception) { + Log.e(TAG, "UNABLE TO SEND EOS FLAG", e) } try { - // receive a buffer if (index < 0) return val inputBuffer = codec.getInputBuffer(index) ?: return - val extractor = extractor ?: return // seek the extractor as we don't need extra data extractor.seekTo(currentTimeInMs.load() * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC) val sampleSize = extractor.readSampleData(inputBuffer, 0) // sample size is zero thus processing done END_OF_STREAM if (sampleSize <= 0) { - _codecState = MediaCodecState.END + _codecState = MediaCodecState.RELEASED Log.d(TAG, "END OF INPUT STREAM") codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) return @@ -98,9 +102,9 @@ internal class MediaCodecPCMDataDecoder( // advance the extractor to read the next sample if not END_OF_STREAM if (!extractor.advance()) { Log.d(TAG, "CANNOT ADVANCE EXTRACTOR ANY MORE") - _codecState = MediaCodecState.END + _codecState = MediaCodecState.RELEASED codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) - } else { + } else if (_codecState == MediaCodecState.EXEC) { // update the current time currentTimeInMs.plusAssign(seekDurationMillis.toLong()) codec.queueInputBuffer(index, 0, sampleSize, extractor.sampleTime, 0) @@ -119,15 +123,23 @@ internal class MediaCodecPCMDataDecoder( index: Int, info: MediaCodec.BufferInfo ) { + if (_isCleaningUp.load()) { + Log.i(TAG, "IGNORING PROCESSING OUTPUT CALLBACK") + return + } + if (_codecState != MediaCodecState.EXEC) { + Log.d(TAG, "WRONG STATE CANNOT PROCESS SHOULD BE EXEC") + return + } if (info.isEndOfStream) { - Log.i(TAG, "EVERYTHING RAN ON ${Thread.currentThread().name}") codec.stop() - _codecState = MediaCodecState.STOP + _codecState = MediaCodecState.RELEASED handleEndOfStream() return } if (info.size > 0) { try { + if (_codecState != MediaCodecState.EXEC) return val outputBuffer = codec.getOutputBuffer(index) ?: return outputBuffer.position(info.offset) outputBuffer.rewind() @@ -138,13 +150,24 @@ internal class MediaCodecPCMDataDecoder( val channelCount = format.channels val floatArray = outputBuffer.asFloatArray(info.size, pcmEncoding, channelCount) handleFloatArray(floatArray) - } catch (e: Exception) { - e.printStackTrace() - } finally { - codec.releaseOutputBuffer(index, false) + + // release the buffer as work is done + if (_codecState == MediaCodecState.EXEC) + codec.releaseOutputBuffer(index, false) + } catch (e: IllegalStateException) { + Log.e(TAG, "WRONG STATE TO HANDLE THE OUTPUT BUFFERS", e) } } + } + + override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { + if (e.isRecoverable) codec.stop() + Log.e(TAG, "ERROR HAPPENED", e) + } + + override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { + Log.d(TAG, "MEDIA FORMAT CHANGED: $format") } private fun handleFloatArray(pcm: FloatArray) { @@ -168,14 +191,14 @@ internal class MediaCodecPCMDataDecoder( add(item) } } - Log.i(TAG, "EVALUATING INFORMATION BATCH :${operations.size}") + Log.i(PROCESSING_TAG, "EVALUATING INFORMATION BATCH :${operations.size}") val resultAsFloatArray = operations.awaitAll().toFloatArray() _onBufferDecoded?.invoke(resultAsFloatArray) } } catch (_: CancellationException) { - Log.d(TAG, "CANCELLATION IN BATCH PROCESSING") + Log.d(PROCESSING_TAG, "CANCELLATION IN BATCH PROCESSING") } catch (e: Exception) { - Log.d(TAG, "Exception at the end of stream", e) + Log.d(PROCESSING_TAG, "Exception at the end of stream", e) } finally { _isBatchProcessing.store(false) } @@ -185,7 +208,7 @@ internal class MediaCodecPCMDataDecoder( private fun handleEndOfStream() { _scope.launch { try { - Log.d(TAG, "END OF BUFFER REACHED, AWAITING OPERATIONS") + Log.d(PROCESSING_TAG, "END OF BUFFER REACHED, AWAITING OPERATIONS") val leftItems = buildSet { while (isActive) { @@ -193,7 +216,7 @@ internal class MediaCodecPCMDataDecoder( add(item) } } - Log.i(TAG, "EVALUATING INFORMATION END :${leftItems.size}") + Log.i(PROCESSING_TAG, "EVALUATING INFORMATION END :${leftItems.size}") _mutex.withLock { if (leftItems.isEmpty()) return@withLock val results = leftItems.awaitAll() @@ -201,22 +224,16 @@ internal class MediaCodecPCMDataDecoder( _onBufferDecoded?.invoke(resultAsFloatArray) _onDecodeComplete?.invoke() } - } catch (_: CancellationException) { - Log.d(TAG, "CANCELLATION IN HANDLING END OF STREAM") } catch (e: Exception) { - Log.d(TAG, "Exception at the end of stream", e) + if (e is CancellationException) { + Log.d(PROCESSING_TAG, "CANCELLATION IN HANDLING END OF STREAM") + return@launch + } + Log.d(PROCESSING_TAG, "Exception at the end of stream", e) } } } - override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { - Log.e(TAG, "ERROR HAPPENED", e) - } - - override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { - Log.d(TAG, "MEDIA FORMAT CHANGED: $format") - } - fun setOnBufferDecode(listener: (FloatArray) -> Unit) { _onBufferDecoded = listener } @@ -225,38 +242,60 @@ internal class MediaCodecPCMDataDecoder( _onDecodeComplete = listener } - fun initiateCodec(format: MediaFormat, mimeType: String) { - if (_handlerThread == null || _handlerThread?.isAlive == false) { - _handlerThread = HandlerThread(threadName).apply { start() } - _handler = Handler(_handlerThread!!.looper) - } + fun initiateCodec(format: MediaFormat, mimeType: String, handler: Handler) { + _isCleaningUp.store(false) + _isBatchProcessing.store(false) + currentTimeInMs.store(0) + _mediaCodec?.reset() _mediaCodec = MediaCodec.createDecoderByType(mimeType).apply { + setCallback(this@MediaCodecPCMDataDecoder, handler) configure(format, null, null, 0) - setCallback(this@MediaCodecPCMDataDecoder, _handler!!) } + Log.i(TAG, "MEDIA CODEC CREATED IN THREAD:${handler.looper.thread.name}") _mediaCodec?.start() - Log.d(TAG, "MEDIA CODEC STARTED") + _codecState = MediaCodecState.EXEC + Log.i(TAG, "MEDIA CODEC STARTED") } fun cleanUp() { - Log.d(TAG, "MEDIA CODEC RELEASING") - _mediaCodec?.stop() - _mediaCodec?.release() - _mediaCodec = null - - // shut down the thread - val quit = _handlerThread?.quitSafely() ?: false - _handlerThread = null - _handler = null - Log.d(TAG, "HANDLER THREAD STOPPED:$quit") - - Log.d(TAG, "CLEANING UP OPERATIONS") + + Log.d(PROCESSING_TAG, "CANCELLING OPERATIONS") _operations.forEach { it.cancel() } _operations.clear() - Log.d(TAG, "CLEARING UP SCOPE") _scope.cancel() + + val mediaCodec = _mediaCodec ?: return + + try { + _isCleaningUp.compareAndSet(expectedValue = false, newValue = true) + if (_codecState == MediaCodecState.EXEC) { + // codec is stopped then switched to stopped state + mediaCodec.stop() + Log.i(TAG, "CODEC STOPPED") + _codecState = MediaCodecState.STOPPED + } + } catch (e: Exception) { + Log.e(TAG, "UNABLE TO STOP CODEC", e) + } finally { + _isCleaningUp.compareAndSet(expectedValue = true, newValue = false) + } + + // release the codec + try { + if (_codecState == MediaCodecState.STOPPED) { + Log.i(TAG, "CODEC CALLBACK CLEANED") + mediaCodec.setCallback(null) + } + mediaCodec.release() + Log.i(TAG, "MEDIA CODEC RELEASED") + } catch (e: Exception) { + Log.e(TAG, "UNABLE TO RELEASE MEDIA CODEC", e) + } finally { + _codecState = MediaCodecState.RELEASED + _mediaCodec = null + } } private suspend fun FloatArray.performRMS(): Float { diff --git a/data/player/src/main/java/com/eva/player/data/reader/MediaCodecState.kt b/data/player/src/main/java/com/eva/player/data/reader/MediaCodecState.kt index 9ac7dd8..bf00e53 100644 --- a/data/player/src/main/java/com/eva/player/data/reader/MediaCodecState.kt +++ b/data/player/src/main/java/com/eva/player/data/reader/MediaCodecState.kt @@ -3,18 +3,19 @@ package com.eva.player.data.reader import android.media.MediaCodec internal enum class MediaCodecState { + /** - * [MediaCodec] stopped, its either started or configured + * [MediaCodec] is not processing anything with configured or stopped or uninitialized */ - STOP, + STOPPED, /** - * [MediaCodec] state executing mediacodec is running + * [MediaCodec] is currently reading/writing the output/input buffers */ EXEC, /** - * [MediaCodec] state released + * [MediaCodec] is not required any more */ - END + RELEASED } \ No newline at end of file diff --git a/data/player/src/main/java/com/eva/player/data/reader/ThreadLifecycleHandler.kt b/data/player/src/main/java/com/eva/player/data/reader/ThreadLifecycleHandler.kt new file mode 100644 index 0000000..8eb4172 --- /dev/null +++ b/data/player/src/main/java/com/eva/player/data/reader/ThreadLifecycleHandler.kt @@ -0,0 +1,141 @@ +package com.eva.player.data.reader + +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.Process +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.time.measureTime + +private const val TAG = "THREAD_CONTROLLER" + +internal class ThreadLifecycleHandler(private val threadName: String) : LifecycleEventObserver { + + @Volatile + private var _handlerThread: HandlerThread? = null + + @Volatile + private var _handler: Handler? = null + + private val _exceptionHandler = Thread.UncaughtExceptionHandler { thread, exc -> + Log.e(TAG, "THREADING ERRORS NAME:${thread.name} STATE:${thread.state}", exc) + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_CREATE) { + Log.d(TAG, "PREPARING THREAD ON CREATE") + createThread() + } else if (event == Lifecycle.Event.ON_DESTROY) { + Log.d(TAG, "STOPPING THREAD ON DESTROY") + stopThread() + source.lifecycle.removeObserver(this) + } + } + + suspend fun bindToLifecycle(lifecycleOwner: LifecycleOwner): Handler { + withContext(Dispatchers.Main) { + lifecycleOwner.lifecycle.addObserver(this@ThreadLifecycleHandler) + } + return readHandler() + } + + @Synchronized + private fun readHandler(): Handler { + if (_handlerThread == null || _handlerThread?.isAlive == false) createThread() + return _handler!! + } + + /** + * Prepares the handler for use + */ + @Suppress("DEPRECATION") + @Synchronized + private fun createThread() { + val newThread = HandlerThread(threadName, Process.THREAD_PRIORITY_AUDIO).apply { + setUncaughtExceptionHandler(_exceptionHandler) + start() + } + val message = buildString { + append("HANDLER THREAD IS SET: ") + append("NAME: ${newThread.name} ") + append("STATE: ${newThread.looper.thread.state} ") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) + append("ID: ${newThread.threadId()} ") + else append("ID: ${newThread.id} ") + + append("PRIORITY :${newThread.priority}") + } + Log.i(TAG, message) + _handlerThread = newThread + _handler = Handler(newThread.looper) + } + + /** + * Stop [_handlerThread] from running anymore + * @param waitTime Time in millis the thread should wait for thread to die + */ + @Synchronized + private fun stopThread(waitTime: Long = 800L) { + val handlerThread = _handlerThread ?: run { + Log.d(TAG, "HANDLER THREAD WAS NOT SET") + return + } + val handler = _handler ?: return + try { + handler.removeCallbacksAndMessages(null) + + val safeRequest = if (handlerThread.isAlive) handlerThread.quitSafely() else false + if (!safeRequest) { + Log.d(TAG, "LOOPER IS NOT SET! OR THREAD IS DEAD") + return + } + Log.i(TAG, "HANDLER THREAD QUIT SAFELY, THREAD STATE: ${handlerThread.state}") + + val duration = measureTime { + if (handlerThread.isAlive) handlerThread.join(waitTime) + } + Log.i(TAG, "WAITED :$duration FOR COMPLETION") + + if (!handlerThread.isAlive) { + Log.d(TAG, "THREAD IS DEAD CURRENT STATE: ${handlerThread.state}") + return + } + + Log.w(TAG, "REQUESTING TO FORCE QUIT") + val requested2 = if (handlerThread.isAlive) handlerThread.quit() else false + if (!requested2) { + Log.i(TAG, "THREAD IS NOT RUNNING STATE IS_ALIVE:${handlerThread.isAlive}") + return + } + + Log.w(TAG, "THREAD IS ACTIVE NEED TO WAIT") + val duration2 = measureTime { + if (handlerThread.isAlive) handlerThread.join(waitTime * 2) + } + Log.w(TAG, "AGAIN WAITED FOR :$duration2") + + // thread killed + if (!handlerThread.isAlive) { + Log.d(TAG, "THREAD SHOULD BE KILLED BY NOW") + return + } + + Log.w(TAG, "THREAD IS STILL ALIVE AFTER QUIT SENDING INTERRUPT") + if (handlerThread.isAlive) handlerThread.interrupt() + if (handlerThread.isAlive) handlerThread.join(200) + + } catch (e: InterruptedException) { + Log.e(TAG, "THREAD JOIN FAILED", e) + } finally { + Log.i(TAG, "AFTER CLEANUP :${_handlerThread?.state} ALIVE: ${_handlerThread?.isAlive}") + _handlerThread?.uncaughtExceptionHandler = null + _handlerThread = null + _handler = null + } + } +} \ No newline at end of file From 9f345f0c414c58b1d9346e7b17e05b9293d6087c Mon Sep 17 00:00:00 2001 From: tuuhin Date: Tue, 4 Nov 2025 22:33:37 +0530 Subject: [PATCH 04/10] Corrected recorder timeline string The recorder timeline was malformatted the seconds field was giving incorrect values corrected that --- .../util/RecorderDrawGraphUtil.kt | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/feature/recorder/src/main/java/com/eva/feature_recorder/util/RecorderDrawGraphUtil.kt b/feature/recorder/src/main/java/com/eva/feature_recorder/util/RecorderDrawGraphUtil.kt index 32bc6bc..f3b52f1 100644 --- a/feature/recorder/src/main/java/com/eva/feature_recorder/util/RecorderDrawGraphUtil.kt +++ b/feature/recorder/src/main/java/com/eva/feature_recorder/util/RecorderDrawGraphUtil.kt @@ -64,22 +64,7 @@ internal fun DrawScope.drawRecorderTimeline( timelineInMillis.forEachIndexed { idx, millis -> val xAxis = spikesWidth * idx.toFloat() if (millis % 2_000 == 0L || idx == 0) { - val readableTime = buildString { - val seconds = (millis / 1_000) % 1_000 - val minutes = (seconds / 60) % 60 - val hours = (minutes % 60) % 60 - if (hours > 0) { - append("$hours".padStart(2, '0')) - append(":") - } - if (minutes >= 0) { - append("$minutes".padStart(2, '0')) - append(":") - } - if (seconds >= 0) { - append("$seconds".padStart(2, '0')) - } - } + val readableTime = toReadableDuration(millis) if (readableTime.isNotBlank()) { val layoutResult = textMeasurer.measure(readableTime, style = textStyle) @@ -152,3 +137,22 @@ internal fun DrawScope.drawRecorderTimeline( } } } + + +private fun toReadableDuration(timeInMillis: Long) = buildString { + val totalSeconds = timeInMillis / 1_000 + val hours = totalSeconds / 3_600 + val minutes = (totalSeconds % 3_600) / 60 + val remainingSeconds = totalSeconds % 60 + if (hours > 0) { + append("$hours".padStart(2, '0')) + append(":") + } + if (minutes >= 0) { + append("$minutes".padStart(2, '0')) + append(":") + } + if (remainingSeconds >= 0) { + append("$remainingSeconds".padStart(2, '0')) + } +} \ No newline at end of file From df40a100113074c3313fbef6279406888520bef1 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Wed, 5 Nov 2025 18:18:26 +0530 Subject: [PATCH 05/10] Fix issue with incomplete pcm data The end of the stream was handled but end of stream cleanup was not called MediaCodec.Callback() onInputBufferAvailable can only queue the buffer not set the _codec state based on the codec buffer info the release state is set, Include locks for batch operations Still the crashing issue is same as the previous time --- .../data/reader/MediaCodecPCMDataDecoder.kt | 123 ++++++++++-------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt b/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt index 7bd7e3a..f9ed230 100644 --- a/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt +++ b/data/player/src/main/java/com/eva/player/data/reader/MediaCodecPCMDataDecoder.kt @@ -27,7 +27,7 @@ import kotlin.concurrent.atomics.plusAssign import kotlin.math.sqrt import kotlin.time.Duration -private const val TAG = "CODEC_CALLBACK" +private const val CODEC_TAG = "CODEC_CALLBACK" private const val PROCESSING_TAG = "CODEC_PROCESSING" @OptIn(ExperimentalAtomicApi::class) @@ -54,32 +54,31 @@ internal class MediaCodecPCMDataDecoder( // need to play with the size to get the optimal results private val batchSize = 50 - private val currentTimeInMs = AtomicLong(0L) + private val _currentTimeInMs = AtomicLong(0L) private val _isBatchProcessing = AtomicBoolean(false) private var _isCleaningUp = AtomicBoolean(false) override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { // clean up is called thus no more processing if (_isCleaningUp.load()) { - Log.i(TAG, "IGNORING READING INPUT CALLBACK") + Log.i(CODEC_TAG, "IGNORING READING INPUT CALLBACK") return } try { // codec state is non exec means work is done return END_OF_STREAM if (_codecState != MediaCodecState.EXEC) { - Log.d(TAG, "WRONG CODEC STATE OR END OF BUFFER") + Log.d(PROCESSING_TAG, "WRONG CODEC STATE") codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) return } - val currentTime = currentTimeInMs.load() // if timeInMs is greater than totalTime+extra return END_OF_STREAM - if (currentTime >= totalTime.inWholeMilliseconds + seekDurationMillis) { - Log.d(TAG, "TOTAL TIME HAS REACHED SENDING END OF STREAM") + if (_currentTimeInMs.load() >= totalTime.inWholeMilliseconds + seekDurationMillis) { + Log.d(PROCESSING_TAG, "TOTAL TIME HAS REACHED SENDING END OF STREAM") codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) return } } catch (e: Exception) { - Log.e(TAG, "UNABLE TO SEND EOS FLAG", e) + Log.e(PROCESSING_TAG, "UNABLE TO SEND EOS FLAG", e) } try { @@ -88,32 +87,30 @@ internal class MediaCodecPCMDataDecoder( val inputBuffer = codec.getInputBuffer(index) ?: return // seek the extractor as we don't need extra data - extractor.seekTo(currentTimeInMs.load() * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC) + extractor.seekTo(_currentTimeInMs.load() * 1_000L, MediaExtractor.SEEK_TO_CLOSEST_SYNC) val sampleSize = extractor.readSampleData(inputBuffer, 0) // sample size is zero thus processing done END_OF_STREAM if (sampleSize <= 0) { - _codecState = MediaCodecState.RELEASED - Log.d(TAG, "END OF INPUT STREAM") codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + Log.d(PROCESSING_TAG, "INPUT BUFFER :END OF INPUT STREAM") return } // advance the extractor to read the next sample if not END_OF_STREAM if (!extractor.advance()) { - Log.d(TAG, "CANNOT ADVANCE EXTRACTOR ANY MORE") - _codecState = MediaCodecState.RELEASED + Log.d(PROCESSING_TAG, "CANNOT ADVANCE EXTRACTOR") + Log.d(PROCESSING_TAG, "INPUT BUFFER :END OF INPUT STREAM") codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) } else if (_codecState == MediaCodecState.EXEC) { // update the current time - currentTimeInMs.plusAssign(seekDurationMillis.toLong()) + _currentTimeInMs.plusAssign(seekDurationMillis.toLong()) codec.queueInputBuffer(index, 0, sampleSize, extractor.sampleTime, 0) } - } catch (e: MediaCodec.CodecException) { - Log.e(TAG, "MEDIA CODEC EXCEPTION ${e.diagnosticInfo} ${e.errorCode} ", e) + Log.e(PROCESSING_TAG, "MEDIA CODEC EXCEPTION ${e.diagnosticInfo} ${e.errorCode} ", e) } catch (e: IllegalStateException) { - Log.e(TAG, "MEDIA CODEC IS NOT IN EXECUTION STATE", e) + Log.e(PROCESSING_TAG, "MEDIA CODEC IS NOT IN EXECUTION STATE", e) } } @@ -123,14 +120,17 @@ internal class MediaCodecPCMDataDecoder( index: Int, info: MediaCodec.BufferInfo ) { + // look is this cleaning up if (_isCleaningUp.load()) { - Log.i(TAG, "IGNORING PROCESSING OUTPUT CALLBACK") + Log.i(CODEC_TAG, "IGNORING PROCESSING OUTPUT CALLBACK") return } + // correct state or not if (_codecState != MediaCodecState.EXEC) { - Log.d(TAG, "WRONG STATE CANNOT PROCESS SHOULD BE EXEC") + Log.d(PROCESSING_TAG, "WRONG STATE SHOULD BE EXEC FOUND :$_codecState") return } + // special case to handle end of stream if (info.isEndOfStream) { codec.stop() _codecState = MediaCodecState.RELEASED @@ -155,7 +155,7 @@ internal class MediaCodecPCMDataDecoder( if (_codecState == MediaCodecState.EXEC) codec.releaseOutputBuffer(index, false) } catch (e: IllegalStateException) { - Log.e(TAG, "WRONG STATE TO HANDLE THE OUTPUT BUFFERS", e) + Log.e(PROCESSING_TAG, "WRONG STATE TO HANDLE THE OUTPUT BUFFERS", e) } } } @@ -163,11 +163,11 @@ internal class MediaCodecPCMDataDecoder( override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { if (e.isRecoverable) codec.stop() - Log.e(TAG, "ERROR HAPPENED", e) + Log.e(CODEC_TAG, "ERROR HAPPENED", e) } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { - Log.d(TAG, "MEDIA FORMAT CHANGED: $format") + Log.d(CODEC_TAG, "MEDIA FORMAT CHANGED: $format") } private fun handleFloatArray(pcm: FloatArray) { @@ -182,22 +182,24 @@ internal class MediaCodecPCMDataDecoder( _scope.launch { try { - if (_operations.size >= batchSize && isActive) { - // batched operation - _isBatchProcessing.compareAndSet(expectedValue = false, newValue = true) - val operations = buildSet { - repeat(batchSize) { - val item = _operations.poll() ?: return@repeat - add(item) + _mutex.withLock { + if (_operations.size >= batchSize && isActive) { + // batched operation + _isBatchProcessing.compareAndSet(expectedValue = false, newValue = true) + val operations = buildSet { + repeat(batchSize) { + val item = _operations.poll() ?: return@repeat + add(item) + } } + Log.i(PROCESSING_TAG, "EVALUATING INFORMATION BATCH :${operations.size}") + val resultAsFloatArray = operations.awaitAll().toFloatArray() + _onBufferDecoded?.invoke(resultAsFloatArray) } - Log.i(PROCESSING_TAG, "EVALUATING INFORMATION BATCH :${operations.size}") - val resultAsFloatArray = operations.awaitAll().toFloatArray() - _onBufferDecoded?.invoke(resultAsFloatArray) } - } catch (_: CancellationException) { - Log.d(PROCESSING_TAG, "CANCELLATION IN BATCH PROCESSING") } catch (e: Exception) { + if (e is CancellationException) + Log.d(PROCESSING_TAG, "CANCELLATION IN BATCH PROCESSING") Log.d(PROCESSING_TAG, "Exception at the end of stream", e) } finally { _isBatchProcessing.store(false) @@ -206,23 +208,21 @@ internal class MediaCodecPCMDataDecoder( } private fun handleEndOfStream() { + Log.d(PROCESSING_TAG, "END OF BUFFER REACHED, AWAITING OPERATIONS") _scope.launch { try { - Log.d(PROCESSING_TAG, "END OF BUFFER REACHED, AWAITING OPERATIONS") - - val leftItems = buildSet { - while (isActive) { - val item = _operations.poll() ?: break - add(item) - } - } - Log.i(PROCESSING_TAG, "EVALUATING INFORMATION END :${leftItems.size}") _mutex.withLock { + val leftItems = buildSet { + while (isActive) { + val item = _operations.poll() ?: break + add(item) + } + } + Log.i(PROCESSING_TAG, "EVALUATING INFORMATION END :${leftItems.size}") if (leftItems.isEmpty()) return@withLock val results = leftItems.awaitAll() val resultAsFloatArray = results.toFloatArray() _onBufferDecoded?.invoke(resultAsFloatArray) - _onDecodeComplete?.invoke() } } catch (e: Exception) { if (e is CancellationException) { @@ -230,6 +230,9 @@ internal class MediaCodecPCMDataDecoder( return@launch } Log.d(PROCESSING_TAG, "Exception at the end of stream", e) + } finally { + // stream read ended + _onDecodeComplete?.invoke() } } } @@ -242,28 +245,31 @@ internal class MediaCodecPCMDataDecoder( _onDecodeComplete = listener } + @Synchronized fun initiateCodec(format: MediaFormat, mimeType: String, handler: Handler) { _isCleaningUp.store(false) _isBatchProcessing.store(false) - currentTimeInMs.store(0) + _currentTimeInMs.store(0) _mediaCodec?.reset() _mediaCodec = MediaCodec.createDecoderByType(mimeType).apply { setCallback(this@MediaCodecPCMDataDecoder, handler) configure(format, null, null, 0) } - Log.i(TAG, "MEDIA CODEC CREATED IN THREAD:${handler.looper.thread.name}") + Log.i(CODEC_TAG, "MEDIA CODEC CREATED IN THREAD:${handler.looper.thread.name}") _mediaCodec?.start() _codecState = MediaCodecState.EXEC - Log.i(TAG, "MEDIA CODEC STARTED") + Log.i(CODEC_TAG, "MEDIA CODEC STARTED") } + @Synchronized fun cleanUp() { - - Log.d(PROCESSING_TAG, "CANCELLING OPERATIONS") - _operations.forEach { it.cancel() } - _operations.clear() - + if (_operations.isNotEmpty()) { + Log.d(PROCESSING_TAG, "CANCELLING DEFERRED CALCULATIONS") + _operations.forEach { it.cancel() } + _operations.clear() + } + Log.d(PROCESSING_TAG, "CANCELLING OPERATIONS SCOPE") _scope.cancel() val mediaCodec = _mediaCodec ?: return @@ -271,13 +277,16 @@ internal class MediaCodecPCMDataDecoder( try { _isCleaningUp.compareAndSet(expectedValue = false, newValue = true) if (_codecState == MediaCodecState.EXEC) { + // flush all the buffers + mediaCodec.flush() // codec is stopped then switched to stopped state mediaCodec.stop() - Log.i(TAG, "CODEC STOPPED") + Thread.sleep(10) + Log.i(CODEC_TAG, "CODEC STOPPED") _codecState = MediaCodecState.STOPPED } } catch (e: Exception) { - Log.e(TAG, "UNABLE TO STOP CODEC", e) + Log.e(CODEC_TAG, "UNABLE TO STOP CODEC", e) } finally { _isCleaningUp.compareAndSet(expectedValue = true, newValue = false) } @@ -285,13 +294,13 @@ internal class MediaCodecPCMDataDecoder( // release the codec try { if (_codecState == MediaCodecState.STOPPED) { - Log.i(TAG, "CODEC CALLBACK CLEANED") + Log.i(CODEC_TAG, "CODEC CALLBACK CLEANED") mediaCodec.setCallback(null) } mediaCodec.release() - Log.i(TAG, "MEDIA CODEC RELEASED") + Log.i(CODEC_TAG, "MEDIA CODEC RELEASED") } catch (e: Exception) { - Log.e(TAG, "UNABLE TO RELEASE MEDIA CODEC", e) + Log.e(CODEC_TAG, "UNABLE TO RELEASE MEDIA CODEC", e) } finally { _codecState = MediaCodecState.RELEASED _mediaCodec = null From 33ca1df999f7928842f6334179e2b4c235f242fd Mon Sep 17 00:00:00 2001 From: tuuhin Date: Fri, 7 Nov 2025 15:38:28 +0530 Subject: [PATCH 06/10] Moved visualization logic to new module Removed Visualizer from PlayerControllerModule.kt In AudioVisualizerImpl.kt, the ThreadController.kt is provided via constructor injection, also included lock in releaseObjects MediaCodecPCMDataDecoder.kt handler need to be passed via constructor then later it need to be initiated ,If we are using custom handler then posting cleanup logic in the specified thread this again reduce the no of crashes ThreadController.kt the interface for threading logic, in its impl the core logic is same we prepare the thread an on lifecycle destroyed the handler thread quits and cancels In PlayerVisualizerViewmodel.kt removed the viewmodel onclear super call its not needed --- .idea/gradle.xml | 1 + .../data/reader/ThreadLifecycleHandler.kt | 141 ------------------ .../eva/player/di/PlayerControllerModule.kt | 6 - data/visualizer/.gitignore | 1 + data/visualizer/build.gradle.kts | 12 ++ data/visualizer/consumer-rules.pro | 0 data/visualizer/proguard-rules.pro | 21 +++ .../visualizer/data}/AudioVisualizerImpl.kt | 72 +++++---- .../com/com/visualizer/data}/ByteBufferExt.kt | 2 +- .../com/com/visualizer/data}/FloatArrayExt.kt | 8 +- .../data}/MediaCodecPCMDataDecoder.kt | 46 ++++-- .../com/visualizer/data}/MediaCodecState.kt | 2 +- .../com/visualizer/data}/MediaFormatExt.kt | 2 +- .../data/ThreadLifecycleControllerImpl.kt | 119 +++++++++++++++ .../visualizer/di/ThreadControllerModule.kt | 18 +++ .../com/com/visualizer/di/VisualizerModule.kt | 24 +++ .../com/visualizer}/domain/AudioVisualizer.kt | 2 +- .../com/visualizer/domain/ThreadController.kt | 10 ++ .../exception}/DecoderExistsException.kt | 2 +- .../ExtractorNoTrackFoundException.kt | 2 +- .../exception}/InvalidMimeTypeException.kt | 2 +- feature/player-shared/build.gradle.kts | 1 + .../PlayerVisualizerViewmodel.kt | 7 +- settings.gradle.kts | 1 + 24 files changed, 297 insertions(+), 205 deletions(-) delete mode 100644 data/player/src/main/java/com/eva/player/data/reader/ThreadLifecycleHandler.kt create mode 100644 data/visualizer/.gitignore create mode 100644 data/visualizer/build.gradle.kts create mode 100644 data/visualizer/consumer-rules.pro create mode 100644 data/visualizer/proguard-rules.pro rename data/{player/src/main/java/com/eva/player/data/reader => visualizer/src/main/java/com/com/visualizer/data}/AudioVisualizerImpl.kt (65%) rename data/{player/src/main/java/com/eva/player/data/reader => visualizer/src/main/java/com/com/visualizer/data}/ByteBufferExt.kt (97%) rename data/{player/src/main/java/com/eva/player/data/reader => visualizer/src/main/java/com/com/visualizer/data}/FloatArrayExt.kt (90%) rename data/{player/src/main/java/com/eva/player/data/reader => visualizer/src/main/java/com/com/visualizer/data}/MediaCodecPCMDataDecoder.kt (90%) rename data/{player/src/main/java/com/eva/player/data/reader => visualizer/src/main/java/com/com/visualizer/data}/MediaCodecState.kt (90%) rename data/{player/src/main/java/com/eva/player/data/reader => visualizer/src/main/java/com/com/visualizer/data}/MediaFormatExt.kt (96%) create mode 100644 data/visualizer/src/main/java/com/com/visualizer/data/ThreadLifecycleControllerImpl.kt create mode 100644 data/visualizer/src/main/java/com/com/visualizer/di/ThreadControllerModule.kt create mode 100644 data/visualizer/src/main/java/com/com/visualizer/di/VisualizerModule.kt rename data/{player/src/main/java/com/eva/player => visualizer/src/main/java/com/com/visualizer}/domain/AudioVisualizer.kt (94%) create mode 100644 data/visualizer/src/main/java/com/com/visualizer/domain/ThreadController.kt rename data/{player/src/main/java/com/eva/player/domain/exceptions => visualizer/src/main/java/com/com/visualizer/domain/exception}/DecoderExistsException.kt (68%) rename data/{player/src/main/java/com/eva/player/domain/exceptions => visualizer/src/main/java/com/com/visualizer/domain/exception}/ExtractorNoTrackFoundException.kt (67%) rename data/{player/src/main/java/com/eva/player/domain/exceptions => visualizer/src/main/java/com/com/visualizer/domain/exception}/InvalidMimeTypeException.kt (66%) diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c02e183..ca70ccb 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -38,6 +38,7 @@