Merge branch 'main' into rtp-h263

This commit is contained in:
Rakesh Kumar 2022-06-10 11:38:51 +05:30 committed by GitHub
commit fc3c57ecfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
338 changed files with 18267 additions and 8450 deletions

View File

@ -12,6 +12,22 @@
by the user of the device. Apps can opt-out of contributing to platform
diagnostics for ExoPlayer with
`ExoPlayer.Builder.setUsePlatformDiagnostics(false)`.
* Fix bug that tracks are reset too often when using `MergingMediaSource`,
for example when side-loading subtitles and changing the selected
subtitle mid-playback
([#10248](https://github.com/google/ExoPlayer/issues/10248)).
* Stop detecting 5G-NSA network type on API 29 and 30. These playbacks
will assume a 4G network.
* Disallow passing `null` to
`MediaSource.Factory.setDrmSessionManagerProvider` and
`MediaSource.Factory.setLoadErrorHandlingPolicy`. Instances of
`DefaultDrmSessionManagerProvider` and `DefaultLoadErrorHandlingPolicy`
can be passed explicitly if required.
* Add `MediaItem.RequestMetadata` to represent metadata needed to play
media when the exact `LocalConfiguration` is not known. Also remove
`MediaMetadata.mediaUrl` as this is now included in `RequestMetadata`.
* Add `Player.Command.COMMAND_SET_MEDIA_ITEM` to enable players to allow
setting a single item.
* Track selection:
* Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`,
and promote `TrackSelectionOverride` to a top level class.
@ -19,17 +35,58 @@
`Tracks.Group`. `Player.getCurrentTracksInfo` and
`Player.Listener.onTracksInfoChanged` have also been renamed to
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`.
* Change `DefaultTrackSelector.buildUponParameters` and
`DefaultTrackSelector.Parameters.buildUpon` to return
`DefaultTrackSelector.Parameters.Builder` instead of the deprecated
`DefaultTrackSelector.ParametersBuilder`.
* Add
`DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities`.
which is enabled by default. When enabled, the `DefaultTrackSelector`
will prefer audio tracks whose channel count does not exceed the device
output capabilities. On handheld devices, the `DefaultTrackSelector`
will prefer stereo/mono over multichannel audio formats, unless the
multichannel format can be
[Spatialized](https://developer.android.com/reference/android/media/Spatializer)
(Android 12L+) or is a Dolby surround sound format. In addition, on
devices that support audio spatialization, the `DefaultTrackSelector`
will monitor for changes in the
[Spatializer properties](https://developer.android.com/reference/android/media/Spatializer.OnSpatializerStateChangedListener)
and trigger a new track selection upon these. Devices with a
`television`
[UI mode](https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier)
are excluded from these constraints and the format with the highest
channel count will be preferred. To enable this feature, the
`DefaultTrackSelector` instance must be constructed with a `Context`.
* Video:
* Rename `DummySurface` to `PlaceHolderSurface`.
* Rename `DummySurface` to `PlaceholderSurface`.
* Add AV1 support to the `MediaCodecVideoRenderer.getCodecMaxInputSize`.
* Audio:
* Use LG AC3 audio decoder advertising non-standard MIME type.
* Change the return type of `AudioAttributes.getAudioAttributesV21()` from
`android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper
class, to prevent slow ART verification on API < 21.
* Query the platform (API 29+) or assume the audio encoding channel count
for audio passthrough when the format audio channel count is unset,
which occurs with HLS chunkless preparation
([10204](https://github.com/google/ExoPlayer/issues/10204)).
* Ad playback / IMA:
* Decrease ad polling rate from every 100ms to every 200ms, to line up with
Media Rating Council (MRC) recommendations.
* Decrease ad polling rate from every 100ms to every 200ms, to line up
with Media Rating Council (MRC) recommendations.
* Text:
* Change `Player.getCurrentCues()` to return `CueGroup` instead of
`List<Cue>`.
* SSA: Support `OutlineColour` style setting when `BorderStyle == 3` (i.e.
`OutlineColour` sets the background of the cue)
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
* CEA-708: Parse data into multiple service blocks and ignore blocks not
associated with the currently selected service number.
* Remove `RawCcExtractor`, which was only used to handle a Google-internal
subtitle format.
* Extractors:
* Matroska: Parse `DiscardPadding` for Opus tracks.
* Parse bitrates from `esds` boxes.
* MP4: Parse initialization data from AV1 tracks.
* MP4: Parse bitrates from `esds` boxes.
* Ogg: Allow duplicate Opus ID and comment headers
([#10038](https://github.com/google/ExoPlayer/issues/10038)).
* UI:
* Fix delivery of events to `OnClickListener`s set on `PlayerView` and
`LegacyPlayerView`, in the case that `useController=false`
@ -49,17 +106,38 @@
* Don't show forced text tracks in the `PlayerView` track selector, and
keep a suitable forced text track selected if "None" is selected
([#9432](https://github.com/google/ExoPlayer/issues/9432)).
* DASH:
* Parse channel count from DTS `AudioChannelConfiguration` elements. This
re-enables audio passthrough for DTS streams
([#10159](https://github.com/google/ExoPlayer/issues/10159)).
* Disallow passing `null` to
`DashMediaSource.Factory.setCompositeSequenceableLoaderFactory`.
Instances of `DefaultCompositeSequenceableLoaderFactory` can be passed
explicitly if required.
* HLS:
* Fallback to chunkful preparation if the playlist CODECS attribute
does not contain the audio codec
* Fallback to chunkful preparation if the playlist CODECS attribute does
not contain the audio codec
([#10065](https://github.com/google/ExoPlayer/issues/10065)).
* Disallow passing `null` to
`HlsMediaSource.Factory.setCompositeSequenceableLoaderFactory`,
`HlsMediaSource.Factory.setPlaylistParserFactory`, and
`HlsMediaSource.Factory.setPlaylistTrackerFactory`. Instances of
`DefaultCompositeSequenceableLoaderFactory`,
`DefaultHlsPlaylistParserFactory`, or a reference to
`DefaultHlsPlaylistTracker.FACTORY` can be passed explicitly if
required.
* Smooth Streaming:
* Disallow passing `null` to
`SsMediaSource.Factory.setCompositeSequenceableLoaderFactory`. Instances
of `DefaultCompositeSequenceableLoaderFactory` can be passed explicitly
if required.
* RTSP:
* Add RTP reader for MPEG4
([#35](https://github.com/androidx/media/pull/35))
([#35](https://github.com/androidx/media/pull/35)).
* Add RTP reader for HEVC
([#36](https://github.com/androidx/media/pull/36)).
* Add RTP reader for AMR. Currently only mono-channel, non-interleaved
AMR streams are supported. Compound AMR RTP payload is not supported.
* Add RTP reader for AMR. Currently only mono-channel, non-interleaved AMR
streams are supported. Compound AMR RTP payload is not supported.
([#46](https://github.com/androidx/media/pull/46))
* Add RTP reader for VP8
([#47](https://github.com/androidx/media/pull/47)).
@ -67,13 +145,37 @@
([#56](https://github.com/androidx/media/pull/56)).
* Fix RTSP basic authorization header.
([#9544](https://github.com/google/ExoPlayer/issues/9544)).
* Stop checking mandatory SDP fields as ExoPlayer doesn't need them
([#10049](https://github.com/google/ExoPlayer/issues/10049)).
* Throw checked exception when parsing RTSP timing
([#10165](https://github.com/google/ExoPlayer/issues/10165)).
* Add RTP reader for VP9
([#47](https://github.com/androidx/media/pull/64)).
* Add RTP reader for OPUS
([#53](https://github.com/androidx/media/pull/53)).
* Session:
* Fix NPE in MediaControllerImplLegacy
([#59](https://github.com/androidx/media/pull/59))
([#59](https://github.com/androidx/media/pull/59)).
* Update session position info on timeline
change([#51](https://github.com/androidx/media/issues/51)).
* Fix NPE in MediaControllerImplBase after releasing controller
([#74](https://github.com/androidx/media/issues/74)).
* Rename `MediaSession.MediaSessionCallback` to `MediaSession.Callback`,
`MediaLibrarySession.MediaLibrarySessionCallback` to
`MediaLibrarySession.Callback` and
`MediaSession.Builder.setSessionCallback` to `setCallback`.
* Replace `MediaSession.MediaItemFiler` with
`MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution
of requests.
* Forward legacy `MediaController` calls to play media to
`MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`.
* Data sources:
* Rename `DummyDataSource` to `PlaceHolderDataSource`.
* Rename `DummyDataSource` to `PlaceholderDataSource`.
* Workaround OkHttp interrupt handling.
* FFmpeg extension:
* Update CMake version to `3.21.0+` to avoid a CMake bug causing
AndroidStudio's gradle sync to fail
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* Remove deprecated symbols:
* Remove `Player.Listener.onTracksChanged`. Use
`Player.Listener.onTracksInfoChanged` instead.
@ -87,10 +189,10 @@
`DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use
`getDefaultTrackSelectorParameters(Context)` instead when possible, and
`DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise.
* FFmpeg extension:
* Update CMake version to `3.21.0+` to avoid a CMake bug causing
AndroidStudio's gradle sync to fail
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`.
Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead.
* Remove `Transformer.Builder.setContext`. The `Context` should be passed
to the `Transformer.Builder` constructor instead.
### 1.0.0-alpha03 (2022-03-14)

View File

@ -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'
}

View File

@ -29,5 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
testOptions {
unitTests.all {
jvmArgs "-Xmx2g"
}
unitTests.includeAndroidResources true
}
}

View File

@ -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'

View File

@ -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());
}
}

View File

@ -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);
}
}
}
}

View File

@ -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)

View File

@ -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 -> {

View File

@ -78,6 +78,12 @@
<data android:scheme="file"/>
<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"/>

View File

@ -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());
}

View File

@ -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();

View File

@ -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());

View File

@ -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. */

View File

@ -45,6 +45,7 @@
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>

View File

@ -19,35 +19,118 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
class PlaybackService : MediaLibraryService() {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
companion object {
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
private inner class CustomMediaLibrarySessionCallback :
MediaLibrarySession.MediaLibrarySessionCallback {
override fun onCreate() {
super.onCreate()
customCommands =
listOf(
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
),
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
initializeSessionAndPlayer()
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onDestroy() {
player.release()
mediaLibrarySession.release()
super.onDestroy()
}
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { commandButton ->
// Add custom command to available session commands.
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
// Let Media3 controller (for instance the MediaNotificationProvider) know about the custom
// layout right after it connected.
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
}
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling.
player.shuffleModeEnabled = true
// Change the custom layout to contain the `Disable shuffling` command.
customLayout = ImmutableList.of(customCommands[1])
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
// Disable shuffling.
player.shuffleModeEnabled = false
// Change the custom layout to contain the `Enable shuffling` command.
customLayout = ImmutableList.of(customCommands[0])
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
override fun onGetLibraryRoot(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
browser: ControllerInfo,
params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
@ -55,7 +138,7 @@ class PlaybackService : MediaLibraryService() {
override fun onGetItem(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
browser: ControllerInfo,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
val item =
@ -66,9 +149,24 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
}
override fun onSubscribe(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
session.notifyChildrenChanged(browser, parentId, children.size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetChildren(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
browser: ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
@ -83,7 +181,21 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}
private fun setMediaItemFromSearchQuery(query: String) {
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems: List<MediaItem> =
mediaItems.map { mediaItem ->
if (mediaItem.requestMetadata.searchQuery != null)
getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!)
else MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
return Futures.immediateFuture(updatedMediaItems)
}
private fun getMediaItemFromSearchQuery(query: String): MediaItem {
// Only accept query with pattern "play [Title]" or "[Title]"
// Where [Title]: must be exactly matched
// If no media with exact name found, play a random media instead
@ -94,54 +206,8 @@ class PlaybackService : MediaLibraryService() {
query
}
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
player.setMediaItem(item)
return MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
}
override fun onSetMediaUri(
session: MediaSession,
controller: MediaSession.ControllerInfo,
uri: Uri,
extras: Bundle
): Int {
if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) ||
uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT)
) {
var searchQuery =
uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
setMediaItemFromSearchQuery(searchQuery)
return SessionResult.RESULT_SUCCESS
} else {
return SessionResult.RESULT_ERROR_NOT_SUPPORTED
}
}
}
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
override fun fillInLocalConfiguration(
session: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItem: MediaItem
): MediaItem {
return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
}
override fun onCreate() {
super.onCreate()
initializeSessionAndPlayer()
}
override fun onDestroy() {
player.release()
mediaLibrarySession.release()
super.onDestroy()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
private fun initializeSessionAndPlayer() {
@ -151,13 +217,10 @@ class PlaybackService : MediaLibraryService() {
.build()
MediaItemTree.initialize(assets)
val parentScreenIntent = Intent(this, MainActivity::class.java)
val intent = Intent(this, PlayerActivity::class.java)
val pendingIntent =
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(parentScreenIntent)
addNextIntent(intent)
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
@ -165,8 +228,29 @@ class PlaybackService : MediaLibraryService() {
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller())
.setSessionActivity(pendingIntent)
.setSessionActivity(sessionActivityPendingIntent)
.build()
if (!customLayout.isEmpty()) {
// Send custom layout to legacy session.
mediaLibrarySession.setCustomLayout(customLayout)
}
}
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder()
.setDisplayName(
getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.setSessionCommand(sessionCommand)
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
.build()
}
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
}

View File

@ -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>

View File

@ -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)

View File

@ -0,0 +1,22 @@
# 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",
)

View File

@ -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

View File

@ -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'
}

View File

@ -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"

View File

@ -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);
}

View File

@ -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);
}
@Override
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 Size getOutputSize() {
return checkStateNotNull(outputSize);
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 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);
}
@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);
}
}
}

View File

@ -32,6 +32,7 @@ import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
import com.google.android.material.slider.RangeSlider;
@ -55,44 +56,55 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String SCALE_X = "scale_x";
public static final String SCALE_Y = "scale_y";
public static final String ROTATE_DEGREES = "rotate_degrees";
public static final String TRIM_START_MS = "trim_start_ms";
public static final String TRIM_END_MS = "trim_end_ms";
public static final String ENABLE_FALLBACK = "enable_fallback";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
public static final String DEMO_FRAME_PROCESSORS_SELECTIONS = "demo_frame_processors_selections";
public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections";
public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x";
public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y";
public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius";
public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius";
private static final String[] INPUT_URIS = {
"https://html5demos.com/assets/dizzy.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4",
"https://html5demos.com/assets/dizzy.mp4",
"https://html5demos.com/assets/dizzy.webm",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4",
};
private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS
"MP4 with H264 video and AAC audio",
"Short MP4 with H265 video and AAC audio",
"MP4 with H265 video and AAC audio",
"Long MP4 with H264 video and AAC audio",
"WebM with VP8 video and Vorbis audio",
"4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)",
"8k 24fps MP4 with H265 video and AAC audio",
"MP4 with H264 video and AAC audio (portrait, H > W, 0\u00B0)",
"MP4 with H264 video and AAC audio (portrait, H < W, 90\u00B0)",
"720p H264 video and AAC audio",
"1080p H265 video and AAC audio",
"360p H264 video and AAC audio",
"360p VP8 video and Vorbis audio",
"4K H264 video and AAC audio (portrait, no B-frames)",
"8k H265 video and AAC audio",
"Short 1080p H265 video and AAC audio",
"Long 180p H264 video and AAC audio",
"H264 video and AAC audio (portrait, H > W, 0\u00B0)",
"H264 video and AAC audio (portrait, H < W, 90\u00B0)",
"SEF slow motion with 240 fps",
"MP4 with HDR (HDR10) H265 video (encoding may fail)",
"480p DASH (non-square pixels)",
"HDR (HDR10) H265 video (encoding may fail)",
};
private static final String[] DEMO_FRAME_PROCESSORS = {
"Dizzy crop", "Periodic vignette", "3D spin", "Overlay logo & timer", "Zoom in start"
private static final String[] DEMO_EFFECTS = {
"Dizzy crop",
"Edge detector (Media Pipe)",
"Periodic vignette",
"3D spin",
"Overlay logo & timer",
"Zoom in start",
};
private static final int PERIODIC_VIGNETTE_INDEX = 1;
private static final int PERIODIC_VIGNETTE_INDEX = 2;
private static final String SAME_AS_INPUT_OPTION = "same as input";
private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2);
@ -106,12 +118,15 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull Spinner resolutionHeightSpinner;
private @MonotonicNonNull Spinner scaleSpinner;
private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox trimCheckBox;
private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private @MonotonicNonNull Button selectDemoFrameProcessorsButton;
private boolean @MonotonicNonNull [] demoFrameProcessorsSelections;
private @MonotonicNonNull Button selectDemoEffectsButton;
private boolean @MonotonicNonNull [] demoEffectsSelections;
private int inputUriPosition;
private long trimStartMs;
private long trimEndMs;
private float periodicVignetteCenterX;
private float periodicVignetteCenterY;
private float periodicVignetteInnerRadius;
@ -179,15 +194,20 @@ public final class ConfigurationActivity extends AppCompatActivity {
rotateSpinner.setAdapter(rotateAdapter);
rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180");
trimCheckBox = findViewById(R.id.trim_checkbox);
trimCheckBox.setOnCheckedChangeListener(this::selectTrimBounds);
trimStartMs = C.TIME_UNSET;
trimEndMs = C.TIME_UNSET;
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
demoFrameProcessorsSelections = new boolean[DEMO_FRAME_PROCESSORS.length];
selectDemoFrameProcessorsButton = findViewById(R.id.select_demo_frameprocessors_button);
selectDemoFrameProcessorsButton.setOnClickListener(this::selectFrameProcessors);
demoEffectsSelections = new boolean[DEMO_EFFECTS.length];
selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button);
selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects);
}
@Override
@ -215,13 +235,14 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"trimCheckBox",
"enableFallbackCheckBox",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"demoFrameProcessorsSelections"
"demoEffectsSelections"
})
private void startTransformation(View view) {
Intent transformerIntent = new Intent(this, TransformerActivity.class);
Intent transformerIntent = new Intent(/* packageContext= */ this, TransformerActivity.class);
Bundle bundle = new Bundle();
bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked());
bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked());
@ -249,11 +270,15 @@ public final class ConfigurationActivity extends AppCompatActivity {
if (!SAME_AS_INPUT_OPTION.equals(selectedRotate)) {
bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate));
}
if (trimCheckBox.isChecked()) {
bundle.putLong(TRIM_START_MS, trimStartMs);
bundle.putLong(TRIM_END_MS, trimEndMs);
}
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
bundle.putBooleanArray(DEMO_FRAME_PROCESSORS_SELECTIONS, demoFrameProcessorsSelections);
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY);
bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius);
@ -276,27 +301,46 @@ public final class ConfigurationActivity extends AppCompatActivity {
.show();
}
private void selectFrameProcessors(View view) {
private void selectDemoEffects(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_demo_frameprocessors)
.setTitle(R.string.select_demo_effects)
.setMultiChoiceItems(
DEMO_FRAME_PROCESSORS,
checkNotNull(demoFrameProcessorsSelections),
this::selectFrameProcessor)
DEMO_EFFECTS, checkNotNull(demoEffectsSelections), this::selectDemoEffect)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
private void selectTrimBounds(View view, boolean isChecked) {
if (!isChecked) {
return;
}
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
RangeSlider radiusRangeSlider =
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
radiusRangeSlider.setValues(0f, 60f); // seconds
new AlertDialog.Builder(/* context= */ this)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
List<Float> radiusRange = radiusRangeSlider.getValues();
trimStartMs = 1000 * radiusRange.get(0).longValue();
trimEndMs = 1000 * radiusRange.get(1).longValue();
})
.create()
.show();
}
@RequiresNonNull("selectedFileTextView")
private void selectFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which;
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
}
@RequiresNonNull("demoFrameProcessorsSelections")
private void selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked) {
demoFrameProcessorsSelections[which] = isChecked;
@RequiresNonNull("demoEffectsSelections")
private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) {
demoEffectsSelections[which] = isChecked;
if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) {
return;
}
@ -335,7 +379,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"rotateSpinner",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoFrameProcessorsButton"
"selectDemoEffectsButton"
})
private void onRemoveAudio(View view) {
if (((CheckBox) view).isChecked()) {
@ -355,7 +399,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"rotateSpinner",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoFrameProcessorsButton"
"selectDemoEffectsButton"
})
private void onRemoveVideo(View view) {
if (((CheckBox) view).isChecked()) {
@ -374,7 +418,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"rotateSpinner",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoFrameProcessorsButton"
"selectDemoEffectsButton"
})
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
audioMimeSpinner.setEnabled(isAudioEnabled);
@ -385,7 +429,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
enableRequestSdrToneMappingCheckBox.setEnabled(
isRequestSdrToneMappingSupported() && isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectDemoFrameProcessorsButton.setEnabled(isVideoEnabled);
selectDemoEffectsButton.setEnabled(isVideoEnabled);
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled);

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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 (selectedFrameProcessors[2]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createSpin3dFrameProcessor());
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[3]) {
frameProcessors.add(new BitmapOverlayFrameProcessor());
if (selectedEffects[3]) {
effects.add(MatrixTransformationFactory.createSpin3dEffect());
}
if (selectedFrameProcessors[4]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor());
if (selectedEffects[4]) {
effects.add(BitmapOverlayProcessor::new);
}
transformerBuilder.setFrameProcessors(frameProcessors.build());
if (selectedEffects[5]) {
effects.add(MatrixTransformationFactory.createZoomInTransition());
}
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

View File

@ -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"
@ -159,6 +159,16 @@
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
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" >
@ -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" />

View File

@ -0,0 +1,48 @@
<?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>

View File

@ -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>

View File

@ -0,0 +1,19 @@
<?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>

View File

@ -0,0 +1,13 @@
# 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"
}

View File

@ -0,0 +1,170 @@
/*
* 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();
}
}

View File

@ -1,5 +1,5 @@
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

View File

@ -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())
}
}

View File

@ -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}. */

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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() {

View File

@ -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 representing a DASH manifest. */
public static final int CONTENT_TYPE_DASH = 0;
/**
* Value returned by {@link Util#inferContentType} for files other than DASH, HLS or Smooth
* Streaming manifests, or RTSP URIs.
* @deprecated Use {@link #CONTENT_TYPE_DASH} instead.
*/
@UnstableApi public static final int TYPE_OTHER = 4;
@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;

View File

@ -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> {
/**

View File

@ -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))

View File

@ -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))

View File

@ -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();
}
@ -992,6 +1055,11 @@ public class ForwardingPlayer implements Player {
listener.onCues(cues);
}
@Override
public void onCues(CueGroup cueGroup) {
listener.onCues(cueGroup);
}
@Override
public void onMetadata(Metadata metadata) {
listener.onMetadata(metadata);

View File

@ -29,6 +29,7 @@ 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.errorprone.annotations.InlineMe;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -84,6 +85,7 @@ public final class MediaItem implements Bundleable {
// TODO: Change this to LiveConfiguration once all the deprecated individual setters
// are removed.
private LiveConfiguration.Builder liveConfiguration;
private RequestMetadata requestMetadata;
/** Creates a builder. */
@SuppressWarnings("deprecation") // Temporarily uses DrmConfiguration.Builder() constructor.
@ -93,6 +95,7 @@ public final class MediaItem implements Bundleable {
streamKeys = Collections.emptyList();
subtitleConfigurations = ImmutableList.of();
liveConfiguration = new LiveConfiguration.Builder();
requestMetadata = RequestMetadata.EMPTY;
}
private Builder(MediaItem mediaItem) {
@ -101,6 +104,7 @@ public final class MediaItem implements Bundleable {
mediaId = mediaItem.mediaId;
mediaMetadata = mediaItem.mediaMetadata;
liveConfiguration = mediaItem.liveConfiguration.buildUpon();
requestMetadata = mediaItem.requestMetadata;
@Nullable LocalConfiguration localConfiguration = mediaItem.localConfiguration;
if (localConfiguration != null) {
customCacheKey = localConfiguration.customCacheKey;
@ -315,12 +319,12 @@ public final class MediaItem implements Bundleable {
/**
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#forceSessionsForAudioAndVideoTracks(boolean)} instead.
* DrmConfiguration.Builder#setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/
@UnstableApi
@Deprecated
public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
drmConfiguration.forceSessionsForAudioAndVideoTracks(sessionForClearPeriods);
drmConfiguration.setForceSessionsForAudioAndVideoTracks(sessionForClearPeriods);
return this;
}
@ -525,6 +529,12 @@ public final class MediaItem implements Bundleable {
return this;
}
/** Sets the request metadata. */
public Builder setRequestMetadata(RequestMetadata requestMetadata) {
this.requestMetadata = requestMetadata;
return this;
}
/** Returns a new {@link MediaItem} instance with the current builder values. */
@SuppressWarnings("deprecation") // Using PlaybackProperties while it exists.
public MediaItem build() {
@ -549,7 +559,8 @@ public final class MediaItem implements Bundleable {
clippingConfiguration.buildClippingProperties(),
localConfiguration,
liveConfiguration.build(),
mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY);
mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY,
requestMetadata);
}
}
@ -659,6 +670,19 @@ public final class MediaItem implements Bundleable {
return this;
}
/**
* @deprecated Use {@link #setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/
@UnstableApi
@Deprecated
@InlineMe(
replacement =
"this.setForceSessionsForAudioAndVideoTracks(forceSessionsForAudioAndVideoTracks)")
public Builder forceSessionsForAudioAndVideoTracks(
boolean forceSessionsForAudioAndVideoTracks) {
return setForceSessionsForAudioAndVideoTracks(forceSessionsForAudioAndVideoTracks);
}
/**
* Sets whether a DRM session should be used for clear tracks of type {@link
* C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_AUDIO}.
@ -666,10 +690,10 @@ public final class MediaItem implements Bundleable {
* <p>This method overrides what has been set by previously calling {@link
* #setForcedSessionTrackTypes(List)}.
*/
public Builder forceSessionsForAudioAndVideoTracks(
boolean useClearSessionsForAudioAndVideoTracks) {
public Builder setForceSessionsForAudioAndVideoTracks(
boolean forceSessionsForAudioAndVideoTracks) {
this.setForcedSessionTrackTypes(
useClearSessionsForAudioAndVideoTracks
forceSessionsForAudioAndVideoTracks
? ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO)
: ImmutableList.of());
return this;
@ -680,10 +704,10 @@ public final class MediaItem implements Bundleable {
* when the tracks are in the clear.
*
* <p>For the common case of using a DRM session for {@link C#TRACK_TYPE_VIDEO} and {@link
* C#TRACK_TYPE_AUDIO}, {@link #forceSessionsForAudioAndVideoTracks(boolean)} can be used.
* C#TRACK_TYPE_AUDIO}, {@link #setForceSessionsForAudioAndVideoTracks(boolean)} can be used.
*
* <p>This method overrides what has been set by previously calling {@link
* #forceSessionsForAudioAndVideoTracks(boolean)}.
* #setForceSessionsForAudioAndVideoTracks(boolean)}.
*/
public Builder setForcedSessionTrackTypes(
List<@C.TrackType Integer> forcedSessionTrackTypes) {
@ -1311,7 +1335,7 @@ public final class MediaItem implements Bundleable {
}
/** Sets the MIME type. */
public Builder setMimeType(String mimeType) {
public Builder setMimeType(@Nullable String mimeType) {
this.mimeType = mimeType;
return this;
}
@ -1716,6 +1740,146 @@ public final class MediaItem implements Bundleable {
}
}
/**
* Metadata that helps the player to understand a playback request represented by a {@link
* MediaItem}.
*
* <p>This metadata is most useful for cases where playback requests are forwarded to other player
* instances (e.g. from a {@link android.media.session.MediaController}) and the player creating
* the request doesn't know the required {@link LocalConfiguration} for playback.
*/
public static final class RequestMetadata implements Bundleable {
/** Empty request metadata. */
public static final RequestMetadata EMPTY = new Builder().build();
/** Builder for {@link RequestMetadata} instances. */
public static final class Builder {
@Nullable private Uri mediaUri;
@Nullable private String searchQuery;
@Nullable private Bundle extras;
/** Constructs an instance. */
public Builder() {}
private Builder(RequestMetadata requestMetadata) {
this.mediaUri = requestMetadata.mediaUri;
this.searchQuery = requestMetadata.searchQuery;
this.extras = requestMetadata.extras;
}
/** Sets the URI of the requested media, or null if not known or applicable. */
public Builder setMediaUri(@Nullable Uri mediaUri) {
this.mediaUri = mediaUri;
return this;
}
/** Sets the search query for the requested media, or null if not applicable. */
public Builder setSearchQuery(@Nullable String searchQuery) {
this.searchQuery = searchQuery;
return this;
}
/** Sets optional extras {@link Bundle}. */
public Builder setExtras(@Nullable Bundle extras) {
this.extras = extras;
return this;
}
/** Builds the request metadata. */
public RequestMetadata build() {
return new RequestMetadata(this);
}
}
/** The URI of the requested media, or null if not known or applicable. */
@Nullable public final Uri mediaUri;
/** The search query for the requested media, or null if not applicable. */
@Nullable public final String searchQuery;
/**
* Optional extras {@link Bundle}.
*
* <p>Given the complexities of checking the equality of two {@link Bundle}s, this is not
* considered in the {@link #equals(Object)} or {@link #hashCode()}.
*/
@Nullable public final Bundle extras;
private RequestMetadata(Builder builder) {
this.mediaUri = builder.mediaUri;
this.searchQuery = builder.searchQuery;
this.extras = builder.extras;
}
/** Returns a {@link Builder} initialized with the values of this instance. */
public Builder buildUpon() {
return new Builder(this);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RequestMetadata)) {
return false;
}
RequestMetadata that = (RequestMetadata) o;
return Util.areEqual(mediaUri, that.mediaUri) && Util.areEqual(searchQuery, that.searchQuery);
}
@Override
public int hashCode() {
int result = mediaUri == null ? 0 : mediaUri.hashCode();
result = 31 * result + (searchQuery == null ? 0 : searchQuery.hashCode());
return result;
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_MEDIA_URI, FIELD_SEARCH_QUERY, FIELD_EXTRAS})
private @interface FieldNumber {}
private static final int FIELD_MEDIA_URI = 0;
private static final int FIELD_SEARCH_QUERY = 1;
private static final int FIELD_EXTRAS = 2;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
if (mediaUri != null) {
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
}
if (searchQuery != null) {
bundle.putString(keyForField(FIELD_SEARCH_QUERY), searchQuery);
}
if (extras != null) {
bundle.putBundle(keyForField(FIELD_EXTRAS), extras);
}
return bundle;
}
/** Object that can restore {@link RequestMetadata} from a {@link Bundle}. */
@UnstableApi
public static final Creator<RequestMetadata> CREATOR =
bundle ->
new RequestMetadata.Builder()
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
.setSearchQuery(bundle.getString(keyForField(FIELD_SEARCH_QUERY)))
.setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS)))
.build();
private static String keyForField(@RequestMetadata.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
/**
* The default media ID that is used if the media ID is not explicitly set by {@link
* Builder#setMediaId(String)}.
@ -1751,6 +1915,9 @@ public final class MediaItem implements Bundleable {
*/
@UnstableApi @Deprecated public final ClippingProperties clippingProperties;
/** The media {@link RequestMetadata}. */
public final RequestMetadata requestMetadata;
// Using PlaybackProperties and ClippingProperties until they're deleted.
@SuppressWarnings("deprecation")
private MediaItem(
@ -1758,7 +1925,8 @@ public final class MediaItem implements Bundleable {
ClippingProperties clippingConfiguration,
@Nullable PlaybackProperties localConfiguration,
LiveConfiguration liveConfiguration,
MediaMetadata mediaMetadata) {
MediaMetadata mediaMetadata,
RequestMetadata requestMetadata) {
this.mediaId = mediaId;
this.localConfiguration = localConfiguration;
this.playbackProperties = localConfiguration;
@ -1766,6 +1934,7 @@ public final class MediaItem implements Bundleable {
this.mediaMetadata = mediaMetadata;
this.clippingConfiguration = clippingConfiguration;
this.clippingProperties = clippingConfiguration;
this.requestMetadata = requestMetadata;
}
/** Returns a {@link Builder} initialized with the values of this instance. */
@ -1788,7 +1957,8 @@ public final class MediaItem implements Bundleable {
&& clippingConfiguration.equals(other.clippingConfiguration)
&& Util.areEqual(localConfiguration, other.localConfiguration)
&& Util.areEqual(liveConfiguration, other.liveConfiguration)
&& Util.areEqual(mediaMetadata, other.mediaMetadata);
&& Util.areEqual(mediaMetadata, other.mediaMetadata)
&& Util.areEqual(requestMetadata, other.requestMetadata);
}
@Override
@ -1798,6 +1968,7 @@ public final class MediaItem implements Bundleable {
result = 31 * result + liveConfiguration.hashCode();
result = 31 * result + clippingConfiguration.hashCode();
result = 31 * result + mediaMetadata.hashCode();
result = 31 * result + requestMetadata.hashCode();
return result;
}
@ -1810,7 +1981,8 @@ public final class MediaItem implements Bundleable {
FIELD_MEDIA_ID,
FIELD_LIVE_CONFIGURATION,
FIELD_MEDIA_METADATA,
FIELD_CLIPPING_PROPERTIES
FIELD_CLIPPING_PROPERTIES,
FIELD_REQUEST_METADATA
})
private @interface FieldNumber {}
@ -1818,6 +1990,7 @@ public final class MediaItem implements Bundleable {
private static final int FIELD_LIVE_CONFIGURATION = 1;
private static final int FIELD_MEDIA_METADATA = 2;
private static final int FIELD_CLIPPING_PROPERTIES = 3;
private static final int FIELD_REQUEST_METADATA = 4;
/**
* {@inheritDoc}
@ -1833,6 +2006,7 @@ public final class MediaItem implements Bundleable {
bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle());
bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle());
bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle());
return bundle;
}
@ -1869,12 +2043,20 @@ public final class MediaItem implements Bundleable {
} else {
clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle);
}
@Nullable Bundle requestMetadataBundle = bundle.getBundle(keyForField(FIELD_REQUEST_METADATA));
RequestMetadata requestMetadata;
if (requestMetadataBundle == null) {
requestMetadata = RequestMetadata.EMPTY;
} else {
requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle);
}
return new MediaItem(
mediaId,
clippingConfiguration,
/* localConfiguration= */ null,
liveConfiguration,
mediaMetadata);
mediaMetadata,
requestMetadata);
}
private static String keyForField(@FieldNumber int field) {

View File

@ -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))

View File

@ -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";

View File

@ -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);
}

View File

@ -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,16 +1027,29 @@ 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.
*
@ -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();

View File

@ -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() {}

View File

@ -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]));
};

View File

@ -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
public static final Creator<TrackSelectionParameters> CREATOR =
bundle -> new Builder(bundle).build();
/** Construct an instance from a {@link Bundle} produced by {@link #toBundle()}. */
public static TrackSelectionParameters fromBundle(Bundle bundle) {
return new Builder(bundle).build();
}
private static String keyForField(@FieldNumber int field) {
/**
* @deprecated Use {@link #fromBundle(Bundle)} instead.
*/
@UnstableApi @Deprecated
public static final Creator<TrackSelectionParameters> CREATOR =
TrackSelectionParameters::fromBundle;
/**
* 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);
}
}

View File

@ -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);
};

View File

@ -0,0 +1,105 @@
/*
* 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();
}
}

View File

@ -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();
@ -81,34 +53,6 @@ public final class BundleableUtil {
return builder.build();
}
/**
* 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}
@ -123,6 +67,19 @@ public final class BundleableUtil {
return arrayList;
}
/**
* 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

View File

@ -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();
}

View File

@ -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);

View File

@ -35,6 +35,7 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.List;
import javax.microedition.khronos.egl.EGL10;
/** OpenGL ES utilities. */
@ -42,19 +43,16 @@ import javax.microedition.khronos.egl.EGL10;
@UnstableApi
public final class GlUtil {
/** Thrown when an OpenGL error occurs and {@link #glAssertionsEnabled} is {@code true}. */
public static final class GlException extends RuntimeException {
/** Thrown when an OpenGL error occurs. */
public static final class GlException extends Exception {
/** Creates an instance with the specified error message. */
public GlException(String message) {
super(message);
}
}
/** Whether to throw a {@link GlException} in case of an OpenGL error. */
public static boolean glAssertionsEnabled = false;
/** Number of vertices in a rectangle. */
public static final int RECTANGLE_VERTICES_COUNT = 4;
/** Number of elements in a 3d homogeneous coordinate vector describing a vertex. */
public static final int HOMOGENEOUS_COORDINATE_VECTOR_SIZE = 4;
/** Length of the normalized device coordinate (NDC) space, which spans from -1 to 1. */
public static final float LENGTH_NDC = 2f;
@ -120,6 +118,20 @@ public final class GlUtil {
};
}
/** Flattens the list of 4 element NDC coordinate vectors into a buffer. */
public static float[] createVertexBuffer(List<float[]> vertexList) {
float[] vertexBuffer = new float[HOMOGENEOUS_COORDINATE_VECTOR_SIZE * vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
System.arraycopy(
/* src= */ vertexList.get(i),
/* srcPos= */ 0,
/* dest= */ vertexBuffer,
/* destPos= */ HOMOGENEOUS_COORDINATE_VECTOR_SIZE * i,
/* length= */ HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
}
return vertexBuffer;
}
/**
* Returns whether creating a GL context with {@value #EXTENSION_PROTECTED_CONTENT} is possible.
*
@ -167,13 +179,13 @@ public final class GlUtil {
/** Returns an initialized default {@link EGLDisplay}. */
@RequiresApi(17)
public static EGLDisplay createEglDisplay() {
public static EGLDisplay createEglDisplay() throws GlException {
return Api17.createEglDisplay();
}
/** Returns a new {@link EGLContext} for the specified {@link EGLDisplay}. */
@RequiresApi(17)
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
public static EGLContext createEglContext(EGLDisplay eglDisplay) throws GlException {
return Api17.createEglContext(eglDisplay, /* version= */ 2, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
}
@ -182,7 +194,8 @@ public final class GlUtil {
* RGBA 1010102 config.
*/
@RequiresApi(17)
public static EGLContext createEglContextEs3Rgba1010102(EGLDisplay eglDisplay) {
public static EGLContext createEglContextEs3Rgba1010102(EGLDisplay eglDisplay)
throws GlException {
return Api17.createEglContext(eglDisplay, /* version= */ 3, EGL_CONFIG_ATTRIBUTES_RGBA_1010102);
}
@ -193,7 +206,7 @@ public final class GlUtil {
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
*/
@RequiresApi(17)
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) {
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) throws GlException {
return Api17.getEglSurface(
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
}
@ -206,7 +219,8 @@ public final class GlUtil {
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
*/
@RequiresApi(17)
public static EGLSurface getEglSurfaceBt2020Pq(EGLDisplay eglDisplay, Object surface) {
public static EGLSurface getEglSurfaceBt2020Pq(EGLDisplay eglDisplay, Object surface)
throws GlException {
return Api17.getEglSurface(
eglDisplay,
surface,
@ -222,7 +236,8 @@ public final class GlUtil {
* @param height The height of the pixel buffer.
*/
@RequiresApi(17)
private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height) {
private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height)
throws GlException {
int[] pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH, width,
@ -241,7 +256,7 @@ public final class GlUtil {
* @return {@link EGL14#EGL_NO_SURFACE} if supported and a 1x1 pixel buffer surface otherwise.
*/
@RequiresApi(17)
public static EGLSurface createPlaceholderEglSurface(EGLDisplay eglDisplay) {
public static EGLSurface createPlaceholderEglSurface(EGLDisplay eglDisplay) throws GlException {
return isSurfacelessContextExtensionSupported()
? EGL14.EGL_NO_SURFACE
: createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
@ -254,7 +269,8 @@ public final class GlUtil {
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
*/
@RequiresApi(17)
public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) {
public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay)
throws GlException {
EGLSurface eglSurface = createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
}
@ -268,7 +284,7 @@ public final class GlUtil {
*/
@RequiresApi(17)
public static void focusPlaceholderEglSurfaceBt2020Pq(
EGLContext eglContext, EGLDisplay eglDisplay) {
EGLContext eglContext, EGLDisplay eglDisplay) throws GlException {
int[] pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH,
@ -286,10 +302,10 @@ public final class GlUtil {
}
/**
* If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws
* a {@link GlException}.
* Logs all OpenGL errors that occurred since this method was last called and throws a {@link
* GlException} for the last error.
*/
public static void checkGlError() {
public static void checkGlError() throws GlException {
int lastError = GLES20.GL_NO_ERROR;
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
@ -297,7 +313,7 @@ public final class GlUtil {
lastError = error;
}
if (lastError != GLES20.GL_NO_ERROR) {
throwGlException("glError: " + gluErrorString(lastError));
throw new GlException("glError: " + gluErrorString(lastError));
}
}
@ -308,9 +324,10 @@ public final class GlUtil {
* @param height The height for a texture.
* @throws GlException If the texture width or height is invalid.
*/
public static void assertValidTextureSize(int width, int height) {
public static void assertValidTextureSize(int width, int height) throws GlException {
// TODO(b/201293185): Consider handling adjustments for sizes > GL_MAX_TEXTURE_SIZE
// (ex. downscaling appropriately) in a FrameProcessor instead of asserting incorrect values.
// (ex. downscaling appropriately) in a texture processor instead of asserting incorrect
// values.
// For valid GL sizes, see:
// https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml
@ -318,20 +335,29 @@ public final class GlUtil {
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeBuffer, 0);
int maxTextureSize = maxTextureSizeBuffer[0];
if (width < 0 || height < 0) {
throwGlException("width or height is less than 0");
throw new GlException("width or height is less than 0");
}
if (width > maxTextureSize || height > maxTextureSize) {
throwGlException("width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
throw new GlException(
"width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
}
}
/** Fills the pixels in the current output render target with (r=0, g=0, b=0, a=0). */
public static void clearOutputFrame() throws GlException {
GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GlUtil.checkGlError();
}
/**
* Makes the specified {@code eglSurface} the render target, using a viewport of {@code width} by
* {@code height} pixels.
*/
@RequiresApi(17)
public static void focusEglSurface(
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height) {
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height)
throws GlException {
Api17.focusRenderTarget(
eglDisplay, eglContext, eglSurface, /* framebuffer= */ 0, width, height);
}
@ -347,16 +373,34 @@ public final class GlUtil {
EGLSurface eglSurface,
int framebuffer,
int width,
int height) {
int height)
throws GlException {
Api17.focusRenderTarget(eglDisplay, eglContext, eglSurface, framebuffer, width, height);
}
/**
* Makes the specified {@code framebuffer} the render target, using a viewport of {@code width} by
* {@code height} pixels.
*
* <p>The caller must ensure that there is a current OpenGL context before calling this method.
*
* @param framebuffer The identifier of the framebuffer object to bind as the output render
* target.
* @param width The viewport width, in pixels.
* @param height The viewport height, in pixels.
*/
@RequiresApi(17)
public static void focusFramebufferUsingCurrentContext(int framebuffer, int width, int height)
throws GlException {
Api17.focusFramebufferUsingCurrentContext(framebuffer, width, height);
}
/**
* Deletes a GL texture.
*
* @param textureId The ID of the texture to delete.
*/
public static void deleteTexture(int textureId) {
public static void deleteTexture(int textureId) throws GlException {
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
checkGlError();
}
@ -367,7 +411,7 @@ public final class GlUtil {
*/
@RequiresApi(17)
public static void destroyEglContext(
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) {
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
Api17.destroyEglContext(eglDisplay, eglContext);
}
@ -412,7 +456,7 @@ public final class GlUtil {
* Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and
* GL_CLAMP_TO_EDGE wrapping.
*/
public static int createExternalTexture() {
public static int createExternalTexture() throws GlException {
int texId = generateTexture();
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
return texId;
@ -424,7 +468,7 @@ public final class GlUtil {
* @param width of the new texture in pixels
* @param height of the new texture in pixels
*/
public static int createTexture(int width, int height) {
public static int createTexture(int width, int height) throws GlException {
assertValidTextureSize(width, height);
int texId = generateTexture();
bindTexture(GLES20.GL_TEXTURE_2D, texId);
@ -444,8 +488,8 @@ public final class GlUtil {
}
/** Returns a new GL texture identifier. */
private static int generateTexture() {
checkEglException(
private static int generateTexture() throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] texId = new int[1];
@ -463,7 +507,7 @@ public final class GlUtil {
* GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
* GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
*/
/* package */ static void bindTexture(int textureTarget, int texId) {
public static void bindTexture(int textureTarget, int texId) throws GlException {
GLES20.glBindTexture(textureTarget, texId);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
@ -481,8 +525,8 @@ public final class GlUtil {
*
* @param texId The identifier of the texture to attach to the framebuffer.
*/
public static int createFboForTexture(int texId) {
checkEglException(
public static int createFboForTexture(int texId) throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] fboId = new int[1];
@ -496,23 +540,19 @@ public final class GlUtil {
return fboId[0];
}
/* package */ static void throwGlException(String errorMsg) {
if (glAssertionsEnabled) {
throw new GlException(errorMsg);
} else {
Log.e(TAG, errorMsg);
}
}
private static void checkEglException(boolean expression, String errorMessage) {
/**
* Throws a {@link GlException} with the given message if {@code expression} evaluates to {@code
* false}.
*/
public static void checkGlException(boolean expression, String errorMessage) throws GlException {
if (!expression) {
throwGlException(errorMessage);
throw new GlException(errorMessage);
}
}
private static void checkEglException(String errorMessage) {
private static void checkEglException(String errorMessage) throws GlException {
int error = EGL14.eglGetError();
checkEglException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
checkGlException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
}
@RequiresApi(17)
@ -520,24 +560,24 @@ public final class GlUtil {
private Api17() {}
@DoNotInline
public static EGLDisplay createEglDisplay() {
public static EGLDisplay createEglDisplay() throws GlException {
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
checkEglException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
if (!EGL14.eglInitialize(
eglDisplay,
/* unusedMajor */ new int[1],
/* majorOffset= */ 0,
/* unusedMinor */ new int[1],
/* minorOffset= */ 0)) {
throwGlException("Error in eglInitialize.");
}
checkGlException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
checkGlException(
EGL14.eglInitialize(
eglDisplay,
/* unusedMajor */ new int[1],
/* majorOffset= */ 0,
/* unusedMinor */ new int[1],
/* minorOffset= */ 0),
"Error in eglInitialize.");
checkGlError();
return eglDisplay;
}
@DoNotInline
public static EGLContext createEglContext(
EGLDisplay eglDisplay, int version, int[] configAttributes) {
EGLDisplay eglDisplay, int version, int[] configAttributes) throws GlException {
int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, version, EGL14.EGL_NONE};
EGLContext eglContext =
EGL14.eglCreateContext(
@ -548,7 +588,7 @@ public final class GlUtil {
/* offset= */ 0);
if (eglContext == null) {
EGL14.eglTerminate(eglDisplay);
throwGlException(
throw new GlException(
"eglCreateContext() failed to create a valid context. The device may not support EGL"
+ " version "
+ version);
@ -562,7 +602,8 @@ public final class GlUtil {
EGLDisplay eglDisplay,
Object surface,
int[] configAttributes,
int[] windowSurfaceAttributes) {
int[] windowSurfaceAttributes)
throws GlException {
EGLSurface eglSurface =
EGL14.eglCreateWindowSurface(
eglDisplay,
@ -576,7 +617,7 @@ public final class GlUtil {
@DoNotInline
public static EGLSurface createEglPbufferSurface(
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) {
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) throws GlException {
EGLSurface eglSurface =
EGL14.eglCreatePbufferSurface(
eglDisplay,
@ -594,22 +635,32 @@ public final class GlUtil {
EGLSurface eglSurface,
int framebuffer,
int width,
int height) {
int height)
throws GlException {
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
checkEglException("Error making context current");
focusFramebufferUsingCurrentContext(framebuffer, width, height);
}
@DoNotInline
public static void focusFramebufferUsingCurrentContext(int framebuffer, int width, int height)
throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] boundFramebuffer = new int[1];
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, boundFramebuffer, /* offset= */ 0);
if (boundFramebuffer[0] != framebuffer) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer);
}
checkGlError();
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
checkEglException("Error making context current");
GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height);
checkGlError();
}
@DoNotInline
public static void destroyEglContext(
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) {
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
if (eglDisplay == null) {
return;
}
@ -627,7 +678,8 @@ public final class GlUtil {
}
@DoNotInline
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes) {
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes)
throws GlException {
EGLConfig[] eglConfigs = new EGLConfig[1];
if (!EGL14.eglChooseConfig(
eglDisplay,
@ -638,7 +690,7 @@ public final class GlUtil {
/* config_size= */ 1,
/* unusedNumConfig */ new int[1],
/* num_configOffset= */ 0)) {
throwGlException("eglChooseConfig failed.");
throw new GlException("eglChooseConfig failed.");
}
return eglConfigs[0];
}

View File

@ -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);
}
}
}
}

View File

@ -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)

View File

@ -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)) {

View File

@ -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)
@ -579,6 +581,24 @@ public class MediaItemTest {
assertThat(mediaItem.liveConfiguration.maxOffsetMs).isEqualTo(1234);
}
@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() {
@ -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();

View File

@ -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(

View File

@ -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)

View File

@ -0,0 +1,55 @@
/*
* 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();
}
}
}

View File

@ -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");

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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();
@ -448,6 +455,35 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
return builder.build();
}
/**
* 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.
*

View File

@ -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(

View File

@ -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

View File

@ -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;
}

View File

@ -484,6 +484,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
extensionRendererIndex--;
}
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");

View File

@ -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;

View File

@ -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. */

View File

@ -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

View File

@ -168,15 +168,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
@ -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

View File

@ -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)}

View File

@ -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) {

View File

@ -49,6 +49,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.UnstableApi;
import androidx.media3.decoder.DecoderException;
import androidx.media3.exoplayer.DecoderCounters;
@ -81,10 +82,10 @@ import java.util.List;
* #onIsPlayingChanged(EventTime, boolean)}) or {@link #onEvents(Player, Events)}, which is called
* after one or more events occurred together.
*/
@UnstableApi
public interface AnalyticsListener {
/** A set of {@link EventFlags}. */
@UnstableApi
final class Events {
private final FlagSet flags;
@ -164,6 +165,7 @@ public interface AnalyticsListener {
*/
// @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})
@ -233,140 +235,146 @@ public interface AnalyticsListener {
})
@interface EventFlags {}
/** {@link Player#getCurrentTimeline()} changed. */
int EVENT_TIMELINE_CHANGED = Player.EVENT_TIMELINE_CHANGED;
@UnstableApi int EVENT_TIMELINE_CHANGED = Player.EVENT_TIMELINE_CHANGED;
/**
* {@link Player#getCurrentMediaItem()} changed or the player started repeating the current item.
*/
int EVENT_MEDIA_ITEM_TRANSITION = Player.EVENT_MEDIA_ITEM_TRANSITION;
@UnstableApi int EVENT_MEDIA_ITEM_TRANSITION = Player.EVENT_MEDIA_ITEM_TRANSITION;
/** {@link Player#getCurrentTracks()} changed. */
int EVENT_TRACKS_CHANGED = Player.EVENT_TRACKS_CHANGED;
@UnstableApi int EVENT_TRACKS_CHANGED = Player.EVENT_TRACKS_CHANGED;
/** {@link Player#isLoading()} ()} changed. */
int EVENT_IS_LOADING_CHANGED = Player.EVENT_IS_LOADING_CHANGED;
@UnstableApi int EVENT_IS_LOADING_CHANGED = Player.EVENT_IS_LOADING_CHANGED;
/** {@link Player#getPlaybackState()} changed. */
int EVENT_PLAYBACK_STATE_CHANGED = Player.EVENT_PLAYBACK_STATE_CHANGED;
@UnstableApi int EVENT_PLAYBACK_STATE_CHANGED = Player.EVENT_PLAYBACK_STATE_CHANGED;
/** {@link Player#getPlayWhenReady()} changed. */
int EVENT_PLAY_WHEN_READY_CHANGED = Player.EVENT_PLAY_WHEN_READY_CHANGED;
@UnstableApi int EVENT_PLAY_WHEN_READY_CHANGED = Player.EVENT_PLAY_WHEN_READY_CHANGED;
/** {@link Player#getPlaybackSuppressionReason()} changed. */
@UnstableApi
int EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED = Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED;
/** {@link Player#isPlaying()} changed. */
int EVENT_IS_PLAYING_CHANGED = Player.EVENT_IS_PLAYING_CHANGED;
@UnstableApi int EVENT_IS_PLAYING_CHANGED = Player.EVENT_IS_PLAYING_CHANGED;
/** {@link Player#getRepeatMode()} changed. */
int EVENT_REPEAT_MODE_CHANGED = Player.EVENT_REPEAT_MODE_CHANGED;
@UnstableApi int EVENT_REPEAT_MODE_CHANGED = Player.EVENT_REPEAT_MODE_CHANGED;
/** {@link Player#getShuffleModeEnabled()} changed. */
int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED;
@UnstableApi int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED;
/** {@link Player#getPlayerError()} changed. */
int EVENT_PLAYER_ERROR = Player.EVENT_PLAYER_ERROR;
@UnstableApi int EVENT_PLAYER_ERROR = Player.EVENT_PLAYER_ERROR;
/**
* A position discontinuity occurred. See {@link
* Player.Listener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)}.
*/
int EVENT_POSITION_DISCONTINUITY = Player.EVENT_POSITION_DISCONTINUITY;
@UnstableApi int EVENT_POSITION_DISCONTINUITY = Player.EVENT_POSITION_DISCONTINUITY;
/** {@link Player#getPlaybackParameters()} changed. */
int EVENT_PLAYBACK_PARAMETERS_CHANGED = Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
@UnstableApi int EVENT_PLAYBACK_PARAMETERS_CHANGED = Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
/** {@link Player#getAvailableCommands()} changed. */
int EVENT_AVAILABLE_COMMANDS_CHANGED = Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
@UnstableApi int EVENT_AVAILABLE_COMMANDS_CHANGED = Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
/** {@link Player#getMediaMetadata()} changed. */
int EVENT_MEDIA_METADATA_CHANGED = Player.EVENT_MEDIA_METADATA_CHANGED;
@UnstableApi int EVENT_MEDIA_METADATA_CHANGED = Player.EVENT_MEDIA_METADATA_CHANGED;
/** {@link Player#getPlaylistMetadata()} changed. */
int EVENT_PLAYLIST_METADATA_CHANGED = Player.EVENT_PLAYLIST_METADATA_CHANGED;
@UnstableApi int EVENT_PLAYLIST_METADATA_CHANGED = Player.EVENT_PLAYLIST_METADATA_CHANGED;
/** {@link Player#getSeekBackIncrement()} changed. */
int EVENT_SEEK_BACK_INCREMENT_CHANGED = Player.EVENT_SEEK_BACK_INCREMENT_CHANGED;
@UnstableApi int EVENT_SEEK_BACK_INCREMENT_CHANGED = Player.EVENT_SEEK_BACK_INCREMENT_CHANGED;
/** {@link Player#getSeekForwardIncrement()} changed. */
@UnstableApi
int EVENT_SEEK_FORWARD_INCREMENT_CHANGED = Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED;
/** {@link Player#getMaxSeekToPreviousPosition()} changed. */
@UnstableApi
int EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED =
Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED;
/** {@link Player#getTrackSelectionParameters()} changed. */
@UnstableApi
int EVENT_TRACK_SELECTION_PARAMETERS_CHANGED = Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED;
/** Audio attributes changed. */
int EVENT_AUDIO_ATTRIBUTES_CHANGED = Player.EVENT_AUDIO_ATTRIBUTES_CHANGED;
@UnstableApi int EVENT_AUDIO_ATTRIBUTES_CHANGED = Player.EVENT_AUDIO_ATTRIBUTES_CHANGED;
/** An audio session id was set. */
int EVENT_AUDIO_SESSION_ID = Player.EVENT_AUDIO_SESSION_ID;
@UnstableApi int EVENT_AUDIO_SESSION_ID = Player.EVENT_AUDIO_SESSION_ID;
/** The volume changed. */
int EVENT_VOLUME_CHANGED = Player.EVENT_VOLUME_CHANGED;
@UnstableApi int EVENT_VOLUME_CHANGED = Player.EVENT_VOLUME_CHANGED;
/** Skipping silences was enabled or disabled in the audio stream. */
int EVENT_SKIP_SILENCE_ENABLED_CHANGED = Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED;
@UnstableApi int EVENT_SKIP_SILENCE_ENABLED_CHANGED = Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED;
/** The surface size changed. */
int EVENT_SURFACE_SIZE_CHANGED = Player.EVENT_SURFACE_SIZE_CHANGED;
@UnstableApi int EVENT_SURFACE_SIZE_CHANGED = Player.EVENT_SURFACE_SIZE_CHANGED;
/** The video size changed. */
int EVENT_VIDEO_SIZE_CHANGED = Player.EVENT_VIDEO_SIZE_CHANGED;
@UnstableApi int EVENT_VIDEO_SIZE_CHANGED = Player.EVENT_VIDEO_SIZE_CHANGED;
/**
* The first frame has been rendered since setting the surface, since the renderer was reset or
* since the stream changed.
*/
int EVENT_RENDERED_FIRST_FRAME = Player.EVENT_RENDERED_FIRST_FRAME;
@UnstableApi int EVENT_RENDERED_FIRST_FRAME = Player.EVENT_RENDERED_FIRST_FRAME;
/** Metadata associated with the current playback time was reported. */
int EVENT_METADATA = Player.EVENT_METADATA;
@UnstableApi int EVENT_METADATA = Player.EVENT_METADATA;
/** {@link Player#getCurrentCues()} changed. */
int EVENT_CUES = Player.EVENT_CUES;
@UnstableApi int EVENT_CUES = Player.EVENT_CUES;
/** {@link Player#getDeviceInfo()} changed. */
int EVENT_DEVICE_INFO_CHANGED = Player.EVENT_DEVICE_INFO_CHANGED;
@UnstableApi int EVENT_DEVICE_INFO_CHANGED = Player.EVENT_DEVICE_INFO_CHANGED;
/** {@link Player#getDeviceVolume()} changed. */
int EVENT_DEVICE_VOLUME_CHANGED = Player.EVENT_DEVICE_VOLUME_CHANGED;
@UnstableApi int EVENT_DEVICE_VOLUME_CHANGED = Player.EVENT_DEVICE_VOLUME_CHANGED;
/** A source started loading data. */
@UnstableApi
int EVENT_LOAD_STARTED = 1000; // Intentional gap to leave space for new Player events
/** A source started completed loading data. */
int EVENT_LOAD_COMPLETED = 1001;
@UnstableApi int EVENT_LOAD_COMPLETED = 1001;
/** A source canceled loading data. */
int EVENT_LOAD_CANCELED = 1002;
@UnstableApi int EVENT_LOAD_CANCELED = 1002;
/** A source had a non-fatal error loading data. */
int EVENT_LOAD_ERROR = 1003;
@UnstableApi int EVENT_LOAD_ERROR = 1003;
/** The downstream format sent to renderers changed. */
int EVENT_DOWNSTREAM_FORMAT_CHANGED = 1004;
@UnstableApi int EVENT_DOWNSTREAM_FORMAT_CHANGED = 1004;
/** Data was removed from the end of the media buffer. */
int EVENT_UPSTREAM_DISCARDED = 1005;
@UnstableApi int EVENT_UPSTREAM_DISCARDED = 1005;
/** The bandwidth estimate has been updated. */
int EVENT_BANDWIDTH_ESTIMATE = 1006;
@UnstableApi int EVENT_BANDWIDTH_ESTIMATE = 1006;
/** An audio renderer was enabled. */
int EVENT_AUDIO_ENABLED = 1007;
@UnstableApi int EVENT_AUDIO_ENABLED = 1007;
/** An audio renderer created a decoder. */
int EVENT_AUDIO_DECODER_INITIALIZED = 1008;
@UnstableApi int EVENT_AUDIO_DECODER_INITIALIZED = 1008;
/** The format consumed by an audio renderer changed. */
int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 1009;
@UnstableApi int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 1009;
/** The audio position has increased for the first time since the last pause or position reset. */
int EVENT_AUDIO_POSITION_ADVANCING = 1010;
@UnstableApi int EVENT_AUDIO_POSITION_ADVANCING = 1010;
/** An audio underrun occurred. */
int EVENT_AUDIO_UNDERRUN = 1011;
@UnstableApi int EVENT_AUDIO_UNDERRUN = 1011;
/** An audio renderer released a decoder. */
int EVENT_AUDIO_DECODER_RELEASED = 1012;
@UnstableApi int EVENT_AUDIO_DECODER_RELEASED = 1012;
/** An audio renderer was disabled. */
int EVENT_AUDIO_DISABLED = 1013;
@UnstableApi int EVENT_AUDIO_DISABLED = 1013;
/** The audio sink encountered a non-fatal error. */
int EVENT_AUDIO_SINK_ERROR = 1014;
@UnstableApi int EVENT_AUDIO_SINK_ERROR = 1014;
/** A video renderer was enabled. */
int EVENT_VIDEO_ENABLED = 1015;
@UnstableApi int EVENT_VIDEO_ENABLED = 1015;
/** A video renderer created a decoder. */
int EVENT_VIDEO_DECODER_INITIALIZED = 1016;
@UnstableApi int EVENT_VIDEO_DECODER_INITIALIZED = 1016;
/** The format consumed by a video renderer changed. */
int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 1017;
@UnstableApi int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 1017;
/** Video frames have been dropped. */
int EVENT_DROPPED_VIDEO_FRAMES = 1018;
@UnstableApi int EVENT_DROPPED_VIDEO_FRAMES = 1018;
/** A video renderer released a decoder. */
int EVENT_VIDEO_DECODER_RELEASED = 1019;
@UnstableApi int EVENT_VIDEO_DECODER_RELEASED = 1019;
/** A video renderer was disabled. */
int EVENT_VIDEO_DISABLED = 1020;
@UnstableApi int EVENT_VIDEO_DISABLED = 1020;
/** Video frame processing offset data has been reported. */
int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 1021;
@UnstableApi int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 1021;
/** A DRM session has been acquired. */
int EVENT_DRM_SESSION_ACQUIRED = 1022;
@UnstableApi int EVENT_DRM_SESSION_ACQUIRED = 1022;
/** DRM keys were loaded. */
int EVENT_DRM_KEYS_LOADED = 1023;
@UnstableApi int EVENT_DRM_KEYS_LOADED = 1023;
/** A non-fatal DRM session manager error occurred. */
int EVENT_DRM_SESSION_MANAGER_ERROR = 1024;
@UnstableApi int EVENT_DRM_SESSION_MANAGER_ERROR = 1024;
/** DRM keys were restored. */
int EVENT_DRM_KEYS_RESTORED = 1025;
@UnstableApi int EVENT_DRM_KEYS_RESTORED = 1025;
/** DRM keys were removed. */
int EVENT_DRM_KEYS_REMOVED = 1026;
@UnstableApi int EVENT_DRM_KEYS_REMOVED = 1026;
/** A DRM session has been released. */
int EVENT_DRM_SESSION_RELEASED = 1027;
@UnstableApi int EVENT_DRM_SESSION_RELEASED = 1027;
/** The player was released. */
int EVENT_PLAYER_RELEASED = 1028;
@UnstableApi int EVENT_PLAYER_RELEASED = 1028;
/** The audio codec encountered an error. */
int EVENT_AUDIO_CODEC_ERROR = 1029;
@UnstableApi int EVENT_AUDIO_CODEC_ERROR = 1029;
/** The video codec encountered an error. */
int EVENT_VIDEO_CODEC_ERROR = 1030;
@UnstableApi int EVENT_VIDEO_CODEC_ERROR = 1030;
/** Time information of an event. */
@UnstableApi
final class EventTime {
/**
@ -515,6 +523,7 @@ public interface AnalyticsListener {
* @deprecated Use {@link #onPlaybackStateChanged(EventTime, int)} and {@link
* #onPlayWhenReadyChanged(EventTime, boolean, int)} instead.
*/
@UnstableApi
@Deprecated
default void onPlayerStateChanged(
EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {}
@ -525,6 +534,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param state The new {@link Player.State playback state}.
*/
@UnstableApi
default void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {}
/**
@ -534,6 +544,7 @@ public interface AnalyticsListener {
* @param playWhenReady Whether playback will proceed when ready.
* @param reason The {@link Player.PlayWhenReadyChangeReason reason} of the change.
*/
@UnstableApi
default void onPlayWhenReadyChanged(
EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {}
@ -543,6 +554,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param playbackSuppressionReason The new {@link PlaybackSuppressionReason}.
*/
@UnstableApi
default void onPlaybackSuppressionReasonChanged(
EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {}
@ -552,6 +564,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param isPlaying Whether the player is playing.
*/
@UnstableApi
default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {}
/**
@ -560,6 +573,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param reason The reason for the timeline change.
*/
@UnstableApi
default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {}
/**
@ -569,6 +583,7 @@ public interface AnalyticsListener {
* @param mediaItem The media item.
* @param reason The reason for the media item transition.
*/
@UnstableApi
default void onMediaItemTransition(
EventTime eventTime,
@Nullable MediaItem mediaItem,
@ -578,6 +593,7 @@ public interface AnalyticsListener {
* @deprecated Use {@link #onPositionDiscontinuity(EventTime, Player.PositionInfo,
* Player.PositionInfo, int)} instead.
*/
@UnstableApi
@Deprecated
default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {}
@ -589,6 +605,7 @@ public interface AnalyticsListener {
* @param newPosition The position after the discontinuity.
* @param reason The reason for the position discontinuity.
*/
@UnstableApi
default void onPositionDiscontinuity(
EventTime eventTime,
Player.PositionInfo oldPosition,
@ -600,6 +617,7 @@ public interface AnalyticsListener {
* Player.PositionInfo, int)} instead, listening to changes with {@link
* Player#DISCONTINUITY_REASON_SEEK}.
*/
@UnstableApi
@Deprecated
default void onSeekStarted(EventTime eventTime) {}
@ -607,6 +625,7 @@ public interface AnalyticsListener {
* @deprecated Seeks are processed without delay. Use {@link #onPositionDiscontinuity(EventTime,
* int)} with reason {@link Player#DISCONTINUITY_REASON_SEEK} instead.
*/
@UnstableApi
@Deprecated
default void onSeekProcessed(EventTime eventTime) {}
@ -616,6 +635,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param playbackParameters The new playback parameters.
*/
@UnstableApi
default void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {}
@ -625,6 +645,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param seekBackIncrementMs The seek back increment, in milliseconds.
*/
@UnstableApi
default void onSeekBackIncrementChanged(EventTime eventTime, long seekBackIncrementMs) {}
/**
@ -633,6 +654,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param seekForwardIncrementMs The seek forward increment, in milliseconds.
*/
@UnstableApi
default void onSeekForwardIncrementChanged(EventTime eventTime, long seekForwardIncrementMs) {}
/**
@ -642,6 +664,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param maxSeekToPreviousPositionMs The maximum seek to previous position, in milliseconds.
*/
@UnstableApi
default void onMaxSeekToPreviousPositionChanged(
EventTime eventTime, long maxSeekToPreviousPositionMs) {}
@ -651,6 +674,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param repeatMode The new repeat mode.
*/
@UnstableApi
default void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {}
/**
@ -659,6 +683,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param shuffleModeEnabled Whether the shuffle mode is enabled.
*/
@UnstableApi
default void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {}
/**
@ -667,11 +692,13 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param isLoading Whether the player is loading.
*/
@UnstableApi
default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {}
/**
* @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead.
*/
@UnstableApi
@Deprecated
default void onLoadingChanged(EventTime eventTime, boolean isLoading) {}
@ -681,6 +708,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param availableCommands The available commands.
*/
@UnstableApi
default void onAvailableCommandsChanged(EventTime eventTime, Player.Commands availableCommands) {}
/**
@ -692,6 +720,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param error The error.
*/
@UnstableApi
default void onPlayerError(EventTime eventTime, PlaybackException error) {}
/**
@ -703,6 +732,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param error The new error, or null if the error is being cleared.
*/
@UnstableApi
default void onPlayerErrorChanged(EventTime eventTime, @Nullable PlaybackException error) {}
/**
@ -711,6 +741,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param tracks The tracks. Never null, but may be of length zero.
*/
@UnstableApi
default void onTracksChanged(EventTime eventTime, Tracks tracks) {}
/**
@ -719,6 +750,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param trackSelectionParameters The new {@link TrackSelectionParameters}.
*/
@UnstableApi
default void onTrackSelectionParametersChanged(
EventTime eventTime, TrackSelectionParameters trackSelectionParameters) {}
@ -732,6 +764,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param mediaMetadata The combined {@link MediaMetadata}.
*/
@UnstableApi
default void onMediaMetadataChanged(EventTime eventTime, MediaMetadata mediaMetadata) {}
/**
@ -740,6 +773,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param playlistMetadata The playlist {@link MediaMetadata}.
*/
@UnstableApi
default void onPlaylistMetadataChanged(EventTime eventTime, MediaMetadata playlistMetadata) {}
/**
@ -749,6 +783,7 @@ public interface AnalyticsListener {
* @param loadEventInfo The {@link LoadEventInfo} defining the load event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
@UnstableApi
default void onLoadStarted(
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
@ -759,6 +794,7 @@ public interface AnalyticsListener {
* @param loadEventInfo The {@link LoadEventInfo} defining the load event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
@UnstableApi
default void onLoadCompleted(
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
@ -769,6 +805,7 @@ public interface AnalyticsListener {
* @param loadEventInfo The {@link LoadEventInfo} defining the load event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
@UnstableApi
default void onLoadCanceled(
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
@ -788,6 +825,7 @@ public interface AnalyticsListener {
* @param error The load error.
* @param wasCanceled Whether the load was canceled as a result of the error.
*/
@UnstableApi
default void onLoadError(
EventTime eventTime,
LoadEventInfo loadEventInfo,
@ -801,6 +839,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data.
*/
@UnstableApi
default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}
/**
@ -810,6 +849,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.
*/
@UnstableApi
default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}
/**
@ -820,6 +860,7 @@ public interface AnalyticsListener {
* @param totalBytesLoaded The total bytes loaded this update is based on.
* @param bitrateEstimate The bandwidth estimate, in bits per second.
*/
@UnstableApi
default void onBandwidthEstimate(
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}
@ -829,23 +870,40 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param metadata The metadata.
*/
@UnstableApi
default void onMetadata(EventTime eventTime, Metadata metadata) {}
/**
* 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(EventTime, List)} and {@link #onCues(EventTime, CueGroup)} are called
* when there is a change in the cues. You should only implement one or the other.
*
* @param eventTime The event time.
* @param cues The {@link Cue Cues}. May be empty.
* @param cues The {@link Cue Cues}.
* @deprecated Use {@link #onCues(EventTime, CueGroup)} instead.
*/
@Deprecated
@UnstableApi
default void onCues(EventTime eventTime, List<Cue> cues) {}
/**
* Called when there is a change in the {@link CueGroup}.
*
* <p>Both {@link #onCues(EventTime, List)} and {@link #onCues(EventTime, CueGroup)} are called
* when there is a change in the cues. You should only implement one or the other.
*
* @param eventTime The event time.
* @param cueGroup The {@link CueGroup}.
*/
@UnstableApi
default void onCues(EventTime eventTime, CueGroup cueGroup) {}
/**
* @deprecated Use {@link #onAudioEnabled} and {@link #onVideoEnabled} instead.
*/
@Deprecated
@UnstableApi
default void onDecoderEnabled(
EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
@ -853,6 +911,7 @@ public interface AnalyticsListener {
* @deprecated Use {@link #onAudioDecoderInitialized} and {@link #onVideoDecoderInitialized}
* instead.
*/
@UnstableApi
@Deprecated
default void onDecoderInitialized(
EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {}
@ -861,12 +920,14 @@ public interface AnalyticsListener {
* @deprecated Use {@link #onAudioInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}
* and {@link #onVideoInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}. instead.
*/
@UnstableApi
@Deprecated
default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {}
/**
* @deprecated Use {@link #onAudioDisabled} and {@link #onVideoDisabled} instead.
*/
@UnstableApi
@Deprecated
default void onDecoderDisabled(
EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
@ -878,6 +939,7 @@ public interface AnalyticsListener {
* @param decoderCounters {@link DecoderCounters} that will be updated by the renderer for as long
* as it remains enabled.
*/
@UnstableApi
default void onAudioEnabled(EventTime eventTime, DecoderCounters decoderCounters) {}
/**
@ -889,6 +951,7 @@ public interface AnalyticsListener {
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
@UnstableApi
default void onAudioDecoderInitialized(
EventTime eventTime,
String decoderName,
@ -898,6 +961,7 @@ public interface AnalyticsListener {
/**
* @deprecated Use {@link #onAudioDecoderInitialized(EventTime, String, long, long)}.
*/
@UnstableApi
@Deprecated
default void onAudioDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {}
@ -905,6 +969,7 @@ public interface AnalyticsListener {
/**
* @deprecated Use {@link #onAudioInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}.
*/
@UnstableApi
@Deprecated
default void onAudioInputFormatChanged(EventTime eventTime, Format format) {}
@ -917,6 +982,7 @@ public interface AnalyticsListener {
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@UnstableApi
default void onAudioInputFormatChanged(
EventTime eventTime,
Format format,
@ -930,6 +996,7 @@ public interface AnalyticsListener {
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
* which playout started.
*/
@UnstableApi
default void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {}
/**
@ -941,6 +1008,7 @@ public interface AnalyticsListener {
* encoded audio. {@link C#TIME_UNSET} if the output buffer contains non-PCM encoded audio.
* @param elapsedSinceLastFeedMs The time since audio was last written to the output buffer.
*/
@UnstableApi
default void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
@ -950,6 +1018,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param decoderName The decoder that was released.
*/
@UnstableApi
default void onAudioDecoderReleased(EventTime eventTime, String decoderName) {}
/**
@ -958,6 +1027,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param decoderCounters {@link DecoderCounters} that were updated by the renderer.
*/
@UnstableApi
default void onAudioDisabled(EventTime eventTime, DecoderCounters decoderCounters) {}
/**
@ -966,6 +1036,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param audioSessionId The audio session ID.
*/
@UnstableApi
default void onAudioSessionIdChanged(EventTime eventTime, int audioSessionId) {}
/**
@ -974,6 +1045,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param audioAttributes The audio attributes.
*/
@UnstableApi
default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {}
/**
@ -982,6 +1054,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled.
*/
@UnstableApi
default void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) {}
/**
@ -999,6 +1072,7 @@ public interface AnalyticsListener {
* AudioSink.InitializationException}, a {@link AudioSink.WriteException}, or an {@link
* AudioSink.UnexpectedDiscontinuityException}.
*/
@UnstableApi
default void onAudioSinkError(EventTime eventTime, Exception audioSinkError) {}
/**
@ -1015,6 +1089,7 @@ public interface AnalyticsListener {
* @param audioCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
@UnstableApi
default void onAudioCodecError(EventTime eventTime, Exception audioCodecError) {}
/**
@ -1023,6 +1098,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param volume The new volume, with 0 being silence and 1 being unity gain.
*/
@UnstableApi
default void onVolumeChanged(EventTime eventTime, float volume) {}
/**
@ -1031,6 +1107,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param deviceInfo The new {@link DeviceInfo}.
*/
@UnstableApi
default void onDeviceInfoChanged(EventTime eventTime, DeviceInfo deviceInfo) {}
/**
@ -1040,6 +1117,7 @@ public interface AnalyticsListener {
* @param volume The new device volume, with 0 being silence and 1 being unity gain.
* @param muted Whether the device is muted.
*/
@UnstableApi
default void onDeviceVolumeChanged(EventTime eventTime, int volume, boolean muted) {}
/**
@ -1049,6 +1127,7 @@ public interface AnalyticsListener {
* @param decoderCounters {@link DecoderCounters} that will be updated by the renderer for as long
* as it remains enabled.
*/
@UnstableApi
default void onVideoEnabled(EventTime eventTime, DecoderCounters decoderCounters) {}
/**
@ -1060,6 +1139,7 @@ public interface AnalyticsListener {
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
@UnstableApi
default void onVideoDecoderInitialized(
EventTime eventTime,
String decoderName,
@ -1069,6 +1149,7 @@ public interface AnalyticsListener {
/**
* @deprecated Use {@link #onVideoDecoderInitialized(EventTime, String, long, long)}.
*/
@UnstableApi
@Deprecated
default void onVideoDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {}
@ -1076,6 +1157,7 @@ public interface AnalyticsListener {
/**
* @deprecated Use {@link #onVideoInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}.
*/
@UnstableApi
@Deprecated
default void onVideoInputFormatChanged(EventTime eventTime, Format format) {}
@ -1088,6 +1170,7 @@ public interface AnalyticsListener {
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@UnstableApi
default void onVideoInputFormatChanged(
EventTime eventTime,
Format format,
@ -1102,6 +1185,7 @@ public interface AnalyticsListener {
* is timed from when the renderer was started or from when dropped frames were last reported
* (whichever was more recent), and not from when the first of the reported drops occurred.
*/
@UnstableApi
default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}
/**
@ -1110,6 +1194,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param decoderName The decoder that was released.
*/
@UnstableApi
default void onVideoDecoderReleased(EventTime eventTime, String decoderName) {}
/**
@ -1118,6 +1203,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param decoderCounters {@link DecoderCounters} that were updated by the renderer.
*/
@UnstableApi
default void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) {}
/**
@ -1135,6 +1221,7 @@ public interface AnalyticsListener {
* rendered since the last call to this method.
* @param frameCount The number to samples included in {@code totalProcessingOffsetUs}.
*/
@UnstableApi
default void onVideoFrameProcessingOffset(
EventTime eventTime, long totalProcessingOffsetUs, int frameCount) {}
@ -1152,6 +1239,7 @@ public interface AnalyticsListener {
* @param videoCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
@UnstableApi
default void onVideoCodecError(EventTime eventTime, Exception videoCodecError) {}
/**
@ -1163,6 +1251,7 @@ public interface AnalyticsListener {
* however may also be other output types (e.g., a {@link VideoDecoderOutputBufferRenderer}).
* @param renderTimeMs {@link SystemClock#elapsedRealtime()} when the first frame was rendered.
*/
@UnstableApi
default void onRenderedFirstFrame(EventTime eventTime, Object output, long renderTimeMs) {}
/**
@ -1172,11 +1261,13 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param videoSize The new size of the video.
*/
@UnstableApi
default void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) {}
/**
* @deprecated Implement {@link #onVideoSizeChanged(EventTime eventTime, VideoSize)} instead.
*/
@UnstableApi
@Deprecated
default void onVideoSizeChanged(
EventTime eventTime,
@ -1194,11 +1285,13 @@ public interface AnalyticsListener {
* @param height The surface height in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if
* the video is not rendered onto a surface.
*/
@UnstableApi
default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}
/**
* @deprecated Implement {@link #onDrmSessionAcquired(EventTime, int)} instead.
*/
@UnstableApi
@Deprecated
default void onDrmSessionAcquired(EventTime eventTime) {}
@ -1208,6 +1301,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param state The {@link DrmSession.State} of the session when the acquisition completed.
*/
@UnstableApi
default void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {}
/**
@ -1215,6 +1309,7 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
*/
@UnstableApi
default void onDrmKeysLoaded(EventTime eventTime) {}
/**
@ -1230,6 +1325,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param error The error.
*/
@UnstableApi
default void onDrmSessionManagerError(EventTime eventTime, Exception error) {}
/**
@ -1237,6 +1333,7 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
*/
@UnstableApi
default void onDrmKeysRestored(EventTime eventTime) {}
/**
@ -1244,6 +1341,7 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
*/
@UnstableApi
default void onDrmKeysRemoved(EventTime eventTime) {}
/**
@ -1251,6 +1349,7 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
*/
@UnstableApi
default void onDrmSessionReleased(EventTime eventTime) {}
/**
@ -1258,6 +1357,7 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
*/
@UnstableApi
default void onPlayerReleased(EventTime eventTime) {}
/**
@ -1287,5 +1387,6 @@ public interface AnalyticsListener {
* @param player The {@link Player}.
* @param events The {@link Events} that occurred in this iteration.
*/
@UnstableApi
default void onEvents(Player player, Events events) {}
}

View File

@ -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() {

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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.

View File

@ -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

View File

@ -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;
}
}

View File

@ -856,9 +856,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_FLOAT);
}
if (Util.SDK_INT >= 32) {
// TODO[b/190759307] Use MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT once the
// compile SDK target is set to 32.
mediaFormat.setInteger("max-output-channel-count", 99);
mediaFormat.setInteger(MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT, 99);
}
return mediaFormat;
@ -932,9 +930,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
@Override
public void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {
public void onOffloadBufferFull() {
if (wakeupListener != null) {
wakeupListener.onSleep(bufferEmptyingDeadlineMs);
wakeupListener.onSleep();
}
}

View File

@ -94,12 +94,15 @@ public interface ExoMediaDrm {
}
/** Event indicating that keys need to be requested from the license server. */
@UnstableApi
@SuppressWarnings("InlinedApi")
int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED;
/** Event indicating that keys have expired, and are no longer usable. */
@UnstableApi
@SuppressWarnings("InlinedApi")
int EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED;
/** Event indicating that a certificate needs to be requested from the provisioning server. */
@UnstableApi
@SuppressWarnings("InlinedApi")
int EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED;
@ -107,15 +110,18 @@ public interface ExoMediaDrm {
* Key request type for keys that will be used for online use. Streaming keys will not be saved to
* the device for subsequent use when the device is not connected to a network.
*/
@UnstableApi
@SuppressWarnings("InlinedApi")
int KEY_TYPE_STREAMING = MediaDrm.KEY_TYPE_STREAMING;
/**
* Key request type for keys that will be used for offline use. They will be saved to the device
* for subsequent use when the device is not connected to a network.
*/
@UnstableApi
@SuppressWarnings("InlinedApi")
int KEY_TYPE_OFFLINE = MediaDrm.KEY_TYPE_OFFLINE;
/** Key request type indicating that saved offline keys should be released. */
@UnstableApi
@SuppressWarnings("InlinedApi")
int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE;

View File

@ -53,7 +53,6 @@ import java.util.UUID;
/** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */
@RequiresApi(18)
@UnstableApi
public final class FrameworkMediaDrm implements ExoMediaDrm {
private static final String TAG = "FrameworkMediaDrm";
@ -63,6 +62,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
* UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID
* is not supported by the device.
*/
@UnstableApi
public static final Provider DEFAULT_PROVIDER =
uuid -> {
try {
@ -99,6 +99,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
* @return The created instance.
* @throws UnsupportedDrmException If the DRM scheme is unsupported or cannot be instantiated.
*/
@UnstableApi
public static FrameworkMediaDrm newInstance(UUID uuid) throws UnsupportedDrmException {
try {
return new FrameworkMediaDrm(uuid);
@ -121,6 +122,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
}
}
@UnstableApi
@Override
public void setOnEventListener(@Nullable ExoMediaDrm.OnEventListener listener) {
mediaDrm.setOnEventListener(
@ -136,6 +138,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
* @param listener The listener to receive events, or {@code null} to stop receiving events.
* @throws UnsupportedOperationException on API levels lower than 23.
*/
@UnstableApi
@Override
@RequiresApi(23)
public void setOnKeyStatusChangeListener(
@ -164,6 +167,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
* @param listener The listener to receive events, or {@code null} to stop receiving events.
* @throws UnsupportedOperationException on API levels lower than 23.
*/
@UnstableApi
@Override
@RequiresApi(23)
public void setOnExpirationUpdateListener(@Nullable OnExpirationUpdateListener listener) {
@ -179,16 +183,19 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
/* handler= */ null);
}
@UnstableApi
@Override
public byte[] openSession() throws MediaDrmException {
return mediaDrm.openSession();
}
@UnstableApi
@Override
public void closeSession(byte[] sessionId) {
mediaDrm.closeSession(sessionId);
}
@UnstableApi
@Override
public void setPlayerIdForSession(byte[] sessionId, PlayerId playerId) {
if (Util.SDK_INT >= 31) {
@ -202,6 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
// Return values of MediaDrm.KeyRequest.getRequestType are equal to KeyRequest.RequestType.
@SuppressLint("WrongConstant")
@UnstableApi
@Override
public KeyRequest getKeyRequest(
byte[] scope,
@ -239,6 +247,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
return new KeyRequest(requestData, licenseServerUrl, requestType);
}
@UnstableApi
@Override
@Nullable
public byte[] provideKeyResponse(byte[] scope, byte[] response)
@ -250,22 +259,26 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
return mediaDrm.provideKeyResponse(scope, response);
}
@UnstableApi
@Override
public ProvisionRequest getProvisionRequest() {
final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest();
return new ProvisionRequest(request.getData(), request.getDefaultUrl());
}
@UnstableApi
@Override
public void provideProvisionResponse(byte[] response) throws DeniedByServerException {
mediaDrm.provideProvisionResponse(response);
}
@UnstableApi
@Override
public Map<String, String> queryKeyStatus(byte[] sessionId) {
return mediaDrm.queryKeyStatus(sessionId);
}
@UnstableApi
@Override
public boolean requiresSecureDecoder(byte[] sessionId, String mimeType) {
if (Util.SDK_INT >= 31) {
@ -286,12 +299,14 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
}
}
@UnstableApi
@Override
public synchronized void acquire() {
Assertions.checkState(referenceCount > 0);
referenceCount++;
}
@UnstableApi
@Override
public synchronized void release() {
if (--referenceCount == 0) {
@ -299,11 +314,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
}
}
@UnstableApi
@Override
public void restoreKeys(byte[] sessionId, byte[] keySetId) {
mediaDrm.restoreKeys(sessionId, keySetId);
}
@UnstableApi
@Override
@Nullable
public PersistableBundle getMetrics() {
@ -313,26 +330,31 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
return mediaDrm.getMetrics();
}
@UnstableApi
@Override
public String getPropertyString(String propertyName) {
return mediaDrm.getPropertyString(propertyName);
}
@UnstableApi
@Override
public byte[] getPropertyByteArray(String propertyName) {
return mediaDrm.getPropertyByteArray(propertyName);
}
@UnstableApi
@Override
public void setPropertyString(String propertyName, String value) {
mediaDrm.setPropertyString(propertyName, value);
}
@UnstableApi
@Override
public void setPropertyByteArray(String propertyName, byte[] value) {
mediaDrm.setPropertyByteArray(propertyName, value);
}
@UnstableApi
@Override
public FrameworkCryptoConfig createCryptoConfig(byte[] sessionId) throws MediaCryptoException {
// Work around a bug prior to Lollipop where L1 Widevine forced into L3 mode would still
@ -345,6 +367,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
adjustUuid(uuid), sessionId, forceAllowInsecureDecoderComponents);
}
@UnstableApi
@Override
public @C.CryptoType int getCryptoType() {
return C.CRYPTO_TYPE_FRAMEWORK;
@ -462,7 +485,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
return requestData;
}
@SuppressLint("WrongConstant") // Suppress spurious lint error [Internal ref: b/32137960]
private static void forceWidevineL3(MediaDrm mediaDrm) {
mediaDrm.setPropertyString("securityLevel", "L3");
}

View File

@ -1994,7 +1994,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*
* @throws ExoPlaybackException If an error occurs processing the signal.
*/
@TargetApi(23) // codecDrainAction == DRAIN_ACTION_UPDATE_DRM_SESSION implies SDK_INT >= 23.
// codecDrainAction == DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION implies SDK_INT >= 23.
@TargetApi(23)
private void processEndOfStream() throws ExoPlaybackException {
switch (codecDrainAction) {
case DRAIN_ACTION_REINITIALIZE:

View File

@ -73,11 +73,11 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
@C.ContentType
int contentType = Util.inferContentTypeForUriAndMimeType(request.uri, request.mimeType);
switch (contentType) {
case C.TYPE_DASH:
case C.TYPE_HLS:
case C.TYPE_SS:
case C.CONTENT_TYPE_DASH:
case C.CONTENT_TYPE_HLS:
case C.CONTENT_TYPE_SS:
return createDownloader(request, contentType);
case C.TYPE_OTHER:
case C.CONTENT_TYPE_OTHER:
return new ProgressiveDownloader(
new MediaItem.Builder()
.setUri(request.uri)
@ -113,7 +113,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
SparseArray<Constructor<? extends Downloader>> array = new SparseArray<>();
try {
array.put(
C.TYPE_DASH,
C.CONTENT_TYPE_DASH,
getDownloaderConstructor(
Class.forName("androidx.media3.exoplayer.dash.offline.DashDownloader")));
} catch (ClassNotFoundException e) {
@ -122,7 +122,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
try {
array.put(
C.TYPE_HLS,
C.CONTENT_TYPE_HLS,
getDownloaderConstructor(
Class.forName("androidx.media3.exoplayer.hls.offline.HlsDownloader")));
} catch (ClassNotFoundException e) {
@ -130,7 +130,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
}
try {
array.put(
C.TYPE_SS,
C.CONTENT_TYPE_SS,
getDownloaderConstructor(
Class.forName("androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader")));
} catch (ClassNotFoundException e) {

View File

@ -110,6 +110,7 @@ public final class DownloadHelper {
DefaultTrackSelector.Parameters.DEFAULT_WITHOUT_CONTEXT
.buildUpon()
.setForceHighestSupportedBitrate(true)
.setConstrainAudioChannelCountToDeviceCapabilities(false)
.build();
/** Returns the default parameters used for track selection for downloading. */
@ -117,6 +118,7 @@ public final class DownloadHelper {
return DefaultTrackSelector.Parameters.getDefaults(context)
.buildUpon()
.setForceHighestSupportedBitrate(true)
.setConstrainAudioChannelCountToDeviceCapabilities(false)
.build();
}
@ -516,6 +518,7 @@ public final class DownloadHelper {
if (mediaPreparer != null) {
mediaPreparer.release();
}
trackSelector.release();
}
/**
@ -745,7 +748,7 @@ public final class DownloadHelper {
List<SelectionOverride> overrides) {
try {
assertPreparedWithMedia();
DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon();
DefaultTrackSelector.Parameters.Builder builder = trackSelectorParameters.buildUpon();
for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {
builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex);
}
@ -951,16 +954,18 @@ public final class DownloadHelper {
MediaItem mediaItem,
DataSource.Factory dataSourceFactory,
@Nullable DrmSessionManager drmSessionManager) {
return new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY)
.setDrmSessionManagerProvider(
drmSessionManager != null ? unusedMediaItem -> drmSessionManager : null)
.createMediaSource(mediaItem);
DefaultMediaSourceFactory mediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY);
if (drmSessionManager != null) {
mediaSourceFactory.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
}
return mediaSourceFactory.createMediaSource(mediaItem);
}
private static boolean isProgressive(MediaItem.LocalConfiguration localConfiguration) {
return Util.inferContentTypeForUriAndMimeType(
localConfiguration.uri, localConfiguration.mimeType)
== C.TYPE_OTHER;
== C.CONTENT_TYPE_OTHER;
}
private static final class MediaPreparer

View File

@ -138,7 +138,9 @@ public final class DownloadRequest implements Parcelable {
@Nullable String customCacheKey,
@Nullable byte[] data) {
@C.ContentType int contentType = Util.inferContentTypeForUriAndMimeType(uri, mimeType);
if (contentType == C.TYPE_DASH || contentType == C.TYPE_HLS || contentType == C.TYPE_SS) {
if (contentType == C.CONTENT_TYPE_DASH
|| contentType == C.CONTENT_TYPE_HLS
|| contentType == C.CONTENT_TYPE_SS) {
Assertions.checkArgument(
customCacheKey == null, "customCacheKey must be null for type: " + contentType);
}

View File

@ -58,6 +58,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* The default {@link MediaSource.Factory} implementation.
@ -106,9 +107,9 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
private static final String TAG = "DMediaSourceFactory";
private final DataSource.Factory dataSourceFactory;
private final DelegateFactoryLoader delegateFactoryLoader;
private DataSource.Factory dataSourceFactory;
@Nullable private MediaSource.Factory serverSideAdInsertionMediaSourceFactory;
@Nullable private AdsLoader.Provider adsLoaderProvider;
@Nullable private AdViewProvider adViewProvider;
@ -132,6 +133,9 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Creates a new instance.
*
* <p>Note that this constructor is only useful to try and ensure that ExoPlayer's {@link
* DefaultExtractorsFactory} can be removed by ProGuard or R8.
*
* @param context Any context.
* @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from
* its container.
@ -144,9 +148,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Creates a new instance.
*
* <p>Note that this constructor is only useful to try and ensure that ExoPlayer's {@link
* DefaultDataSource.Factory} can be removed by ProGuard or R8.
*
* @param dataSourceFactory A {@link DataSource.Factory} to create {@link DataSource} instances
* for requesting media data.
*/
@UnstableApi
public DefaultMediaSourceFactory(DataSource.Factory dataSourceFactory) {
this(dataSourceFactory, new DefaultExtractorsFactory());
}
@ -154,6 +162,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Creates a new instance.
*
* <p>Note that this constructor is only useful to try and ensure that ExoPlayer's {@link
* DefaultDataSource.Factory} and {@link DefaultExtractorsFactory} can be removed by ProGuard or
* R8.
*
* @param dataSourceFactory A {@link DataSource.Factory} to create {@link DataSource} instances
* for requesting media data.
* @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from
@ -163,7 +175,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
public DefaultMediaSourceFactory(
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this.dataSourceFactory = dataSourceFactory;
delegateFactoryLoader = new DelegateFactoryLoader(dataSourceFactory, extractorsFactory);
delegateFactoryLoader = new DelegateFactoryLoader(extractorsFactory);
delegateFactoryLoader.setDataSourceFactory(dataSourceFactory);
liveTargetOffsetMs = C.TIME_UNSET;
liveMinOffsetMs = C.TIME_UNSET;
liveMaxOffsetMs = C.TIME_UNSET;
@ -193,9 +206,16 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
* Sets the {@link AdsLoader.Provider} that provides {@link AdsLoader} instances for media items
* that have {@link MediaItem.LocalConfiguration#adsConfiguration ads configurations}.
*
* <p>This will override or clear the {@link AdsLoader.Provider} set by {@link
* #setLocalAdInsertionComponents(AdsLoader.Provider, AdViewProvider)}.
*
* @param adsLoaderProvider A provider for {@link AdsLoader} instances.
* @return This factory, for convenience.
* @deprecated Use {@link #setLocalAdInsertionComponents(AdsLoader.Provider, AdViewProvider)}
* instead.
*/
@UnstableApi
@Deprecated
public DefaultMediaSourceFactory setAdsLoaderProvider(
@Nullable AdsLoader.Provider adsLoaderProvider) {
this.adsLoaderProvider = adsLoaderProvider;
@ -205,14 +225,66 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Sets the {@link AdViewProvider} that provides information about views for the ad playback UI.
*
* @param adViewProvider A provider for {@link AdsLoader} instances.
* <p>This will override or clear the {@link AdViewProvider} set by {@link
* #setLocalAdInsertionComponents(AdsLoader.Provider, AdViewProvider)}.
*
* @param adViewProvider A provider for information about views for the ad playback UI.
* @return This factory, for convenience.
* @deprecated Use {@link #setLocalAdInsertionComponents(AdsLoader.Provider, AdViewProvider)}
* instead.
*/
@UnstableApi
@Deprecated
public DefaultMediaSourceFactory setAdViewProvider(@Nullable AdViewProvider adViewProvider) {
this.adViewProvider = adViewProvider;
return this;
}
/**
* Sets the components required for local ad insertion for media items that have {@link
* MediaItem.LocalConfiguration#adsConfiguration ads configurations}
*
* <p>This will override the values set by {@link #setAdsLoaderProvider(AdsLoader.Provider)} and
* {@link #setAdViewProvider(AdViewProvider)}.
*
* @param adsLoaderProvider A provider for {@link AdsLoader} instances.
* @param adViewProvider A provider for information about views for the ad playback UI.
* @return This factory, for convenience.
*/
public DefaultMediaSourceFactory setLocalAdInsertionComponents(
AdsLoader.Provider adsLoaderProvider, AdViewProvider adViewProvider) {
this.adsLoaderProvider = checkNotNull(adsLoaderProvider);
this.adViewProvider = checkNotNull(adViewProvider);
return this;
}
/**
* Clear any values set via {@link #setLocalAdInsertionComponents(AdsLoader.Provider,
* AdViewProvider)}.
*
* <p>This will also clear any values set by {@link #setAdsLoaderProvider(AdsLoader.Provider)} and
* {@link #setAdViewProvider(AdViewProvider)}.
*
* @return This factory, for convenience.
*/
public DefaultMediaSourceFactory clearLocalAdInsertionComponents() {
this.adsLoaderProvider = null;
this.adViewProvider = null;
return this;
}
/**
* Sets the {@link DataSource.Factory} used to create {@link DataSource} instances for requesting
* media data.
*
* @param dataSourceFactory The {@link DataSource.Factory}.
* @return This factory, for convenience.
*/
public DefaultMediaSourceFactory setDataSourceFactory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
return this;
}
/**
* Sets the {@link MediaSource.Factory} used to handle {@link MediaItem} instances containing a
* {@link Uri} identified as resolving to content with server side ad insertion (SSAI).
@ -298,16 +370,26 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
@UnstableApi
@Override
public DefaultMediaSourceFactory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
delegateFactoryLoader.setDrmSessionManagerProvider(drmSessionManagerProvider);
DrmSessionManagerProvider drmSessionManagerProvider) {
delegateFactoryLoader.setDrmSessionManagerProvider(
checkNotNull(
drmSessionManagerProvider,
"MediaSource.Factory#setDrmSessionManagerProvider no longer handles null by"
+ " instantiating a new DefaultDrmSessionManagerProvider. Explicitly construct and"
+ " pass an instance in order to retain the old behavior."));
return this;
}
@UnstableApi
@Override
public DefaultMediaSourceFactory setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy =
checkNotNull(
loadErrorHandlingPolicy,
"MediaSource.Factory#setLoadErrorHandlingPolicy no longer handles null by"
+ " instantiating a new DefaultLoadErrorHandlingPolicy. Explicitly construct and"
+ " pass an instance in order to retain the old behavior.");
delegateFactoryLoader.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
return this;
}
@ -384,16 +466,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
SubtitleDecoderFactory.DEFAULT.createDecoder(format), format)
: new UnknownSubtitlesExtractor(format)
};
ProgressiveMediaSource.Factory progressiveMediaSourceFactory =
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
if (loadErrorHandlingPolicy != null) {
progressiveMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
}
mediaSources[i + 1] =
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
.createMediaSource(
MediaItem.fromUri(subtitleConfigurations.get(i).uri.toString()));
progressiveMediaSourceFactory.createMediaSource(
MediaItem.fromUri(subtitleConfigurations.get(i).uri.toString()));
} else {
SingleSampleMediaSource.Factory singleSampleMediaSourceFactory =
new SingleSampleMediaSource.Factory(dataSourceFactory);
if (loadErrorHandlingPolicy != null) {
singleSampleMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
}
mediaSources[i + 1] =
new SingleSampleMediaSource.Factory(dataSourceFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
.createMediaSource(subtitleConfigurations.get(i), /* durationUs= */ C.TIME_UNSET);
singleSampleMediaSourceFactory.createMediaSource(
subtitleConfigurations.get(i), /* durationUs= */ C.TIME_UNSET);
}
}
@ -454,19 +543,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/** Loads media source factories lazily. */
private static final class DelegateFactoryLoader {
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final Map<Integer, @NullableType Supplier<MediaSource.Factory>>
mediaSourceFactorySuppliers;
private final Set<Integer> supportedTypes;
private final Map<Integer, MediaSource.Factory> mediaSourceFactories;
private DataSource.@MonotonicNonNull Factory dataSourceFactory;
@Nullable private DrmSessionManagerProvider drmSessionManagerProvider;
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
public DelegateFactoryLoader(
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this.dataSourceFactory = dataSourceFactory;
public DelegateFactoryLoader(ExtractorsFactory extractorsFactory) {
this.extractorsFactory = extractorsFactory;
mediaSourceFactorySuppliers = new HashMap<>();
supportedTypes = new HashSet<>();
@ -502,16 +589,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return mediaSourceFactory;
}
public void setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
public void setDataSourceFactory(DataSource.Factory dataSourceFactory) {
if (dataSourceFactory != this.dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
// TODO(b/233577470): Call MediaSource.Factory.setDataSourceFactory on each value when it
// exists on the interface.
mediaSourceFactories.clear();
}
}
public void setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
this.drmSessionManagerProvider = drmSessionManagerProvider;
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
}
}
public void setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
public void setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
@ -519,11 +613,11 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
private void ensureAllSuppliersAreLoaded() {
maybeLoadSupplier(C.TYPE_DASH);
maybeLoadSupplier(C.TYPE_SS);
maybeLoadSupplier(C.TYPE_HLS);
maybeLoadSupplier(C.TYPE_RTSP);
maybeLoadSupplier(C.TYPE_OTHER);
maybeLoadSupplier(C.CONTENT_TYPE_DASH);
maybeLoadSupplier(C.CONTENT_TYPE_SS);
maybeLoadSupplier(C.CONTENT_TYPE_HLS);
maybeLoadSupplier(C.CONTENT_TYPE_RTSP);
maybeLoadSupplier(C.CONTENT_TYPE_OTHER);
}
@Nullable
@ -536,33 +630,35 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
try {
Class<? extends MediaSource.Factory> clazz;
switch (contentType) {
case C.TYPE_DASH:
case C.CONTENT_TYPE_DASH:
clazz =
Class.forName("androidx.media3.exoplayer.dash.DashMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
break;
case C.TYPE_SS:
case C.CONTENT_TYPE_SS:
clazz =
Class.forName("androidx.media3.exoplayer.smoothstreaming.SsMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
break;
case C.TYPE_HLS:
case C.CONTENT_TYPE_HLS:
clazz =
Class.forName("androidx.media3.exoplayer.hls.HlsMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
break;
case C.TYPE_RTSP:
case C.CONTENT_TYPE_RTSP:
clazz =
Class.forName("androidx.media3.exoplayer.rtsp.RtspMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz);
break;
case C.TYPE_OTHER:
case C.CONTENT_TYPE_OTHER:
mediaSourceFactorySupplier =
() -> new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
() ->
new ProgressiveMediaSource.Factory(
checkNotNull(dataSourceFactory), extractorsFactory);
break;
default:
// Do nothing.

View File

@ -24,12 +24,10 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import java.io.IOException;
@ -69,23 +67,18 @@ public interface MediaSource {
* Sets the {@link DrmSessionManagerProvider} used to obtain a {@link DrmSessionManager} for a
* {@link MediaItem}.
*
* <p>If not set, {@link DefaultDrmSessionManagerProvider} is used.
*
* @return This factory, for convenience.
*/
@UnstableApi
Factory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider);
Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider);
/**
* Sets an optional {@link LoadErrorHandlingPolicy}.
*
* @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}, or {@code null} to use the
* {@link DefaultLoadErrorHandlingPolicy}.
* @return This factory, for convenience.
*/
@UnstableApi
Factory setLoadErrorHandlingPolicy(@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy);
Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy);
/**
* Returns the {@link C.ContentType content types} supported by media sources created by this

View File

@ -608,5 +608,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public boolean isBlacklisted(int index, long nowMs) {
return trackSelection.isBlacklisted(index, nowMs);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ForwardingTrackSelection)) {
return false;
}
ForwardingTrackSelection that = (ForwardingTrackSelection) o;
return trackSelection.equals(that.trackSelection) && trackGroup.equals(that.trackGroup);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + trackGroup.hashCode();
result = 31 * result + trackSelection.hashCode();
return result;
}
}
}

View File

@ -65,10 +65,18 @@ public final class ProgressiveMediaSource extends BaseMediaSource
@Nullable private Object tag;
/**
* Creates a new factory for {@link ProgressiveMediaSource}s, using the extractors provided by
* {@link DefaultExtractorsFactory}.
* Creates a new factory for {@link ProgressiveMediaSource}s.
*
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
* <p>The factory will use the following default components:
*
* <ul>
* <li>{@link DefaultExtractorsFactory}
* <li>{@link DefaultDrmSessionManagerProvider}
* <li>{@link DefaultLoadErrorHandlingPolicy}
* </ul>
*
* @param dataSourceFactory A factory for {@linkplain DataSource data sources} to read the
* media.
*/
public Factory(DataSource.Factory dataSourceFactory) {
this(dataSourceFactory, new DefaultExtractorsFactory());
@ -77,6 +85,18 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/**
* Equivalent to {@link #Factory(DataSource.Factory, ProgressiveMediaExtractor.Factory) new
* Factory(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory)}.
*
* <p>The factory will use the following default components:
*
* <ul>
* <li>{@link DefaultDrmSessionManagerProvider}
* <li>{@link DefaultLoadErrorHandlingPolicy}
* </ul>
*
* @param dataSourceFactory A factory for {@linkplain DataSource data sources} to read the
* media.
* @param extractorsFactory A factory for the {@linkplain Extractor extractors} used to extract
* the media from its container.
*/
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this(dataSourceFactory, playerId -> new BundledExtractorsAdapter(extractorsFactory));
@ -85,9 +105,17 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/**
* Creates a new factory for {@link ProgressiveMediaSource}s.
*
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
* <p>The factory will use the following default components:
*
* <ul>
* <li>{@link DefaultDrmSessionManagerProvider}
* <li>{@link DefaultLoadErrorHandlingPolicy}
* </ul>
*
* @param dataSourceFactory A factory for {@linkplain DataSource data sources} to read the
* media.
* @param progressiveMediaExtractorFactory A factory for the {@link ProgressiveMediaExtractor}
* to extract media from its container.
* to extract the media from its container.
*/
public Factory(
DataSource.Factory dataSourceFactory,
@ -103,7 +131,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/**
* Creates a new factory for {@link ProgressiveMediaSource}s.
*
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
* @param dataSourceFactory A factory for {@linkplain DataSource data sources} to read the
* media.
* @param progressiveMediaExtractorFactory A factory for the {@link ProgressiveMediaExtractor}
* to extract media from its container.
* @param drmSessionManagerProvider A provider to obtain a {@link DrmSessionManager} for a
@ -126,19 +155,14 @@ public final class ProgressiveMediaSource extends BaseMediaSource
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
}
/**
* Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link
* DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.
*
* @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
* @return This factory, for convenience.
*/
public Factory setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
@Override
public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy =
loadErrorHandlingPolicy != null
? loadErrorHandlingPolicy
: new DefaultLoadErrorHandlingPolicy();
checkNotNull(
loadErrorHandlingPolicy,
"MediaSource.Factory#setLoadErrorHandlingPolicy no longer handles null by"
+ " instantiating a new DefaultLoadErrorHandlingPolicy. Explicitly construct and"
+ " pass an instance in order to retain the old behavior.");
return this;
}
@ -159,11 +183,13 @@ public final class ProgressiveMediaSource extends BaseMediaSource
@Override
public Factory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
DrmSessionManagerProvider drmSessionManagerProvider) {
this.drmSessionManagerProvider =
drmSessionManagerProvider != null
? drmSessionManagerProvider
: new DefaultDrmSessionManagerProvider();
checkNotNull(
drmSessionManagerProvider,
"MediaSource.Factory#setDrmSessionManagerProvider no longer handles null by"
+ " instantiating a new DefaultDrmSessionManagerProvider. Explicitly construct"
+ " and pass an instance in order to retain the old behavior.");
return this;
}
@ -198,7 +224,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_OTHER};
return new int[] {C.CONTENT_TYPE_OTHER};
}
}

View File

@ -24,6 +24,9 @@ import java.util.Random;
* Shuffled order of indices.
*
* <p>The shuffle order must be immutable to ensure thread safety.
*
* <p>The order must be consistent when traversed both {@linkplain #getNextIndex(int) forwards} and
* {@linkplain #getPreviousIndex(int) backwards}.
*/
@UnstableApi
public interface ShuffleOrder {

Some files were not shown because too many files have changed in this diff Show More