Merge branch 'main' into rtp-h263
This commit is contained in:
commit
fc3c57ecfd
134
RELEASENOTES.md
134
RELEASENOTES.md
@ -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)
|
||||
|
||||
|
@ -17,7 +17,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
|
||||
}
|
||||
|
@ -29,5 +29,10 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
jvmArgs "-Xmx2g"
|
||||
}
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ project.ext {
|
||||
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
||||
guavaVersion = '31.0.1-android'
|
||||
mockitoVersion = '3.12.4'
|
||||
robolectricVersion = '4.8-alpha-1'
|
||||
robolectricVersion = '4.8.1'
|
||||
// Keep this in sync with Google's internal Checker Framework version.
|
||||
checkerframeworkVersion = '3.13.0'
|
||||
checkerframeworkCompatVersion = '2.5.5'
|
||||
|
@ -230,8 +230,8 @@ public class MainActivity extends AppCompatActivity
|
||||
@Override
|
||||
public boolean onMove(
|
||||
RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) {
|
||||
int fromPosition = origin.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
int fromPosition = origin.getBindingAdapterPosition();
|
||||
int toPosition = target.getBindingAdapterPosition();
|
||||
if (draggingFromPosition == C.INDEX_UNSET) {
|
||||
// A drag has started, but changes to the media queue will be reflected in clearView().
|
||||
draggingFromPosition = fromPosition;
|
||||
@ -243,7 +243,7 @@ public class MainActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
int position = viewHolder.getBindingAdapterPosition();
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
if (playerManager.removeItem(queueItemHolder.item)) {
|
||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||
@ -282,7 +282,7 @@ public class MainActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playerManager.selectQueueItem(getAdapterPosition());
|
||||
playerManager.selectQueueItem(getBindingAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ import android.opengl.GLUtils;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.common.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
@ -41,6 +42,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
/* package */ final class BitmapOverlayVideoProcessor
|
||||
implements VideoProcessingGLSurfaceView.VideoProcessor {
|
||||
|
||||
private static final String TAG = "BitmapOverlayVP";
|
||||
private static final int OVERLAY_WIDTH = 512;
|
||||
private static final int OVERLAY_HEIGHT = 256;
|
||||
|
||||
@ -85,11 +87,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to initialize the shader program", e);
|
||||
return;
|
||||
}
|
||||
program.setBufferAttribute(
|
||||
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
|
||||
"aFramePosition",
|
||||
GlUtil.getNormalizedCoordinateBounds(),
|
||||
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||
program.setBufferAttribute(
|
||||
"aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
|
||||
"aTexCoords",
|
||||
GlUtil.getTextureCoordinateBounds(),
|
||||
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||
GLES20.glGenTextures(1, textures, 0);
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
|
||||
@ -115,7 +124,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLUtils.texSubImage2D(
|
||||
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
|
||||
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);
|
||||
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);
|
||||
try {
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to draw a frame", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (program != null) {
|
||||
try {
|
||||
program.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to delete the shader program", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -156,13 +157,18 @@ public final class MainActivity extends Activity {
|
||||
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
@Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
|
||||
@C.ContentType
|
||||
int type =
|
||||
TextUtils.isEmpty(fileExtension)
|
||||
? Util.inferContentType(uri)
|
||||
: Util.inferContentTypeForExtension(fileExtension);
|
||||
if (type == C.CONTENT_TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
} else if (type == C.CONTENT_TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
|
@ -28,6 +28,7 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.TimedValueQueue;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||
@ -70,6 +71,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
}
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
private static final String TAG = "VPGlSurfaceView";
|
||||
|
||||
private final VideoRenderer renderer;
|
||||
private final Handler mainHandler;
|
||||
@ -239,7 +241,11 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
|
||||
@Override
|
||||
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
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 -> {
|
||||
|
@ -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"/>
|
||||
|
@ -177,7 +177,7 @@ public class IntentUtil {
|
||||
headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
@Nullable UUID drmUuid = Util.getDrmUuid(Util.castNonNull(drmSchemeExtra));
|
||||
@Nullable UUID drmUuid = Util.getDrmUuid(drmSchemeExtra);
|
||||
if (drmUuid != null) {
|
||||
builder.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(drmUuid)
|
||||
@ -188,7 +188,7 @@ public class IntentUtil {
|
||||
intent.getBooleanExtra(
|
||||
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
|
||||
.setLicenseRequestHeaders(headers)
|
||||
.forceSessionsForAudioAndVideoTracks(
|
||||
.setForceSessionsForAudioAndVideoTracks(
|
||||
intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false))
|
||||
.build());
|
||||
}
|
||||
|
@ -38,10 +38,12 @@ import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
|
||||
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
|
||||
import androidx.media3.exoplayer.ima.ImaAdsLoader;
|
||||
import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource;
|
||||
@ -53,7 +55,6 @@ import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.ads.AdsLoader;
|
||||
import androidx.media3.exoplayer.util.DebugTextViewHelper;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.ui.PlayerControlView;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -62,7 +63,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** An activity that plays media using {@link ExoPlayer}. */
|
||||
public class PlayerActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlayerControlView.VisibilityListener {
|
||||
implements OnClickListener, PlayerView.ControllerVisibilityListener {
|
||||
|
||||
// Saved instance state keys.
|
||||
|
||||
@ -91,7 +92,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||
// For ad playback only.
|
||||
|
||||
@Nullable private AdsLoader clientSideAdsLoader;
|
||||
|
||||
// TODO: Annotate this and serverSideAdsLoaderState below with @OptIn when it can be applied to
|
||||
// fields (needs http://r.android.com/2004032 to be released into a version of
|
||||
// androidx.annotation:annotation-experimental).
|
||||
@Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader;
|
||||
|
||||
private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State
|
||||
serverSideAdsLoaderState;
|
||||
|
||||
@ -115,17 +121,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
trackSelectionParameters =
|
||||
TrackSelectionParameters.CREATOR.fromBundle(
|
||||
TrackSelectionParameters.fromBundle(
|
||||
savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS));
|
||||
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
|
||||
startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX);
|
||||
startPosition = savedInstanceState.getLong(KEY_POSITION);
|
||||
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
|
||||
if (adsLoaderStateBundle != null) {
|
||||
serverSideAdsLoaderState =
|
||||
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
|
||||
adsLoaderStateBundle);
|
||||
}
|
||||
restoreServerSideAdsLoaderState(savedInstanceState);
|
||||
} else {
|
||||
trackSelectionParameters = new TrackSelectionParameters.Builder(/* context= */ this).build();
|
||||
clearStartPosition();
|
||||
@ -217,9 +218,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
|
||||
outState.putInt(KEY_ITEM_INDEX, startItemIndex);
|
||||
outState.putLong(KEY_POSITION, startPosition);
|
||||
if (serverSideAdsLoaderState != null) {
|
||||
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
|
||||
}
|
||||
saveServerSideAdsLoaderState(outState);
|
||||
}
|
||||
|
||||
// Activity input
|
||||
@ -246,10 +245,10 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerControlView.VisibilityListener implementation
|
||||
// PlayerView.ControllerVisibilityListener implementation
|
||||
|
||||
@Override
|
||||
public void onVisibilityChange(int visibility) {
|
||||
public void onVisibilityChanged(int visibility) {
|
||||
debugRootView.setVisibility(visibility);
|
||||
}
|
||||
|
||||
@ -271,24 +270,20 @@ public class PlayerActivity extends AppCompatActivity
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean preferExtensionDecoders =
|
||||
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
RenderersFactory renderersFactory =
|
||||
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
|
||||
|
||||
lastSeenTracks = Tracks.EMPTY;
|
||||
player =
|
||||
ExoPlayer.Builder playerBuilder =
|
||||
new ExoPlayer.Builder(/* context= */ this)
|
||||
.setRenderersFactory(renderersFactory)
|
||||
.setMediaSourceFactory(createMediaSourceFactory())
|
||||
.build();
|
||||
.setMediaSourceFactory(createMediaSourceFactory());
|
||||
setRenderersFactory(
|
||||
playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false));
|
||||
player = playerBuilder.build();
|
||||
player.setTrackSelectionParameters(trackSelectionParameters);
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.addAnalyticsListener(new EventLogger());
|
||||
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
playerView.setPlayer(player);
|
||||
serverSideAdsLoader.setPlayer(player);
|
||||
configurePlayerWithServerSideAdsLoader();
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
}
|
||||
@ -302,7 +297,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||
return true;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class) // SSAI configuration
|
||||
private MediaSource.Factory createMediaSourceFactory() {
|
||||
DefaultDrmSessionManagerProvider drmSessionManagerProvider =
|
||||
new DefaultDrmSessionManagerProvider();
|
||||
drmSessionManagerProvider.setDrmHttpDataSourceFactory(
|
||||
DemoUtil.getHttpDataSourceFactory(/* context= */ this));
|
||||
ImaServerSideAdInsertionMediaSource.AdsLoader.Builder serverSideAdLoaderBuilder =
|
||||
new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(/* context= */ this, playerView);
|
||||
if (serverSideAdsLoaderState != null) {
|
||||
@ -311,13 +311,30 @@ public class PlayerActivity extends AppCompatActivity
|
||||
serverSideAdsLoader = serverSideAdLoaderBuilder.build();
|
||||
ImaServerSideAdInsertionMediaSource.Factory imaServerSideAdInsertionMediaSourceFactory =
|
||||
new ImaServerSideAdInsertionMediaSource.Factory(
|
||||
serverSideAdsLoader, new DefaultMediaSourceFactory(dataSourceFactory));
|
||||
return new DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setAdsLoaderProvider(this::getClientSideAdsLoader)
|
||||
.setAdViewProvider(playerView)
|
||||
serverSideAdsLoader,
|
||||
new DefaultMediaSourceFactory(/* context= */ this)
|
||||
.setDataSourceFactory(dataSourceFactory));
|
||||
return new DefaultMediaSourceFactory(/* context= */ this)
|
||||
.setDataSourceFactory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider(drmSessionManagerProvider)
|
||||
.setLocalAdInsertionComponents(
|
||||
this::getClientSideAdsLoader, /* adViewProvider= */ playerView)
|
||||
.setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory);
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void setRenderersFactory(
|
||||
ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) {
|
||||
RenderersFactory renderersFactory =
|
||||
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
|
||||
playerBuilder.setRenderersFactory(renderersFactory);
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void configurePlayerWithServerSideAdsLoader() {
|
||||
serverSideAdsLoader.setPlayer(player);
|
||||
}
|
||||
|
||||
private List<MediaItem> createMediaItems(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
|
||||
@ -371,8 +388,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
if (player != null) {
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
serverSideAdsLoaderState = serverSideAdsLoader.release();
|
||||
serverSideAdsLoader = null;
|
||||
releaseServerSideAdsLoader();
|
||||
debugViewHelper.stop();
|
||||
debugViewHelper = null;
|
||||
player.release();
|
||||
@ -387,6 +403,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void releaseServerSideAdsLoader() {
|
||||
serverSideAdsLoaderState = serverSideAdsLoader.release();
|
||||
serverSideAdsLoader = null;
|
||||
}
|
||||
|
||||
private void releaseClientSideAdsLoader() {
|
||||
if (clientSideAdsLoader != null) {
|
||||
clientSideAdsLoader.release();
|
||||
@ -395,6 +417,23 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void saveServerSideAdsLoaderState(Bundle outState) {
|
||||
if (serverSideAdsLoaderState != null) {
|
||||
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) {
|
||||
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
|
||||
if (adsLoaderStateBundle != null) {
|
||||
serverSideAdsLoaderState =
|
||||
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
|
||||
adsLoaderStateBundle);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTrackSelectorParameters() {
|
||||
if (player != null) {
|
||||
trackSelectionParameters = player.getTrackSelectionParameters();
|
||||
|
@ -27,6 +27,7 @@ import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.JsonReader;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -443,7 +444,10 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
} else {
|
||||
@Nullable
|
||||
String adaptiveMimeType =
|
||||
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension));
|
||||
Util.getAdaptiveMimeTypeForContentType(
|
||||
TextUtils.isEmpty(extension)
|
||||
? Util.inferContentType(uri)
|
||||
: Util.inferContentTypeForExtension(extension));
|
||||
mediaItem
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||
@ -454,7 +458,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
new MediaItem.DrmConfiguration.Builder(drmUuid)
|
||||
.setLicenseUri(drmLicenseUri)
|
||||
.setLicenseRequestHeaders(drmLicenseRequestHeaders)
|
||||
.forceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
|
||||
.setForceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
|
||||
.setMultiSession(drmMultiSession)
|
||||
.setForceDefaultLicenseUri(drmForceDefaultLicenseUri)
|
||||
.build());
|
||||
|
@ -25,6 +25,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.appcompat.app.AppCompatDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@ -36,6 +37,7 @@ import androidx.media3.common.TrackGroup;
|
||||
import androidx.media3.common.TrackSelectionOverride;
|
||||
import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.ui.TrackSelectionView;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
@ -47,6 +49,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Dialog to select tracks. */
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public final class TrackSelectionDialog extends DialogFragment {
|
||||
|
||||
/** Called when tracks are selected. */
|
||||
|
@ -45,6 +45,7 @@
|
||||
|
||||
<service
|
||||
android:name=".PlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
private inner class CustomMediaLibrarySessionCallback :
|
||||
MediaLibrarySession.MediaLibrarySessionCallback {
|
||||
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. */
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,12 @@
|
||||
<data android:scheme="asset"/>
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.demo.surface.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
@ -19,6 +19,7 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceHolder;
|
||||
@ -201,13 +202,18 @@ public final class MainActivity extends Activity {
|
||||
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
@Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
|
||||
@C.ContentType
|
||||
int type =
|
||||
TextUtils.isEmpty(fileExtension)
|
||||
? Util.inferContentType(uri)
|
||||
: Util.inferContentTypeForExtension(fileExtension);
|
||||
if (type == C.CONTENT_TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
} else if (type == C.CONTENT_TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
|
22
demos/transformer/BUILD.bazel
Normal file
22
demos/transformer/BUILD.bazel
Normal 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",
|
||||
)
|
@ -6,4 +6,61 @@ example by removing audio or video.
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
this demo.
|
||||
|
||||
## MediaPipe frame processing demo
|
||||
|
||||
Building the demo app with [MediaPipe][] integration enabled requires some extra
|
||||
manual steps.
|
||||
|
||||
1. Follow the
|
||||
[instructions](https://google.github.io/mediapipe/getting_started/install.html)
|
||||
to install MediaPipe.
|
||||
1. Copy the Transformer demo's build configuration and MediaPipe graph text
|
||||
protocol buffer under the MediaPipe source tree. This makes it easy to
|
||||
[build an AAR][] with bazel by reusing MediaPipe's workspace.
|
||||
|
||||
```sh
|
||||
cd "<path to MediaPipe checkout>"
|
||||
MEDIAPIPE_ROOT="$(pwd)"
|
||||
MEDIAPIPE_TRANSFORMER_ROOT="${MEDIAPIPE_ROOT}/mediapipe/java/com/google/mediapipe/transformer"
|
||||
cd "<path to the transformer demo (containing this readme)>"
|
||||
TRANSFORMER_DEMO_ROOT="$(pwd)"
|
||||
mkdir -p "${MEDIAPIPE_TRANSFORMER_ROOT}"
|
||||
mkdir -p "${TRANSFORMER_DEMO_ROOT}/libs"
|
||||
cp ${TRANSFORMER_DEMO_ROOT}/BUILD.bazel ${MEDIAPIPE_TRANSFORMER_ROOT}/BUILD
|
||||
cp ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets/edge_detector_mediapipe_graph.pbtxt \
|
||||
${MEDIAPIPE_TRANSFORMER_ROOT}
|
||||
```
|
||||
|
||||
1. Build the AAR and the binary proto for the demo's MediaPipe graph, then copy
|
||||
them to Transformer.
|
||||
|
||||
```sh
|
||||
cd ${MEDIAPIPE_ROOT}
|
||||
bazel build -c opt --strip=ALWAYS \
|
||||
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
|
||||
--fat_apk_cpu=arm64-v8a,armeabi-v7a \
|
||||
--legacy_whole_archive=0 \
|
||||
--features=-legacy_whole_archive \
|
||||
--copt=-fvisibility=hidden \
|
||||
--copt=-ffunction-sections \
|
||||
--copt=-fdata-sections \
|
||||
--copt=-fstack-protector \
|
||||
--copt=-Oz \
|
||||
--copt=-fomit-frame-pointer \
|
||||
--copt=-DABSL_MIN_LOG_LEVEL=2 \
|
||||
--linkopt=-Wl,--gc-sections,--strip-all \
|
||||
mediapipe/java/com/google/mediapipe/transformer:edge_detector_mediapipe_aar.aar
|
||||
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_aar.aar \
|
||||
${TRANSFORMER_DEMO_ROOT}/libs
|
||||
bazel build mediapipe/java/com/google/mediapipe/transformer:edge_detector_binary_graph
|
||||
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_graph.binarypb \
|
||||
${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets
|
||||
```
|
||||
|
||||
1. In Android Studio, gradle sync and select the `withMediaPipe` build variant
|
||||
(this will only appear if the AAR is present), then build and run the demo
|
||||
app and select a MediaPipe-based effect.
|
||||
|
||||
[Transformer]: https://exoplayer.dev/transforming-media.html
|
||||
[MediaPipe]: https://google.github.io/mediapipe/
|
||||
[build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html
|
||||
|
@ -45,6 +45,27 @@ android {
|
||||
// This demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
|
||||
flavorDimensions "mediaPipe"
|
||||
|
||||
productFlavors {
|
||||
noMediaPipe {
|
||||
dimension "mediaPipe"
|
||||
}
|
||||
withMediaPipe {
|
||||
dimension "mediaPipe"
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the withMediaPipe variant if the MediaPipe AAR is not present.
|
||||
if (!project.file("libs/edge_detector_mediapipe_aar.aar").exists()) {
|
||||
variantFilter { variant ->
|
||||
def names = variant.flavors*.name
|
||||
if (names.contains("withMediaPipe")) {
|
||||
setIgnore(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -56,6 +77,14 @@ dependencies {
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + 'lib-transformer')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
|
||||
// For MediaPipe and its dependencies:
|
||||
withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||
withMediaPipeImplementation 'com.google.flogger:flogger:latest.release'
|
||||
withMediaPipeImplementation 'com.google.flogger:flogger-system-backend:latest.release'
|
||||
withMediaPipeImplementation 'com.google.code.findbugs:jsr305:latest.release'
|
||||
withMediaPipeImplementation 'com.google.protobuf:protobuf-javalite:3.19.1'
|
||||
}
|
||||
|
@ -49,6 +49,12 @@
|
||||
<data android:scheme="asset"/>
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.demo.transformer.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".TransformerActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
|
@ -16,9 +16,8 @@
|
||||
// ES 2 vertex shader that leaves the coordinates unchanged.
|
||||
|
||||
attribute vec4 aFramePosition;
|
||||
attribute vec4 aTexSamplingCoord;
|
||||
varying vec2 vTexSamplingCoord;
|
||||
void main() {
|
||||
gl_Position = aFramePosition;
|
||||
vTexSamplingCoord = aTexSamplingCoord.xy;
|
||||
vTexSamplingCoord = vec2(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5);
|
||||
}
|
||||
|
@ -31,22 +31,20 @@ import android.util.Size;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.transformer.GlFrameProcessor;
|
||||
import androidx.media3.transformer.FrameProcessingException;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A {@link GlFrameProcessor} that overlays a bitmap with a logo and timer on each frame.
|
||||
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
|
||||
* frame.
|
||||
*
|
||||
* <p>The bitmap is drawn using an Android {@link Canvas}.
|
||||
*/
|
||||
// TODO(b/227625365): Delete this class and use a frame processor from the Transformer library, once
|
||||
// overlaying a bitmap and text is supported in Transformer.
|
||||
/* package */ final class BitmapOverlayFrameProcessor implements GlFrameProcessor {
|
||||
static {
|
||||
GlUtil.glAssertionsEnabled = true;
|
||||
}
|
||||
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
|
||||
// once overlaying a bitmap and text is supported in Transformer.
|
||||
/* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
|
||||
|
||||
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
|
||||
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
|
||||
@ -55,16 +53,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
private final Paint paint;
|
||||
private final Bitmap overlayBitmap;
|
||||
private final Bitmap logoBitmap;
|
||||
private final Canvas overlayCanvas;
|
||||
private final GlProgram glProgram;
|
||||
|
||||
private float bitmapScaleX;
|
||||
private float bitmapScaleY;
|
||||
private int bitmapTexId;
|
||||
private @MonotonicNonNull Size outputSize;
|
||||
private @MonotonicNonNull Bitmap logoBitmap;
|
||||
private @MonotonicNonNull GlProgram glProgram;
|
||||
|
||||
public BitmapOverlayFrameProcessor() {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @throws FrameProcessingException If a problem occurs while reading shader files.
|
||||
*/
|
||||
public BitmapOverlayProcessor(Context context) throws FrameProcessingException {
|
||||
paint = new Paint();
|
||||
paint.setTextSize(64);
|
||||
paint.setAntiAlias(true);
|
||||
@ -73,19 +75,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
overlayBitmap =
|
||||
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
overlayCanvas = new Canvas(overlayBitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
|
||||
throws IOException {
|
||||
if (inputWidth > inputHeight) {
|
||||
bitmapScaleX = inputWidth / (float) inputHeight;
|
||||
bitmapScaleY = 1f;
|
||||
} else {
|
||||
bitmapScaleX = 1f;
|
||||
bitmapScaleY = inputHeight / (float) inputWidth;
|
||||
}
|
||||
outputSize = new Size(inputWidth, inputHeight);
|
||||
|
||||
try {
|
||||
logoBitmap =
|
||||
@ -95,30 +84,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawFrame(long presentationTimeUs) {
|
||||
checkStateNotNull(glProgram);
|
||||
glProgram.use();
|
||||
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 =
|
||||
@ -135,15 +136,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
flipBitmapVertically(overlayBitmap));
|
||||
GlUtil.checkGlError();
|
||||
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
|
||||
glProgram.bindAttributesAndUniforms();
|
||||
// The four-vertex triangle strip forms a quad.
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
public void release() throws FrameProcessingException {
|
||||
super.release();
|
||||
if (glProgram != null) {
|
||||
try {
|
||||
glProgram.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -18,39 +18,38 @@ package androidx.media3.demo.transformer;
|
||||
import android.graphics.Matrix;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.transformer.AdvancedFrameProcessor;
|
||||
import androidx.media3.transformer.GlFrameProcessor;
|
||||
import androidx.media3.transformer.GlMatrixTransformation;
|
||||
import androidx.media3.transformer.MatrixTransformation;
|
||||
|
||||
/**
|
||||
* Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying
|
||||
* transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}.
|
||||
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
|
||||
* MatrixTransformation MatrixTransformations} that create video effects by applying transformation
|
||||
* matrices to the individual video frames.
|
||||
*/
|
||||
/* package */ final class AdvancedFrameProcessorFactory {
|
||||
/* package */ final class MatrixTransformationFactory {
|
||||
/**
|
||||
* Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value
|
||||
* Returns a {@link MatrixTransformation} that rescales the frames over the first {@value
|
||||
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
|
||||
* linearly in size from a single point to filling the full output frame.
|
||||
*/
|
||||
public static GlFrameProcessor createZoomInTransitionFrameProcessor() {
|
||||
return new AdvancedFrameProcessor(
|
||||
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix);
|
||||
public static MatrixTransformation createZoomInTransition() {
|
||||
return MatrixTransformationFactory::calculateZoomInTransitionMatrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse.
|
||||
* Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an
|
||||
* ellipse.
|
||||
*/
|
||||
public static GlFrameProcessor createDizzyCropFrameProcessor() {
|
||||
return new AdvancedFrameProcessor(
|
||||
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix);
|
||||
public static MatrixTransformation createDizzyCropEffect() {
|
||||
return MatrixTransformationFactory::calculateDizzyCropMatrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies
|
||||
* perspective projection to 2D.
|
||||
* Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and
|
||||
* applies perspective projection to 2D.
|
||||
*/
|
||||
public static GlFrameProcessor createSpin3dFrameProcessor() {
|
||||
return new AdvancedFrameProcessor(
|
||||
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix);
|
||||
public static GlMatrixTransformation createSpin3dEffect() {
|
||||
return MatrixTransformationFactory::calculate3dSpinMatrix;
|
||||
}
|
||||
|
||||
private static final float ZOOM_DURATION_SECONDS = 2f;
|
@ -16,38 +16,29 @@
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLES20;
|
||||
import android.util.Size;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.transformer.GlFrameProcessor;
|
||||
import androidx.media3.transformer.FrameProcessingException;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A {@link GlFrameProcessor} that periodically dims the frames such that pixels are darker the
|
||||
* further they are away from the frame center.
|
||||
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
|
||||
* darker the further they are away from the frame center.
|
||||
*/
|
||||
/* package */ final class PeriodicVignetteFrameProcessor implements GlFrameProcessor {
|
||||
static {
|
||||
GlUtil.glAssertionsEnabled = true;
|
||||
}
|
||||
/* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor {
|
||||
|
||||
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
|
||||
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
|
||||
private static final float DIMMING_PERIOD_US = 5_600_000f;
|
||||
|
||||
private float centerX;
|
||||
private float centerY;
|
||||
private float minInnerRadius;
|
||||
private float deltaInnerRadius;
|
||||
private float outerRadius;
|
||||
|
||||
private @MonotonicNonNull Size outputSize;
|
||||
private @MonotonicNonNull GlProgram glProgram;
|
||||
private final GlProgram glProgram;
|
||||
private final float minInnerRadius;
|
||||
private final float deltaInnerRadius;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
@ -66,53 +57,65 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
|
||||
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
|
||||
* @param outerRadius The radius after which all pixels are black.
|
||||
* @throws FrameProcessingException If a problem occurs while reading shader files.
|
||||
*/
|
||||
public PeriodicVignetteFrameProcessor(
|
||||
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
|
||||
public PeriodicVignetteProcessor(
|
||||
Context context,
|
||||
float centerX,
|
||||
float centerY,
|
||||
float minInnerRadius,
|
||||
float maxInnerRadius,
|
||||
float outerRadius)
|
||||
throws FrameProcessingException {
|
||||
checkArgument(minInnerRadius <= maxInnerRadius);
|
||||
checkArgument(maxInnerRadius <= outerRadius);
|
||||
this.centerX = centerX;
|
||||
this.centerY = centerY;
|
||||
this.minInnerRadius = minInnerRadius;
|
||||
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
|
||||
this.outerRadius = outerRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
|
||||
throws IOException {
|
||||
outputSize = new Size(inputWidth, inputHeight);
|
||||
try {
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
|
||||
} 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();
|
||||
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));
|
||||
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) {
|
||||
try {
|
||||
glProgram.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
@ -31,6 +32,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
@ -40,8 +42,9 @@ import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.util.DebugTextViewHelper;
|
||||
import androidx.media3.transformer.DefaultEncoderFactory;
|
||||
import androidx.media3.transformer.EncoderSelector;
|
||||
import androidx.media3.transformer.GlFrameProcessor;
|
||||
import androidx.media3.transformer.GlEffect;
|
||||
import androidx.media3.transformer.ProgressHolder;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import androidx.media3.transformer.TransformationException;
|
||||
import androidx.media3.transformer.TransformationRequest;
|
||||
import androidx.media3.transformer.TransformationResult;
|
||||
@ -54,6 +57,7 @@ import com.google.common.base.Ticker;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
@ -149,9 +153,10 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
externalCacheFile = createExternalCacheFile("transformer-output.mp4");
|
||||
String filePath = externalCacheFile.getAbsolutePath();
|
||||
@Nullable Bundle bundle = intent.getExtras();
|
||||
MediaItem mediaItem = createMediaItem(bundle, uri);
|
||||
Transformer transformer = createTransformer(bundle, filePath);
|
||||
transformationStopwatch.start();
|
||||
transformer.startTransformation(MediaItem.fromUri(uri), filePath);
|
||||
transformer.startTransformation(mediaItem, filePath);
|
||||
this.transformer = transformer;
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
@ -178,6 +183,24 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private MediaItem createMediaItem(@Nullable Bundle bundle, Uri uri) {
|
||||
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri);
|
||||
if (bundle != null) {
|
||||
long trimStartMs =
|
||||
bundle.getLong(ConfigurationActivity.TRIM_START_MS, /* defaultValue= */ C.TIME_UNSET);
|
||||
long trimEndMs =
|
||||
bundle.getLong(ConfigurationActivity.TRIM_END_MS, /* defaultValue= */ C.TIME_UNSET);
|
||||
if (trimStartMs != C.TIME_UNSET && trimEndMs != C.TIME_UNSET) {
|
||||
mediaItemBuilder.setClippingConfiguration(
|
||||
new MediaItem.ClippingConfiguration.Builder()
|
||||
.setStartPositionMs(trimStartMs)
|
||||
.setEndPositionMs(trimEndMs)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
return mediaItemBuilder.build();
|
||||
}
|
||||
|
||||
// Create a cache file, resetting it if it already exists.
|
||||
private File createExternalCacheFile(String fileName) throws IOException {
|
||||
File file = new File(getExternalCacheDir(), fileName);
|
||||
@ -237,20 +260,46 @@ 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(
|
||||
if (selectedEffects[1]) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
|
||||
Constructor<?> constructor =
|
||||
clazz.getConstructor(Context.class, String.class, String.class, String.class);
|
||||
effects.add(
|
||||
(Context context) -> {
|
||||
try {
|
||||
return (SingleFrameGlTextureProcessor)
|
||||
constructor.newInstance(
|
||||
context,
|
||||
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
|
||||
/* inputStreamName= */ "input_video",
|
||||
/* outputStreamName= */ "output_video");
|
||||
} catch (Exception e) {
|
||||
runOnUiThread(() -> showToast(R.string.no_media_pipe_error));
|
||||
throw new RuntimeException("Failed to load MediaPipe processor", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
showToast(R.string.no_media_pipe_error);
|
||||
}
|
||||
}
|
||||
if (selectedEffects[2]) {
|
||||
effects.add(
|
||||
(Context context) ->
|
||||
new PeriodicVignetteProcessor(
|
||||
context,
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
|
||||
/* minInnerRadius= */ bundle.getFloat(
|
||||
@ -259,16 +308,16 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
|
||||
}
|
||||
if (selectedFrameProcessors[2]) {
|
||||
frameProcessors.add(AdvancedFrameProcessorFactory.createSpin3dFrameProcessor());
|
||||
if (selectedEffects[3]) {
|
||||
effects.add(MatrixTransformationFactory.createSpin3dEffect());
|
||||
}
|
||||
if (selectedFrameProcessors[3]) {
|
||||
frameProcessors.add(new BitmapOverlayFrameProcessor());
|
||||
if (selectedEffects[4]) {
|
||||
effects.add(BitmapOverlayProcessor::new);
|
||||
}
|
||||
if (selectedFrameProcessors[4]) {
|
||||
frameProcessors.add(AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor());
|
||||
if (selectedEffects[5]) {
|
||||
effects.add(MatrixTransformationFactory.createZoomInTransition());
|
||||
}
|
||||
transformerBuilder.setFrameProcessors(frameProcessors.build());
|
||||
transformerBuilder.setVideoFrameEffects(effects.build());
|
||||
}
|
||||
}
|
||||
return transformerBuilder
|
||||
@ -362,6 +411,10 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void showToast(@StringRes int messageResource) {
|
||||
Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
|
||||
|
||||
@Nullable
|
||||
|
@ -64,7 +64,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
|
||||
app:layout_constraintBottom_toTopOf="@+id/select_demo_frameprocessors_button">
|
||||
app:layout_constraintBottom_toTopOf="@+id/select_demo_effects_button">
|
||||
<TableLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -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" />
|
||||
|
48
demos/transformer/src/main/res/layout/trim_options.xml
Normal file
48
demos/transformer/src/main/res/layout/trim_options.xml
Normal 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>
|
@ -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>
|
||||
|
19
demos/transformer/src/withMediaPipe/AndroidManifest.xml
Normal file
19
demos/transformer/src/withMediaPipe/AndroidManifest.xml
Normal 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>
|
@ -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"
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
@ -48,9 +48,21 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
||||
libraryModule.android.libraryVariants.all { variant ->
|
||||
def name = variant.buildType.name
|
||||
if (name == "release") {
|
||||
// Works around b/234569640 that causes different versions of the androidx.media
|
||||
// jar to be on the classpath.
|
||||
def allJarFiles = []
|
||||
allJarFiles.addAll(variant.javaCompileProvider.get().classpath.files)
|
||||
def filteredJarFiles = allJarFiles.findAll { file ->
|
||||
if (file ==~ /.*media-.\..\..-api.jar$/
|
||||
&& !file.path.endsWith(
|
||||
"media-" + project.ext.androidxMediaVersion + "-api.jar")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
classpath +=
|
||||
libraryModule.project.files(
|
||||
variant.javaCompileProvider.get().classpath.files,
|
||||
filteredJarFiles,
|
||||
libraryModule.project.android.getBootClasspath())
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ import androidx.media3.common.TrackGroup;
|
||||
import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ListenerSet;
|
||||
@ -102,7 +102,8 @@ public final class CastPlayer extends BasePlayer {
|
||||
COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEMS_METADATA,
|
||||
COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
COMMAND_GET_TRACKS)
|
||||
COMMAND_GET_TRACKS,
|
||||
COMMAND_SET_MEDIA_ITEM)
|
||||
.build();
|
||||
|
||||
public static final float MIN_SPEED_SUPPORTED = 0.5f;
|
||||
@ -458,6 +459,11 @@ public final class CastPlayer extends BasePlayer {
|
||||
stop(/* reset= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
|
||||
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
|
||||
* {@link #prepare() re-preparing} the player.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
@ -705,10 +711,10 @@ public final class CastPlayer extends BasePlayer {
|
||||
return VideoSize.UNKNOWN;
|
||||
}
|
||||
|
||||
/** This method is not supported and returns an empty list. */
|
||||
/** This method is not supported and returns an empty {@link CueGroup}. */
|
||||
@Override
|
||||
public ImmutableList<Cue> getCurrentCues() {
|
||||
return ImmutableList.of();
|
||||
public CueGroup getCurrentCues() {
|
||||
return CueGroup.EMPTY;
|
||||
}
|
||||
|
||||
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
|
||||
|
@ -36,6 +36,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
|
||||
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
|
||||
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
|
||||
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
|
||||
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
|
||||
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
|
||||
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
|
||||
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
|
||||
@ -1359,6 +1360,7 @@ public class CastPlayerTest {
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse();
|
||||
|
@ -28,7 +28,6 @@ import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Attributes for audio playback, which configure the underlying platform {@link
|
||||
@ -43,10 +42,31 @@ import java.lang.reflect.Method;
|
||||
*/
|
||||
public final class AudioAttributes implements Bundleable {
|
||||
|
||||
/** A direct wrapper around {@link android.media.AudioAttributes}. */
|
||||
@RequiresApi(21)
|
||||
public static final class AudioAttributesV21 {
|
||||
public final android.media.AudioAttributes audioAttributes;
|
||||
|
||||
private AudioAttributesV21(AudioAttributes audioAttributes) {
|
||||
android.media.AudioAttributes.Builder builder =
|
||||
new android.media.AudioAttributes.Builder()
|
||||
.setContentType(audioAttributes.contentType)
|
||||
.setFlags(audioAttributes.flags)
|
||||
.setUsage(audioAttributes.usage);
|
||||
if (Util.SDK_INT >= 29) {
|
||||
Api29.setAllowedCapturePolicy(builder, audioAttributes.allowedCapturePolicy);
|
||||
}
|
||||
if (Util.SDK_INT >= 32) {
|
||||
Api32.setSpatializationBehavior(builder, audioAttributes.spatializationBehavior);
|
||||
}
|
||||
this.audioAttributes = builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default audio attributes, where the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage
|
||||
* is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are
|
||||
* set.
|
||||
* The default audio attributes, where the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN},
|
||||
* usage is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags
|
||||
* are set.
|
||||
*/
|
||||
public static final AudioAttributes DEFAULT = new Builder().build();
|
||||
|
||||
@ -62,11 +82,11 @@ public final class AudioAttributes implements Bundleable {
|
||||
/**
|
||||
* Creates a new builder for {@link AudioAttributes}.
|
||||
*
|
||||
* <p>By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link
|
||||
* <p>By default the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN}, usage is {@link
|
||||
* C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set.
|
||||
*/
|
||||
public Builder() {
|
||||
contentType = C.CONTENT_TYPE_UNKNOWN;
|
||||
contentType = C.AUDIO_CONTENT_TYPE_UNKNOWN;
|
||||
flags = 0;
|
||||
usage = C.USAGE_MEDIA;
|
||||
allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
|
||||
@ -97,9 +117,7 @@ public final class AudioAttributes implements Bundleable {
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO[b/190759307] Update javadoc to link to AudioAttributes.Builder#setSpatializationBehavior
|
||||
// once compile SDK target is set to 32.
|
||||
/** See {@code android.media.AudioAttributes.Builder.setSpatializationBehavior(int)}. */
|
||||
/** See {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)}. */
|
||||
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
|
||||
this.spatializationBehavior = spatializationBehavior;
|
||||
return this;
|
||||
@ -123,7 +141,7 @@ public final class AudioAttributes implements Bundleable {
|
||||
/** The {@link C.SpatializationBehavior}. */
|
||||
public final @C.SpatializationBehavior int spatializationBehavior;
|
||||
|
||||
@Nullable private android.media.AudioAttributes audioAttributesV21;
|
||||
@Nullable private AudioAttributesV21 audioAttributesV21;
|
||||
|
||||
private AudioAttributes(
|
||||
@C.AudioContentType int contentType,
|
||||
@ -139,25 +157,15 @@ public final class AudioAttributes implements Bundleable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link android.media.AudioAttributes} from this instance.
|
||||
* Returns a {@link AudioAttributesV21} from this instance.
|
||||
*
|
||||
* <p>Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29.
|
||||
* <p>Some fields are ignored if the corresponding {@link android.media.AudioAttributes.Builder}
|
||||
* setter is not available on the current API level.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
public android.media.AudioAttributes getAudioAttributesV21() {
|
||||
public AudioAttributesV21 getAudioAttributesV21() {
|
||||
if (audioAttributesV21 == null) {
|
||||
android.media.AudioAttributes.Builder builder =
|
||||
new android.media.AudioAttributes.Builder()
|
||||
.setContentType(contentType)
|
||||
.setFlags(flags)
|
||||
.setUsage(usage);
|
||||
if (Util.SDK_INT >= 29) {
|
||||
Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
|
||||
}
|
||||
if (Util.SDK_INT >= 32) {
|
||||
Api32.setSpatializationBehavior(builder, spatializationBehavior);
|
||||
}
|
||||
audioAttributesV21 = builder.build();
|
||||
audioAttributesV21 = new AudioAttributesV21(this);
|
||||
}
|
||||
return audioAttributesV21;
|
||||
}
|
||||
@ -251,8 +259,6 @@ public final class AudioAttributes implements Bundleable {
|
||||
|
||||
@RequiresApi(29)
|
||||
private static final class Api29 {
|
||||
private Api29() {}
|
||||
|
||||
@DoNotInline
|
||||
public static void setAllowedCapturePolicy(
|
||||
android.media.AudioAttributes.Builder builder,
|
||||
@ -263,20 +269,11 @@ public final class AudioAttributes implements Bundleable {
|
||||
|
||||
@RequiresApi(32)
|
||||
private static final class Api32 {
|
||||
private Api32() {}
|
||||
|
||||
@DoNotInline
|
||||
public static void setSpatializationBehavior(
|
||||
android.media.AudioAttributes.Builder builder,
|
||||
@C.SpatializationBehavior int spatializationBehavior) {
|
||||
try {
|
||||
// TODO[b/190759307]: Remove reflection once compile SDK target is set to 32.
|
||||
Method setSpatializationBehavior =
|
||||
builder.getClass().getMethod("setSpatializationBehavior", Integer.TYPE);
|
||||
setSpatializationBehavior.invoke(builder, spatializationBehavior);
|
||||
} catch (Exception e) {
|
||||
// Do nothing if reflection fails.
|
||||
}
|
||||
builder.setSpatializationBehavior(spatializationBehavior);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ public abstract class BasePlayer implements Player {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>BasePlayer and its descendents will return {@code true}.
|
||||
* <p>BasePlayer and its descendants will return {@code true}.
|
||||
*/
|
||||
@Override
|
||||
public final boolean canAdvertiseSession() {
|
||||
@ -143,12 +143,18 @@ public abstract class BasePlayer implements Player {
|
||||
seekToOffset(getSeekForwardIncrement());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean hasPrevious() {
|
||||
return hasPreviousMediaItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean hasPreviousWindow() {
|
||||
@ -160,12 +166,18 @@ public abstract class BasePlayer implements Player {
|
||||
return getPreviousMediaItemIndex() != C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final void previous() {
|
||||
seekToPreviousMediaItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final void seekToPreviousWindow() {
|
||||
@ -198,12 +210,18 @@ public abstract class BasePlayer implements Player {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #hasNextMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean hasNext() {
|
||||
return hasNextMediaItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #hasNextMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean hasNextWindow() {
|
||||
@ -215,12 +233,18 @@ public abstract class BasePlayer implements Player {
|
||||
return getNextMediaItemIndex() != C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #seekToNextMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final void next() {
|
||||
seekToNextMediaItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #seekToNextMediaItem()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final void seekToNextWindow() {
|
||||
@ -253,12 +277,18 @@ public abstract class BasePlayer implements Player {
|
||||
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final int getCurrentWindowIndex() {
|
||||
return getCurrentMediaItemIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final int getNextWindowIndex() {
|
||||
@ -274,6 +304,9 @@ public abstract class BasePlayer implements Player {
|
||||
getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final int getPreviousWindowIndex() {
|
||||
@ -326,6 +359,9 @@ public abstract class BasePlayer implements Player {
|
||||
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean isCurrentWindowDynamic() {
|
||||
@ -338,6 +374,9 @@ public abstract class BasePlayer implements Player {
|
||||
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean isCurrentWindowLive() {
|
||||
@ -364,6 +403,9 @@ public abstract class BasePlayer implements Player {
|
||||
return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final boolean isCurrentWindowSeekable() {
|
||||
|
@ -333,12 +333,16 @@ public final class C {
|
||||
@IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER})
|
||||
public @interface SpatializationBehavior {}
|
||||
|
||||
// TODO[b/190759307]: Update constant values and javadoc to use SDK once compile SDK target is set
|
||||
// to 32.
|
||||
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO */
|
||||
public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0;
|
||||
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER */
|
||||
public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1;
|
||||
/**
|
||||
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO
|
||||
*/
|
||||
public static final int SPATIALIZATION_BEHAVIOR_AUTO =
|
||||
AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO;
|
||||
/**
|
||||
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER
|
||||
*/
|
||||
public static final int SPATIALIZATION_BEHAVIOR_NEVER =
|
||||
AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER;
|
||||
|
||||
/**
|
||||
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
|
||||
@ -396,9 +400,15 @@ public final class C {
|
||||
@UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
|
||||
|
||||
/**
|
||||
* Content types for audio attributes. One of {@link #CONTENT_TYPE_MOVIE}, {@link
|
||||
* #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link #CONTENT_TYPE_SPEECH} or
|
||||
* {@link #CONTENT_TYPE_UNKNOWN}.
|
||||
* Content types for audio attributes. One of:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #AUDIO_CONTENT_TYPE_MOVIE}
|
||||
* <li>{@link #AUDIO_CONTENT_TYPE_MUSIC}
|
||||
* <li>{@link #AUDIO_CONTENT_TYPE_SONIFICATION}
|
||||
* <li>{@link #AUDIO_CONTENT_TYPE_SPEECH}
|
||||
* <li>{@link #AUDIO_CONTENT_TYPE_UNKNOWN}
|
||||
* </ul>
|
||||
*/
|
||||
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
||||
// with Kotlin usages from before TYPE_USE was added.
|
||||
@ -406,34 +416,46 @@ public final class C {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({
|
||||
CONTENT_TYPE_MOVIE,
|
||||
CONTENT_TYPE_MUSIC,
|
||||
CONTENT_TYPE_SONIFICATION,
|
||||
CONTENT_TYPE_SPEECH,
|
||||
CONTENT_TYPE_UNKNOWN
|
||||
AUDIO_CONTENT_TYPE_MOVIE,
|
||||
AUDIO_CONTENT_TYPE_MUSIC,
|
||||
AUDIO_CONTENT_TYPE_SONIFICATION,
|
||||
AUDIO_CONTENT_TYPE_SPEECH,
|
||||
AUDIO_CONTENT_TYPE_UNKNOWN
|
||||
})
|
||||
public @interface AudioContentType {}
|
||||
/** See {@link AudioAttributes#CONTENT_TYPE_MOVIE}. */
|
||||
public static final int AUDIO_CONTENT_TYPE_MOVIE = AudioAttributes.CONTENT_TYPE_MOVIE;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE
|
||||
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_MOVIE} instead.
|
||||
*/
|
||||
public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE;
|
||||
@UnstableApi @Deprecated public static final int CONTENT_TYPE_MOVIE = AUDIO_CONTENT_TYPE_MOVIE;
|
||||
/** See {@link AudioAttributes#CONTENT_TYPE_MUSIC}. */
|
||||
public static final int AUDIO_CONTENT_TYPE_MUSIC = AudioAttributes.CONTENT_TYPE_MUSIC;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC
|
||||
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_MUSIC} instead.
|
||||
*/
|
||||
public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
|
||||
@UnstableApi @Deprecated public static final int CONTENT_TYPE_MUSIC = AUDIO_CONTENT_TYPE_MUSIC;
|
||||
/** See {@link AudioAttributes#CONTENT_TYPE_SONIFICATION}. */
|
||||
public static final int AUDIO_CONTENT_TYPE_SONIFICATION =
|
||||
AudioAttributes.CONTENT_TYPE_SONIFICATION;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION
|
||||
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_SONIFICATION} instead.
|
||||
*/
|
||||
public static final int CONTENT_TYPE_SONIFICATION =
|
||||
android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
|
||||
@UnstableApi @Deprecated
|
||||
public static final int CONTENT_TYPE_SONIFICATION = AUDIO_CONTENT_TYPE_SONIFICATION;
|
||||
/** See {@link AudioAttributes#CONTENT_TYPE_SPEECH}. */
|
||||
public static final int AUDIO_CONTENT_TYPE_SPEECH = AudioAttributes.CONTENT_TYPE_SPEECH;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH
|
||||
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_SPEECH} instead.
|
||||
*/
|
||||
public static final int CONTENT_TYPE_SPEECH = android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
|
||||
@UnstableApi @Deprecated public static final int CONTENT_TYPE_SPEECH = AUDIO_CONTENT_TYPE_SPEECH;
|
||||
/** See {@link AudioAttributes#CONTENT_TYPE_UNKNOWN}. */
|
||||
public static final int AUDIO_CONTENT_TYPE_UNKNOWN = AudioAttributes.CONTENT_TYPE_UNKNOWN;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN
|
||||
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_UNKNOWN} instead.
|
||||
*/
|
||||
public static final int CONTENT_TYPE_UNKNOWN = android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN;
|
||||
@UnstableApi @Deprecated
|
||||
public static final int CONTENT_TYPE_UNKNOWN = AUDIO_CONTENT_TYPE_UNKNOWN;
|
||||
|
||||
/**
|
||||
* Flags for audio attributes. Possible flag value is {@link #FLAG_AUDIBILITY_ENFORCED}.
|
||||
@ -725,30 +747,59 @@ public final class C {
|
||||
public static final String LANGUAGE_UNDETERMINED = "und";
|
||||
|
||||
/**
|
||||
* Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link
|
||||
* #TYPE_HLS}, {@link #TYPE_RTSP} or {@link #TYPE_OTHER}.
|
||||
* Represents a streaming or other media type. One of:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #CONTENT_TYPE_DASH}
|
||||
* <li>{@link #CONTENT_TYPE_SS}
|
||||
* <li>{@link #CONTENT_TYPE_HLS}
|
||||
* <li>{@link #CONTENT_TYPE_RTSP}
|
||||
* <li>{@link #CONTENT_TYPE_OTHER}
|
||||
* </ul>
|
||||
*/
|
||||
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
||||
// with Kotlin usages from before TYPE_USE was added.
|
||||
@UnstableApi
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER})
|
||||
@IntDef({
|
||||
CONTENT_TYPE_DASH,
|
||||
CONTENT_TYPE_SS,
|
||||
CONTENT_TYPE_HLS,
|
||||
CONTENT_TYPE_RTSP,
|
||||
CONTENT_TYPE_OTHER
|
||||
})
|
||||
public @interface ContentType {}
|
||||
/** Value returned by {@link Util#inferContentType} for DASH manifests. */
|
||||
@UnstableApi public static final int TYPE_DASH = 0;
|
||||
/** Value returned by {@link Util#inferContentType} for Smooth Streaming manifests. */
|
||||
@UnstableApi public static final int TYPE_SS = 1;
|
||||
/** Value returned by {@link Util#inferContentType} for HLS manifests. */
|
||||
@UnstableApi public static final int TYPE_HLS = 2;
|
||||
/** Value returned by {@link Util#inferContentType} for RTSP. */
|
||||
@UnstableApi public static final int TYPE_RTSP = 3;
|
||||
/** Value 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;
|
||||
|
@ -16,10 +16,8 @@
|
||||
package androidx.media3.common;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/** Converts throwables into error codes and user readable error messages. */
|
||||
@UnstableApi
|
||||
public interface ErrorMessageProvider<T extends Throwable> {
|
||||
|
||||
/**
|
||||
|
@ -37,13 +37,14 @@ public final class FileTypes {
|
||||
/**
|
||||
* File types. One of {@link #UNKNOWN}, {@link #AC3}, {@link #AC4}, {@link #ADTS}, {@link #AMR},
|
||||
* {@link #FLAC}, {@link #FLV}, {@link #MATROSKA}, {@link #MP3}, {@link #MP4}, {@link #OGG},
|
||||
* {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT} and {@link #JPEG}.
|
||||
* {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT}, {@link #JPEG} and {@link #MIDI}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({
|
||||
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG
|
||||
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG,
|
||||
MIDI
|
||||
})
|
||||
public @interface Type {}
|
||||
/** Unknown file type. */
|
||||
@ -78,6 +79,8 @@ public final class FileTypes {
|
||||
public static final int WEBVTT = 13;
|
||||
/** File type for the JPEG format. */
|
||||
public static final int JPEG = 14;
|
||||
/** File type for the MIDI format. */
|
||||
public static final int MIDI = 15;
|
||||
|
||||
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
@ -89,6 +92,9 @@ public final class FileTypes {
|
||||
private static final String EXTENSION_AMR = ".amr";
|
||||
private static final String EXTENSION_FLAC = ".flac";
|
||||
private static final String EXTENSION_FLV = ".flv";
|
||||
private static final String EXTENSION_MID = ".mid";
|
||||
private static final String EXTENSION_MIDI = ".midi";
|
||||
private static final String EXTENSION_SMF = ".smf";
|
||||
private static final String EXTENSION_PREFIX_MK = ".mk";
|
||||
private static final String EXTENSION_WEBM = ".webm";
|
||||
private static final String EXTENSION_PREFIX_OG = ".og";
|
||||
@ -147,6 +153,8 @@ public final class FileTypes {
|
||||
return FileTypes.FLAC;
|
||||
case MimeTypes.VIDEO_FLV:
|
||||
return FileTypes.FLV;
|
||||
case MimeTypes.AUDIO_MIDI:
|
||||
return FileTypes.MIDI;
|
||||
case MimeTypes.VIDEO_MATROSKA:
|
||||
case MimeTypes.AUDIO_MATROSKA:
|
||||
case MimeTypes.VIDEO_WEBM:
|
||||
@ -193,6 +201,10 @@ public final class FileTypes {
|
||||
return FileTypes.FLAC;
|
||||
} else if (filename.endsWith(EXTENSION_FLV)) {
|
||||
return FileTypes.FLV;
|
||||
} else if (filename.endsWith(EXTENSION_MID)
|
||||
|| filename.endsWith(EXTENSION_MIDI)
|
||||
|| filename.endsWith(EXTENSION_SMF)) {
|
||||
return FileTypes.MIDI;
|
||||
} else if (filename.startsWith(
|
||||
EXTENSION_PREFIX_MK,
|
||||
/* toffset= */ filename.length() - (EXTENSION_PREFIX_MK.length() + 1))
|
||||
|
@ -1548,7 +1548,9 @@ public final class Format implements Bundleable {
|
||||
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio);
|
||||
bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData);
|
||||
bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode);
|
||||
bundle.putBundle(keyForField(FIELD_COLOR_INFO), BundleableUtil.toNullableBundle(colorInfo));
|
||||
if (colorInfo != null) {
|
||||
bundle.putBundle(keyForField(FIELD_COLOR_INFO), colorInfo.toBundle());
|
||||
}
|
||||
// Audio specific.
|
||||
bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount);
|
||||
bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate);
|
||||
@ -1615,11 +1617,13 @@ public final class Format implements Bundleable {
|
||||
bundle.getFloat(
|
||||
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio))
|
||||
.setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA)))
|
||||
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode))
|
||||
.setColorInfo(
|
||||
BundleableUtil.fromNullableBundle(
|
||||
ColorInfo.CREATOR, bundle.getBundle(keyForField(FIELD_COLOR_INFO))))
|
||||
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode));
|
||||
Bundle colorInfoBundle = bundle.getBundle(keyForField(FIELD_COLOR_INFO));
|
||||
if (colorInfoBundle != null) {
|
||||
builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle));
|
||||
}
|
||||
// Audio specific.
|
||||
builder
|
||||
.setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount))
|
||||
.setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate))
|
||||
.setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding))
|
||||
|
@ -22,6 +22,7 @@ import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.util.List;
|
||||
|
||||
@ -298,7 +299,11 @@ public class ForwardingPlayer implements Player {
|
||||
player.seekForward();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#hasPrevious()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#hasPrevious()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -306,7 +311,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.hasPrevious();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -320,7 +329,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.hasPreviousMediaItem();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#previous()} on the delegate. */
|
||||
/**
|
||||
* Calls {@link Player#previous()} on the delegate.
|
||||
*
|
||||
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -328,7 +341,11 @@ public class ForwardingPlayer implements Player {
|
||||
player.previous();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#seekToPreviousWindow()} on the delegate. */
|
||||
/**
|
||||
* Calls {@link Player#seekToPreviousWindow()} on the delegate.
|
||||
*
|
||||
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -354,7 +371,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getMaxSeekToPreviousPosition();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#hasNext()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#hasNext()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #hasNextMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -362,7 +383,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.hasNext();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#hasNextWindow()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#hasNextWindow()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #hasNextMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -376,7 +401,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.hasNextMediaItem();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#next()} on the delegate. */
|
||||
/**
|
||||
* Calls {@link Player#next()} on the delegate.
|
||||
*
|
||||
* @deprecated Use {@link #seekToNextMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -384,7 +413,11 @@ public class ForwardingPlayer implements Player {
|
||||
player.next();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#seekToNextWindow()} on the delegate. */
|
||||
/**
|
||||
* Calls {@link Player#seekToNextWindow()} on the delegate.
|
||||
*
|
||||
* @deprecated Use {@link #seekToNextMediaItem()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -428,7 +461,13 @@ public class ForwardingPlayer implements Player {
|
||||
player.stop();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#stop(boolean)} on the delegate. */
|
||||
/**
|
||||
* Calls {@link Player#stop(boolean)} on the delegate.
|
||||
*
|
||||
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
|
||||
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
|
||||
* {@link #prepare() re-preparing} the player.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -497,7 +536,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getCurrentPeriodIndex();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -511,7 +554,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getCurrentMediaItemIndex();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -525,7 +572,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getNextMediaItemIndex();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -588,7 +639,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getTotalBufferedDuration();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -602,7 +657,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.isCurrentMediaItemDynamic();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -622,7 +681,11 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getCurrentLiveOffset();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result. */
|
||||
/**
|
||||
* Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result.
|
||||
*
|
||||
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method
|
||||
@Deprecated
|
||||
@Override
|
||||
@ -752,7 +815,7 @@ public class ForwardingPlayer implements Player {
|
||||
|
||||
/** Calls {@link Player#getCurrentCues()} on the delegate and returns the result. */
|
||||
@Override
|
||||
public List<Cue> getCurrentCues() {
|
||||
public CueGroup getCurrentCues() {
|
||||
return player.getCurrentCues();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -52,7 +52,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
@Nullable private CharSequence displayTitle;
|
||||
@Nullable private CharSequence subtitle;
|
||||
@Nullable private CharSequence description;
|
||||
@Nullable private Uri mediaUri;
|
||||
@Nullable private Rating userRating;
|
||||
@Nullable private Rating overallRating;
|
||||
@Nullable private byte[] artworkData;
|
||||
@ -88,7 +87,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
this.displayTitle = mediaMetadata.displayTitle;
|
||||
this.subtitle = mediaMetadata.subtitle;
|
||||
this.description = mediaMetadata.description;
|
||||
this.mediaUri = mediaMetadata.mediaUri;
|
||||
this.userRating = mediaMetadata.userRating;
|
||||
this.overallRating = mediaMetadata.overallRating;
|
||||
this.artworkData = mediaMetadata.artworkData;
|
||||
@ -161,12 +159,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the media {@link Uri}. */
|
||||
public Builder setMediaUri(@Nullable Uri mediaUri) {
|
||||
this.mediaUri = mediaUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the user {@link Rating}. */
|
||||
public Builder setUserRating(@Nullable Rating userRating) {
|
||||
this.userRating = userRating;
|
||||
@ -431,9 +423,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
if (mediaMetadata.description != null) {
|
||||
setDescription(mediaMetadata.description);
|
||||
}
|
||||
if (mediaMetadata.mediaUri != null) {
|
||||
setMediaUri(mediaMetadata.mediaUri);
|
||||
}
|
||||
if (mediaMetadata.userRating != null) {
|
||||
setUserRating(mediaMetadata.userRating);
|
||||
}
|
||||
@ -636,8 +625,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
@Nullable public final CharSequence subtitle;
|
||||
/** Optional description. */
|
||||
@Nullable public final CharSequence description;
|
||||
/** Optional media {@link Uri}. */
|
||||
@Nullable public final Uri mediaUri;
|
||||
/** Optional user {@link Rating}. */
|
||||
@Nullable public final Rating userRating;
|
||||
/** Optional overall {@link Rating}. */
|
||||
@ -722,7 +709,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
this.displayTitle = builder.displayTitle;
|
||||
this.subtitle = builder.subtitle;
|
||||
this.description = builder.description;
|
||||
this.mediaUri = builder.mediaUri;
|
||||
this.userRating = builder.userRating;
|
||||
this.overallRating = builder.overallRating;
|
||||
this.artworkData = builder.artworkData;
|
||||
@ -771,7 +757,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
&& Util.areEqual(displayTitle, that.displayTitle)
|
||||
&& Util.areEqual(subtitle, that.subtitle)
|
||||
&& Util.areEqual(description, that.description)
|
||||
&& Util.areEqual(mediaUri, that.mediaUri)
|
||||
&& Util.areEqual(userRating, that.userRating)
|
||||
&& Util.areEqual(overallRating, that.overallRating)
|
||||
&& Arrays.equals(artworkData, that.artworkData)
|
||||
@ -807,7 +792,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
displayTitle,
|
||||
subtitle,
|
||||
description,
|
||||
mediaUri,
|
||||
userRating,
|
||||
overallRating,
|
||||
Arrays.hashCode(artworkData),
|
||||
@ -918,7 +902,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle);
|
||||
bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle);
|
||||
bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description);
|
||||
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
|
||||
bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData);
|
||||
bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri);
|
||||
bundle.putCharSequence(keyForField(FIELD_WRITER), writer);
|
||||
@ -992,7 +975,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
.setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE)))
|
||||
.setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE)))
|
||||
.setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION)))
|
||||
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
|
||||
.setArtworkData(
|
||||
bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)),
|
||||
bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE))
|
||||
|
@ -87,6 +87,7 @@ public final class MimeTypes {
|
||||
public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
|
||||
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
|
||||
public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
|
||||
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
|
||||
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
|
||||
public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
|
||||
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg";
|
||||
|
@ -401,28 +401,6 @@ public class PlaybackException extends Exception implements Bundleable {
|
||||
|
||||
// Bundleable implementation.
|
||||
|
||||
/**
|
||||
* Identifiers for fields in a {@link Bundle} which represents a playback exception. Subclasses
|
||||
* may use {@link #FIELD_CUSTOM_ID_BASE} to generate more keys using {@link #keyForField(int)}.
|
||||
*
|
||||
* <p>Note: Changes to the Bundleable implementation must be backwards compatible, so as to avoid
|
||||
* breaking communication across different Bundleable implementation versions.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef(
|
||||
open = true,
|
||||
value = {
|
||||
FIELD_INT_ERROR_CODE,
|
||||
FIELD_LONG_TIMESTAMP_MS,
|
||||
FIELD_STRING_MESSAGE,
|
||||
FIELD_STRING_CAUSE_CLASS_NAME,
|
||||
FIELD_STRING_CAUSE_MESSAGE,
|
||||
})
|
||||
@UnstableApi
|
||||
protected @interface FieldNumber {}
|
||||
|
||||
private static final int FIELD_INT_ERROR_CODE = 0;
|
||||
private static final int FIELD_LONG_TIMESTAMP_MS = 1;
|
||||
private static final int FIELD_STRING_MESSAGE = 2;
|
||||
@ -430,7 +408,7 @@ public class PlaybackException extends Exception implements Bundleable {
|
||||
private static final int FIELD_STRING_CAUSE_MESSAGE = 4;
|
||||
|
||||
/**
|
||||
* Defines a minimum field id value for subclasses to use when implementing {@link #toBundle()}
|
||||
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
|
||||
* and {@link Bundleable.Creator}.
|
||||
*
|
||||
* <p>Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative
|
||||
@ -458,11 +436,14 @@ public class PlaybackException extends Exception implements Bundleable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link FieldNumber} to a string which can be used as a field key when
|
||||
* implementing {@link #toBundle()} and {@link Bundleable.Creator}.
|
||||
* Converts the given field number to a string which can be used as a field key when implementing
|
||||
* {@link #toBundle()} and {@link Bundleable.Creator}.
|
||||
*
|
||||
* <p>Subclasses should use {@code field} values greater than or equal to {@link
|
||||
* #FIELD_CUSTOM_ID_BASE}.
|
||||
*/
|
||||
@UnstableApi
|
||||
protected static String keyForField(@FieldNumber int field) {
|
||||
protected static String keyForField(int field) {
|
||||
return Integer.toString(field, Character.MAX_RADIX);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ import androidx.annotation.IntDef;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
@ -294,7 +294,9 @@ public interface Player {
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(keyForField(FIELD_MEDIA_ITEM_INDEX), mediaItemIndex);
|
||||
bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), BundleableUtil.toNullableBundle(mediaItem));
|
||||
if (mediaItem != null) {
|
||||
bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle());
|
||||
}
|
||||
bundle.putInt(keyForField(FIELD_PERIOD_INDEX), periodIndex);
|
||||
bundle.putLong(keyForField(FIELD_POSITION_MS), positionMs);
|
||||
bundle.putLong(keyForField(FIELD_CONTENT_POSITION_MS), contentPositionMs);
|
||||
@ -309,10 +311,10 @@ public interface Player {
|
||||
private static PositionInfo fromBundle(Bundle bundle) {
|
||||
int mediaItemIndex =
|
||||
bundle.getInt(keyForField(FIELD_MEDIA_ITEM_INDEX), /* defaultValue= */ C.INDEX_UNSET);
|
||||
@Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM));
|
||||
@Nullable
|
||||
MediaItem mediaItem =
|
||||
BundleableUtil.fromNullableBundle(
|
||||
MediaItem.CREATOR, bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)));
|
||||
mediaItemBundle == null ? null : MediaItem.CREATOR.fromBundle(mediaItemBundle);
|
||||
int periodIndex =
|
||||
bundle.getInt(keyForField(FIELD_PERIOD_INDEX), /* defaultValue= */ C.INDEX_UNSET);
|
||||
long positionMs =
|
||||
@ -382,6 +384,7 @@ public interface Player {
|
||||
COMMAND_GET_TEXT,
|
||||
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
|
||||
COMMAND_GET_TRACKS,
|
||||
COMMAND_SET_MEDIA_ITEM,
|
||||
};
|
||||
|
||||
private final FlagSet.Builder flagsBuilder;
|
||||
@ -1024,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();
|
||||
|
@ -32,7 +32,7 @@ import java.lang.annotation.Target;
|
||||
public abstract class Rating implements Bundleable {
|
||||
|
||||
/** A float value that denotes the rating is unset. */
|
||||
public static final float RATING_UNSET = -1.0f;
|
||||
/* package */ static final float RATING_UNSET = -1.0f;
|
||||
|
||||
// Default package-private constructor to prevent extending Rating class outside this package.
|
||||
/* package */ Rating() {}
|
||||
|
@ -189,11 +189,12 @@ public final class TrackGroup implements Bundleable {
|
||||
@UnstableApi
|
||||
public static final Creator<TrackGroup> CREATOR =
|
||||
bundle -> {
|
||||
@Nullable
|
||||
List<Bundle> formatBundles = bundle.getParcelableArrayList(keyForField(FIELD_FORMATS));
|
||||
List<Format> formats =
|
||||
BundleableUtil.fromBundleNullableList(
|
||||
Format.CREATOR,
|
||||
bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)),
|
||||
ImmutableList.of());
|
||||
formatBundles == null
|
||||
? ImmutableList.of()
|
||||
: BundleableUtil.fromBundleList(Format.CREATOR, formatBundles);
|
||||
String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ "");
|
||||
return new TrackGroup(id, formats.toArray(new Format[0]));
|
||||
};
|
||||
|
@ -16,7 +16,6 @@
|
||||
package androidx.media3.common;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.BundleableUtil.fromBundleNullableList;
|
||||
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
|
||||
@ -25,18 +24,15 @@ import android.graphics.Point;
|
||||
import android.os.Bundle;
|
||||
import android.os.Looper;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
@ -247,11 +243,13 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
bundle.getBoolean(
|
||||
keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE),
|
||||
DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
|
||||
@Nullable
|
||||
List<Bundle> overrideBundleList =
|
||||
bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES));
|
||||
List<TrackSelectionOverride> overrideList =
|
||||
fromBundleNullableList(
|
||||
TrackSelectionOverride.CREATOR,
|
||||
bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES)),
|
||||
ImmutableList.of());
|
||||
overrideBundleList == null
|
||||
? ImmutableList.of()
|
||||
: BundleableUtil.fromBundleList(TrackSelectionOverride.CREATOR, overrideBundleList);
|
||||
overrides = new HashMap<>();
|
||||
for (int i = 0; i < overrideList.size(); i++) {
|
||||
TrackSelectionOverride override = overrideList.get(i);
|
||||
@ -1071,42 +1069,6 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
|
||||
// Bundleable implementation
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
// Video
|
||||
FIELD_MAX_VIDEO_WIDTH,
|
||||
FIELD_MAX_VIDEO_HEIGHT,
|
||||
FIELD_MAX_VIDEO_FRAMERATE,
|
||||
FIELD_MAX_VIDEO_BITRATE,
|
||||
FIELD_MIN_VIDEO_WIDTH,
|
||||
FIELD_MIN_VIDEO_HEIGHT,
|
||||
FIELD_MIN_VIDEO_FRAMERATE,
|
||||
FIELD_MIN_VIDEO_BITRATE,
|
||||
FIELD_VIEWPORT_WIDTH,
|
||||
FIELD_VIEWPORT_HEIGHT,
|
||||
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
|
||||
FIELD_PREFERRED_VIDEO_MIMETYPES,
|
||||
FIELD_PREFERRED_VIDEO_ROLE_FLAGS,
|
||||
// Audio
|
||||
FIELD_PREFERRED_AUDIO_LANGUAGES,
|
||||
FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
|
||||
FIELD_MAX_AUDIO_CHANNEL_COUNT,
|
||||
FIELD_MAX_AUDIO_BITRATE,
|
||||
FIELD_PREFERRED_AUDIO_MIME_TYPES,
|
||||
// Text
|
||||
FIELD_PREFERRED_TEXT_LANGUAGES,
|
||||
FIELD_PREFERRED_TEXT_ROLE_FLAGS,
|
||||
FIELD_IGNORED_TEXT_SELECTION_FLAGS,
|
||||
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
|
||||
// General
|
||||
FIELD_FORCE_LOWEST_BITRATE,
|
||||
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
|
||||
FIELD_SELECTION_OVERRIDES,
|
||||
FIELD_DISABLED_TRACK_TYPE,
|
||||
})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1;
|
||||
private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2;
|
||||
private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3;
|
||||
@ -1134,7 +1096,15 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25;
|
||||
private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26;
|
||||
|
||||
@UnstableApi
|
||||
/**
|
||||
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
|
||||
* and {@link Bundleable.Creator}.
|
||||
*
|
||||
* <p>Subclasses should obtain keys for their {@link Bundle} representation by applying a
|
||||
* non-negative offset on this constant and passing the result to {@link #keyForField(int)}.
|
||||
*/
|
||||
@UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000;
|
||||
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
@ -1184,12 +1154,27 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/** Object that can restore {@code TrackSelectionParameters} from a {@link Bundle}. */
|
||||
@UnstableApi
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
GlUtil.checkGlException(
|
||||
success && numConfigs[0] > 0 && configs[0] != null,
|
||||
Util.formatInvariant(
|
||||
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
|
||||
success, numConfigs[0], configs[0]));
|
||||
}
|
||||
|
||||
return configs[0];
|
||||
}
|
||||
|
||||
private static EGLContext createEGLContext(
|
||||
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
|
||||
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) throws GlUtil.GlException {
|
||||
int[] glAttributes;
|
||||
if (secureMode == SECURE_MODE_NONE) {
|
||||
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
|
||||
@ -262,14 +250,13 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
EGLContext context =
|
||||
EGL14.eglCreateContext(
|
||||
display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
|
||||
if (context == null) {
|
||||
throw new GlException("eglCreateContext failed");
|
||||
}
|
||||
GlUtil.checkGlException(context != null, "eglCreateContext failed");
|
||||
return context;
|
||||
}
|
||||
|
||||
private static EGLSurface createEGLSurface(
|
||||
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
|
||||
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode)
|
||||
throws GlUtil.GlException {
|
||||
EGLSurface surface;
|
||||
if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
|
||||
surface = EGL14.EGL_NO_SURFACE;
|
||||
@ -297,20 +284,16 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
};
|
||||
}
|
||||
surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
|
||||
if (surface == null) {
|
||||
throw new GlException("eglCreatePbufferSurface failed");
|
||||
}
|
||||
GlUtil.checkGlException(surface != null, "eglCreatePbufferSurface failed");
|
||||
}
|
||||
|
||||
boolean eglMadeCurrent =
|
||||
EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
|
||||
if (!eglMadeCurrent) {
|
||||
throw new GlException("eglMakeCurrent failed");
|
||||
}
|
||||
GlUtil.checkGlException(eglMadeCurrent, "eglMakeCurrent failed");
|
||||
return surface;
|
||||
}
|
||||
|
||||
private static void generateTextureIds(int[] textureIdHolder) {
|
||||
private static void generateTextureIds(int[] textureIdHolder) throws GlUtil.GlException {
|
||||
GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public final class GlProgram {
|
||||
* @throws IOException When failing to read shader files.
|
||||
*/
|
||||
public GlProgram(Context context, String vertexShaderFilePath, String fragmentShaderFilePath)
|
||||
throws IOException {
|
||||
throws IOException, GlUtil.GlException {
|
||||
this(
|
||||
GlUtil.loadAsset(context, vertexShaderFilePath),
|
||||
GlUtil.loadAsset(context, fragmentShaderFilePath));
|
||||
@ -69,7 +69,7 @@ public final class GlProgram {
|
||||
* @param vertexShaderGlsl The vertex shader program.
|
||||
* @param fragmentShaderGlsl The fragment shader program.
|
||||
*/
|
||||
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) {
|
||||
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) throws GlUtil.GlException {
|
||||
programId = GLES20.glCreateProgram();
|
||||
GlUtil.checkGlError();
|
||||
|
||||
@ -81,10 +81,9 @@ public final class GlProgram {
|
||||
GLES20.glLinkProgram(programId);
|
||||
int[] linkStatus = new int[] {GLES20.GL_FALSE};
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
|
||||
if (linkStatus[0] != GLES20.GL_TRUE) {
|
||||
GlUtil.throwGlException(
|
||||
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);
|
||||
|
@ -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(
|
||||
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)) {
|
||||
throwGlException("Error in eglInitialize.");
|
||||
}
|
||||
/* 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];
|
||||
}
|
||||
|
@ -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);
|
||||
Api31.disambiguate4gAnd5gNsa(context, /* instance= */ NetworkTypeObserver.this);
|
||||
} 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.
|
||||
}
|
||||
}
|
||||
updateNetworkType(networkType);
|
||||
}
|
||||
}
|
||||
|
||||
private class TelephonyManagerListener extends PhoneStateListener {
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private static final class Api31 {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
|
||||
|| 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -29,7 +29,7 @@ public class AudioAttributesTest {
|
||||
public void roundTripViaBundle_yieldsEqualInstance() {
|
||||
AudioAttributes audioAttributes =
|
||||
new AudioAttributes.Builder()
|
||||
.setContentType(C.CONTENT_TYPE_SONIFICATION)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_SONIFICATION)
|
||||
.setFlags(C.FLAG_AUDIBILITY_ENFORCED)
|
||||
.setUsage(C.USAGE_ALARM)
|
||||
.setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM)
|
||||
|
@ -18,22 +18,17 @@ package androidx.media3.common;
|
||||
import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED;
|
||||
import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION;
|
||||
import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import androidx.media3.test.utils.StubPlayer;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -105,7 +100,7 @@ public class ForwardingPlayerTest {
|
||||
@Test
|
||||
public void forwardingPlayer_overridesAllPlayerMethods() throws Exception {
|
||||
// Check with reflection that ForwardingPlayer overrides all Player methods.
|
||||
List<Method> methods = getPublicMethods(Player.class);
|
||||
List<Method> methods = TestUtil.getPublicMethods(Player.class);
|
||||
for (Method method : methods) {
|
||||
assertThat(
|
||||
ForwardingPlayer.class
|
||||
@ -119,7 +114,7 @@ public class ForwardingPlayerTest {
|
||||
public void forwardingListener_overridesAllListenerMethods() throws Exception {
|
||||
// Check with reflection that ForwardingListener overrides all Listener methods.
|
||||
Class<?> forwardingListenerClass = getInnerClass("ForwardingListener");
|
||||
List<Method> methods = getPublicMethods(Player.Listener.class);
|
||||
List<Method> methods = TestUtil.getPublicMethods(Player.Listener.class);
|
||||
for (Method method : methods) {
|
||||
assertThat(
|
||||
forwardingListenerClass
|
||||
@ -129,32 +124,6 @@ public class ForwardingPlayerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns all the public methods of a Java interface. */
|
||||
private static List<Method> getPublicMethods(Class<?> anInterface) {
|
||||
checkArgument(anInterface.isInterface());
|
||||
// Run a BFS over all extended interfaces to inspect them all.
|
||||
Queue<Class<?>> interfacesQueue = new ArrayDeque<>();
|
||||
interfacesQueue.add(anInterface);
|
||||
Set<Class<?>> interfaces = new HashSet<>();
|
||||
while (!interfacesQueue.isEmpty()) {
|
||||
Class<?> currentInterface = interfacesQueue.remove();
|
||||
if (interfaces.add(currentInterface)) {
|
||||
Collections.addAll(interfacesQueue, currentInterface.getInterfaces());
|
||||
}
|
||||
}
|
||||
|
||||
List<Method> list = new ArrayList<>();
|
||||
for (Class<?> currentInterface : interfaces) {
|
||||
for (Method method : currentInterface.getDeclaredMethods()) {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
list.add(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static Class<?> getInnerClass(String className) {
|
||||
for (Class<?> innerClass : ForwardingPlayer.class.getDeclaredClasses()) {
|
||||
if (innerClass.getSimpleName().equals(className)) {
|
||||
|
@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.media3.common.MediaItem.RequestMetadata;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@ -223,7 +225,7 @@ public class MediaItemTest {
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri(licenseUri)
|
||||
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
|
||||
.forceSessionsForAudioAndVideoTracks(true)
|
||||
.setForceSessionsForAudioAndVideoTracks(true)
|
||||
.build();
|
||||
|
||||
assertThat(drmConfiguration.sessionForClearTypes)
|
||||
@ -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();
|
||||
|
@ -41,7 +41,6 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.displayTitle).isNull();
|
||||
assertThat(mediaMetadata.subtitle).isNull();
|
||||
assertThat(mediaMetadata.description).isNull();
|
||||
assertThat(mediaMetadata.mediaUri).isNull();
|
||||
assertThat(mediaMetadata.userRating).isNull();
|
||||
assertThat(mediaMetadata.overallRating).isNull();
|
||||
assertThat(mediaMetadata.artworkData).isNull();
|
||||
@ -127,7 +126,6 @@ public class MediaMetadataTest {
|
||||
.setDisplayTitle("display title")
|
||||
.setSubtitle("subtitle")
|
||||
.setDescription("description")
|
||||
.setMediaUri(Uri.parse("https://www.google.com"))
|
||||
.setUserRating(new HeartRating(false))
|
||||
.setOverallRating(new PercentageRating(87.4f))
|
||||
.setArtworkData(
|
||||
|
@ -201,7 +201,7 @@ public final class TrackSelectionParametersTest {
|
||||
new TrackSelectionParameters.Builder(getApplicationContext()).addOverride(override).build();
|
||||
|
||||
TrackSelectionParameters fromBundle =
|
||||
TrackSelectionParameters.CREATOR.fromBundle(trackSelectionParameters.toBundle());
|
||||
TrackSelectionParameters.fromBundle(trackSelectionParameters.toBundle());
|
||||
|
||||
assertThat(fromBundle).isEqualTo(trackSelectionParameters);
|
||||
assertThat(trackSelectionParameters.overrides)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -103,50 +103,109 @@ public class UtilTest {
|
||||
|
||||
@Test
|
||||
public void inferContentType_handlesHlsIsmUris() {
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl)"))
|
||||
.isEqualTo(C.TYPE_HLS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)"))
|
||||
.isEqualTo(C.TYPE_HLS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)"))
|
||||
.isEqualTo(C.TYPE_HLS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inferContentType_handlesHlsIsmV3Uris() {
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)"))
|
||||
.isEqualTo(C.TYPE_HLS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)"))
|
||||
.isEqualTo(C.TYPE_HLS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)"))
|
||||
.isEqualTo(C.TYPE_HLS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inferContentType_handlesDashIsmUris() {
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf)"))
|
||||
.isEqualTo(C.TYPE_DASH);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)"))
|
||||
.isEqualTo(C.TYPE_DASH);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)"))
|
||||
.isEqualTo(C.TYPE_DASH);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest(format=mpd-time-csf)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inferContentType_handlesSmoothStreamingIsmUris() {
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType("http://a.b/c.isml/manifest_hd")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism"))).isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml"))).isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/"))).isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/"))).isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/Manifest")))
|
||||
.isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest")))
|
||||
.isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest(filter=x)")))
|
||||
.isEqualTo(C.CONTENT_TYPE_SS);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest_hd")))
|
||||
.isEqualTo(C.CONTENT_TYPE_SS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inferContentType_handlesOtherIsmUris() {
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/video.mp4")).isEqualTo(C.TYPE_OTHER);
|
||||
assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/video.mp4")))
|
||||
.isEqualTo(C.CONTENT_TYPE_OTHER);
|
||||
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/prefix-manifest")))
|
||||
.isEqualTo(C.CONTENT_TYPE_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the deprecated {@link Util#inferContentType(String)} works when passed only a file
|
||||
* extension and the leading dot.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void inferContentType_extensionAsPath() {
|
||||
assertThat(Util.inferContentType(".m3u8")).isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
assertThat(Util.inferContentType(".mpd")).isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
assertThat(Util.inferContentType(".ism")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType(".isml")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentType(".mp4")).isEqualTo(C.CONTENT_TYPE_OTHER);
|
||||
}
|
||||
|
||||
// Testing deprecated method.
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void inferContentType_extensionOverride() {
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ null))
|
||||
.isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ ""))
|
||||
.isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
assertThat(
|
||||
Util.inferContentType(
|
||||
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ "m3u8"))
|
||||
.isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inferContentTypeForExtension() {
|
||||
assertThat(Util.inferContentTypeForExtension("m3u8")).isEqualTo(C.CONTENT_TYPE_HLS);
|
||||
assertThat(Util.inferContentTypeForExtension("mpd")).isEqualTo(C.CONTENT_TYPE_DASH);
|
||||
assertThat(Util.inferContentTypeForExtension("ism")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentTypeForExtension("isml")).isEqualTo(C.TYPE_SS);
|
||||
assertThat(Util.inferContentTypeForExtension("mp4")).isEqualTo(C.CONTENT_TYPE_OTHER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1103,6 +1162,7 @@ public class UtilTest {
|
||||
assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl");
|
||||
|
||||
// Special case of short codes that are actually part of a macrolanguage.
|
||||
assertThat(Util.normalizeLanguageCode("arb")).isEqualTo("ar-arb");
|
||||
assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob");
|
||||
assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno");
|
||||
assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob");
|
||||
|
@ -21,19 +21,13 @@ import static java.lang.Math.min;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.media.ApplicationMediaCapabilities;
|
||||
import android.media.MediaFeature;
|
||||
import android.media.MediaFormat;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@ -78,6 +72,7 @@ public final class ContentDataSource extends BaseDataSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("InlinedApi") // We are inlining EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT.
|
||||
public long open(DataSpec dataSpec) throws ContentDataSourceException {
|
||||
try {
|
||||
Uri uri = dataSpec.uri;
|
||||
@ -88,9 +83,8 @@ public final class ContentDataSource extends BaseDataSource {
|
||||
AssetFileDescriptor assetFileDescriptor;
|
||||
if ("content".equals(dataSpec.uri.getScheme())) {
|
||||
Bundle providerOptions = new Bundle();
|
||||
if (Util.SDK_INT >= 31) {
|
||||
Api31.disableTranscoding(providerOptions);
|
||||
}
|
||||
// We don't want compatible media transcoding.
|
||||
providerOptions.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
|
||||
assetFileDescriptor =
|
||||
resolver.openTypedAssetFileDescriptor(uri, /* mimeType= */ "*/*", providerOptions);
|
||||
} else {
|
||||
@ -232,21 +226,4 @@ public final class ContentDataSource extends BaseDataSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private static final class Api31 {
|
||||
|
||||
@DoNotInline
|
||||
public static void disableTranscoding(Bundle providerOptions) {
|
||||
ApplicationMediaCapabilities mediaCapabilities =
|
||||
new ApplicationMediaCapabilities.Builder()
|
||||
.addSupportedVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC)
|
||||
.addSupportedHdrType(MediaFeature.HdrType.DOLBY_VISION)
|
||||
.addSupportedHdrType(MediaFeature.HdrType.HDR10)
|
||||
.addSupportedHdrType(MediaFeature.HdrType.HDR10_PLUS)
|
||||
.addSupportedHdrType(MediaFeature.HdrType.HLG)
|
||||
.build();
|
||||
providerOptions.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, mediaCapabilities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -418,7 +418,7 @@ public interface HttpDataSource extends DataSource {
|
||||
@Nullable public final String responseMessage;
|
||||
|
||||
/** An unmodifiable map of the response header fields and values. */
|
||||
public final Map<String, List<String>> headerFields;
|
||||
@UnstableApi public final Map<String, List<String>> headerFields;
|
||||
|
||||
/** The response body. */
|
||||
public final byte[] responseBody;
|
||||
|
@ -481,11 +481,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
|
||||
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
||||
*
|
||||
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
|
||||
* predicate that was previously set.
|
||||
* @deprecated Use {@link CronetDataSource.Factory#setContentTypePredicate(Predicate)} instead.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
|
@ -32,10 +32,14 @@ import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSourceException;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException;
|
||||
import androidx.media3.datasource.HttpDataSource.InvalidContentTypeException;
|
||||
import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException;
|
||||
import androidx.media3.datasource.HttpUtil;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
@ -43,8 +47,10 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
@ -299,8 +305,9 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
Request request = makeRequest(dataSpec);
|
||||
Response response;
|
||||
ResponseBody responseBody;
|
||||
Call call = callFactory.newCall(request);
|
||||
try {
|
||||
this.response = callFactory.newCall(request).execute();
|
||||
this.response = executeCall(call);
|
||||
response = this.response;
|
||||
responseBody = Assertions.checkNotNull(response.body());
|
||||
responseByteStream = responseBody.byteStream();
|
||||
@ -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.
|
||||
*
|
||||
|
@ -16,6 +16,7 @@
|
||||
package androidx.media3.decoder.opus;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
@ -26,7 +27,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@ -69,11 +69,6 @@ public final class OpusDecoderTest {
|
||||
private static final ImmutableList<byte[]> FULL_INITIALIZATION_DATA =
|
||||
ImmutableList.of(HEADER, CUSTOM_PRE_SKIP_BYTES, CUSTOM_SEEK_PRE_ROLL_BYTES);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
assertThat(LOADER.isAvailable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getChannelCount() {
|
||||
int channelCount = OpusDecoder.getChannelCount(HEADER);
|
||||
@ -120,6 +115,7 @@ public final class OpusDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decode_removesPreSkipFromOutput() throws OpusDecoderException {
|
||||
assumeTrue(LOADER.isAvailable());
|
||||
OpusDecoder decoder =
|
||||
new OpusDecoder(
|
||||
/* numInputBuffers= */ 0,
|
||||
@ -139,6 +135,7 @@ public final class OpusDecoderTest {
|
||||
@Test
|
||||
public void decode_whenDiscardPaddingDisabled_returnsDiscardPadding()
|
||||
throws OpusDecoderException {
|
||||
assumeTrue(LOADER.isAvailable());
|
||||
OpusDecoder decoder =
|
||||
new OpusDecoder(
|
||||
/* numInputBuffers= */ 0,
|
||||
@ -159,6 +156,7 @@ public final class OpusDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decode_whenDiscardPaddingEnabled_removesDiscardPadding() throws OpusDecoderException {
|
||||
assumeTrue(LOADER.isAvailable());
|
||||
OpusDecoder decoder =
|
||||
new OpusDecoder(
|
||||
/* numInputBuffers= */ 0,
|
||||
@ -200,8 +198,12 @@ public final class OpusDecoderTest {
|
||||
return ImmutableList.of(HEADER, preSkip, CUSTOM_SEEK_PRE_ROLL_BYTES);
|
||||
}
|
||||
|
||||
// The cast to ByteBuffer is required for Java 8 compatibility. See
|
||||
// https://issues.apache.org/jira/browse/MRESOLVER-85
|
||||
@SuppressWarnings("UnnecessaryCast")
|
||||
private static ByteBuffer createSupplementalData(long value) {
|
||||
return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind();
|
||||
return (ByteBuffer)
|
||||
ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind();
|
||||
}
|
||||
|
||||
private static DecoderInputBuffer createInputBuffer(
|
||||
|
@ -25,6 +25,7 @@ import androidx.media3.common.MediaItem.SubtitleConfiguration;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.exoplayer.source.ClippingMediaSource;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@ -135,8 +136,8 @@ public final class ClippedPlaybackTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
this.cues.add(cues);
|
||||
public void onCues(CueGroup cueGroup) {
|
||||
this.cues.add(cueGroup.cues);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -276,7 +276,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
boolean willPauseWhenDucked = willPauseWhenDucked();
|
||||
audioFocusRequest =
|
||||
builder
|
||||
.setAudioAttributes(checkNotNull(audioAttributes).getAudioAttributesV21())
|
||||
.setAudioAttributes(
|
||||
checkNotNull(audioAttributes).getAudioAttributesV21().audioAttributes)
|
||||
.setWillPauseWhenDucked(willPauseWhenDucked)
|
||||
.setOnAudioFocusChangeListener(focusListener)
|
||||
.build();
|
||||
@ -298,7 +299,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
|
||||
private boolean willPauseWhenDucked() {
|
||||
return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH;
|
||||
return audioAttributes != null && audioAttributes.contentType == C.AUDIO_CONTENT_TYPE_SPEECH;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -369,7 +370,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
// Special usages:
|
||||
case C.USAGE_ASSISTANCE_ACCESSIBILITY:
|
||||
if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) {
|
||||
if (audioAttributes.contentType == C.AUDIO_CONTENT_TYPE_SPEECH) {
|
||||
// Voice shouldn't be interrupted by other playback.
|
||||
return AUDIOFOCUS_GAIN_TRANSIENT;
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -33,7 +33,6 @@ import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MediaPeriodId;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
@ -255,9 +254,9 @@ public final class ExoPlaybackException extends PlaybackException {
|
||||
rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME));
|
||||
rendererIndex =
|
||||
bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET);
|
||||
@Nullable Bundle rendererFormatBundle = bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT));
|
||||
rendererFormat =
|
||||
BundleableUtil.fromNullableBundle(
|
||||
Format.CREATOR, bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT)));
|
||||
rendererFormatBundle == null ? null : Format.CREATOR.fromBundle(rendererFormatBundle);
|
||||
rendererFormatSupport =
|
||||
bundle.getInt(
|
||||
keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED);
|
||||
@ -424,8 +423,9 @@ public final class ExoPlaybackException extends PlaybackException {
|
||||
bundle.putInt(keyForField(FIELD_TYPE), type);
|
||||
bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName);
|
||||
bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex);
|
||||
bundle.putBundle(
|
||||
keyForField(FIELD_RENDERER_FORMAT), BundleableUtil.toNullableBundle(rendererFormat));
|
||||
if (rendererFormat != null) {
|
||||
bundle.putBundle(keyForField(FIELD_RENDERER_FORMAT), rendererFormat.toBundle());
|
||||
}
|
||||
bundle.putInt(keyForField(FIELD_RENDERER_FORMAT_SUPPORT), rendererFormatSupport);
|
||||
bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable);
|
||||
return bundle;
|
||||
|
@ -40,7 +40,7 @@ import androidx.media3.common.PriorityTaskManager;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
@ -358,7 +358,7 @@ public interface ExoPlayer extends Player {
|
||||
* @deprecated Use {@link Player#getCurrentCues()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
List<Cue> getCurrentCues();
|
||||
CueGroup getCurrentCues();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1175,7 +1175,6 @@ public interface ExoPlayer extends Player {
|
||||
*
|
||||
* @param listener The listener to be added.
|
||||
*/
|
||||
@UnstableApi
|
||||
void addAnalyticsListener(AnalyticsListener listener);
|
||||
|
||||
/**
|
||||
@ -1183,7 +1182,6 @@ public interface ExoPlayer extends Player {
|
||||
*
|
||||
* @param listener The listener to be removed.
|
||||
*/
|
||||
@UnstableApi
|
||||
void removeAnalyticsListener(AnalyticsListener listener);
|
||||
|
||||
/** Returns the number of renderers. */
|
||||
|
@ -73,6 +73,7 @@ import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
@ -196,7 +197,7 @@ import java.util.concurrent.TimeoutException;
|
||||
private AudioAttributes audioAttributes;
|
||||
private float volume;
|
||||
private boolean skipSilenceEnabled;
|
||||
private List<Cue> currentCues;
|
||||
private CueGroup currentCueGroup;
|
||||
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||
@Nullable private CameraMotionListener cameraMotionListener;
|
||||
private boolean throwsWhenUsingWrongThread;
|
||||
@ -302,7 +303,8 @@ import java.util.concurrent.TimeoutException;
|
||||
COMMAND_SET_DEVICE_VOLUME,
|
||||
COMMAND_ADJUST_DEVICE_VOLUME,
|
||||
COMMAND_SET_VIDEO_SURFACE,
|
||||
COMMAND_GET_TEXT)
|
||||
COMMAND_GET_TEXT,
|
||||
COMMAND_SET_MEDIA_ITEM)
|
||||
.addIf(
|
||||
COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported())
|
||||
.build();
|
||||
@ -353,7 +355,7 @@ import java.util.concurrent.TimeoutException;
|
||||
} else {
|
||||
audioSessionId = Util.generateAudioSessionIdV21(applicationContext);
|
||||
}
|
||||
currentCues = ImmutableList.of();
|
||||
currentCueGroup = CueGroup.EMPTY;
|
||||
throwsWhenUsingWrongThread = true;
|
||||
|
||||
addListener(analyticsCollector);
|
||||
@ -378,6 +380,7 @@ import java.util.concurrent.TimeoutException;
|
||||
deviceInfo = createDeviceInfo(streamVolumeManager);
|
||||
videoSize = VideoSize.UNKNOWN;
|
||||
|
||||
trackSelector.setAudioAttributes(audioAttributes);
|
||||
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
|
||||
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
|
||||
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes);
|
||||
@ -936,7 +939,7 @@ import java.util.concurrent.TimeoutException;
|
||||
verifyApplicationThread();
|
||||
audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE);
|
||||
stopInternal(reset, /* error= */ null);
|
||||
currentCues = ImmutableList.of();
|
||||
currentCueGroup = CueGroup.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -980,6 +983,7 @@ import java.util.concurrent.TimeoutException;
|
||||
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
|
||||
playbackInfo.totalBufferedDurationUs = 0;
|
||||
analyticsCollector.release();
|
||||
trackSelector.release();
|
||||
removeSurfaceCallbacks();
|
||||
if (ownedSurface != null) {
|
||||
ownedSurface.release();
|
||||
@ -989,7 +993,7 @@ import java.util.concurrent.TimeoutException;
|
||||
checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);
|
||||
isPriorityTaskManagerRegistered = false;
|
||||
}
|
||||
currentCues = ImmutableList.of();
|
||||
currentCueGroup = CueGroup.EMPTY;
|
||||
playerReleased = true;
|
||||
}
|
||||
|
||||
@ -1372,6 +1376,7 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
|
||||
audioFocusManager.setAudioAttributes(handleAudioFocus ? newAudioAttributes : null);
|
||||
trackSelector.setAudioAttributes(newAudioAttributes);
|
||||
boolean playWhenReady = getPlayWhenReady();
|
||||
@AudioFocusManager.PlayerCommand
|
||||
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
|
||||
@ -1586,9 +1591,9 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cue> getCurrentCues() {
|
||||
public CueGroup getCurrentCues() {
|
||||
verifyApplicationThread();
|
||||
return currentCues;
|
||||
return currentCueGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -2849,13 +2854,17 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
|
||||
// TextOutput implementation
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
currentCues = cues;
|
||||
listeners.sendEvent(EVENT_CUES, listener -> listener.onCues(cues));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(CueGroup cueGroup) {
|
||||
currentCueGroup = cueGroup;
|
||||
listeners.sendEvent(EVENT_CUES, listener -> listener.onCues(cueGroup));
|
||||
}
|
||||
|
||||
// MetadataOutput implementation
|
||||
|
||||
@Override
|
||||
|
@ -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,12 +2477,9 @@ 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) {
|
||||
public void onSleep() {
|
||||
requestForRendererSleep = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWakeup() {
|
||||
|
@ -66,11 +66,8 @@ public interface Renderer extends PlayerMessage.Target {
|
||||
* The renderer no longer needs to render until the next wakeup.
|
||||
*
|
||||
* <p>Must be called from the thread ExoPlayer invokes the renderer from.
|
||||
*
|
||||
* @param wakeupDeadlineMs Maximum time in milliseconds until {@link #onWakeup()} will be
|
||||
* called.
|
||||
*/
|
||||
void onSleep(long wakeupDeadlineMs);
|
||||
void onSleep();
|
||||
|
||||
/**
|
||||
* The renderer needs to render some frames. The client should call {@link #render(long, long)}
|
||||
|
@ -33,12 +33,13 @@ import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.PlaybackParameters;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.PriorityTaskManager;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
@ -426,24 +427,44 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
return player.experimentalIsSleepingForOffload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ExoPlayer}, as the {@link AudioComponent} methods are defined by that
|
||||
* interface.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public AudioComponent getAudioComponent() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ExoPlayer}, as the {@link VideoComponent} methods are defined by that
|
||||
* interface.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public VideoComponent getVideoComponent() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Player}, as the {@link TextComponent} methods are defined by that
|
||||
* interface.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public TextComponent getTextComponent() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Player}, as the {@link DeviceComponent} methods are defined by that
|
||||
* interface.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public DeviceComponent getDeviceComponent() {
|
||||
@ -690,7 +711,7 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cue> getCurrentCues() {
|
||||
public CueGroup getCurrentCues() {
|
||||
blockUntilConstructorFinished();
|
||||
return player.getCurrentCues();
|
||||
}
|
||||
@ -1003,6 +1024,11 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
player.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
|
||||
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
|
||||
* {@link #prepare() re-preparing} the player.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
@ -1046,12 +1072,20 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
return player.getTrackSelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getCurrentTracks()}.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public TrackGroupArray getCurrentTrackGroups() {
|
||||
blockUntilConstructorFinished();
|
||||
return player.getCurrentTrackGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getCurrentTracks()}.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public TrackSelectionArray getCurrentTrackSelections() {
|
||||
blockUntilConstructorFinished();
|
||||
@ -1166,6 +1200,9 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
return player.getContentBufferedPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setWakeMode(int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setHandleWakeLock(boolean handleWakeLock) {
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.ListenerSet;
|
||||
@ -695,6 +696,7 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
|
||||
listener -> listener.onMetadata(eventTime, metadata));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
@ -702,6 +704,13 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
|
||||
eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(CueGroup cueGroup) {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
sendEvent(
|
||||
eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cueGroup));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
|
||||
@Override
|
||||
public final void onSeekProcessed() {
|
||||
|
@ -679,13 +679,13 @@ public final class MediaMetricsListener
|
||||
Util.inferContentTypeForUriAndMimeType(
|
||||
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
|
||||
switch (contentType) {
|
||||
case C.TYPE_HLS:
|
||||
case C.CONTENT_TYPE_HLS:
|
||||
return PlaybackMetrics.STREAM_TYPE_HLS;
|
||||
case C.TYPE_DASH:
|
||||
case C.CONTENT_TYPE_DASH:
|
||||
return PlaybackMetrics.STREAM_TYPE_DASH;
|
||||
case C.TYPE_SS:
|
||||
case C.CONTENT_TYPE_SS:
|
||||
return PlaybackMetrics.STREAM_TYPE_SS;
|
||||
case C.TYPE_RTSP:
|
||||
case C.CONTENT_TYPE_RTSP:
|
||||
default:
|
||||
return PlaybackMetrics.STREAM_TYPE_OTHER;
|
||||
}
|
||||
|
@ -15,22 +15,29 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.audio;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings.Global;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -54,18 +61,20 @@ public final class AudioCapabilities {
|
||||
},
|
||||
DEFAULT_MAX_CHANNEL_COUNT);
|
||||
|
||||
/** Array of all surround sound encodings that a device may be capable of playing. */
|
||||
@SuppressWarnings("InlinedApi")
|
||||
private static final int[] ALL_SURROUND_ENCODINGS =
|
||||
new int[] {
|
||||
AudioFormat.ENCODING_AC3,
|
||||
AudioFormat.ENCODING_E_AC3,
|
||||
AudioFormat.ENCODING_E_AC3_JOC,
|
||||
AudioFormat.ENCODING_AC4,
|
||||
AudioFormat.ENCODING_DOLBY_TRUEHD,
|
||||
AudioFormat.ENCODING_DTS,
|
||||
AudioFormat.ENCODING_DTS_HD,
|
||||
};
|
||||
/**
|
||||
* All surround sound encodings that a device may be capable of playing mapped to a maximum
|
||||
* channel count.
|
||||
*/
|
||||
private static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
|
||||
new ImmutableMap.Builder<Integer, Integer>()
|
||||
.put(C.ENCODING_AC3, 6)
|
||||
.put(C.ENCODING_AC4, 6)
|
||||
.put(C.ENCODING_DTS, 6)
|
||||
.put(C.ENCODING_E_AC3_JOC, 6)
|
||||
.put(C.ENCODING_E_AC3, 8)
|
||||
.put(C.ENCODING_DTS_HD, 8)
|
||||
.put(C.ENCODING_DOLBY_TRUEHD, 8)
|
||||
.buildOrThrow();
|
||||
|
||||
/** Global settings key for devices that can specify external surround sound. */
|
||||
private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled";
|
||||
@ -158,6 +167,62 @@ public final class AudioCapabilities {
|
||||
return maxChannelCount;
|
||||
}
|
||||
|
||||
/** Returns whether the device can do passthrough playback for {@code format}. */
|
||||
public boolean isPassthroughPlaybackSupported(Format format) {
|
||||
return getEncodingAndChannelConfigForPassthrough(format) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoding and channel config to use when configuring an {@link AudioTrack} in
|
||||
* passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the
|
||||
* format is unsupported.
|
||||
*
|
||||
* @param format The {@link Format}.
|
||||
* @return The encoding and channel config to use, or {@code null} if passthrough of the format is
|
||||
* unsupported.
|
||||
*/
|
||||
@Nullable
|
||||
public Pair<Integer, Integer> getEncodingAndChannelConfigForPassthrough(Format format) {
|
||||
@C.Encoding
|
||||
int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs);
|
||||
// Check that this is an encoding known to work for passthrough. This avoids trying to use
|
||||
// passthrough with an encoding where the device/app reports it's capable but it is untested or
|
||||
// known to be broken (for example AAC-LC).
|
||||
if (!ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.containsKey(encoding)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (encoding == C.ENCODING_E_AC3_JOC && !supportsEncoding(C.ENCODING_E_AC3_JOC)) {
|
||||
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
|
||||
encoding = C.ENCODING_E_AC3;
|
||||
} else if (encoding == C.ENCODING_DTS_HD && !supportsEncoding(C.ENCODING_DTS_HD)) {
|
||||
// DTS receivers support DTS-HD streams (but decode only the core layer).
|
||||
encoding = C.ENCODING_DTS;
|
||||
}
|
||||
if (!supportsEncoding(encoding)) {
|
||||
return null;
|
||||
}
|
||||
int channelCount;
|
||||
if (format.channelCount == Format.NO_VALUE || encoding == C.ENCODING_E_AC3_JOC) {
|
||||
// In HLS chunkless preparation, the format channel count and sample rate may be unset. See
|
||||
// https://github.com/google/ExoPlayer/issues/10204 and b/222127949 for more details.
|
||||
// For E-AC3 JOC, the format is object based so the format channel count is arbitrary.
|
||||
int sampleRate =
|
||||
format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ;
|
||||
channelCount = getMaxSupportedChannelCountForPassthrough(encoding, sampleRate);
|
||||
} else {
|
||||
channelCount = format.channelCount;
|
||||
if (channelCount > maxChannelCount) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
int channelConfig = getChannelConfigForPassthrough(channelCount);
|
||||
if (channelConfig == AudioFormat.CHANNEL_INVALID) {
|
||||
return null;
|
||||
}
|
||||
return Pair.create(encoding, channelConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
@ -190,28 +255,93 @@ public final class AudioCapabilities {
|
||||
&& ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of channels supported for passthrough playback of audio in the given
|
||||
* encoding, or {@code 0} if the format is unsupported.
|
||||
*/
|
||||
private static int getMaxSupportedChannelCountForPassthrough(
|
||||
@C.Encoding int encoding, int sampleRate) {
|
||||
// From API 29 we can get the channel count from the platform, but before then there is no way
|
||||
// to query the platform so we assume the channel count matches the maximum channel count per
|
||||
// audio encoding spec.
|
||||
if (Util.SDK_INT >= 29) {
|
||||
return Api29.getMaxSupportedChannelCountForPassthrough(encoding, sampleRate);
|
||||
}
|
||||
return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0));
|
||||
}
|
||||
|
||||
private static int getChannelConfigForPassthrough(int channelCount) {
|
||||
if (Util.SDK_INT <= 28) {
|
||||
// In passthrough mode the channel count used to configure the audio track doesn't affect how
|
||||
// the stream is handled, except that some devices do overly-strict channel configuration
|
||||
// checks. Therefore we override the channel count so that a known-working channel
|
||||
// configuration is chosen in all cases. See [Internal: b/29116190].
|
||||
if (channelCount == 7) {
|
||||
channelCount = 8;
|
||||
} else if (channelCount == 3 || channelCount == 4 || channelCount == 5) {
|
||||
channelCount = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for Nexus Player not reporting support for mono passthrough. See
|
||||
// [Internal: b/34268671].
|
||||
if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) {
|
||||
channelCount = 2;
|
||||
}
|
||||
|
||||
return Util.getAudioTrackChannelConfig(channelCount);
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private static final class Api29 {
|
||||
private static final AudioAttributes DEFAULT_AUDIO_ATTRIBUTES =
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
|
||||
.setFlags(0)
|
||||
.build();
|
||||
|
||||
private Api29() {}
|
||||
|
||||
@DoNotInline
|
||||
public static int[] getDirectPlaybackSupportedEncodings() {
|
||||
ImmutableList.Builder<Integer> supportedEncodingsListBuilder = ImmutableList.builder();
|
||||
for (int encoding : ALL_SURROUND_ENCODINGS) {
|
||||
for (int encoding : ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.keySet()) {
|
||||
if (AudioTrack.isDirectPlaybackSupported(
|
||||
new AudioFormat.Builder()
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
|
||||
.setEncoding(encoding)
|
||||
.setSampleRate(DEFAULT_SAMPLE_RATE_HZ)
|
||||
.build(),
|
||||
new android.media.AudioAttributes.Builder()
|
||||
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
|
||||
.setFlags(0)
|
||||
.build())) {
|
||||
DEFAULT_AUDIO_ATTRIBUTES)) {
|
||||
supportedEncodingsListBuilder.add(encoding);
|
||||
}
|
||||
}
|
||||
supportedEncodingsListBuilder.add(AudioFormat.ENCODING_PCM_16BIT);
|
||||
return Ints.toArray(supportedEncodingsListBuilder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of channels supported for passthrough playback of audio in the
|
||||
* given format, or {@code 0} if the format is unsupported.
|
||||
*/
|
||||
@DoNotInline
|
||||
public static int getMaxSupportedChannelCountForPassthrough(
|
||||
@C.Encoding int encoding, int sampleRate) {
|
||||
// TODO(internal b/234351617): Query supported channel masks directly once it's supported,
|
||||
// see also b/25994457.
|
||||
for (int channelCount = DEFAULT_MAX_CHANNEL_COUNT; channelCount > 0; channelCount--) {
|
||||
AudioFormat audioFormat =
|
||||
new AudioFormat.Builder()
|
||||
.setEncoding(encoding)
|
||||
.setSampleRate(sampleRate)
|
||||
.setChannelMask(Util.getAudioTrackChannelConfig(channelCount))
|
||||
.build();
|
||||
if (AudioTrack.isDirectPlaybackSupported(audioFormat, DEFAULT_AUDIO_ATTRIBUTES)) {
|
||||
return channelCount;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,13 +108,8 @@ public interface AudioSink {
|
||||
/** Called when the offload buffer has been partially emptied. */
|
||||
default void onOffloadBufferEmptying() {}
|
||||
|
||||
/**
|
||||
* Called when the offload buffer has been filled completely.
|
||||
*
|
||||
* @param bufferEmptyingDeadlineMs Maximum time in milliseconds until {@link
|
||||
* #onOffloadBufferEmptying()} will be called.
|
||||
*/
|
||||
default void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {}
|
||||
/** Called when the offload buffer has been filled completely. */
|
||||
default void onOffloadBufferFull() {}
|
||||
|
||||
/**
|
||||
* Called when {@link AudioSink} has encountered an error.
|
||||
|
@ -386,11 +386,6 @@ import java.lang.reflect.Method;
|
||||
return bufferSize - bytesPending;
|
||||
}
|
||||
|
||||
/** Returns the duration of audio that is buffered but unplayed. */
|
||||
public long getPendingBufferDurationMs(long writtenFrames) {
|
||||
return Util.usToMs(framesToDurationUs(writtenFrames - getPlaybackHeadPosition()));
|
||||
}
|
||||
|
||||
/** Returns whether the track is in an invalid state and must be recreated. */
|
||||
public boolean isStalled(long writtenFrames) {
|
||||
return forceResetWorkaroundTimeMs != C.TIME_UNSET
|
||||
|
@ -684,7 +684,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) {
|
||||
return SINK_FORMAT_SUPPORTED_DIRECTLY;
|
||||
}
|
||||
if (isPassthroughPlaybackSupported(format, audioCapabilities)) {
|
||||
if (audioCapabilities.isPassthroughPlaybackSupported(format)) {
|
||||
return SINK_FORMAT_SUPPORTED_DIRECTLY;
|
||||
}
|
||||
return SINK_FORMAT_UNSUPPORTED;
|
||||
@ -767,7 +767,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
outputMode = OUTPUT_MODE_PASSTHROUGH;
|
||||
@Nullable
|
||||
Pair<Integer, Integer> encodingAndChannelConfig =
|
||||
getEncodingAndChannelConfigForPassthrough(inputFormat, audioCapabilities);
|
||||
audioCapabilities.getEncodingAndChannelConfigForPassthrough(inputFormat);
|
||||
if (encodingAndChannelConfig == null) {
|
||||
throw new ConfigurationException(
|
||||
"Unable to configure passthrough for: " + inputFormat, inputFormat);
|
||||
@ -914,7 +914,11 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
pendingConfiguration = null;
|
||||
if (isOffloadedPlayback(audioTrack)
|
||||
&& offloadMode != OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user