diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 493337eca..ae6198072 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,12 @@ - + = 2; this.sampleRate = audioInfo.getSampleRate(); - this.duration = audioInfo.getSampleRate(); + this.duration = audioInfo.getDuration(); fixBuffer(); return true; } catch (Exception e) { diff --git a/extra-sources/build.gradle.kts b/extra-sources/build.gradle.kts index 660b63501..dfa4de4da 100644 --- a/extra-sources/build.gradle.kts +++ b/extra-sources/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.bundles.androidx.camera) implementation(libs.uvcandroid) + implementation(libs.androidx.media3.exoplayer) testImplementation(libs.junit) api(project(":encoder")) } \ No newline at end of file diff --git a/extra-sources/src/main/java/com/pedro/extrasources/extractor/Media3Extractor.kt b/extra-sources/src/main/java/com/pedro/extrasources/extractor/Media3Extractor.kt new file mode 100644 index 000000000..9b2d7c30c --- /dev/null +++ b/extra-sources/src/main/java/com/pedro/extrasources/extractor/Media3Extractor.kt @@ -0,0 +1,162 @@ +/* + * + * * Copyright (C) 2024 pedroSG94. + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package com.pedro.extrasources.extractor + +import android.annotation.SuppressLint +import android.content.Context +import android.media.MediaFormat +import android.media.MediaMetadataRetriever +import android.net.Uri +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.media3.exoplayer.MediaExtractorCompat +import com.pedro.common.frame.MediaFrame +import com.pedro.common.getIntegerSafe +import com.pedro.common.validMessage +import com.pedro.encoder.input.decoder.AudioInfo +import com.pedro.encoder.input.decoder.Extractor +import com.pedro.encoder.input.decoder.VideoInfo +import java.io.File +import java.io.FileDescriptor +import java.io.IOException +import java.nio.ByteBuffer + +/** + * Created by pedro on 30/10/24. + * + * Extractor implementation using media3 library. + * + * Note: + * Using this implementation the library can't extract the duration so we are using MediaMetadataRetriever to do it. + */ +@SuppressLint("UnsafeOptInUsageError") +class Media3Extractor(private val context: Context): Extractor { + + private var mediaExtractor = MediaExtractorCompat(context) + private var sleepTime: Long = 0 + private var accumulativeTs: Long = 0 + @Volatile + private var lastExtractorTs: Long = 0 + private var format: MediaFormat? = null + private var duration = -1L + + override fun selectTrack(type: MediaFrame.Type): String { + return when (type) { + MediaFrame.Type.VIDEO -> selectTrack("video/") + MediaFrame.Type.AUDIO -> selectTrack("audio/") + } + } + + override fun initialize(path: String) { + initialize(context, path.toUri()) + } + + override fun initialize(context: Context, uri: Uri) { + try { + reset() + val metadata = MediaMetadataRetriever() + metadata.setDataSource(context, uri) + val duration = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + this.duration = duration?.toLongOrNull()?.times(1000) ?: 0 + mediaExtractor = MediaExtractorCompat(context) + mediaExtractor.setDataSource(uri, 0) + } catch (e: Exception) { + throw IOException(e.validMessage()) + } + } + + override fun initialize(fileDescriptor: FileDescriptor) { + try { + val file = File(fileDescriptor.toString()) + val uri = FileProvider.getUriForFile(context, context.packageName + ".fileprovider", file) + initialize(context, uri) + } catch (e: Exception) { + throw IOException(e.validMessage()) + } + } + + override fun readFrame(buffer: ByteBuffer): Int { + return mediaExtractor.readSampleData(buffer, 0) + } + + override fun advance(): Boolean { + return mediaExtractor.advance() + } + + override fun getTimeStamp(): Long { + return mediaExtractor.sampleTime + } + + override fun getSleepTime(ts: Long): Long { + val extractorTs = getTimeStamp() + accumulativeTs += extractorTs - lastExtractorTs + lastExtractorTs = getTimeStamp() + sleepTime = if (accumulativeTs > ts) (accumulativeTs - ts) / 1000 else 0 + return sleepTime + } + + override fun seekTo(time: Long) { + mediaExtractor.seekTo(time, MediaExtractorCompat.SEEK_TO_PREVIOUS_SYNC) + lastExtractorTs = getTimeStamp() + } + + override fun release() { + mediaExtractor.release() + } + + override fun getVideoInfo(): VideoInfo { + val format = this.format ?: throw IOException("Extractor track not selected") + val width = format.getIntegerSafe(MediaFormat.KEY_WIDTH) ?: throw IOException("Width info is required") + val height = format.getIntegerSafe(MediaFormat.KEY_HEIGHT) ?: throw IOException("Height info is required") + val fps = format.getIntegerSafe(MediaFormat.KEY_FRAME_RATE) ?: 30 + return VideoInfo(width, height, fps, duration) + } + + override fun getAudioInfo(): AudioInfo { + val format = this.format ?: throw IOException("Extractor track not selected") + val sampleRate = format.getIntegerSafe(MediaFormat.KEY_SAMPLE_RATE) ?: throw IOException("Channels info is required") + val channels = format.getIntegerSafe(MediaFormat.KEY_CHANNEL_COUNT) ?: throw IOException("SampleRate info is required") + return AudioInfo(sampleRate, channels, duration) + } + + override fun getFormat(): MediaFormat { + return format ?: throw IOException("Extractor track not selected") + } + + private fun selectTrack(type: String): String { + for (i in 0 until mediaExtractor.trackCount) { + val format = mediaExtractor.getTrackFormat(i) + val mime = format.getString(MediaFormat.KEY_MIME) ?: continue + if (mime.startsWith(type, ignoreCase = true)) { + mediaExtractor.selectTrack(i) + this.format = format + return mime + } + } + throw IOException("track not found") + } + + private fun reset() { + duration = -1 + sleepTime = 0 + accumulativeTs = 0 + lastExtractorTs = 0 + format = null + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 85fd97b2b..5f88cb104 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ junit = "4.13.2" mockito = "5.4.0" ktor = "3.0.0" uvcandroid = "1.0.7" +media3 = "1.4.1" [libraries] androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -36,6 +37,7 @@ ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" } ktor-network-tls = { module = "io.ktor:ktor-network-tls", version.ref = "ktor" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito" } uvcandroid = { module = "com.herohan:UVCAndroid", version.ref = "uvcandroid" } +androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }