diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa268f28d..a7941b863 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,8 +67,8 @@ maps-compose = "6.12.2" material = "1.14.0-alpha07" material3-adaptive = "1.2.0" material3-adaptive-navigation-suite = "1.4.0" -media3 = "1.8.0" -media3Ui = "1.8.0" +media3 = "1.9.0-alpha01" +media3Ui = "1.9.0-alpha01" # @keep minSdk = "36" okHttp = "5.3.2" @@ -163,6 +163,7 @@ androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-view androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } +androidx-media3-inspector = { module = "androidx.media3:media3-inspector", version.ref = "media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } diff --git a/misc/build.gradle.kts b/misc/build.gradle.kts index 6a2dc60d1..59f916b1d 100644 --- a/misc/build.gradle.kts +++ b/misc/build.gradle.kts @@ -60,11 +60,13 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.media3.common) implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.inspector) implementation(libs.androidx.tracing) implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.guava) ksp(libs.hilt.compiler) implementation(libs.androidx.constraintlayout) diff --git a/misc/src/main/java/com/example/snippets/InspectorModuleJavaSnippets.java b/misc/src/main/java/com/example/snippets/InspectorModuleJavaSnippets.java new file mode 100644 index 000000000..328b03fbb --- /dev/null +++ b/misc/src/main/java/com/example/snippets/InspectorModuleJavaSnippets.java @@ -0,0 +1,235 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * 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 + * + * https://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.example.snippets; + +import static android.media.MediaMetadataRetriever.METADATA_KEY_MIMETYPE; +import static android.media.MediaMetadataRetriever.OPTION_CLOSEST; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Timeline; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.MediaExtractorCompat; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.inspector.FrameExtractor; +import androidx.media3.inspector.MetadataRetriever; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import kotlin.Suppress; + +@Suppress(names = "unused_parameter") +@OptIn(markerClass = UnstableApi.class) +public class InspectorModuleJavaSnippets { + private final String TAG = "InspectorModuleLog"; + + // [START android_dev_retriever_media3_java] + public void retrieveMetadata(Context context, MediaItem mediaItem) { + try (MetadataRetriever retriever = new MetadataRetriever.Builder(context, mediaItem).build()) { + ListenableFuture trackGroupsFuture = retriever.retrieveTrackGroups(); + ListenableFuture timelineFuture = retriever.retrieveTimeline(); + ListenableFuture durationUsFuture = retriever.retrieveDurationUs(); + + ListenableFuture> allFutures = Futures.allAsList(trackGroupsFuture, timelineFuture, durationUsFuture); + Executor executor = Executors.newSingleThreadExecutor(); + Futures.addCallback(allFutures, new FutureCallback<>() { + @Override + public void onSuccess(List result) { + handleMetadata( + Futures.getUnchecked(trackGroupsFuture), + Futures.getUnchecked(timelineFuture), + Futures.getUnchecked(durationUsFuture) + ); + } + + @Override + public void onFailure(@NonNull Throwable t) { + handleFailure(t); + } + }, executor); + } + } + // [END android_dev_retriever_media3_java] + + // [START android_migration_retriever_platform_java] + public void retrieveMetadataPlatform(String mediaPath) { + try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) { + retriever.setDataSource(mediaPath); + String mimeType = retriever.extractMetadata(METADATA_KEY_MIMETYPE); + Log.d(TAG, "MIME type: " + mimeType); + retriever.release(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + // [END android_migration_retriever_platform_java] + + // [START android_migration_retriever_media3_java] + public void retrieveMetadataMedia3(Context context, MediaItem mediaItem) { + try (MetadataRetriever retriever = new MetadataRetriever.Builder(context, mediaItem).build()) { + ListenableFuture trackGroupsFuture = retriever.retrieveTrackGroups(); + + Executor executor = Executors.newSingleThreadExecutor(); + Futures.addCallback(trackGroupsFuture, new FutureCallback() { + @Override + public void onSuccess(Object trackGroupsObject) { + TrackGroupArray trackGroups = (TrackGroupArray) trackGroupsObject; + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups.get(i); + for (int j = 0; j < trackGroup.length; j++) { + Format format = trackGroup.getFormat(j); + String mimeType = format.containerMimeType; + Log.d(TAG, "MIME type: " + mimeType); + } + } + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.e(TAG, "Error retrieving metadata: " + t.getMessage()); + } + }, executor); + } + } + // [END android_migration_retriever_media3_java] + + // [START android_dev_frame_media3_java] + public void extractFrame(Context context, MediaItem mediaItem) { + try (FrameExtractor frameExtractor = new FrameExtractor.Builder(context, mediaItem).build()) { + ListenableFuture frameFuture = frameExtractor.getFrame(5000L); + + Executor executor = Executors.newSingleThreadExecutor(); + Futures.addCallback(frameFuture, new FutureCallback() { + @Override + public void onSuccess(Object frameObject) { + FrameExtractor.Frame frame = (FrameExtractor.Frame) frameObject; + long presentationTimeMs = frame.presentationTimeMs; + Log.d(TAG, "Extracted frame at " + presentationTimeMs); + } + + @Override + public void onFailure(@NonNull Throwable t) { + handleFailure(t); + } + }, executor); + } + } + // [END android_dev_frame_media3_java] + + // [START android_migration_frame_platform_java] + public Bitmap extractFramePlatform(String mediaPath, Long frameTimeMs) { + Bitmap bitmap; + try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) { + retriever.setDataSource(mediaPath); + bitmap = retriever.getFrameAtTime(frameTimeMs * 1000L, // Time is in microseconds + OPTION_CLOSEST); + Log.d(TAG, "Extracted frame " + bitmap); + retriever.release(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return bitmap; + } + // [END android_migration_frame_platform_java] + + // [START android_migration_frame_media3_java] + public void extractFrameMedia3(Context context, MediaItem mediaItem, Long frameTimeMs) { + try (FrameExtractor frameExtractor = new FrameExtractor.Builder(context, mediaItem).build()) { + ListenableFuture frameFuture = frameExtractor.getFrame(frameTimeMs); + + Executor executor = Executors.newSingleThreadExecutor(); + Futures.addCallback(frameFuture, new FutureCallback() { + @Override + public void onSuccess(Object frameObject) { + FrameExtractor.Frame frame = (FrameExtractor.Frame) frameObject; + long presentationTimeMs = frame.presentationTimeMs; + Log.d(TAG, "Extracted frame at " + presentationTimeMs); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.e(TAG, "Error extracting frame: " + t.getMessage()); + } + }, executor); + } + } + // [END android_migration_frame_media3_java] + + // [START android_dev_extractor_media3_java] + public void extractSamples(Context context, String mediaPath) { + MediaExtractorCompat extractor = new MediaExtractorCompat(context); + try { + // 1. Setup the extractor + extractor.setDataSource(mediaPath); + + // Find and select available tracks + for (int i = 0; i < extractor.getTrackCount(); i++) { + MediaFormat format = extractor.getTrackFormat(i); + extractor.selectTrack(i); + } + + // 2. Process samples + ByteBuffer buffer = ByteBuffer.allocate(10 * 1024 * 1024); + while (true) { + // Read an encoded sample into the buffer. + int bytesRead = extractor.readSampleData(buffer, 0); + if (bytesRead < 0) break; + + // Access sample metadata + int trackIndex = extractor.getSampleTrackIndex(); + Long presentationTimeUs = extractor.getSampleTime(); + Long sampleSize = extractor.getSampleSize(); + + extractor.advance(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + // 3. Release the extractor + extractor.release(); + } + } + // [END android_dev_extractor_media3_java] + + private void handleMetadata(TrackGroupArray trackGroups, Timeline timeline, Long durationUs) { + Log.d(TAG, "TrackGroups: " + trackGroups); + Log.d(TAG, "Timeline: " + timeline); + Log.d(TAG, "Duration: " + durationUs); + } + + private void handleFailure(@NonNull Throwable t) { + Log.e(TAG, "Error retrieving metadata: " + t.getMessage()); + } + +} diff --git a/misc/src/main/java/com/example/snippets/InspectorModuleKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/InspectorModuleKotlinSnippets.kt new file mode 100644 index 000000000..2204ff955 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/InspectorModuleKotlinSnippets.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * 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 + * + * https://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.example.snippets + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.media.MediaMetadataRetriever.METADATA_KEY_MIMETYPE +import android.media.MediaMetadataRetriever.OPTION_CLOSEST +import android.util.Log +import androidx.annotation.OptIn +import androidx.media3.common.MediaItem +import androidx.media3.common.Timeline +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.MediaExtractorCompat +import androidx.media3.exoplayer.source.TrackGroupArray +import androidx.media3.inspector.FrameExtractor +import androidx.media3.inspector.MetadataRetriever +import java.io.IOException +import java.nio.ByteBuffer +import kotlinx.coroutines.guava.await + +const val TAG = "InspectorModuleLog" + +@Suppress("unused_parameter") +@OptIn(UnstableApi::class) +class InspectorModuleKotlinSnippets { + + // [START android_dev_retriever_media3_kotlin] + suspend fun retrieveMetadata(context: Context, mediaItem: MediaItem) { + try { + // 1. Build the retriever and open a .use block. + // This automatically calls close() when the block finishes. + MetadataRetriever.Builder(context, mediaItem).build().use { retriever -> + // 2. Retrieve metadata asynchronously. + val trackGroups = retriever.retrieveTrackGroups().await() + val timeline = retriever.retrieveTimeline().await() + val durationUs = retriever.retrieveDurationUs().await() + handleMetadata(trackGroups, timeline, durationUs) + } + } catch (e: Exception) { + throw RuntimeException(e) + } + } + // [END android_dev_retriever_media3_kotlin] + + // [START android_migration_retriever_platform_kotlin] + fun retrieveMetadataPlatform(mediaPath: String) { + val retriever = MediaMetadataRetriever() + retriever.setDataSource(mediaPath) + try { + val mimeType = retriever.extractMetadata(METADATA_KEY_MIMETYPE) + Log.d(TAG, "MIME type: $mimeType") + } catch (e: Exception) { + throw RuntimeException(e) + } finally { + retriever.release() + } + } + // [END android_migration_retriever_platform_kotlin] + + // [START android_migration_retriever_media3_kotlin] + suspend fun retrieveMetadataMedia3(context: Context, mediaItem: MediaItem) { + try { + MetadataRetriever.Builder(context, mediaItem).build().use { retriever -> + val trackGroups = retriever.retrieveTrackGroups().await() + + for (i in 0 until trackGroups.length) { + val trackGroup = trackGroups.get(i) + for (j in 0 until trackGroup.length) { + val format = trackGroup.getFormat(j) + val mimeType = format.containerMimeType + Log.d(TAG, "MIME type: $mimeType") + } + } + } + } catch (e: Exception) { + throw RuntimeException(e) + } + } + // [END android_migration_retriever_media3_kotlin] + + // [START android_dev_frame_media3_kotlin] + suspend fun extractFrame(context: Context, mediaItem: MediaItem): Bitmap? { + return try { + // 1. Build the frame extractor and open a .use block. + // This automatically calls close() when the block finishes. + FrameExtractor.Builder(context, mediaItem).build().use { extractor -> + // 2. Extract the specific frame at the 5000ms (5-second) mark + val frame = extractor.getFrame(5000L).await() + Log.d(TAG, "Extracted frame at ${frame.presentationTimeMs} ms") + frame.bitmap + } + } catch (e: Exception) { + Log.e(TAG, "Exception: $e") + null + } + } + // [END android_dev_frame_media3_kotlin] + + // [START android_migration_frame_platform_kotlin] + fun extractFramePlatform(mediaPath: String, frameTimeMs: Long): Bitmap? { + var retriever: MediaMetadataRetriever? = null + val bitmap: Bitmap? + try { + retriever = MediaMetadataRetriever() + retriever.setDataSource(mediaPath) + + bitmap = retriever.getFrameAtTime( + frameTimeMs * 1000L, // Time is in microseconds + OPTION_CLOSEST + ) + Log.d(TAG, "Extracted frame : $bitmap") + } catch (e: Exception) { + throw RuntimeException(e) + } finally { + retriever?.release() + } + return bitmap + } + // [END android_migration_frame_platform_kotlin] + + // [START android_migration_frame_media3_kotlin] + suspend fun extractFrameMedia3( + context: Context, + mediaItem: MediaItem, + frameTimeMs: Long + ): Bitmap? { + return try { + FrameExtractor.Builder(context, mediaItem).build().use { extractor -> + val frame = extractor.getFrame(frameTimeMs).await() + Log.d(TAG, "Extracted frame at ${frame.presentationTimeMs} ms") + frame.bitmap + } + } catch (e: Exception) { + Log.e(TAG, "Exception: $e") + null + } + } + // [END android_migration_frame_media3_kotlin] + + // [START android_dev_extractor_media3_kotlin] + fun extractSamples(context: Context, mediaPath: String) { + val extractor = MediaExtractorCompat(context) + try { + // 1. Setup the extractor + extractor.setDataSource(mediaPath) + + // Find and select available tracks + for (i in 0 until extractor.trackCount) { + val format = extractor.getTrackFormat(i) + extractor.selectTrack(i) + } + + // 2. Process samples + val buffer = ByteBuffer.allocate(10 * 1024 * 1024) + while (true) { + // Read an encoded sample into the buffer. + val bytesRead = extractor.readSampleData(buffer, 0) + if (bytesRead < 0) break + + // Access sample metadata + val trackIndex = extractor.sampleTrackIndex + val presentationTimeUs: Long = extractor.sampleTime + val sampleSize: Long = extractor.sampleSize + + extractor.advance() + } + } catch (e: IOException) { + throw RuntimeException(e) + } finally { + // 3. Release the extractor + extractor.release() + } + } + // [END android_dev_extractor_media3_kotlin] + + private fun handleMetadata(trackGroups: TrackGroupArray, timeline: Timeline, durationUs: Long) { + Log.d(TAG, "TrackGroups: $trackGroups us") + Log.d(TAG, "Timeline: $timeline us") + Log.d(TAG, "Duration: $durationUs us") + } +} diff --git a/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt index 9a67db280..fb2c38d77 100644 --- a/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt +++ b/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt @@ -37,7 +37,7 @@ class MyTargetPreloadStatusControl( currentPlayingIndex: Int = C.INDEX_UNSET ) : TargetPreloadStatusControl { - override fun getTargetPreloadStatus(index: Int): DefaultPreloadManager.PreloadStatus? { + override fun getTargetPreloadStatus(index: Int): DefaultPreloadManager.PreloadStatus { if (index - currentPlayingIndex == 1) { // next track // return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and // suggest loading 3000ms from the default start position @@ -48,12 +48,12 @@ class MyTargetPreloadStatusControl( return DefaultPreloadManager.PreloadStatus.specifiedRangeLoaded(3000L) } else if (abs(index - currentPlayingIndex) == 2) { // return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED - return DefaultPreloadManager.PreloadStatus.TRACKS_SELECTED + return DefaultPreloadManager.PreloadStatus.PRELOAD_STATUS_TRACKS_SELECTED } else if (abs(index - currentPlayingIndex) <= 4) { // return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED - return DefaultPreloadManager.PreloadStatus.SOURCE_PREPARED + return DefaultPreloadManager.PreloadStatus.PRELOAD_STATUS_SOURCE_PREPARED } - return null + return DefaultPreloadManager.PreloadStatus.PRELOAD_STATUS_NOT_PRELOADED } } // [END android_defaultpreloadmanager_MyTargetPreloadStatusControl]