Commit fc3c57ec by Rakesh Kumar Committed by GitHub

Merge branch 'main' into rtp-h263

parents dfc424db a105d033
Showing with 1831 additions and 928 deletions
......@@ -17,7 +17,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
}
......
......@@ -29,5 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
testOptions {
unitTests.all {
jvmArgs "-Xmx2g"
}
unitTests.includeAndroidResources true
}
}
......@@ -26,7 +26,7 @@ project.ext {
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
guavaVersion = '31.0.1-android'
mockitoVersion = '3.12.4'
robolectricVersion = '4.8-alpha-1'
robolectricVersion = '4.8.1'
// Keep this in sync with Google's internal Checker Framework version.
checkerframeworkVersion = '3.13.0'
checkerframeworkCompatVersion = '2.5.5'
......
......@@ -230,8 +230,8 @@ public class MainActivity extends AppCompatActivity
@Override
public boolean onMove(
RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) {
int fromPosition = origin.getAdapterPosition();
int toPosition = target.getAdapterPosition();
int fromPosition = origin.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
if (draggingFromPosition == C.INDEX_UNSET) {
// A drag has started, but changes to the media queue will be reflected in clearView().
draggingFromPosition = fromPosition;
......@@ -243,7 +243,7 @@ public class MainActivity extends AppCompatActivity
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
int position = viewHolder.getBindingAdapterPosition();
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
if (playerManager.removeItem(queueItemHolder.item)) {
mediaQueueListAdapter.notifyItemRemoved(position);
......@@ -282,7 +282,7 @@ public class MainActivity extends AppCompatActivity
@Override
public void onClick(View v) {
playerManager.selectQueueItem(getAdapterPosition());
playerManager.selectQueueItem(getBindingAdapterPosition());
}
}
......
......@@ -29,6 +29,7 @@ import android.opengl.GLUtils;
import androidx.media3.common.C;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import java.io.IOException;
import java.util.Locale;
import javax.microedition.khronos.opengles.GL10;
......@@ -41,6 +42,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ final class BitmapOverlayVideoProcessor
implements VideoProcessingGLSurfaceView.VideoProcessor {
private static final String TAG = "BitmapOverlayVP";
private static final int OVERLAY_WIDTH = 512;
private static final int OVERLAY_HEIGHT = 256;
......@@ -85,11 +87,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to initialize the shader program", e);
return;
}
program.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
program.setBufferAttribute(
"aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aTexCoords",
GlUtil.getTextureCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
......@@ -115,7 +124,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLUtils.texSubImage2D(
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
GlUtil.checkGlError();
try {
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to populate the texture", e);
}
// Run the shader program.
GlProgram program = checkNotNull(this.program);
......@@ -124,16 +137,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
program.setFloatUniform("uScaleX", bitmapScaleX);
program.setFloatUniform("uScaleY", bitmapScaleY);
program.setFloatsUniform("uTexTransform", transformMatrix);
program.bindAttributesAndUniforms();
try {
program.bindAttributesAndUniforms();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to update the shader program", e);
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
try {
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to draw a frame", e);
}
}
@Override
public void release() {
if (program != null) {
program.delete();
try {
program.delete();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to delete the shader program", e);
}
}
}
}
......@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
......@@ -156,13 +157,18 @@ public final class MainActivity extends Activity {
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
@Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
@C.ContentType
int type =
TextUtils.isEmpty(fileExtension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(fileExtension);
if (type == C.CONTENT_TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
} else if (type == C.CONTENT_TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
......
......@@ -28,6 +28,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.TimedValueQueue;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
......@@ -70,6 +71,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
}
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
private static final String TAG = "VPGlSurfaceView";
private final VideoRenderer renderer;
private final Handler mainHandler;
......@@ -239,7 +241,11 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
@Override
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
texture = GlUtil.createExternalTexture();
try {
texture = GlUtil.createExternalTexture();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to create an external texture", e);
}
surfaceTexture = new SurfaceTexture(texture);
surfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> {
......
......@@ -79,6 +79,12 @@
<data android:scheme="ssai"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.main.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.main.action.VIEW_LIST"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
......
......@@ -177,7 +177,7 @@ public class IntentUtil {
headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
}
}
@Nullable UUID drmUuid = Util.getDrmUuid(Util.castNonNull(drmSchemeExtra));
@Nullable UUID drmUuid = Util.getDrmUuid(drmSchemeExtra);
if (drmUuid != null) {
builder.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(drmUuid)
......@@ -188,7 +188,7 @@ public class IntentUtil {
intent.getBooleanExtra(
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
.setLicenseRequestHeaders(headers)
.forceSessionsForAudioAndVideoTracks(
.setForceSessionsForAudioAndVideoTracks(
intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false))
.build());
}
......
......@@ -38,10 +38,12 @@ import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
import androidx.media3.exoplayer.ima.ImaAdsLoader;
import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource;
......@@ -53,7 +55,6 @@ import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.exoplayer.util.EventLogger;
import androidx.media3.ui.PlayerControlView;
import androidx.media3.ui.PlayerView;
import java.util.ArrayList;
import java.util.Collections;
......@@ -62,7 +63,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An activity that plays media using {@link ExoPlayer}. */
public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlayerControlView.VisibilityListener {
implements OnClickListener, PlayerView.ControllerVisibilityListener {
// Saved instance state keys.
......@@ -91,7 +92,12 @@ public class PlayerActivity extends AppCompatActivity
// For ad playback only.
@Nullable private AdsLoader clientSideAdsLoader;
// TODO: Annotate this and serverSideAdsLoaderState below with @OptIn when it can be applied to
// fields (needs http://r.android.com/2004032 to be released into a version of
// androidx.annotation:annotation-experimental).
@Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader;
private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State
serverSideAdsLoaderState;
......@@ -115,17 +121,12 @@ public class PlayerActivity extends AppCompatActivity
if (savedInstanceState != null) {
trackSelectionParameters =
TrackSelectionParameters.CREATOR.fromBundle(
TrackSelectionParameters.fromBundle(
savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS));
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX);
startPosition = savedInstanceState.getLong(KEY_POSITION);
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
if (adsLoaderStateBundle != null) {
serverSideAdsLoaderState =
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
adsLoaderStateBundle);
}
restoreServerSideAdsLoaderState(savedInstanceState);
} else {
trackSelectionParameters = new TrackSelectionParameters.Builder(/* context= */ this).build();
clearStartPosition();
......@@ -217,9 +218,7 @@ public class PlayerActivity extends AppCompatActivity
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
outState.putInt(KEY_ITEM_INDEX, startItemIndex);
outState.putLong(KEY_POSITION, startPosition);
if (serverSideAdsLoaderState != null) {
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
}
saveServerSideAdsLoaderState(outState);
}
// Activity input
......@@ -246,10 +245,10 @@ public class PlayerActivity extends AppCompatActivity
}
}
// PlayerControlView.VisibilityListener implementation
// PlayerView.ControllerVisibilityListener implementation
@Override
public void onVisibilityChange(int visibility) {
public void onVisibilityChanged(int visibility) {
debugRootView.setVisibility(visibility);
}
......@@ -271,24 +270,20 @@ public class PlayerActivity extends AppCompatActivity
return false;
}
boolean preferExtensionDecoders =
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
lastSeenTracks = Tracks.EMPTY;
player =
ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder(/* context= */ this)
.setRenderersFactory(renderersFactory)
.setMediaSourceFactory(createMediaSourceFactory())
.build();
.setMediaSourceFactory(createMediaSourceFactory());
setRenderersFactory(
playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false));
player = playerBuilder.build();
player.setTrackSelectionParameters(trackSelectionParameters);
player.addListener(new PlayerEventListener());
player.addAnalyticsListener(new EventLogger());
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay);
playerView.setPlayer(player);
serverSideAdsLoader.setPlayer(player);
configurePlayerWithServerSideAdsLoader();
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
}
......@@ -302,7 +297,12 @@ public class PlayerActivity extends AppCompatActivity
return true;
}
@OptIn(markerClass = UnstableApi.class) // SSAI configuration
private MediaSource.Factory createMediaSourceFactory() {
DefaultDrmSessionManagerProvider drmSessionManagerProvider =
new DefaultDrmSessionManagerProvider();
drmSessionManagerProvider.setDrmHttpDataSourceFactory(
DemoUtil.getHttpDataSourceFactory(/* context= */ this));
ImaServerSideAdInsertionMediaSource.AdsLoader.Builder serverSideAdLoaderBuilder =
new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(/* context= */ this, playerView);
if (serverSideAdsLoaderState != null) {
......@@ -311,13 +311,30 @@ public class PlayerActivity extends AppCompatActivity
serverSideAdsLoader = serverSideAdLoaderBuilder.build();
ImaServerSideAdInsertionMediaSource.Factory imaServerSideAdInsertionMediaSourceFactory =
new ImaServerSideAdInsertionMediaSource.Factory(
serverSideAdsLoader, new DefaultMediaSourceFactory(dataSourceFactory));
return new DefaultMediaSourceFactory(dataSourceFactory)
.setAdsLoaderProvider(this::getClientSideAdsLoader)
.setAdViewProvider(playerView)
serverSideAdsLoader,
new DefaultMediaSourceFactory(/* context= */ this)
.setDataSourceFactory(dataSourceFactory));
return new DefaultMediaSourceFactory(/* context= */ this)
.setDataSourceFactory(dataSourceFactory)
.setDrmSessionManagerProvider(drmSessionManagerProvider)
.setLocalAdInsertionComponents(
this::getClientSideAdsLoader, /* adViewProvider= */ playerView)
.setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory);
}
@OptIn(markerClass = UnstableApi.class)
private void setRenderersFactory(
ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) {
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
playerBuilder.setRenderersFactory(renderersFactory);
}
@OptIn(markerClass = UnstableApi.class)
private void configurePlayerWithServerSideAdsLoader() {
serverSideAdsLoader.setPlayer(player);
}
private List<MediaItem> createMediaItems(Intent intent) {
String action = intent.getAction();
boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
......@@ -371,8 +388,7 @@ public class PlayerActivity extends AppCompatActivity
if (player != null) {
updateTrackSelectorParameters();
updateStartPosition();
serverSideAdsLoaderState = serverSideAdsLoader.release();
serverSideAdsLoader = null;
releaseServerSideAdsLoader();
debugViewHelper.stop();
debugViewHelper = null;
player.release();
......@@ -387,6 +403,12 @@ public class PlayerActivity extends AppCompatActivity
}
}
@OptIn(markerClass = UnstableApi.class)
private void releaseServerSideAdsLoader() {
serverSideAdsLoaderState = serverSideAdsLoader.release();
serverSideAdsLoader = null;
}
private void releaseClientSideAdsLoader() {
if (clientSideAdsLoader != null) {
clientSideAdsLoader.release();
......@@ -395,6 +417,23 @@ public class PlayerActivity extends AppCompatActivity
}
}
@OptIn(markerClass = UnstableApi.class)
private void saveServerSideAdsLoaderState(Bundle outState) {
if (serverSideAdsLoaderState != null) {
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
}
}
@OptIn(markerClass = UnstableApi.class)
private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) {
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
if (adsLoaderStateBundle != null) {
serverSideAdsLoaderState =
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
adsLoaderStateBundle);
}
}
private void updateTrackSelectorParameters() {
if (player != null) {
trackSelectionParameters = player.getTrackSelectionParameters();
......
......@@ -27,6 +27,7 @@ import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.JsonReader;
import android.view.Menu;
import android.view.MenuInflater;
......@@ -443,7 +444,10 @@ public class SampleChooserActivity extends AppCompatActivity
} else {
@Nullable
String adaptiveMimeType =
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension));
Util.getAdaptiveMimeTypeForContentType(
TextUtils.isEmpty(extension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(extension));
mediaItem
.setUri(uri)
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
......@@ -454,7 +458,7 @@ public class SampleChooserActivity extends AppCompatActivity
new MediaItem.DrmConfiguration.Builder(drmUuid)
.setLicenseUri(drmLicenseUri)
.setLicenseRequestHeaders(drmLicenseRequestHeaders)
.forceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
.setForceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
.setMultiSession(drmMultiSession)
.setForceDefaultLicenseUri(drmForceDefaultLicenseUri)
.build());
......
......@@ -25,6 +25,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
......@@ -36,6 +37,7 @@ import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.ui.TrackSelectionView;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
......@@ -47,6 +49,7 @@ import java.util.List;
import java.util.Map;
/** Dialog to select tracks. */
@OptIn(markerClass = UnstableApi.class)
public final class TrackSelectionDialog extends DialogFragment {
/** Called when tracks are selected. */
......
......@@ -45,6 +45,7 @@
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
......
......@@ -43,6 +43,12 @@
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.surface.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
</application>
......
......@@ -19,6 +19,7 @@ import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
......@@ -201,13 +202,18 @@ public final class MainActivity extends Activity {
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
@Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
@C.ContentType
int type =
TextUtils.isEmpty(fileExtension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(fileExtension);
if (type == C.CONTENT_TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
} else if (type == C.CONTENT_TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
......
# Build targets for a demo MediaPipe graph.
# See README.md for instructions on using MediaPipe in the demo.
load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar")
load(
"//mediapipe/framework/tool:mediapipe_graph.bzl",
"mediapipe_binary_graph",
)
mediapipe_aar(
name = "edge_detector_mediapipe_aar",
calculators = [
"//mediapipe/calculators/image:luminance_calculator",
"//mediapipe/calculators/image:sobel_edges_calculator",
],
)
mediapipe_binary_graph(
name = "edge_detector_binary_graph",
graph = "edge_detector_mediapipe_graph.pbtxt",
output_name = "edge_detector_mediapipe_graph.binarypb",
)
......@@ -6,4 +6,61 @@ example by removing audio or video.
See the [demos README](../README.md) for instructions on how to build and run
this demo.
## MediaPipe frame processing demo
Building the demo app with [MediaPipe][] integration enabled requires some extra
manual steps.
1. Follow the
[instructions](https://google.github.io/mediapipe/getting_started/install.html)
to install MediaPipe.
1. Copy the Transformer demo's build configuration and MediaPipe graph text
protocol buffer under the MediaPipe source tree. This makes it easy to
[build an AAR][] with bazel by reusing MediaPipe's workspace.
```sh
cd "<path to MediaPipe checkout>"
MEDIAPIPE_ROOT="$(pwd)"
MEDIAPIPE_TRANSFORMER_ROOT="${MEDIAPIPE_ROOT}/mediapipe/java/com/google/mediapipe/transformer"
cd "<path to the transformer demo (containing this readme)>"
TRANSFORMER_DEMO_ROOT="$(pwd)"
mkdir -p "${MEDIAPIPE_TRANSFORMER_ROOT}"
mkdir -p "${TRANSFORMER_DEMO_ROOT}/libs"
cp ${TRANSFORMER_DEMO_ROOT}/BUILD.bazel ${MEDIAPIPE_TRANSFORMER_ROOT}/BUILD
cp ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets/edge_detector_mediapipe_graph.pbtxt \
${MEDIAPIPE_TRANSFORMER_ROOT}
```
1. Build the AAR and the binary proto for the demo's MediaPipe graph, then copy
them to Transformer.
```sh
cd ${MEDIAPIPE_ROOT}
bazel build -c opt --strip=ALWAYS \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--fat_apk_cpu=arm64-v8a,armeabi-v7a \
--legacy_whole_archive=0 \
--features=-legacy_whole_archive \
--copt=-fvisibility=hidden \
--copt=-ffunction-sections \
--copt=-fdata-sections \
--copt=-fstack-protector \
--copt=-Oz \
--copt=-fomit-frame-pointer \
--copt=-DABSL_MIN_LOG_LEVEL=2 \
--linkopt=-Wl,--gc-sections,--strip-all \
mediapipe/java/com/google/mediapipe/transformer:edge_detector_mediapipe_aar.aar
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_aar.aar \
${TRANSFORMER_DEMO_ROOT}/libs
bazel build mediapipe/java/com/google/mediapipe/transformer:edge_detector_binary_graph
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_graph.binarypb \
${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets
```
1. In Android Studio, gradle sync and select the `withMediaPipe` build variant
(this will only appear if the AAR is present), then build and run the demo
app and select a MediaPipe-based effect.
[Transformer]: https://exoplayer.dev/transforming-media.html
[MediaPipe]: https://google.github.io/mediapipe/
[build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html
......@@ -45,6 +45,27 @@ android {
// This demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
flavorDimensions "mediaPipe"
productFlavors {
noMediaPipe {
dimension "mediaPipe"
}
withMediaPipe {
dimension "mediaPipe"
}
}
// Ignore the withMediaPipe variant if the MediaPipe AAR is not present.
if (!project.file("libs/edge_detector_mediapipe_aar.aar").exists()) {
variantFilter { variant ->
def names = variant.flavors*.name
if (names.contains("withMediaPipe")) {
setIgnore(true)
}
}
}
}
dependencies {
......@@ -56,6 +77,14 @@ dependencies {
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-transformer')
implementation project(modulePrefix + 'lib-ui')
// For MediaPipe and its dependencies:
withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar'])
withMediaPipeImplementation 'com.google.flogger:flogger:latest.release'
withMediaPipeImplementation 'com.google.flogger:flogger-system-backend:latest.release'
withMediaPipeImplementation 'com.google.code.findbugs:jsr305:latest.release'
withMediaPipeImplementation 'com.google.protobuf:protobuf-javalite:3.19.1'
}
......@@ -49,6 +49,12 @@
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.transformer.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".TransformerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
......
......@@ -16,9 +16,8 @@
// ES 2 vertex shader that leaves the coordinates unchanged.
attribute vec4 aFramePosition;
attribute vec4 aTexSamplingCoord;
varying vec2 vTexSamplingCoord;
void main() {
gl_Position = aFramePosition;
vTexSamplingCoord = aTexSamplingCoord.xy;
vTexSamplingCoord = vec2(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5);
}
......@@ -31,22 +31,20 @@ import android.util.Size;
import androidx.media3.common.C;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.GlFrameProcessor;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import java.io.IOException;
import java.util.Locale;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link GlFrameProcessor} that overlays a bitmap with a logo and timer on each frame.
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
* frame.
*
* <p>The bitmap is drawn using an Android {@link Canvas}.
*/
// TODO(b/227625365): Delete this class and use a frame processor from the Transformer library, once
// overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayFrameProcessor implements GlFrameProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
// once overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
......@@ -55,16 +53,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Paint paint;
private final Bitmap overlayBitmap;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas;
private final GlProgram glProgram;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Bitmap logoBitmap;
private @MonotonicNonNull GlProgram glProgram;
public BitmapOverlayFrameProcessor() {
/**
* Creates a new instance.
*
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public BitmapOverlayProcessor(Context context) throws FrameProcessingException {
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
......@@ -73,19 +75,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
overlayBitmap =
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
outputSize = new Size(inputWidth, inputHeight);
try {
logoBitmap =
......@@ -95,55 +84,77 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
try {
bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (GlUtil.GlException | IOException e) {
throw new FrameProcessingException(e);
}
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setBufferAttribute(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
public Size configure(int inputWidth, int inputHeight) {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) {
checkStateNotNull(glProgram);
glProgram.use();
// Draw to the canvas and store it in a texture.
String text =
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint);
overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId);
GLUtils.texSubImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
/* xoffset= */ 0,
/* yoffset= */ 0,
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
// Draw to the canvas and store it in a texture.
String text =
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint);
overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId);
GLUtils.texSubImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
/* xoffset= */ 0,
/* yoffset= */ 0,
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e, presentationTimeUs);
}
}
@Override
public void release() {
public void release() throws FrameProcessingException {
super.release();
if (glProgram != null) {
glProgram.delete();
try {
glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
}
......
......@@ -18,39 +18,38 @@ package androidx.media3.demo.transformer;
import android.graphics.Matrix;
import androidx.media3.common.C;
import androidx.media3.common.util.Util;
import androidx.media3.transformer.AdvancedFrameProcessor;
import androidx.media3.transformer.GlFrameProcessor;
import androidx.media3.transformer.GlMatrixTransformation;
import androidx.media3.transformer.MatrixTransformation;
/**
* Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying
* transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}.
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
* MatrixTransformation MatrixTransformations} that create video effects by applying transformation
* matrices to the individual video frames.
*/
/* package */ final class AdvancedFrameProcessorFactory {
/* package */ final class MatrixTransformationFactory {
/**
* Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value
* Returns a {@link MatrixTransformation} that rescales the frames over the first {@value
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
* linearly in size from a single point to filling the full output frame.
*/
public static GlFrameProcessor createZoomInTransitionFrameProcessor() {
return new AdvancedFrameProcessor(
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix);
public static MatrixTransformation createZoomInTransition() {
return MatrixTransformationFactory::calculateZoomInTransitionMatrix;
}
/**
* Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse.
* Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an
* ellipse.
*/
public static GlFrameProcessor createDizzyCropFrameProcessor() {
return new AdvancedFrameProcessor(
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix);
public static MatrixTransformation createDizzyCropEffect() {
return MatrixTransformationFactory::calculateDizzyCropMatrix;
}
/**
* Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies
* perspective projection to 2D.
* Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and
* applies perspective projection to 2D.
*/
public static GlFrameProcessor createSpin3dFrameProcessor() {
return new AdvancedFrameProcessor(
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix);
public static GlMatrixTransformation createSpin3dEffect() {
return MatrixTransformationFactory::calculate3dSpinMatrix;
}
private static final float ZOOM_DURATION_SECONDS = 2f;
......
......@@ -16,38 +16,29 @@
package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.GlFrameProcessor;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link GlFrameProcessor} that periodically dims the frames such that pixels are darker the
* further they are away from the frame center.
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
* darker the further they are away from the frame center.
*/
/* package */ final class PeriodicVignetteFrameProcessor implements GlFrameProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
/* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor {
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
private static final float DIMMING_PERIOD_US = 5_600_000f;
private float centerX;
private float centerY;
private float minInnerRadius;
private float deltaInnerRadius;
private float outerRadius;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
private final GlProgram glProgram;
private final float minInnerRadius;
private final float deltaInnerRadius;
/**
* Creates a new instance.
......@@ -66,53 +57,65 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
* @param outerRadius The radius after which all pixels are black.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public PeriodicVignetteFrameProcessor(
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
public PeriodicVignetteProcessor(
Context context,
float centerX,
float centerY,
float minInnerRadius,
float maxInnerRadius,
float outerRadius)
throws FrameProcessingException {
checkArgument(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius);
this.centerX = centerX;
this.centerY = centerY;
this.minInnerRadius = minInnerRadius;
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
this.outerRadius = outerRadius;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
outputSize = new Size(inputWidth, inputHeight);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
try {
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (IOException | GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setBufferAttribute(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
public Size configure(int inputWidth, int inputHeight) {
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) {
checkStateNotNull(glProgram).use();
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius = minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
glProgram.setFloatsUniform("uInnerRadius", new float[] {innerRadius});
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius =
minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
glProgram.setFloatsUniform("uInnerRadius", new float[] {innerRadius});
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e, presentationTimeUs);
}
}
@Override
public void release() {
public void release() throws FrameProcessingException {
super.release();
if (glProgram != null) {
glProgram.delete();
try {
glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
}
}
......@@ -19,6 +19,7 @@ import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
......@@ -31,6 +32,7 @@ import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
......@@ -40,8 +42,9 @@ import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.transformer.DefaultEncoderFactory;
import androidx.media3.transformer.EncoderSelector;
import androidx.media3.transformer.GlFrameProcessor;
import androidx.media3.transformer.GlEffect;
import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.TransformationResult;
......@@ -54,6 +57,7 @@ import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
......@@ -149,9 +153,10 @@ public final class TransformerActivity extends AppCompatActivity {
externalCacheFile = createExternalCacheFile("transformer-output.mp4");
String filePath = externalCacheFile.getAbsolutePath();
@Nullable Bundle bundle = intent.getExtras();
MediaItem mediaItem = createMediaItem(bundle, uri);
Transformer transformer = createTransformer(bundle, filePath);
transformationStopwatch.start();
transformer.startTransformation(MediaItem.fromUri(uri), filePath);
transformer.startTransformation(mediaItem, filePath);
this.transformer = transformer;
} catch (IOException e) {
throw new IllegalStateException(e);
......@@ -178,6 +183,24 @@ public final class TransformerActivity extends AppCompatActivity {
});
}
private MediaItem createMediaItem(@Nullable Bundle bundle, Uri uri) {
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri);
if (bundle != null) {
long trimStartMs =
bundle.getLong(ConfigurationActivity.TRIM_START_MS, /* defaultValue= */ C.TIME_UNSET);
long trimEndMs =
bundle.getLong(ConfigurationActivity.TRIM_END_MS, /* defaultValue= */ C.TIME_UNSET);
if (trimStartMs != C.TIME_UNSET && trimEndMs != C.TIME_UNSET) {
mediaItemBuilder.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(trimStartMs)
.setEndPositionMs(trimEndMs)
.build());
}
}
return mediaItemBuilder.build();
}
// Create a cache file, resetting it if it already exists.
private File createExternalCacheFile(String fileName) throws IOException {
File file = new File(getExternalCacheDir(), fileName);
......@@ -237,38 +260,64 @@ public final class TransformerActivity extends AppCompatActivity {
.setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO))
.setEncoderFactory(
new DefaultEncoderFactory(
/* context= */ this,
EncoderSelector.DEFAULT,
/* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)));
ImmutableList.Builder<GlFrameProcessor> frameProcessors = new ImmutableList.Builder<>();
ImmutableList.Builder<GlEffect> effects = new ImmutableList.Builder<>();
@Nullable
boolean[] selectedFrameProcessors =
bundle.getBooleanArray(ConfigurationActivity.DEMO_FRAME_PROCESSORS_SELECTIONS);
if (selectedFrameProcessors != null) {
if (selectedFrameProcessors[0]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createDizzyCropFrameProcessor());
boolean[] selectedEffects =
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
if (selectedEffects != null) {
if (selectedEffects[0]) {
effects.add(MatrixTransformationFactory.createDizzyCropEffect());
}
if (selectedFrameProcessors[1]) {
frameProcessors.add(
new PeriodicVignetteFrameProcessor(
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
/* minInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS),
/* maxInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
if (selectedEffects[1]) {
try {
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
Constructor<?> constructor =
clazz.getConstructor(Context.class, String.class, String.class, String.class);
effects.add(
(Context context) -> {
try {
return (SingleFrameGlTextureProcessor)
constructor.newInstance(
context,
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
/* inputStreamName= */ "input_video",
/* outputStreamName= */ "output_video");
} catch (Exception e) {
runOnUiThread(() -> showToast(R.string.no_media_pipe_error));
throw new RuntimeException("Failed to load MediaPipe processor", e);
}
});
} catch (Exception e) {
showToast(R.string.no_media_pipe_error);
}
}
if (selectedEffects[2]) {
effects.add(
(Context context) ->
new PeriodicVignetteProcessor(
context,
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
/* minInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS),
/* maxInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
}
if (selectedFrameProcessors[2]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createSpin3dFrameProcessor());
if (selectedEffects[3]) {
effects.add(MatrixTransformationFactory.createSpin3dEffect());
}
if (selectedFrameProcessors[3]) {
frameProcessors.add(new BitmapOverlayFrameProcessor());
if (selectedEffects[4]) {
effects.add(BitmapOverlayProcessor::new);
}
if (selectedFrameProcessors[4]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor());
if (selectedEffects[5]) {
effects.add(MatrixTransformationFactory.createZoomInTransition());
}
transformerBuilder.setFrameProcessors(frameProcessors.build());
transformerBuilder.setVideoFrameEffects(effects.build());
}
}
return transformerBuilder
......@@ -362,6 +411,10 @@ public final class TransformerActivity extends AppCompatActivity {
}
}
private void showToast(@StringRes int messageResource) {
Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show();
}
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
@Nullable
......
......@@ -64,7 +64,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/select_demo_frameprocessors_button">
app:layout_constraintBottom_toTopOf="@+id/select_demo_effects_button">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
......@@ -163,6 +163,16 @@
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/trim"
android:text="@string/trim" />
<CheckBox
android:id="@+id/trim_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/enable_fallback" />
<CheckBox
android:id="@+id/enable_fallback_checkbox"
......@@ -192,13 +202,13 @@
</TableLayout>
</androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/select_demo_frameprocessors_button"
android:id="@+id/select_demo_effects_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/select_demo_frameprocessors"
android:text="@string/select_demo_effects"
app:layout_constraintBottom_toTopOf="@+id/transform_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
......
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 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
~
~ 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/trim_range" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/trim_bounds_range_slider"
android:valueFrom="0.0"
android:valueTo="60.0"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -27,10 +27,12 @@
<string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="trim" translatable="false">Trim</string>
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string>
<string name="select_demo_frameprocessors" translatable="false">Add demo effects</string>
<string name="select_demo_effects" translatable="false">Add demo effects</string>
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</string>
<string name="transform" translatable="false">Transform</string>
<string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
......@@ -41,4 +43,5 @@
<string name="center_x">Center X</string>
<string name="center_y">Center Y</string>
<string name="radius_range">Radius range</string>
<string name="trim_range">Bounds in seconds</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 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
~
~ 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.
-->
<manifest package="androidx.media3.demo.transformer">
<uses-sdk />
</manifest>
# Demo MediaPipe graph that shows edges using a SobelEdgesCalculator.
input_stream: "input_video"
output_stream: "output_video"
node: {
calculator: "LuminanceCalculator"
input_stream: "input_video"
output_stream: "luma_video"
}
node: {
calculator: "SobelEdgesCalculator"
input_stream: "luma_video"
output_stream: "output_video"
}
/*
* Copyright 2022 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
*
* 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 androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.EGL14;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.LibraryLoader;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import com.google.mediapipe.components.FrameProcessor;
import com.google.mediapipe.framework.AndroidAssetUtil;
import com.google.mediapipe.framework.AppTextureFrame;
import com.google.mediapipe.framework.TextureFrame;
import com.google.mediapipe.glutil.EglManager;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Runs a MediaPipe graph on input frames. The implementation is currently limited to graphs that
* can immediately produce one output frame per input frame.
*/
/* package */ final class MediaPipeProcessor extends SingleFrameGlTextureProcessor {
private static final LibraryLoader LOADER =
new LibraryLoader("mediapipe_jni") {
@Override
protected void loadLibrary(String name) {
System.loadLibrary(name);
}
};
static {
// Not all build configurations require OpenCV to be loaded separately, so attempt to load the
// library but ignore the error if it's not present.
try {
System.loadLibrary("opencv_java3");
} catch (UnsatisfiedLinkError e) {
// Do nothing.
}
}
private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl";
private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl";
private final ConditionVariable frameProcessorConditionVariable;
private final FrameProcessor frameProcessor;
private final GlProgram glProgram;
private int inputWidth;
private int inputHeight;
private @MonotonicNonNull TextureFrame outputFrame;
private @MonotonicNonNull RuntimeException frameProcessorPendingError;
/**
* Creates a new texture processor that wraps a MediaPipe graph.
*
* @param context The {@link Context}.
* @param graphName Name of a MediaPipe graph asset to load.
* @param inputStreamName Name of the input video stream in the graph.
* @param outputStreamName Name of the input video stream in the graph.
* @throws FrameProcessingException If a problem occurs while reading shader files or initializing
* MediaPipe resources.
*/
public MediaPipeProcessor(
Context context, String graphName, String inputStreamName, String outputStreamName)
throws FrameProcessingException {
checkState(LOADER.isAvailable());
frameProcessorConditionVariable = new ConditionVariable();
AndroidAssetUtil.initializeNativeAssetManager(context);
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor =
new FrameProcessor(
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName);
// Unblock drawFrame when there is an output frame or an error.
frameProcessor.setConsumer(
frame -> {
outputFrame = frame;
frameProcessorConditionVariable.open();
});
frameProcessor.setAsynchronousErrorListener(
error -> {
frameProcessorPendingError = error;
frameProcessorConditionVariable.open();
});
try {
glProgram = new GlProgram(context, COPY_VERTEX_SHADER_NAME, COPY_FRAGMENT_SHADER_NAME);
} catch (IOException | GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
@Override
public Size configure(int inputWidth, int inputHeight) {
this.inputWidth = inputWidth;
this.inputHeight = inputHeight;
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
frameProcessorConditionVariable.close();
// Pass the input frame to MediaPipe.
AppTextureFrame appTextureFrame = new AppTextureFrame(inputTexId, inputWidth, inputHeight);
appTextureFrame.setTimestamp(presentationTimeUs);
checkStateNotNull(frameProcessor).onNewFrame(appTextureFrame);
// Wait for output to be passed to the consumer.
try {
frameProcessorConditionVariable.block();
} catch (InterruptedException e) {
// Propagate the interrupted flag so the next blocking operation will throw.
// TODO(b/230469581): The next processor that runs will not have valid input due to returning
// early here. This could be fixed by checking for interruption in the outer loop that runs
// through the texture processors.
Thread.currentThread().interrupt();
return;
}
if (frameProcessorPendingError != null) {
throw new FrameProcessingException(frameProcessorPendingError);
}
// Copy from MediaPipe's output texture to the current output.
try {
checkStateNotNull(glProgram).use();
glProgram.setSamplerTexIdUniform(
"uTexSampler", checkStateNotNull(outputFrame).getTextureName(), /* texUnitIndex= */ 0);
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.bindAttributesAndUniforms();
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e, presentationTimeUs);
} finally {
checkStateNotNull(outputFrame).release();
}
}
@Override
public void release() throws FrameProcessingException {
super.release();
checkStateNotNull(frameProcessor).close();
}
}
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
......@@ -48,9 +48,21 @@ class CombinedJavadocPlugin implements Plugin<Project> {
libraryModule.android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name == "release") {
// Works around b/234569640 that causes different versions of the androidx.media
// jar to be on the classpath.
def allJarFiles = []
allJarFiles.addAll(variant.javaCompileProvider.get().classpath.files)
def filteredJarFiles = allJarFiles.findAll { file ->
if (file ==~ /.*media-.\..\..-api.jar$/
&& !file.path.endsWith(
"media-" + project.ext.androidxMediaVersion + "-api.jar")) {
return false;
}
return true;
}
classpath +=
libraryModule.project.files(
variant.javaCompileProvider.get().classpath.files,
filteredJarFiles,
libraryModule.project.android.getBootClasspath())
}
}
......
......@@ -42,7 +42,7 @@ import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
......@@ -102,7 +102,8 @@ public final class CastPlayer extends BasePlayer {
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS)
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM)
.build();
public static final float MIN_SPEED_SUPPORTED = 0.5f;
......@@ -458,6 +459,11 @@ public final class CastPlayer extends BasePlayer {
stop(/* reset= */ false);
}
/**
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@Deprecated
@Override
public void stop(boolean reset) {
......@@ -705,10 +711,10 @@ public final class CastPlayer extends BasePlayer {
return VideoSize.UNKNOWN;
}
/** This method is not supported and returns an empty list. */
/** This method is not supported and returns an empty {@link CueGroup}. */
@Override
public ImmutableList<Cue> getCurrentCues() {
return ImmutableList.of();
public CueGroup getCurrentCues() {
return CueGroup.EMPTY;
}
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
......
......@@ -36,6 +36,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
......@@ -1359,6 +1360,7 @@ public class CastPlayerTest {
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse();
......
......@@ -28,7 +28,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
/**
* Attributes for audio playback, which configure the underlying platform {@link
......@@ -43,10 +42,31 @@ import java.lang.reflect.Method;
*/
public final class AudioAttributes implements Bundleable {
/** A direct wrapper around {@link android.media.AudioAttributes}. */
@RequiresApi(21)
public static final class AudioAttributesV21 {
public final android.media.AudioAttributes audioAttributes;
private AudioAttributesV21(AudioAttributes audioAttributes) {
android.media.AudioAttributes.Builder builder =
new android.media.AudioAttributes.Builder()
.setContentType(audioAttributes.contentType)
.setFlags(audioAttributes.flags)
.setUsage(audioAttributes.usage);
if (Util.SDK_INT >= 29) {
Api29.setAllowedCapturePolicy(builder, audioAttributes.allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, audioAttributes.spatializationBehavior);
}
this.audioAttributes = builder.build();
}
}
/**
* The default audio attributes, where the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage
* is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are
* set.
* The default audio attributes, where the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN},
* usage is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags
* are set.
*/
public static final AudioAttributes DEFAULT = new Builder().build();
......@@ -62,11 +82,11 @@ public final class AudioAttributes implements Bundleable {
/**
* Creates a new builder for {@link AudioAttributes}.
*
* <p>By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link
* <p>By default the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN}, usage is {@link
* C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set.
*/
public Builder() {
contentType = C.CONTENT_TYPE_UNKNOWN;
contentType = C.AUDIO_CONTENT_TYPE_UNKNOWN;
flags = 0;
usage = C.USAGE_MEDIA;
allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
......@@ -97,9 +117,7 @@ public final class AudioAttributes implements Bundleable {
return this;
}
// TODO[b/190759307] Update javadoc to link to AudioAttributes.Builder#setSpatializationBehavior
// once compile SDK target is set to 32.
/** See {@code android.media.AudioAttributes.Builder.setSpatializationBehavior(int)}. */
/** See {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)}. */
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
this.spatializationBehavior = spatializationBehavior;
return this;
......@@ -123,7 +141,7 @@ public final class AudioAttributes implements Bundleable {
/** The {@link C.SpatializationBehavior}. */
public final @C.SpatializationBehavior int spatializationBehavior;
@Nullable private android.media.AudioAttributes audioAttributesV21;
@Nullable private AudioAttributesV21 audioAttributesV21;
private AudioAttributes(
@C.AudioContentType int contentType,
......@@ -139,25 +157,15 @@ public final class AudioAttributes implements Bundleable {
}
/**
* Returns a {@link android.media.AudioAttributes} from this instance.
* Returns a {@link AudioAttributesV21} from this instance.
*
* <p>Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29.
* <p>Some fields are ignored if the corresponding {@link android.media.AudioAttributes.Builder}
* setter is not available on the current API level.
*/
@RequiresApi(21)
public android.media.AudioAttributes getAudioAttributesV21() {
public AudioAttributesV21 getAudioAttributesV21() {
if (audioAttributesV21 == null) {
android.media.AudioAttributes.Builder builder =
new android.media.AudioAttributes.Builder()
.setContentType(contentType)
.setFlags(flags)
.setUsage(usage);
if (Util.SDK_INT >= 29) {
Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, spatializationBehavior);
}
audioAttributesV21 = builder.build();
audioAttributesV21 = new AudioAttributesV21(this);
}
return audioAttributesV21;
}
......@@ -251,8 +259,6 @@ public final class AudioAttributes implements Bundleable {
@RequiresApi(29)
private static final class Api29 {
private Api29() {}
@DoNotInline
public static void setAllowedCapturePolicy(
android.media.AudioAttributes.Builder builder,
......@@ -263,20 +269,11 @@ public final class AudioAttributes implements Bundleable {
@RequiresApi(32)
private static final class Api32 {
private Api32() {}
@DoNotInline
public static void setSpatializationBehavior(
android.media.AudioAttributes.Builder builder,
@C.SpatializationBehavior int spatializationBehavior) {
try {
// TODO[b/190759307]: Remove reflection once compile SDK target is set to 32.
Method setSpatializationBehavior =
builder.getClass().getMethod("setSpatializationBehavior", Integer.TYPE);
setSpatializationBehavior.invoke(builder, spatializationBehavior);
} catch (Exception e) {
// Do nothing if reflection fails.
}
builder.setSpatializationBehavior(spatializationBehavior);
}
}
}
......@@ -94,7 +94,7 @@ public abstract class BasePlayer implements Player {
/**
* {@inheritDoc}
*
* <p>BasePlayer and its descendents will return {@code true}.
* <p>BasePlayer and its descendants will return {@code true}.
*/
@Override
public final boolean canAdvertiseSession() {
......@@ -143,12 +143,18 @@ public abstract class BasePlayer implements Player {
seekToOffset(getSeekForwardIncrement());
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasPrevious() {
return hasPreviousMediaItem();
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasPreviousWindow() {
......@@ -160,12 +166,18 @@ public abstract class BasePlayer implements Player {
return getPreviousMediaItemIndex() != C.INDEX_UNSET;
}
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final void previous() {
seekToPreviousMediaItem();
}
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final void seekToPreviousWindow() {
......@@ -198,12 +210,18 @@ public abstract class BasePlayer implements Player {
}
}
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasNext() {
return hasNextMediaItem();
}
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasNextWindow() {
......@@ -215,12 +233,18 @@ public abstract class BasePlayer implements Player {
return getNextMediaItemIndex() != C.INDEX_UNSET;
}
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated
@Override
public final void next() {
seekToNextMediaItem();
}
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated
@Override
public final void seekToNextWindow() {
......@@ -253,12 +277,18 @@ public abstract class BasePlayer implements Player {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
}
/**
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getCurrentWindowIndex() {
return getCurrentMediaItemIndex();
}
/**
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getNextWindowIndex() {
......@@ -274,6 +304,9 @@ public abstract class BasePlayer implements Player {
getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
}
/**
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getPreviousWindowIndex() {
......@@ -326,6 +359,9 @@ public abstract class BasePlayer implements Player {
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
}
/**
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowDynamic() {
......@@ -338,6 +374,9 @@ public abstract class BasePlayer implements Player {
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic;
}
/**
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowLive() {
......@@ -364,6 +403,9 @@ public abstract class BasePlayer implements Player {
return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition();
}
/**
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowSeekable() {
......
......@@ -333,12 +333,16 @@ public final class C {
@IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER})
public @interface SpatializationBehavior {}
// TODO[b/190759307]: Update constant values and javadoc to use SDK once compile SDK target is set
// to 32.
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO */
public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0;
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER */
public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1;
/**
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO
*/
public static final int SPATIALIZATION_BEHAVIOR_AUTO =
AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO;
/**
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER
*/
public static final int SPATIALIZATION_BEHAVIOR_NEVER =
AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER;
/**
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
......@@ -396,9 +400,15 @@ public final class C {
@UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
/**
* Content types for audio attributes. One of {@link #CONTENT_TYPE_MOVIE}, {@link
* #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link #CONTENT_TYPE_SPEECH} or
* {@link #CONTENT_TYPE_UNKNOWN}.
* Content types for audio attributes. One of:
*
* <ul>
* <li>{@link #AUDIO_CONTENT_TYPE_MOVIE}
* <li>{@link #AUDIO_CONTENT_TYPE_MUSIC}
* <li>{@link #AUDIO_CONTENT_TYPE_SONIFICATION}
* <li>{@link #AUDIO_CONTENT_TYPE_SPEECH}
* <li>{@link #AUDIO_CONTENT_TYPE_UNKNOWN}
* </ul>
*/
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added.
......@@ -406,34 +416,46 @@ public final class C {
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({
CONTENT_TYPE_MOVIE,
CONTENT_TYPE_MUSIC,
CONTENT_TYPE_SONIFICATION,
CONTENT_TYPE_SPEECH,
CONTENT_TYPE_UNKNOWN
AUDIO_CONTENT_TYPE_MOVIE,
AUDIO_CONTENT_TYPE_MUSIC,
AUDIO_CONTENT_TYPE_SONIFICATION,
AUDIO_CONTENT_TYPE_SPEECH,
AUDIO_CONTENT_TYPE_UNKNOWN
})
public @interface AudioContentType {}
/** See {@link AudioAttributes#CONTENT_TYPE_MOVIE}. */
public static final int AUDIO_CONTENT_TYPE_MOVIE = AudioAttributes.CONTENT_TYPE_MOVIE;
/**
* @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_MOVIE} instead.
*/
public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE;
@UnstableApi @Deprecated public static final int CONTENT_TYPE_MOVIE = AUDIO_CONTENT_TYPE_MOVIE;
/** See {@link AudioAttributes#CONTENT_TYPE_MUSIC}. */
public static final int AUDIO_CONTENT_TYPE_MUSIC = AudioAttributes.CONTENT_TYPE_MUSIC;
/**
* @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_MUSIC} instead.
*/
public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
@UnstableApi @Deprecated public static final int CONTENT_TYPE_MUSIC = AUDIO_CONTENT_TYPE_MUSIC;
/** See {@link AudioAttributes#CONTENT_TYPE_SONIFICATION}. */
public static final int AUDIO_CONTENT_TYPE_SONIFICATION =
AudioAttributes.CONTENT_TYPE_SONIFICATION;
/**
* @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_SONIFICATION} instead.
*/
public static final int CONTENT_TYPE_SONIFICATION =
android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
@UnstableApi @Deprecated
public static final int CONTENT_TYPE_SONIFICATION = AUDIO_CONTENT_TYPE_SONIFICATION;
/** See {@link AudioAttributes#CONTENT_TYPE_SPEECH}. */
public static final int AUDIO_CONTENT_TYPE_SPEECH = AudioAttributes.CONTENT_TYPE_SPEECH;
/**
* @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_SPEECH} instead.
*/
public static final int CONTENT_TYPE_SPEECH = android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
@UnstableApi @Deprecated public static final int CONTENT_TYPE_SPEECH = AUDIO_CONTENT_TYPE_SPEECH;
/** See {@link AudioAttributes#CONTENT_TYPE_UNKNOWN}. */
public static final int AUDIO_CONTENT_TYPE_UNKNOWN = AudioAttributes.CONTENT_TYPE_UNKNOWN;
/**
* @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_UNKNOWN} instead.
*/
public static final int CONTENT_TYPE_UNKNOWN = android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN;
@UnstableApi @Deprecated
public static final int CONTENT_TYPE_UNKNOWN = AUDIO_CONTENT_TYPE_UNKNOWN;
/**
* Flags for audio attributes. Possible flag value is {@link #FLAG_AUDIBILITY_ENFORCED}.
......@@ -725,30 +747,59 @@ public final class C {
public static final String LANGUAGE_UNDETERMINED = "und";
/**
* Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link
* #TYPE_HLS}, {@link #TYPE_RTSP} or {@link #TYPE_OTHER}.
* Represents a streaming or other media type. One of:
*
* <ul>
* <li>{@link #CONTENT_TYPE_DASH}
* <li>{@link #CONTENT_TYPE_SS}
* <li>{@link #CONTENT_TYPE_HLS}
* <li>{@link #CONTENT_TYPE_RTSP}
* <li>{@link #CONTENT_TYPE_OTHER}
* </ul>
*/
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added.
@UnstableApi
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER})
@IntDef({
CONTENT_TYPE_DASH,
CONTENT_TYPE_SS,
CONTENT_TYPE_HLS,
CONTENT_TYPE_RTSP,
CONTENT_TYPE_OTHER
})
public @interface ContentType {}
/** Value returned by {@link Util#inferContentType} for DASH manifests. */
@UnstableApi public static final int TYPE_DASH = 0;
/** Value returned by {@link Util#inferContentType} for Smooth Streaming manifests. */
@UnstableApi public static final int TYPE_SS = 1;
/** Value returned by {@link Util#inferContentType} for HLS manifests. */
@UnstableApi public static final int TYPE_HLS = 2;
/** Value returned by {@link Util#inferContentType} for RTSP. */
@UnstableApi public static final int TYPE_RTSP = 3;
/**
* Value returned by {@link Util#inferContentType} for files other than DASH, HLS or Smooth
* Streaming manifests, or RTSP URIs.
*/
@UnstableApi public static final int TYPE_OTHER = 4;
/** Value representing a DASH manifest. */
public static final int CONTENT_TYPE_DASH = 0;
/**
* @deprecated Use {@link #CONTENT_TYPE_DASH} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_DASH = CONTENT_TYPE_DASH;
/** Value representing a Smooth Streaming manifest. */
public static final int CONTENT_TYPE_SS = 1;
/**
* @deprecated Use {@link #CONTENT_TYPE_SS} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_SS = CONTENT_TYPE_SS;
/** Value representing an HLS manifest. */
public static final int CONTENT_TYPE_HLS = 2;
/**
* @deprecated Use {@link #CONTENT_TYPE_HLS} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_HLS = CONTENT_TYPE_HLS;
/** Value representing an RTSP stream. */
public static final int CONTENT_TYPE_RTSP = 3;
/**
* @deprecated Use {@link #CONTENT_TYPE_RTSP} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_RTSP = CONTENT_TYPE_RTSP;
/** Value representing files other than DASH, HLS or Smooth Streaming manifests, or RTSP URIs. */
public static final int CONTENT_TYPE_OTHER = 4;
/**
* @deprecated Use {@link #CONTENT_TYPE_OTHER} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_OTHER = CONTENT_TYPE_OTHER;
/** A return value for methods where the end of an input was encountered. */
@UnstableApi public static final int RESULT_END_OF_INPUT = -1;
......
......@@ -16,10 +16,8 @@
package androidx.media3.common;
import android.util.Pair;
import androidx.media3.common.util.UnstableApi;
/** Converts throwables into error codes and user readable error messages. */
@UnstableApi
public interface ErrorMessageProvider<T extends Throwable> {
/**
......
......@@ -37,13 +37,14 @@ public final class FileTypes {
/**
* File types. One of {@link #UNKNOWN}, {@link #AC3}, {@link #AC4}, {@link #ADTS}, {@link #AMR},
* {@link #FLAC}, {@link #FLV}, {@link #MATROSKA}, {@link #MP3}, {@link #MP4}, {@link #OGG},
* {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT} and {@link #JPEG}.
* {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT}, {@link #JPEG} and {@link #MIDI}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG,
MIDI
})
public @interface Type {}
/** Unknown file type. */
......@@ -78,6 +79,8 @@ public final class FileTypes {
public static final int WEBVTT = 13;
/** File type for the JPEG format. */
public static final int JPEG = 14;
/** File type for the MIDI format. */
public static final int MIDI = 15;
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
......@@ -89,6 +92,9 @@ public final class FileTypes {
private static final String EXTENSION_AMR = ".amr";
private static final String EXTENSION_FLAC = ".flac";
private static final String EXTENSION_FLV = ".flv";
private static final String EXTENSION_MID = ".mid";
private static final String EXTENSION_MIDI = ".midi";
private static final String EXTENSION_SMF = ".smf";
private static final String EXTENSION_PREFIX_MK = ".mk";
private static final String EXTENSION_WEBM = ".webm";
private static final String EXTENSION_PREFIX_OG = ".og";
......@@ -147,6 +153,8 @@ public final class FileTypes {
return FileTypes.FLAC;
case MimeTypes.VIDEO_FLV:
return FileTypes.FLV;
case MimeTypes.AUDIO_MIDI:
return FileTypes.MIDI;
case MimeTypes.VIDEO_MATROSKA:
case MimeTypes.AUDIO_MATROSKA:
case MimeTypes.VIDEO_WEBM:
......@@ -193,6 +201,10 @@ public final class FileTypes {
return FileTypes.FLAC;
} else if (filename.endsWith(EXTENSION_FLV)) {
return FileTypes.FLV;
} else if (filename.endsWith(EXTENSION_MID)
|| filename.endsWith(EXTENSION_MIDI)
|| filename.endsWith(EXTENSION_SMF)) {
return FileTypes.MIDI;
} else if (filename.startsWith(
EXTENSION_PREFIX_MK,
/* toffset= */ filename.length() - (EXTENSION_PREFIX_MK.length() + 1))
......
......@@ -1548,7 +1548,9 @@ public final class Format implements Bundleable {
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio);
bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData);
bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode);
bundle.putBundle(keyForField(FIELD_COLOR_INFO), BundleableUtil.toNullableBundle(colorInfo));
if (colorInfo != null) {
bundle.putBundle(keyForField(FIELD_COLOR_INFO), colorInfo.toBundle());
}
// Audio specific.
bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount);
bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate);
......@@ -1615,11 +1617,13 @@ public final class Format implements Bundleable {
bundle.getFloat(
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio))
.setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA)))
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode))
.setColorInfo(
BundleableUtil.fromNullableBundle(
ColorInfo.CREATOR, bundle.getBundle(keyForField(FIELD_COLOR_INFO))))
// Audio specific.
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode));
Bundle colorInfoBundle = bundle.getBundle(keyForField(FIELD_COLOR_INFO));
if (colorInfoBundle != null) {
builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle));
}
// Audio specific.
builder
.setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount))
.setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate))
.setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding))
......
......@@ -22,6 +22,7 @@ import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.UnstableApi;
import java.util.List;
......@@ -298,7 +299,11 @@ public class ForwardingPlayer implements Player {
player.seekForward();
}
/** Calls {@link Player#hasPrevious()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasPrevious()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -306,7 +311,11 @@ public class ForwardingPlayer implements Player {
return player.hasPrevious();
}
/** Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -320,7 +329,11 @@ public class ForwardingPlayer implements Player {
return player.hasPreviousMediaItem();
}
/** Calls {@link Player#previous()} on the delegate. */
/**
* Calls {@link Player#previous()} on the delegate.
*
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -328,7 +341,11 @@ public class ForwardingPlayer implements Player {
player.previous();
}
/** Calls {@link Player#seekToPreviousWindow()} on the delegate. */
/**
* Calls {@link Player#seekToPreviousWindow()} on the delegate.
*
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -354,7 +371,11 @@ public class ForwardingPlayer implements Player {
return player.getMaxSeekToPreviousPosition();
}
/** Calls {@link Player#hasNext()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasNext()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -362,7 +383,11 @@ public class ForwardingPlayer implements Player {
return player.hasNext();
}
/** Calls {@link Player#hasNextWindow()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasNextWindow()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -376,7 +401,11 @@ public class ForwardingPlayer implements Player {
return player.hasNextMediaItem();
}
/** Calls {@link Player#next()} on the delegate. */
/**
* Calls {@link Player#next()} on the delegate.
*
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -384,7 +413,11 @@ public class ForwardingPlayer implements Player {
player.next();
}
/** Calls {@link Player#seekToNextWindow()} on the delegate. */
/**
* Calls {@link Player#seekToNextWindow()} on the delegate.
*
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -428,7 +461,13 @@ public class ForwardingPlayer implements Player {
player.stop();
}
/** Calls {@link Player#stop(boolean)} on the delegate. */
/**
* Calls {@link Player#stop(boolean)} on the delegate.
*
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -497,7 +536,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentPeriodIndex();
}
/** Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result. */
/**
* Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -511,7 +554,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentMediaItemIndex();
}
/** Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result. */
/**
* Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -525,7 +572,11 @@ public class ForwardingPlayer implements Player {
return player.getNextMediaItemIndex();
}
/** Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result. */
/**
* Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -588,7 +639,11 @@ public class ForwardingPlayer implements Player {
return player.getTotalBufferedDuration();
}
/** Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result. */
/**
* Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -602,7 +657,11 @@ public class ForwardingPlayer implements Player {
return player.isCurrentMediaItemDynamic();
}
/** Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result. */
/**
* Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -622,7 +681,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentLiveOffset();
}
/** Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result. */
/**
* Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -752,7 +815,7 @@ public class ForwardingPlayer implements Player {
/** Calls {@link Player#getCurrentCues()} on the delegate and returns the result. */
@Override
public List<Cue> getCurrentCues() {
public CueGroup getCurrentCues() {
return player.getCurrentCues();
}
......@@ -993,6 +1056,11 @@ public class ForwardingPlayer implements Player {
}
@Override
public void onCues(CueGroup cueGroup) {
listener.onCues(cueGroup);
}
@Override
public void onMetadata(Metadata metadata) {
listener.onMetadata(metadata);
}
......
......@@ -52,7 +52,6 @@ public final class MediaMetadata implements Bundleable {
@Nullable private CharSequence displayTitle;
@Nullable private CharSequence subtitle;
@Nullable private CharSequence description;
@Nullable private Uri mediaUri;
@Nullable private Rating userRating;
@Nullable private Rating overallRating;
@Nullable private byte[] artworkData;
......@@ -88,7 +87,6 @@ public final class MediaMetadata implements Bundleable {
this.displayTitle = mediaMetadata.displayTitle;
this.subtitle = mediaMetadata.subtitle;
this.description = mediaMetadata.description;
this.mediaUri = mediaMetadata.mediaUri;
this.userRating = mediaMetadata.userRating;
this.overallRating = mediaMetadata.overallRating;
this.artworkData = mediaMetadata.artworkData;
......@@ -161,12 +159,6 @@ public final class MediaMetadata implements Bundleable {
return this;
}
/** Sets the media {@link Uri}. */
public Builder setMediaUri(@Nullable Uri mediaUri) {
this.mediaUri = mediaUri;
return this;
}
/** Sets the user {@link Rating}. */
public Builder setUserRating(@Nullable Rating userRating) {
this.userRating = userRating;
......@@ -431,9 +423,6 @@ public final class MediaMetadata implements Bundleable {
if (mediaMetadata.description != null) {
setDescription(mediaMetadata.description);
}
if (mediaMetadata.mediaUri != null) {
setMediaUri(mediaMetadata.mediaUri);
}
if (mediaMetadata.userRating != null) {
setUserRating(mediaMetadata.userRating);
}
......@@ -636,8 +625,6 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final CharSequence subtitle;
/** Optional description. */
@Nullable public final CharSequence description;
/** Optional media {@link Uri}. */
@Nullable public final Uri mediaUri;
/** Optional user {@link Rating}. */
@Nullable public final Rating userRating;
/** Optional overall {@link Rating}. */
......@@ -722,7 +709,6 @@ public final class MediaMetadata implements Bundleable {
this.displayTitle = builder.displayTitle;
this.subtitle = builder.subtitle;
this.description = builder.description;
this.mediaUri = builder.mediaUri;
this.userRating = builder.userRating;
this.overallRating = builder.overallRating;
this.artworkData = builder.artworkData;
......@@ -771,7 +757,6 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(displayTitle, that.displayTitle)
&& Util.areEqual(subtitle, that.subtitle)
&& Util.areEqual(description, that.description)
&& Util.areEqual(mediaUri, that.mediaUri)
&& Util.areEqual(userRating, that.userRating)
&& Util.areEqual(overallRating, that.overallRating)
&& Arrays.equals(artworkData, that.artworkData)
......@@ -807,7 +792,6 @@ public final class MediaMetadata implements Bundleable {
displayTitle,
subtitle,
description,
mediaUri,
userRating,
overallRating,
Arrays.hashCode(artworkData),
......@@ -918,7 +902,6 @@ public final class MediaMetadata implements Bundleable {
bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle);
bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle);
bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description);
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData);
bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri);
bundle.putCharSequence(keyForField(FIELD_WRITER), writer);
......@@ -992,7 +975,6 @@ public final class MediaMetadata implements Bundleable {
.setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE)))
.setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE)))
.setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION)))
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
.setArtworkData(
bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)),
bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE))
......
......@@ -87,6 +87,7 @@ public final class MimeTypes {
public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg";
......
......@@ -401,28 +401,6 @@ public class PlaybackException extends Exception implements Bundleable {
// Bundleable implementation.
/**
* Identifiers for fields in a {@link Bundle} which represents a playback exception. Subclasses
* may use {@link #FIELD_CUSTOM_ID_BASE} to generate more keys using {@link #keyForField(int)}.
*
* <p>Note: Changes to the Bundleable implementation must be backwards compatible, so as to avoid
* breaking communication across different Bundleable implementation versions.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
open = true,
value = {
FIELD_INT_ERROR_CODE,
FIELD_LONG_TIMESTAMP_MS,
FIELD_STRING_MESSAGE,
FIELD_STRING_CAUSE_CLASS_NAME,
FIELD_STRING_CAUSE_MESSAGE,
})
@UnstableApi
protected @interface FieldNumber {}
private static final int FIELD_INT_ERROR_CODE = 0;
private static final int FIELD_LONG_TIMESTAMP_MS = 1;
private static final int FIELD_STRING_MESSAGE = 2;
......@@ -430,7 +408,7 @@ public class PlaybackException extends Exception implements Bundleable {
private static final int FIELD_STRING_CAUSE_MESSAGE = 4;
/**
* Defines a minimum field id value for subclasses to use when implementing {@link #toBundle()}
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}.
*
* <p>Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative
......@@ -458,11 +436,14 @@ public class PlaybackException extends Exception implements Bundleable {
}
/**
* Converts the given {@link FieldNumber} to a string which can be used as a field key when
* implementing {@link #toBundle()} and {@link Bundleable.Creator}.
* Converts the given field number to a string which can be used as a field key when implementing
* {@link #toBundle()} and {@link Bundleable.Creator}.
*
* <p>Subclasses should use {@code field} values greater than or equal to {@link
* #FIELD_CUSTOM_ID_BASE}.
*/
@UnstableApi
protected static String keyForField(@FieldNumber int field) {
protected static String keyForField(int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
......
......@@ -32,7 +32,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
......@@ -294,7 +294,9 @@ public interface Player {
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_MEDIA_ITEM_INDEX), mediaItemIndex);
bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), BundleableUtil.toNullableBundle(mediaItem));
if (mediaItem != null) {
bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle());
}
bundle.putInt(keyForField(FIELD_PERIOD_INDEX), periodIndex);
bundle.putLong(keyForField(FIELD_POSITION_MS), positionMs);
bundle.putLong(keyForField(FIELD_CONTENT_POSITION_MS), contentPositionMs);
......@@ -309,10 +311,10 @@ public interface Player {
private static PositionInfo fromBundle(Bundle bundle) {
int mediaItemIndex =
bundle.getInt(keyForField(FIELD_MEDIA_ITEM_INDEX), /* defaultValue= */ C.INDEX_UNSET);
@Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM));
@Nullable
MediaItem mediaItem =
BundleableUtil.fromNullableBundle(
MediaItem.CREATOR, bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)));
mediaItemBundle == null ? null : MediaItem.CREATOR.fromBundle(mediaItemBundle);
int periodIndex =
bundle.getInt(keyForField(FIELD_PERIOD_INDEX), /* defaultValue= */ C.INDEX_UNSET);
long positionMs =
......@@ -382,6 +384,7 @@ public interface Player {
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
};
private final FlagSet.Builder flagsBuilder;
......@@ -1024,17 +1027,30 @@ public interface Player {
/**
* Called when there is a change in the {@link Cue Cues}.
*
* <p>{@code cues} is in ascending order of priority. If any of the cue boxes overlap when
* displayed, the {@link Cue} nearer the end of the list should be shown on top.
* <p>Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
* in the cues. You should only implement one or the other.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param cues The {@link Cue Cues}. May be empty.
* @deprecated Use {@link #onCues(CueGroup)} instead.
*/
@Deprecated
@UnstableApi
default void onCues(List<Cue> cues) {}
/**
* Called when there is a change in the {@link CueGroup}.
*
* <p>Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
* in the cues. You should only implement one or the other.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*/
default void onCues(CueGroup cueGroup) {}
/**
* Called when there is metadata associated with the current playback time.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
......@@ -1387,7 +1403,8 @@ public interface Player {
* #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link
* #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link
* #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link
* #COMMAND_SET_TRACK_SELECTION_PARAMETERS} or {@link #COMMAND_GET_TRACKS}.
* #COMMAND_SET_TRACK_SELECTION_PARAMETERS}, {@link #COMMAND_GET_TRACKS} or {@link
* #COMMAND_SET_MEDIA_ITEM}.
*/
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added.
......@@ -1426,6 +1443,7 @@ public interface Player {
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
})
@interface Command {}
/** Command to start, pause or resume playback. */
......@@ -1505,6 +1523,8 @@ public interface Player {
int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29;
/** Command to get details of the current track selection. */
int COMMAND_GET_TRACKS = 30;
/** Command to set a {@link MediaItem MediaItem}. */
int COMMAND_SET_MEDIA_ITEM = 31;
/** Represents an invalid {@link Command}. */
int COMMAND_INVALID = -1;
......@@ -2469,8 +2489,8 @@ public interface Player {
*/
VideoSize getVideoSize();
/** Returns the current {@link Cue Cues}. This list may be empty. */
List<Cue> getCurrentCues();
/** Returns the current {@link CueGroup}. */
CueGroup getCurrentCues();
/** Gets the device information. */
DeviceInfo getDeviceInfo();
......
......@@ -32,7 +32,7 @@ import java.lang.annotation.Target;
public abstract class Rating implements Bundleable {
/** A float value that denotes the rating is unset. */
public static final float RATING_UNSET = -1.0f;
/* package */ static final float RATING_UNSET = -1.0f;
// Default package-private constructor to prevent extending Rating class outside this package.
/* package */ Rating() {}
......
......@@ -189,11 +189,12 @@ public final class TrackGroup implements Bundleable {
@UnstableApi
public static final Creator<TrackGroup> CREATOR =
bundle -> {
@Nullable
List<Bundle> formatBundles = bundle.getParcelableArrayList(keyForField(FIELD_FORMATS));
List<Format> formats =
BundleableUtil.fromBundleNullableList(
Format.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)),
ImmutableList.of());
formatBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Format.CREATOR, formatBundles);
String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ "");
return new TrackGroup(id, formats.toArray(new Format[0]));
};
......
......@@ -16,7 +16,6 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.BundleableUtil.fromBundleNullableList;
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static com.google.common.base.MoreObjects.firstNonNull;
......@@ -25,18 +24,15 @@ import android.graphics.Point;
import android.os.Bundle;
import android.os.Looper;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
......@@ -247,11 +243,13 @@ public class TrackSelectionParameters implements Bundleable {
bundle.getBoolean(
keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE),
DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
@Nullable
List<Bundle> overrideBundleList =
bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES));
List<TrackSelectionOverride> overrideList =
fromBundleNullableList(
TrackSelectionOverride.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES)),
ImmutableList.of());
overrideBundleList == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(TrackSelectionOverride.CREATOR, overrideBundleList);
overrides = new HashMap<>();
for (int i = 0; i < overrideList.size(); i++) {
TrackSelectionOverride override = overrideList.get(i);
......@@ -1071,42 +1069,6 @@ public class TrackSelectionParameters implements Bundleable {
// Bundleable implementation
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
// Video
FIELD_MAX_VIDEO_WIDTH,
FIELD_MAX_VIDEO_HEIGHT,
FIELD_MAX_VIDEO_FRAMERATE,
FIELD_MAX_VIDEO_BITRATE,
FIELD_MIN_VIDEO_WIDTH,
FIELD_MIN_VIDEO_HEIGHT,
FIELD_MIN_VIDEO_FRAMERATE,
FIELD_MIN_VIDEO_BITRATE,
FIELD_VIEWPORT_WIDTH,
FIELD_VIEWPORT_HEIGHT,
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
FIELD_PREFERRED_VIDEO_MIMETYPES,
FIELD_PREFERRED_VIDEO_ROLE_FLAGS,
// Audio
FIELD_PREFERRED_AUDIO_LANGUAGES,
FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
FIELD_MAX_AUDIO_CHANNEL_COUNT,
FIELD_MAX_AUDIO_BITRATE,
FIELD_PREFERRED_AUDIO_MIME_TYPES,
// Text
FIELD_PREFERRED_TEXT_LANGUAGES,
FIELD_PREFERRED_TEXT_ROLE_FLAGS,
FIELD_IGNORED_TEXT_SELECTION_FLAGS,
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
// General
FIELD_FORCE_LOWEST_BITRATE,
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
FIELD_SELECTION_OVERRIDES,
FIELD_DISABLED_TRACK_TYPE,
})
private @interface FieldNumber {}
private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1;
private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2;
private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3;
......@@ -1134,7 +1096,15 @@ public class TrackSelectionParameters implements Bundleable {
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25;
private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26;
@UnstableApi
/**
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}.
*
* <p>Subclasses should obtain keys for their {@link Bundle} representation by applying a
* non-negative offset on this constant and passing the result to {@link #keyForField(int)}.
*/
@UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000;
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
......@@ -1184,12 +1154,27 @@ public class TrackSelectionParameters implements Bundleable {
return bundle;
}
/** Object that can restore {@code TrackSelectionParameters} from a {@link Bundle}. */
@UnstableApi
/** Construct an instance from a {@link Bundle} produced by {@link #toBundle()}. */
public static TrackSelectionParameters fromBundle(Bundle bundle) {
return new Builder(bundle).build();
}
/**
* @deprecated Use {@link #fromBundle(Bundle)} instead.
*/
@UnstableApi @Deprecated
public static final Creator<TrackSelectionParameters> CREATOR =
bundle -> new Builder(bundle).build();
TrackSelectionParameters::fromBundle;
private static String keyForField(@FieldNumber int field) {
/**
* Converts the given field number to a string which can be used as a field key when implementing
* {@link #toBundle()} and {@link Bundleable.Creator}.
*
* <p>Subclasses should use {@code field} values greater than or equal to {@link
* #FIELD_CUSTOM_ID_BASE}.
*/
@UnstableApi
protected static String keyForField(int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -17,14 +17,13 @@ package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.BundleableUtil.fromBundleNullableList;
import static androidx.media3.common.util.BundleableUtil.fromNullableBundle;
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
......@@ -252,10 +251,10 @@ public final class Tracks implements Bundleable {
@UnstableApi
public static final Creator<Group> CREATOR =
bundle -> {
// Can't create a Tracks.Group without a TrackGroup
TrackGroup trackGroup =
fromNullableBundle(
TrackGroup.CREATOR, bundle.getBundle(keyForField(FIELD_TRACK_GROUP)));
checkNotNull(trackGroup); // Can't create a trackGroup info without a trackGroup
TrackGroup.CREATOR.fromBundle(
checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))));
final @C.FormatSupport int[] trackSupport =
MoreObjects.firstNonNull(
bundle.getIntArray(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]);
......@@ -274,7 +273,7 @@ public final class Tracks implements Bundleable {
}
/** Empty tracks. */
@UnstableApi public static final Tracks EMPTY = new Tracks(ImmutableList.of());
public static final Tracks EMPTY = new Tracks(ImmutableList.of());
private final ImmutableList<Group> groups;
......@@ -408,11 +407,12 @@ public final class Tracks implements Bundleable {
@UnstableApi
public static final Creator<Tracks> CREATOR =
bundle -> {
@Nullable
List<Bundle> groupBundles = bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
List<Group> groups =
fromBundleNullableList(
Group.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS)),
/* defaultValue= */ ImmutableList.of());
groupBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Group.CREATOR, groupBundles);
return new Tracks(groups);
};
......
/*
* Copyright (C) 2022 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
*
* 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 androidx.media3.common.text;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.graphics.Bitmap;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
/** Class to represent the state of active {@link Cue Cues} at a particular time. */
public final class CueGroup implements Bundleable {
/** Empty {@link CueGroup}. */
@UnstableApi public static final CueGroup EMPTY = new CueGroup(ImmutableList.of());
/**
* The cues in this group.
*
* <p>This list is in ascending order of priority. If any of the cue boxes overlap when displayed,
* the {@link Cue} nearer the end of the list should be shown on top.
*
* <p>This list may be empty if the group represents a state with no cues.
*/
public final ImmutableList<Cue> cues;
/** Creates a CueGroup. */
@UnstableApi
public CueGroup(List<Cue> cues) {
this.cues = ImmutableList.copyOf(cues);
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_CUES})
private @interface FieldNumber {}
private static final int FIELD_CUES = 0;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
return bundle;
}
@UnstableApi public static final Creator<CueGroup> CREATOR = CueGroup::fromBundle;
private static final CueGroup fromBundle(Bundle bundle) {
@Nullable ArrayList<Bundle> cueBundles = bundle.getParcelableArrayList(keyForField(FIELD_CUES));
List<Cue> cues =
cueBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles);
return new CueGroup(cues);
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
/**
* Filters out {@link Cue} objects containing {@link Bitmap}. It is used when transferring cues
* between processes to prevent transferring too much data.
*/
private static ImmutableList<Cue> filterOutBitmapCues(List<Cue> cues) {
ImmutableList.Builder<Cue> builder = ImmutableList.builder();
for (int i = 0; i < cues.size(); i++) {
if (cues.get(i).bitmap != null) {
continue;
}
builder.add(cues.get(i));
}
return builder.build();
}
}
......@@ -31,34 +31,6 @@ import java.util.List;
@UnstableApi
public final class BundleableUtil {
/**
* Converts a {@link Bundleable} to a {@link Bundle}. It's a convenience wrapper of {@link
* Bundleable#toBundle} that can take nullable values.
*/
@Nullable
public static Bundle toNullableBundle(@Nullable Bundleable bundleable) {
return bundleable == null ? null : bundleable.toBundle();
}
/**
* Converts a {@link Bundle} to a {@link Bundleable}. It's a convenience wrapper of {@link
* Bundleable.Creator#fromBundle} that can take nullable values.
*/
@Nullable
public static <T extends Bundleable> T fromNullableBundle(
Bundleable.Creator<T> creator, @Nullable Bundle bundle) {
return bundle == null ? null : creator.fromBundle(bundle);
}
/**
* Converts a {@link Bundle} to a {@link Bundleable}. It's a convenience wrapper of {@link
* Bundleable.Creator#fromBundle} that provides default value to ensure non-null.
*/
public static <T extends Bundleable> T fromNullableBundle(
Bundleable.Creator<T> creator, @Nullable Bundle bundle, T defaultValue) {
return bundle == null ? defaultValue : creator.fromBundle(bundle);
}
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) {
ImmutableList.Builder<Bundle> builder = ImmutableList.builder();
......@@ -82,34 +54,6 @@ public final class BundleableUtil {
}
/**
* Converts a list of {@link Bundle} to a list of {@link Bundleable}. Returns {@code defaultValue}
* if {@code bundleList} is null.
*/
public static <T extends Bundleable> List<T> fromBundleNullableList(
Bundleable.Creator<T> creator, @Nullable List<Bundle> bundleList, List<T> defaultValue) {
return (bundleList == null) ? defaultValue : fromBundleList(creator, bundleList);
}
/**
* Converts a {@link SparseArray} of {@link Bundle} to a {@link SparseArray} of {@link
* Bundleable}. Returns {@code defaultValue} if {@code bundleSparseArray} is null.
*/
public static <T extends Bundleable> SparseArray<T> fromBundleNullableSparseArray(
Bundleable.Creator<T> creator,
@Nullable SparseArray<Bundle> bundleSparseArray,
SparseArray<T> defaultValue) {
if (bundleSparseArray == null) {
return defaultValue;
}
// Can't use ImmutableList as it doesn't support null elements.
SparseArray<T> result = new SparseArray<>(bundleSparseArray.size());
for (int i = 0; i < bundleSparseArray.size(); i++) {
result.put(bundleSparseArray.keyAt(i), creator.fromBundle(bundleSparseArray.valueAt(i)));
}
return result;
}
/**
* Converts a collection of {@link Bundleable} to an {@link ArrayList} of {@link Bundle} so that
* the returned list can be put to {@link Bundle} using {@link Bundle#putParcelableArrayList}
* conveniently.
......@@ -124,6 +68,19 @@ public final class BundleableUtil {
}
/**
* Converts a {@link SparseArray} of {@link Bundle} to a {@link SparseArray} of {@link
* Bundleable}.
*/
public static <T extends Bundleable> SparseArray<T> fromBundleSparseArray(
Bundleable.Creator<T> creator, SparseArray<Bundle> bundleSparseArray) {
SparseArray<T> result = new SparseArray<>(bundleSparseArray.size());
for (int i = 0; i < bundleSparseArray.size(); i++) {
result.put(bundleSparseArray.keyAt(i), creator.fromBundle(bundleSparseArray.valueAt(i)));
}
return result;
}
/**
* Converts a {@link SparseArray} of {@link Bundleable} to an {@link SparseArray} of {@link
* Bundle} so that the returned {@link SparseArray} can be put to {@link Bundle} using {@link
* Bundle#putSparseParcelableArray} conveniently.
......
......@@ -79,13 +79,6 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
/** A runtime exception to be thrown if some EGL operations failed. */
public static final class GlException extends RuntimeException {
private GlException(String msg) {
super(msg);
}
}
private final Handler handler;
private final int[] textureIdHolder;
@Nullable private final TextureImageListener callback;
......@@ -125,7 +118,7 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
*
* @param secureMode The {@link SecureMode} to be used for EGL surface.
*/
public void init(@SecureMode int secureMode) {
public void init(@SecureMode int secureMode) throws GlUtil.GlException {
display = getDefaultDisplay();
EGLConfig config = chooseEGLConfig(display);
context = createEGLContext(display, config, secureMode);
......@@ -206,22 +199,18 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
}
}
private static EGLDisplay getDefaultDisplay() {
private static EGLDisplay getDefaultDisplay() throws GlUtil.GlException {
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (display == null) {
throw new GlException("eglGetDisplay failed");
}
GlUtil.checkGlException(display != null, "eglGetDisplay failed");
int[] version = new int[2];
boolean eglInitialized =
EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);
if (!eglInitialized) {
throw new GlException("eglInitialize failed");
}
GlUtil.checkGlException(eglInitialized, "eglInitialize failed");
return display;
}
private static EGLConfig chooseEGLConfig(EGLDisplay display) {
private static EGLConfig chooseEGLConfig(EGLDisplay display) throws GlUtil.GlException {
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
boolean success =
......@@ -234,18 +223,17 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
/* config_size= */ 1,
numConfigs,
/* num_configOffset= */ 0);
if (!success || numConfigs[0] <= 0 || configs[0] == null) {
throw new GlException(
Util.formatInvariant(
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
success, numConfigs[0], configs[0]));
}
GlUtil.checkGlException(
success && numConfigs[0] > 0 && configs[0] != null,
Util.formatInvariant(
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
success, numConfigs[0], configs[0]));
return configs[0];
}
private static EGLContext createEGLContext(
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) throws GlUtil.GlException {
int[] glAttributes;
if (secureMode == SECURE_MODE_NONE) {
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
......@@ -262,14 +250,13 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
EGLContext context =
EGL14.eglCreateContext(
display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
if (context == null) {
throw new GlException("eglCreateContext failed");
}
GlUtil.checkGlException(context != null, "eglCreateContext failed");
return context;
}
private static EGLSurface createEGLSurface(
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode)
throws GlUtil.GlException {
EGLSurface surface;
if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
surface = EGL14.EGL_NO_SURFACE;
......@@ -297,20 +284,16 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
};
}
surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
if (surface == null) {
throw new GlException("eglCreatePbufferSurface failed");
}
GlUtil.checkGlException(surface != null, "eglCreatePbufferSurface failed");
}
boolean eglMadeCurrent =
EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
if (!eglMadeCurrent) {
throw new GlException("eglMakeCurrent failed");
}
GlUtil.checkGlException(eglMadeCurrent, "eglMakeCurrent failed");
return surface;
}
private static void generateTextureIds(int[] textureIdHolder) {
private static void generateTextureIds(int[] textureIdHolder) throws GlUtil.GlException {
GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
GlUtil.checkGlError();
}
......
......@@ -54,7 +54,7 @@ public final class GlProgram {
* @throws IOException When failing to read shader files.
*/
public GlProgram(Context context, String vertexShaderFilePath, String fragmentShaderFilePath)
throws IOException {
throws IOException, GlUtil.GlException {
this(
GlUtil.loadAsset(context, vertexShaderFilePath),
GlUtil.loadAsset(context, fragmentShaderFilePath));
......@@ -69,7 +69,7 @@ public final class GlProgram {
* @param vertexShaderGlsl The vertex shader program.
* @param fragmentShaderGlsl The fragment shader program.
*/
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) {
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) throws GlUtil.GlException {
programId = GLES20.glCreateProgram();
GlUtil.checkGlError();
......@@ -81,10 +81,9 @@ public final class GlProgram {
GLES20.glLinkProgram(programId);
int[] linkStatus = new int[] {GLES20.GL_FALSE};
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
GlUtil.throwGlException(
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
}
GlUtil.checkGlException(
linkStatus[0] == GLES20.GL_TRUE,
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
GLES20.glUseProgram(programId);
attributeByName = new HashMap<>();
int[] attributeCount = new int[1];
......@@ -107,16 +106,15 @@ public final class GlProgram {
GlUtil.checkGlError();
}
private static void addShader(int programId, int type, String glsl) {
private static void addShader(int programId, int type, String glsl) throws GlUtil.GlException {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, glsl);
GLES20.glCompileShader(shader);
int[] result = new int[] {GLES20.GL_FALSE};
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, /* offset= */ 0);
if (result[0] != GLES20.GL_TRUE) {
GlUtil.throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
}
GlUtil.checkGlException(
result[0] == GLES20.GL_TRUE, GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
GLES20.glAttachShader(programId, shader);
GLES20.glDeleteShader(shader);
......@@ -146,13 +144,13 @@ public final class GlProgram {
*
* <p>Call this in the rendering loop to switch between different programs.
*/
public void use() {
public void use() throws GlUtil.GlException {
GLES20.glUseProgram(programId);
GlUtil.checkGlError();
}
/** Deletes the program. Deleted programs cannot be used again. */
public void delete() {
public void delete() throws GlUtil.GlException {
GLES20.glDeleteProgram(programId);
GlUtil.checkGlError();
}
......@@ -161,7 +159,7 @@ public final class GlProgram {
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
* array.
*/
public int getAttributeArrayLocationAndEnable(String attributeName) {
public int getAttributeArrayLocationAndEnable(String attributeName) throws GlUtil.GlException {
int location = getAttributeLocation(attributeName);
GLES20.glEnableVertexAttribArray(location);
GlUtil.checkGlError();
......@@ -196,7 +194,7 @@ public final class GlProgram {
}
/** Binds all attributes and uniforms in the program. */
public void bindAttributesAndUniforms() {
public void bindAttributesAndUniforms() throws GlUtil.GlException {
for (Attribute attribute : attributes) {
attribute.bind();
}
......@@ -277,7 +275,7 @@ public final class GlProgram {
*
* <p>Should be called before each drawing call.
*/
public void bind() {
public void bind() throws GlUtil.GlException {
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
GLES20.glVertexAttribPointer(
......@@ -363,7 +361,7 @@ public final class GlProgram {
*
* <p>Should be called before each drawing call.
*/
public void bind() {
public void bind() throws GlUtil.GlException {
switch (type) {
case GLES20.GL_FLOAT:
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
......
......@@ -25,8 +25,8 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.Looper;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyCallback.DisplayInfoListener;
import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import androidx.annotation.GuardedBy;
......@@ -59,24 +59,6 @@ public final class NetworkTypeObserver {
void onNetworkTypeChanged(@C.NetworkType int networkType);
}
/*
* Static configuration that may need to be set at app startup time is located in a separate
* static Config class. This allows apps to set their desired config without incurring unnecessary
* class loading costs during startup.
*/
/** Configuration for {@link NetworkTypeObserver}. */
public static final class Config {
private static volatile boolean disable5GNsaDisambiguation;
/** Disables logic to disambiguate 5G-NSA networks from 4G networks. */
public static void disable5GNsaDisambiguation() {
disable5GNsaDisambiguation = true;
}
private Config() {}
}
@Nullable private static NetworkTypeObserver staticInstance;
private final Handler mainHandler;
......@@ -232,55 +214,51 @@ public final class NetworkTypeObserver {
@Override
public void onReceive(Context context, Intent intent) {
@C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context);
if (Util.SDK_INT >= 29
&& !Config.disable5GNsaDisambiguation
&& networkType == C.NETWORK_TYPE_4G) {
if (Util.SDK_INT >= 31 && networkType == C.NETWORK_TYPE_4G) {
// Delay update of the network type to check whether this is actually 5G-NSA.
try {
// We can't access TelephonyManager getters like getServiceState() directly as they
// require special permissions. Attaching a listener is permission-free because the
// callback data is censored to not include sensitive information.
TelephonyManager telephonyManager =
checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
TelephonyManagerListener listener = new TelephonyManagerListener();
if (Util.SDK_INT < 31) {
telephonyManager.listen(listener, PhoneStateListener.LISTEN_SERVICE_STATE);
} else {
// Display info information can only be requested without permission from API 31.
telephonyManager.listen(listener, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED);
}
// We are only interested in the initial response with the current state, so unregister
// the listener immediately.
telephonyManager.listen(listener, PhoneStateListener.LISTEN_NONE);
return;
} catch (RuntimeException e) {
// Ignore problems with listener registration and keep reporting as 4G.
}
Api31.disambiguate4gAnd5gNsa(context, /* instance= */ NetworkTypeObserver.this);
} else {
updateNetworkType(networkType);
}
updateNetworkType(networkType);
}
}
private class TelephonyManagerListener extends PhoneStateListener {
@RequiresApi(31)
private static final class Api31 {
@Override
public void onServiceStateChanged(@Nullable ServiceState serviceState) {
// This workaround to check the toString output of ServiceState only works on API 29 and 30.
String serviceStateString = serviceState == null ? "" : serviceState.toString();
boolean is5gNsa =
serviceStateString.contains("nrState=CONNECTED")
|| serviceStateString.contains("nrState=NOT_RESTRICTED");
updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
public static void disambiguate4gAnd5gNsa(Context context, NetworkTypeObserver instance) {
try {
TelephonyManager telephonyManager =
checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
DisplayInfoCallback callback = new DisplayInfoCallback(instance);
telephonyManager.registerTelephonyCallback(context.getMainExecutor(), callback);
// We are only interested in the initial response with the current state, so unregister
// the listener immediately.
telephonyManager.unregisterTelephonyCallback(callback);
} catch (RuntimeException e) {
// Ignore problems with listener registration and keep reporting as 4G.
instance.updateNetworkType(C.NETWORK_TYPE_4G);
}
}
@RequiresApi(31)
@Override
public void onDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo) {
int overrideNetworkType = telephonyDisplayInfo.getOverrideNetworkType();
boolean is5gNsa =
overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA
|| overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE;
updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
private static final class DisplayInfoCallback extends TelephonyCallback
implements DisplayInfoListener {
private final NetworkTypeObserver instance;
public DisplayInfoCallback(NetworkTypeObserver instance) {
this.instance = instance;
}
@Override
public void onDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo) {
int overrideNetworkType = telephonyDisplayInfo.getOverrideNetworkType();
boolean is5gNsa =
overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA
|| overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE
|| overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED;
instance.updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
}
}
}
}
......@@ -29,7 +29,7 @@ public class AudioAttributesTest {
public void roundTripViaBundle_yieldsEqualInstance() {
AudioAttributes audioAttributes =
new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SONIFICATION)
.setContentType(C.AUDIO_CONTENT_TYPE_SONIFICATION)
.setFlags(C.FLAG_AUDIBILITY_ENFORCED)
.setUsage(C.USAGE_ALARM)
.setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM)
......
......@@ -18,22 +18,17 @@ package androidx.media3.common;
import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED;
import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION;
import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import androidx.media3.test.utils.StubPlayer;
import androidx.media3.test.utils.TestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -105,7 +100,7 @@ public class ForwardingPlayerTest {
@Test
public void forwardingPlayer_overridesAllPlayerMethods() throws Exception {
// Check with reflection that ForwardingPlayer overrides all Player methods.
List<Method> methods = getPublicMethods(Player.class);
List<Method> methods = TestUtil.getPublicMethods(Player.class);
for (Method method : methods) {
assertThat(
ForwardingPlayer.class
......@@ -119,7 +114,7 @@ public class ForwardingPlayerTest {
public void forwardingListener_overridesAllListenerMethods() throws Exception {
// Check with reflection that ForwardingListener overrides all Listener methods.
Class<?> forwardingListenerClass = getInnerClass("ForwardingListener");
List<Method> methods = getPublicMethods(Player.Listener.class);
List<Method> methods = TestUtil.getPublicMethods(Player.Listener.class);
for (Method method : methods) {
assertThat(
forwardingListenerClass
......@@ -129,32 +124,6 @@ public class ForwardingPlayerTest {
}
}
/** Returns all the public methods of a Java interface. */
private static List<Method> getPublicMethods(Class<?> anInterface) {
checkArgument(anInterface.isInterface());
// Run a BFS over all extended interfaces to inspect them all.
Queue<Class<?>> interfacesQueue = new ArrayDeque<>();
interfacesQueue.add(anInterface);
Set<Class<?>> interfaces = new HashSet<>();
while (!interfacesQueue.isEmpty()) {
Class<?> currentInterface = interfacesQueue.remove();
if (interfaces.add(currentInterface)) {
Collections.addAll(interfacesQueue, currentInterface.getInterfaces());
}
}
List<Method> list = new ArrayList<>();
for (Class<?> currentInterface : interfaces) {
for (Method method : currentInterface.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
list.add(method);
}
}
}
return list;
}
private static Class<?> getInnerClass(String className) {
for (Class<?> innerClass : ForwardingPlayer.class.getDeclaredClasses()) {
if (innerClass.getSimpleName().equals(className)) {
......
......@@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import android.os.Bundle;
import androidx.media3.common.MediaItem.RequestMetadata;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
......@@ -223,7 +225,7 @@ public class MediaItemTest {
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(licenseUri)
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.forceSessionsForAudioAndVideoTracks(true)
.setForceSessionsForAudioAndVideoTracks(true)
.build();
assertThat(drmConfiguration.sessionForClearTypes)
......@@ -580,6 +582,24 @@ public class MediaItemTest {
}
@Test
public void builder_setRequestMetadata_setsRequestMetadata() {
Bundle extras = new Bundle();
extras.putString("key", "value");
RequestMetadata requestMetadata =
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("Play media!")
.setExtras(extras)
.build();
MediaItem mediaItem =
new MediaItem.Builder().setMediaId("mediaID").setRequestMetadata(requestMetadata).build();
assertThat(mediaItem.requestMetadata).isEqualTo(requestMetadata);
assertThat(mediaItem.requestMetadata.extras.getString("key")).isEqualTo("value");
}
@Test
@SuppressWarnings("deprecation") // Testing deprecated setter methods
public void buildUpon_individualSetters_equalsToOriginal() {
MediaItem mediaItem =
......@@ -677,6 +697,11 @@ public class MediaItemTest {
.setLabel("label")
.setId("id")
.build()))
.setRequestMetadata(
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("search")
.build())
.setTag(new Object())
.build();
......@@ -704,6 +729,11 @@ public class MediaItemTest {
.setClipRelativeToDefaultPosition(true)
.setClipRelativeToLiveWindow(true)
.setClipStartsAtKeyFrame(true)
.setRequestMetadata(
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("search")
.build())
.build();
assertThat(mediaItem.localConfiguration).isNull();
......
......@@ -41,7 +41,6 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.displayTitle).isNull();
assertThat(mediaMetadata.subtitle).isNull();
assertThat(mediaMetadata.description).isNull();
assertThat(mediaMetadata.mediaUri).isNull();
assertThat(mediaMetadata.userRating).isNull();
assertThat(mediaMetadata.overallRating).isNull();
assertThat(mediaMetadata.artworkData).isNull();
......@@ -127,7 +126,6 @@ public class MediaMetadataTest {
.setDisplayTitle("display title")
.setSubtitle("subtitle")
.setDescription("description")
.setMediaUri(Uri.parse("https://www.google.com"))
.setUserRating(new HeartRating(false))
.setOverallRating(new PercentageRating(87.4f))
.setArtworkData(
......
......@@ -201,7 +201,7 @@ public final class TrackSelectionParametersTest {
new TrackSelectionParameters.Builder(getApplicationContext()).addOverride(override).build();
TrackSelectionParameters fromBundle =
TrackSelectionParameters.CREATOR.fromBundle(trackSelectionParameters.toBundle());
TrackSelectionParameters.fromBundle(trackSelectionParameters.toBundle());
assertThat(fromBundle).isEqualTo(trackSelectionParameters);
assertThat(trackSelectionParameters.overrides)
......
/*
* Copyright (C) 2022 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
*
* 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 androidx.media3.common.text;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Parcel;
import android.text.SpannedString;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link CueGroup}. */
@RunWith(AndroidJUnit4.class)
public class CueGroupTest {
@Test
public void bundleAndUnBundleCueGroup() {
Cue textCue = new Cue.Builder().setText(SpannedString.valueOf("text")).build();
Cue bitmapCue =
new Cue.Builder().setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)).build();
ImmutableList<Cue> cues = ImmutableList.of(textCue, bitmapCue);
CueGroup cueGroup = new CueGroup(cues);
Parcel parcel = Parcel.obtain();
try {
parcel.writeBundle(cueGroup.toBundle());
parcel.setDataPosition(0);
Bundle bundle = parcel.readBundle();
CueGroup filteredCueGroup = CueGroup.CREATOR.fromBundle(bundle);
assertThat(filteredCueGroup.cues).containsExactly(textCue);
} finally {
parcel.recycle();
}
}
}
......@@ -103,50 +103,109 @@ public class UtilTest {
@Test
public void inferContentType_handlesHlsIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
}
@Test
public void inferContentType_handlesHlsIsmV3Uris() {
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
}
@Test
public void inferContentType_handlesDashIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf)"))
.isEqualTo(C.TYPE_DASH);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)"))
.isEqualTo(C.TYPE_DASH);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)"))
.isEqualTo(C.TYPE_DASH);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest(format=mpd-time-csf)")))
.isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)")))
.isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)")))
.isEqualTo(C.CONTENT_TYPE_DASH);
}
@Test
public void inferContentType_handlesSmoothStreamingIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.ism/")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest_hd")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/Manifest")))
.isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest")))
.isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest(filter=x)")))
.isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest_hd")))
.isEqualTo(C.CONTENT_TYPE_SS);
}
@Test
public void inferContentType_handlesOtherIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism/video.mp4")).isEqualTo(C.TYPE_OTHER);
assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/video.mp4")))
.isEqualTo(C.CONTENT_TYPE_OTHER);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/prefix-manifest")))
.isEqualTo(C.CONTENT_TYPE_OTHER);
}
/**
* Test that the deprecated {@link Util#inferContentType(String)} works when passed only a file
* extension and the leading dot.
*/
@SuppressWarnings("deprecation")
@Test
public void inferContentType_extensionAsPath() {
assertThat(Util.inferContentType(".m3u8")).isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(Util.inferContentType(".mpd")).isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(Util.inferContentType(".ism")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType(".isml")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType(".mp4")).isEqualTo(C.CONTENT_TYPE_OTHER);
}
// Testing deprecated method.
@SuppressWarnings("deprecation")
@Test
public void inferContentType_extensionOverride() {
assertThat(
Util.inferContentType(
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ null))
.isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ ""))
.isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ "m3u8"))
.isEqualTo(C.CONTENT_TYPE_HLS);
}
@Test
public void inferContentTypeForExtension() {
assertThat(Util.inferContentTypeForExtension("m3u8")).isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(Util.inferContentTypeForExtension("mpd")).isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(Util.inferContentTypeForExtension("ism")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentTypeForExtension("isml")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentTypeForExtension("mp4")).isEqualTo(C.CONTENT_TYPE_OTHER);
}
@Test
......@@ -1103,6 +1162,7 @@ public class UtilTest {
assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl");
// Special case of short codes that are actually part of a macrolanguage.
assertThat(Util.normalizeLanguageCode("arb")).isEqualTo("ar-arb");
assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob");
assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno");
assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob");
......
......@@ -21,19 +21,13 @@ import static java.lang.Math.min;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.ApplicationMediaCapabilities;
import android.media.MediaFeature;
import android.media.MediaFormat;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
......@@ -78,6 +72,7 @@ public final class ContentDataSource extends BaseDataSource {
}
@Override
@SuppressWarnings("InlinedApi") // We are inlining EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT.
public long open(DataSpec dataSpec) throws ContentDataSourceException {
try {
Uri uri = dataSpec.uri;
......@@ -88,9 +83,8 @@ public final class ContentDataSource extends BaseDataSource {
AssetFileDescriptor assetFileDescriptor;
if ("content".equals(dataSpec.uri.getScheme())) {
Bundle providerOptions = new Bundle();
if (Util.SDK_INT >= 31) {
Api31.disableTranscoding(providerOptions);
}
// We don't want compatible media transcoding.
providerOptions.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
assetFileDescriptor =
resolver.openTypedAssetFileDescriptor(uri, /* mimeType= */ "*/*", providerOptions);
} else {
......@@ -232,21 +226,4 @@ public final class ContentDataSource extends BaseDataSource {
}
}
}
@RequiresApi(31)
private static final class Api31 {
@DoNotInline
public static void disableTranscoding(Bundle providerOptions) {
ApplicationMediaCapabilities mediaCapabilities =
new ApplicationMediaCapabilities.Builder()
.addSupportedVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC)
.addSupportedHdrType(MediaFeature.HdrType.DOLBY_VISION)
.addSupportedHdrType(MediaFeature.HdrType.HDR10)
.addSupportedHdrType(MediaFeature.HdrType.HDR10_PLUS)
.addSupportedHdrType(MediaFeature.HdrType.HLG)
.build();
providerOptions.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, mediaCapabilities);
}
}
}
......@@ -418,7 +418,7 @@ public interface HttpDataSource extends DataSource {
@Nullable public final String responseMessage;
/** An unmodifiable map of the response header fields and values. */
public final Map<String, List<String>> headerFields;
@UnstableApi public final Map<String, List<String>> headerFields;
/** The response body. */
public final byte[] responseBody;
......
......@@ -481,11 +481,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
* predicate that was previously set.
* @deprecated Use {@link CronetDataSource.Factory#setContentTypePredicate(Predicate)} instead.
*/
@UnstableApi
@Deprecated
......
......@@ -32,10 +32,14 @@ import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSourceException;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException;
import androidx.media3.datasource.HttpDataSource.InvalidContentTypeException;
import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException;
import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener;
import com.google.common.base.Predicate;
import com.google.common.net.HttpHeaders;
import com.google.common.util.concurrent.SettableFuture;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
......@@ -43,8 +47,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
......@@ -299,8 +305,9 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
Request request = makeRequest(dataSpec);
Response response;
ResponseBody responseBody;
Call call = callFactory.newCall(request);
try {
this.response = callFactory.newCall(request).execute();
this.response = executeCall(call);
response = this.response;
responseBody = Assertions.checkNotNull(response.body());
responseByteStream = responseBody.byteStream();
......@@ -449,6 +456,35 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* This method is an interrupt safe replacement of OkHttp Call.execute() which can get in bad
* states if interrupted while writing to the shared connection socket.
*/
private Response executeCall(Call call) throws IOException {
SettableFuture<Response> future = SettableFuture.create();
call.enqueue(
new Callback() {
@Override
public void onFailure(Call call, IOException e) {
future.setException(e);
}
@Override
public void onResponse(Call call, Response response) {
future.set(response);
}
});
try {
return future.get();
} catch (InterruptedException e) {
call.cancel();
throw new InterruptedIOException();
} catch (ExecutionException ee) {
throw new IOException(ee);
}
}
/**
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
......
......@@ -16,6 +16,7 @@
package androidx.media3.decoder.opus;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
......@@ -26,7 +27,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -69,11 +69,6 @@ public final class OpusDecoderTest {
private static final ImmutableList<byte[]> FULL_INITIALIZATION_DATA =
ImmutableList.of(HEADER, CUSTOM_PRE_SKIP_BYTES, CUSTOM_SEEK_PRE_ROLL_BYTES);
@Before
public void setUp() {
assertThat(LOADER.isAvailable()).isTrue();
}
@Test
public void getChannelCount() {
int channelCount = OpusDecoder.getChannelCount(HEADER);
......@@ -120,6 +115,7 @@ public final class OpusDecoderTest {
@Test
public void decode_removesPreSkipFromOutput() throws OpusDecoderException {
assumeTrue(LOADER.isAvailable());
OpusDecoder decoder =
new OpusDecoder(
/* numInputBuffers= */ 0,
......@@ -139,6 +135,7 @@ public final class OpusDecoderTest {
@Test
public void decode_whenDiscardPaddingDisabled_returnsDiscardPadding()
throws OpusDecoderException {
assumeTrue(LOADER.isAvailable());
OpusDecoder decoder =
new OpusDecoder(
/* numInputBuffers= */ 0,
......@@ -159,6 +156,7 @@ public final class OpusDecoderTest {
@Test
public void decode_whenDiscardPaddingEnabled_removesDiscardPadding() throws OpusDecoderException {
assumeTrue(LOADER.isAvailable());
OpusDecoder decoder =
new OpusDecoder(
/* numInputBuffers= */ 0,
......@@ -200,8 +198,12 @@ public final class OpusDecoderTest {
return ImmutableList.of(HEADER, preSkip, CUSTOM_SEEK_PRE_ROLL_BYTES);
}
// The cast to ByteBuffer is required for Java 8 compatibility. See
// https://issues.apache.org/jira/browse/MRESOLVER-85
@SuppressWarnings("UnnecessaryCast")
private static ByteBuffer createSupplementalData(long value) {
return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind();
return (ByteBuffer)
ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind();
}
private static DecoderInputBuffer createInputBuffer(
......
......@@ -25,6 +25,7 @@ import androidx.media3.common.MediaItem.SubtitleConfiguration;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.exoplayer.source.ClippingMediaSource;
import androidx.test.ext.junit.runners.AndroidJUnit4;
......@@ -135,8 +136,8 @@ public final class ClippedPlaybackTest {
}
@Override
public void onCues(List<Cue> cues) {
this.cues.add(cues);
public void onCues(CueGroup cueGroup) {
this.cues.add(cueGroup.cues);
}
@Override
......
......@@ -276,7 +276,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
boolean willPauseWhenDucked = willPauseWhenDucked();
audioFocusRequest =
builder
.setAudioAttributes(checkNotNull(audioAttributes).getAudioAttributesV21())
.setAudioAttributes(
checkNotNull(audioAttributes).getAudioAttributesV21().audioAttributes)
.setWillPauseWhenDucked(willPauseWhenDucked)
.setOnAudioFocusChangeListener(focusListener)
.build();
......@@ -298,7 +299,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
private boolean willPauseWhenDucked() {
return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH;
return audioAttributes != null && audioAttributes.contentType == C.AUDIO_CONTENT_TYPE_SPEECH;
}
/**
......@@ -369,7 +370,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Special usages:
case C.USAGE_ASSISTANCE_ACCESSIBILITY:
if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) {
if (audioAttributes.contentType == C.AUDIO_CONTENT_TYPE_SPEECH) {
// Voice shouldn't be interrupted by other playback.
return AUDIOFOCUS_GAIN_TRANSIENT;
}
......
......@@ -485,6 +485,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
}
try {
Class<?> clazz = Class.forName("androidx.media3.decoder.midi.MidiRenderer");
Constructor<?> constructor = clazz.getConstructor();
Renderer renderer = (Renderer) constructor.newInstance();
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded MidiRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
// The extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating MIDI extension", e);
}
try {
// Full class names used for constructor args so the LINT rule triggers if any of them move.
Class<?> clazz = Class.forName("androidx.media3.decoder.opus.LibopusAudioRenderer");
Constructor<?> constructor =
......
......@@ -33,7 +33,6 @@ import androidx.media3.common.Format;
import androidx.media3.common.MediaPeriodId;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.MediaSource;
......@@ -255,9 +254,9 @@ public final class ExoPlaybackException extends PlaybackException {
rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME));
rendererIndex =
bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET);
@Nullable Bundle rendererFormatBundle = bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT));
rendererFormat =
BundleableUtil.fromNullableBundle(
Format.CREATOR, bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT)));
rendererFormatBundle == null ? null : Format.CREATOR.fromBundle(rendererFormatBundle);
rendererFormatSupport =
bundle.getInt(
keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED);
......@@ -424,8 +423,9 @@ public final class ExoPlaybackException extends PlaybackException {
bundle.putInt(keyForField(FIELD_TYPE), type);
bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName);
bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex);
bundle.putBundle(
keyForField(FIELD_RENDERER_FORMAT), BundleableUtil.toNullableBundle(rendererFormat));
if (rendererFormat != null) {
bundle.putBundle(keyForField(FIELD_RENDERER_FORMAT), rendererFormat.toBundle());
}
bundle.putInt(keyForField(FIELD_RENDERER_FORMAT_SUPPORT), rendererFormatSupport);
bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable);
return bundle;
......
......@@ -40,7 +40,7 @@ import androidx.media3.common.PriorityTaskManager;
import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
......@@ -358,7 +358,7 @@ public interface ExoPlayer extends Player {
* @deprecated Use {@link Player#getCurrentCues()} instead.
*/
@Deprecated
List<Cue> getCurrentCues();
CueGroup getCurrentCues();
}
/**
......@@ -1175,7 +1175,6 @@ public interface ExoPlayer extends Player {
*
* @param listener The listener to be added.
*/
@UnstableApi
void addAnalyticsListener(AnalyticsListener listener);
/**
......@@ -1183,7 +1182,6 @@ public interface ExoPlayer extends Player {
*
* @param listener The listener to be removed.
*/
@UnstableApi
void removeAnalyticsListener(AnalyticsListener listener);
/** Returns the number of renderers. */
......
......@@ -73,6 +73,7 @@ import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
......@@ -196,7 +197,7 @@ import java.util.concurrent.TimeoutException;
private AudioAttributes audioAttributes;
private float volume;
private boolean skipSilenceEnabled;
private List<Cue> currentCues;
private CueGroup currentCueGroup;
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
@Nullable private CameraMotionListener cameraMotionListener;
private boolean throwsWhenUsingWrongThread;
......@@ -302,7 +303,8 @@ import java.util.concurrent.TimeoutException;
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT)
COMMAND_GET_TEXT,
COMMAND_SET_MEDIA_ITEM)
.addIf(
COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported())
.build();
......@@ -353,7 +355,7 @@ import java.util.concurrent.TimeoutException;
} else {
audioSessionId = Util.generateAudioSessionIdV21(applicationContext);
}
currentCues = ImmutableList.of();
currentCueGroup = CueGroup.EMPTY;
throwsWhenUsingWrongThread = true;
addListener(analyticsCollector);
......@@ -378,6 +380,7 @@ import java.util.concurrent.TimeoutException;
deviceInfo = createDeviceInfo(streamVolumeManager);
videoSize = VideoSize.UNKNOWN;
trackSelector.setAudioAttributes(audioAttributes);
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes);
......@@ -936,7 +939,7 @@ import java.util.concurrent.TimeoutException;
verifyApplicationThread();
audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE);
stopInternal(reset, /* error= */ null);
currentCues = ImmutableList.of();
currentCueGroup = CueGroup.EMPTY;
}
@Override
......@@ -980,6 +983,7 @@ import java.util.concurrent.TimeoutException;
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
playbackInfo.totalBufferedDurationUs = 0;
analyticsCollector.release();
trackSelector.release();
removeSurfaceCallbacks();
if (ownedSurface != null) {
ownedSurface.release();
......@@ -989,7 +993,7 @@ import java.util.concurrent.TimeoutException;
checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);
isPriorityTaskManagerRegistered = false;
}
currentCues = ImmutableList.of();
currentCueGroup = CueGroup.EMPTY;
playerReleased = true;
}
......@@ -1372,6 +1376,7 @@ import java.util.concurrent.TimeoutException;
}
audioFocusManager.setAudioAttributes(handleAudioFocus ? newAudioAttributes : null);
trackSelector.setAudioAttributes(newAudioAttributes);
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
......@@ -1586,9 +1591,9 @@ import java.util.concurrent.TimeoutException;
}
@Override
public List<Cue> getCurrentCues() {
public CueGroup getCurrentCues() {
verifyApplicationThread();
return currentCues;
return currentCueGroup;
}
@Override
......@@ -2849,13 +2854,17 @@ import java.util.concurrent.TimeoutException;
}
// TextOutput implementation
@Override
public void onCues(List<Cue> cues) {
currentCues = cues;
listeners.sendEvent(EVENT_CUES, listener -> listener.onCues(cues));
}
@Override
public void onCues(CueGroup cueGroup) {
currentCueGroup = cueGroup;
listeners.sendEvent(EVENT_CUES, listener -> listener.onCues(cueGroup));
}
// MetadataOutput implementation
@Override
......
......@@ -169,15 +169,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int ACTIVE_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;
/**
* Duration under which pausing the main DO_SOME_WORK loop is not expected to yield significant
* power saving.
*
* <p>This value is probably too high, power measurements are needed adjust it, but as renderer
* sleep is currently only implemented for audio offload, which uses buffer much bigger than 2s,
* this does not matter for now.
*/
private static final long MIN_RENDERER_SLEEP_DURATION_MS = 2000;
/**
* Duration for which the player needs to appear stuck before the playback is failed on the
* assumption that no further progress will be made. To appear stuck, the player's renderers must
* not be ready, there must be more media available to load, and the LoadControl must be refusing
......@@ -2486,11 +2477,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
Renderer.MSG_SET_WAKEUP_LISTENER,
new Renderer.WakeupListener() {
@Override
public void onSleep(long wakeupDeadlineMs) {
// Do not sleep if the expected sleep time is not long enough to save significant power.
if (wakeupDeadlineMs >= MIN_RENDERER_SLEEP_DURATION_MS) {
requestForRendererSleep = true;
}
public void onSleep() {
requestForRendererSleep = true;
}
@Override
......
......@@ -66,11 +66,8 @@ public interface Renderer extends PlayerMessage.Target {
* The renderer no longer needs to render until the next wakeup.
*
* <p>Must be called from the thread ExoPlayer invokes the renderer from.
*
* @param wakeupDeadlineMs Maximum time in milliseconds until {@link #onWakeup()} will be
* called.
*/
void onSleep(long wakeupDeadlineMs);
void onSleep();
/**
* The renderer needs to render some frames. The client should call {@link #render(long, long)}
......
......@@ -33,12 +33,13 @@ import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.PriorityTaskManager;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.UnstableApi;
......@@ -426,24 +427,44 @@ public class SimpleExoPlayer extends BasePlayer
return player.experimentalIsSleepingForOffload();
}
/**
* @deprecated Use {@link ExoPlayer}, as the {@link AudioComponent} methods are defined by that
* interface.
*/
@Deprecated
@Override
@Nullable
public AudioComponent getAudioComponent() {
return this;
}
/**
* @deprecated Use {@link ExoPlayer}, as the {@link VideoComponent} methods are defined by that
* interface.
*/
@Deprecated
@Override
@Nullable
public VideoComponent getVideoComponent() {
return this;
}
/**
* @deprecated Use {@link Player}, as the {@link TextComponent} methods are defined by that
* interface.
*/
@Deprecated
@Override
@Nullable
public TextComponent getTextComponent() {
return this;
}
/**
* @deprecated Use {@link Player}, as the {@link DeviceComponent} methods are defined by that
* interface.
*/
@Deprecated
@Override
@Nullable
public DeviceComponent getDeviceComponent() {
......@@ -690,7 +711,7 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public List<Cue> getCurrentCues() {
public CueGroup getCurrentCues() {
blockUntilConstructorFinished();
return player.getCurrentCues();
}
......@@ -1003,6 +1024,11 @@ public class SimpleExoPlayer extends BasePlayer
player.stop();
}
/**
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@Deprecated
@Override
public void stop(boolean reset) {
......@@ -1046,12 +1072,20 @@ public class SimpleExoPlayer extends BasePlayer
return player.getTrackSelector();
}
/**
* @deprecated Use {@link #getCurrentTracks()}.
*/
@Deprecated
@Override
public TrackGroupArray getCurrentTrackGroups() {
blockUntilConstructorFinished();
return player.getCurrentTrackGroups();
}
/**
* @deprecated Use {@link #getCurrentTracks()}.
*/
@Deprecated
@Override
public TrackSelectionArray getCurrentTrackSelections() {
blockUntilConstructorFinished();
......@@ -1166,6 +1200,9 @@ public class SimpleExoPlayer extends BasePlayer
return player.getContentBufferedPosition();
}
/**
* @deprecated Use {@link #setWakeMode(int)} instead.
*/
@Deprecated
@Override
public void setHandleWakeLock(boolean handleWakeLock) {
......
......@@ -42,6 +42,7 @@ import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet;
......@@ -695,6 +696,7 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
listener -> listener.onMetadata(eventTime, metadata));
}
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
@Override
public void onCues(List<Cue> cues) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
......@@ -702,6 +704,13 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues));
}
@Override
public void onCues(CueGroup cueGroup) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cueGroup));
}
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
@Override
public final void onSeekProcessed() {
......
......@@ -679,13 +679,13 @@ public final class MediaMetricsListener
Util.inferContentTypeForUriAndMimeType(
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
switch (contentType) {
case C.TYPE_HLS:
case C.CONTENT_TYPE_HLS:
return PlaybackMetrics.STREAM_TYPE_HLS;
case C.TYPE_DASH:
case C.CONTENT_TYPE_DASH:
return PlaybackMetrics.STREAM_TYPE_DASH;
case C.TYPE_SS:
case C.CONTENT_TYPE_SS:
return PlaybackMetrics.STREAM_TYPE_SS;
case C.TYPE_RTSP:
case C.CONTENT_TYPE_RTSP:
default:
return PlaybackMetrics.STREAM_TYPE_OTHER;
}
......
......@@ -15,22 +15,29 @@
*/
package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.net.Uri;
import android.provider.Settings.Global;
import android.util.Pair;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import java.util.Arrays;
......@@ -54,18 +61,20 @@ public final class AudioCapabilities {
},
DEFAULT_MAX_CHANNEL_COUNT);
/** Array of all surround sound encodings that a device may be capable of playing. */
@SuppressWarnings("InlinedApi")
private static final int[] ALL_SURROUND_ENCODINGS =
new int[] {
AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_E_AC3,
AudioFormat.ENCODING_E_AC3_JOC,
AudioFormat.ENCODING_AC4,
AudioFormat.ENCODING_DOLBY_TRUEHD,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_DTS_HD,
};
/**
* All surround sound encodings that a device may be capable of playing mapped to a maximum
* channel count.
*/
private static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
new ImmutableMap.Builder<Integer, Integer>()
.put(C.ENCODING_AC3, 6)
.put(C.ENCODING_AC4, 6)
.put(C.ENCODING_DTS, 6)
.put(C.ENCODING_E_AC3_JOC, 6)
.put(C.ENCODING_E_AC3, 8)
.put(C.ENCODING_DTS_HD, 8)
.put(C.ENCODING_DOLBY_TRUEHD, 8)
.buildOrThrow();
/** Global settings key for devices that can specify external surround sound. */
private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled";
......@@ -158,6 +167,62 @@ public final class AudioCapabilities {
return maxChannelCount;
}
/** Returns whether the device can do passthrough playback for {@code format}. */
public boolean isPassthroughPlaybackSupported(Format format) {
return getEncodingAndChannelConfigForPassthrough(format) != null;
}
/**
* Returns the encoding and channel config to use when configuring an {@link AudioTrack} in
* passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the
* format is unsupported.
*
* @param format The {@link Format}.
* @return The encoding and channel config to use, or {@code null} if passthrough of the format is
* unsupported.
*/
@Nullable
public Pair<Integer, Integer> getEncodingAndChannelConfigForPassthrough(Format format) {
@C.Encoding
int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs);
// Check that this is an encoding known to work for passthrough. This avoids trying to use
// passthrough with an encoding where the device/app reports it's capable but it is untested or
// known to be broken (for example AAC-LC).
if (!ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.containsKey(encoding)) {
return null;
}
if (encoding == C.ENCODING_E_AC3_JOC && !supportsEncoding(C.ENCODING_E_AC3_JOC)) {
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
encoding = C.ENCODING_E_AC3;
} else if (encoding == C.ENCODING_DTS_HD && !supportsEncoding(C.ENCODING_DTS_HD)) {
// DTS receivers support DTS-HD streams (but decode only the core layer).
encoding = C.ENCODING_DTS;
}
if (!supportsEncoding(encoding)) {
return null;
}
int channelCount;
if (format.channelCount == Format.NO_VALUE || encoding == C.ENCODING_E_AC3_JOC) {
// In HLS chunkless preparation, the format channel count and sample rate may be unset. See
// https://github.com/google/ExoPlayer/issues/10204 and b/222127949 for more details.
// For E-AC3 JOC, the format is object based so the format channel count is arbitrary.
int sampleRate =
format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ;
channelCount = getMaxSupportedChannelCountForPassthrough(encoding, sampleRate);
} else {
channelCount = format.channelCount;
if (channelCount > maxChannelCount) {
return null;
}
}
int channelConfig = getChannelConfigForPassthrough(channelCount);
if (channelConfig == AudioFormat.CHANNEL_INVALID) {
return null;
}
return Pair.create(encoding, channelConfig);
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
......@@ -190,28 +255,93 @@ public final class AudioCapabilities {
&& ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER));
}
/**
* Returns the maximum number of channels supported for passthrough playback of audio in the given
* encoding, or {@code 0} if the format is unsupported.
*/
private static int getMaxSupportedChannelCountForPassthrough(
@C.Encoding int encoding, int sampleRate) {
// From API 29 we can get the channel count from the platform, but before then there is no way
// to query the platform so we assume the channel count matches the maximum channel count per
// audio encoding spec.
if (Util.SDK_INT >= 29) {
return Api29.getMaxSupportedChannelCountForPassthrough(encoding, sampleRate);
}
return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0));
}
private static int getChannelConfigForPassthrough(int channelCount) {
if (Util.SDK_INT <= 28) {
// In passthrough mode the channel count used to configure the audio track doesn't affect how
// the stream is handled, except that some devices do overly-strict channel configuration
// checks. Therefore we override the channel count so that a known-working channel
// configuration is chosen in all cases. See [Internal: b/29116190].
if (channelCount == 7) {
channelCount = 8;
} else if (channelCount == 3 || channelCount == 4 || channelCount == 5) {
channelCount = 6;
}
}
// Workaround for Nexus Player not reporting support for mono passthrough. See
// [Internal: b/34268671].
if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) {
channelCount = 2;
}
return Util.getAudioTrackChannelConfig(channelCount);
}
@RequiresApi(29)
private static final class Api29 {
private static final AudioAttributes DEFAULT_AUDIO_ATTRIBUTES =
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
.setFlags(0)
.build();
private Api29() {}
@DoNotInline
public static int[] getDirectPlaybackSupportedEncodings() {
ImmutableList.Builder<Integer> supportedEncodingsListBuilder = ImmutableList.builder();
for (int encoding : ALL_SURROUND_ENCODINGS) {
for (int encoding : ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.keySet()) {
if (AudioTrack.isDirectPlaybackSupported(
new AudioFormat.Builder()
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.setEncoding(encoding)
.setSampleRate(DEFAULT_SAMPLE_RATE_HZ)
.build(),
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
.setFlags(0)
.build())) {
DEFAULT_AUDIO_ATTRIBUTES)) {
supportedEncodingsListBuilder.add(encoding);
}
}
supportedEncodingsListBuilder.add(AudioFormat.ENCODING_PCM_16BIT);
return Ints.toArray(supportedEncodingsListBuilder.build());
}
/**
* Returns the maximum number of channels supported for passthrough playback of audio in the
* given format, or {@code 0} if the format is unsupported.
*/
@DoNotInline
public static int getMaxSupportedChannelCountForPassthrough(
@C.Encoding int encoding, int sampleRate) {
// TODO(internal b/234351617): Query supported channel masks directly once it's supported,
// see also b/25994457.
for (int channelCount = DEFAULT_MAX_CHANNEL_COUNT; channelCount > 0; channelCount--) {
AudioFormat audioFormat =
new AudioFormat.Builder()
.setEncoding(encoding)
.setSampleRate(sampleRate)
.setChannelMask(Util.getAudioTrackChannelConfig(channelCount))
.build();
if (AudioTrack.isDirectPlaybackSupported(audioFormat, DEFAULT_AUDIO_ATTRIBUTES)) {
return channelCount;
}
}
return 0;
}
}
}
......@@ -108,13 +108,8 @@ public interface AudioSink {
/** Called when the offload buffer has been partially emptied. */
default void onOffloadBufferEmptying() {}
/**
* Called when the offload buffer has been filled completely.
*
* @param bufferEmptyingDeadlineMs Maximum time in milliseconds until {@link
* #onOffloadBufferEmptying()} will be called.
*/
default void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {}
/** Called when the offload buffer has been filled completely. */
default void onOffloadBufferFull() {}
/**
* Called when {@link AudioSink} has encountered an error.
......
......@@ -386,11 +386,6 @@ import java.lang.reflect.Method;
return bufferSize - bytesPending;
}
/** Returns the duration of audio that is buffered but unplayed. */
public long getPendingBufferDurationMs(long writtenFrames) {
return Util.usToMs(framesToDurationUs(writtenFrames - getPlaybackHeadPosition()));
}
/** Returns whether the track is in an invalid state and must be recreated. */
public boolean isStalled(long writtenFrames) {
return forceResetWorkaroundTimeMs != C.TIME_UNSET
......
......@@ -684,7 +684,7 @@ public final class DefaultAudioSink implements AudioSink {
if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) {
return SINK_FORMAT_SUPPORTED_DIRECTLY;
}
if (isPassthroughPlaybackSupported(format, audioCapabilities)) {
if (audioCapabilities.isPassthroughPlaybackSupported(format)) {
return SINK_FORMAT_SUPPORTED_DIRECTLY;
}
return SINK_FORMAT_UNSUPPORTED;
......@@ -767,7 +767,7 @@ public final class DefaultAudioSink implements AudioSink {
outputMode = OUTPUT_MODE_PASSTHROUGH;
@Nullable
Pair<Integer, Integer> encodingAndChannelConfig =
getEncodingAndChannelConfigForPassthrough(inputFormat, audioCapabilities);
audioCapabilities.getEncodingAndChannelConfigForPassthrough(inputFormat);
if (encodingAndChannelConfig == null) {
throw new ConfigurationException(
"Unable to configure passthrough for: " + inputFormat, inputFormat);
......@@ -914,7 +914,11 @@ public final class DefaultAudioSink implements AudioSink {
pendingConfiguration = null;
if (isOffloadedPlayback(audioTrack)
&& offloadMode != OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED) {
audioTrack.setOffloadEndOfStream();
// If the first track is very short (typically <1s), the offload AudioTrack might
// not have started yet. Do not call setOffloadEndOfStream as it would throw.
if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.setOffloadEndOfStream();
}
audioTrack.setOffloadDelayPadding(
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
isWaitingForOffloadEndOfStreamHandled = true;
......@@ -1198,9 +1202,7 @@ public final class DefaultAudioSink implements AudioSink {
&& listener != null
&& bytesWritten < bytesRemaining
&& !isWaitingForOffloadEndOfStreamHandled) {
long pendingDurationMs =
audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames);
listener.onOffloadBufferFull(pendingDurationMs);
listener.onOffloadBufferFull();
}
}
......@@ -1691,134 +1693,6 @@ public final class DefaultAudioSink implements AudioSink {
: writtenEncodedFrames;
}
private static boolean isPassthroughPlaybackSupported(
Format format, AudioCapabilities audioCapabilities) {
return getEncodingAndChannelConfigForPassthrough(format, audioCapabilities) != null;
}
/**
* Returns the encoding and channel config to use when configuring an {@link AudioTrack} in
* passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the
* format is unsupported.
*
* @param format The {@link Format}.
* @param audioCapabilities The device audio capabilities.
* @return The encoding and channel config to use, or {@code null} if passthrough of the format is
* unsupported.
*/
@Nullable
private static Pair<Integer, Integer> getEncodingAndChannelConfigForPassthrough(
Format format, AudioCapabilities audioCapabilities) {
@C.Encoding
int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs);
// Check for encodings that are known to work for passthrough with the implementation in this
// class. This avoids trying to use passthrough with an encoding where the device/app reports
// it's capable but it is untested or known to be broken (for example AAC-LC).
boolean supportedEncoding =
encoding == C.ENCODING_AC3
|| encoding == C.ENCODING_E_AC3
|| encoding == C.ENCODING_E_AC3_JOC
|| encoding == C.ENCODING_AC4
|| encoding == C.ENCODING_DTS
|| encoding == C.ENCODING_DTS_HD
|| encoding == C.ENCODING_DOLBY_TRUEHD;
if (!supportedEncoding) {
return null;
}
if (encoding == C.ENCODING_E_AC3_JOC
&& !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) {
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
encoding = C.ENCODING_E_AC3;
} else if (encoding == C.ENCODING_DTS_HD
&& !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) {
// DTS receivers support DTS-HD streams (but decode only the core layer).
encoding = C.ENCODING_DTS;
}
if (!audioCapabilities.supportsEncoding(encoding)) {
return null;
}
int channelCount;
if (encoding == C.ENCODING_E_AC3_JOC) {
// E-AC3 JOC is object based so the format channel count is arbitrary. From API 29 we can get
// the channel count for this encoding, but before then there is no way to query it so we
// assume 6 channel audio is supported.
if (Util.SDK_INT >= 29) {
// Default to 48 kHz if the format doesn't have a sample rate (for example, for chunkless
// HLS preparation). See [Internal: b/222127949].
int sampleRate = format.sampleRate != Format.NO_VALUE ? format.sampleRate : 48000;
channelCount =
getMaxSupportedChannelCountForPassthroughV29(C.ENCODING_E_AC3_JOC, sampleRate);
if (channelCount == 0) {
Log.w(TAG, "E-AC3 JOC encoding supported but no channel count supported");
return null;
}
} else {
channelCount = 6;
}
} else {
channelCount = format.channelCount;
if (channelCount > audioCapabilities.getMaxChannelCount()) {
return null;
}
}
int channelConfig = getChannelConfigForPassthrough(channelCount);
if (channelConfig == AudioFormat.CHANNEL_INVALID) {
return null;
}
return Pair.create(encoding, channelConfig);
}
/**
* Returns the maximum number of channels supported for passthrough playback of audio in the given
* format, or 0 if the format is unsupported.
*/
@RequiresApi(29)
private static int getMaxSupportedChannelCountForPassthroughV29(
@C.Encoding int encoding, int sampleRate) {
android.media.AudioAttributes audioAttributes =
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
.build();
// TODO(internal b/25994457): Query supported channel masks directly once it's supported.
for (int channelCount = 8; channelCount > 0; channelCount--) {
AudioFormat audioFormat =
new AudioFormat.Builder()
.setEncoding(encoding)
.setSampleRate(sampleRate)
.setChannelMask(Util.getAudioTrackChannelConfig(channelCount))
.build();
if (AudioTrack.isDirectPlaybackSupported(audioFormat, audioAttributes)) {
return channelCount;
}
}
return 0;
}
private static int getChannelConfigForPassthrough(int channelCount) {
if (Util.SDK_INT <= 28) {
// In passthrough mode the channel count used to configure the audio track doesn't affect how
// the stream is handled, except that some devices do overly-strict channel configuration
// checks. Therefore we override the channel count so that a known-working channel
// configuration is chosen in all cases. See [Internal: b/29116190].
if (channelCount == 7) {
channelCount = 8;
} else if (channelCount == 3 || channelCount == 4 || channelCount == 5) {
channelCount = 6;
}
}
// Workaround for Nexus Player not reporting support for mono passthrough. See
// [Internal: b/34268671].
if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) {
channelCount = 2;
}
return Util.getAudioTrackChannelConfig(channelCount);
}
private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) {
if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) {
return false;
......@@ -1834,7 +1708,8 @@ public final class DefaultAudioSink implements AudioSink {
}
AudioFormat audioFormat = getAudioFormat(format.sampleRate, channelConfig, encoding);
switch (getOffloadedPlaybackSupport(audioFormat, audioAttributes.getAudioAttributesV21())) {
switch (getOffloadedPlaybackSupport(
audioFormat, audioAttributes.getAudioAttributesV21().audioAttributes)) {
case AudioManager.PLAYBACK_OFFLOAD_NOT_SUPPORTED:
return false;
case AudioManager.PLAYBACK_OFFLOAD_SUPPORTED:
......@@ -2308,7 +2183,7 @@ public final class DefaultAudioSink implements AudioSink {
if (tunneling) {
return getAudioTrackTunnelingAttributesV21();
} else {
return audioAttributes.getAudioAttributesV21();
return audioAttributes.getAudioAttributesV21().audioAttributes;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment