Merge remote-tracking branch 'origin/dev-v2' into vorbis-comments

Line up this branch with the dev branch instead of the release branch.
This commit is contained in:
OxygenCobalt 2022-01-15 19:05:49 -07:00
commit 49967483f6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
301 changed files with 13931 additions and 6276 deletions

View file

@ -1,5 +1,107 @@
# Release notes
### dev-v2 (not yet released)
* Core library:
* Support preferred video role flags in track selection
((#9402)[https://github.com/google/ExoPlayer/issues/9402]).
* Prefer audio content preferences (for example, "default" audio track or
track matching system Locale language) over technical track selection
constraints (for example, preferred MIME type, or maximum channel
count).
* Prohibit duplicate `TrackGroup`s in a `TrackGroupArray`. `TrackGroup`s
can always be made distinguishable by setting an `id` in the
`TrackGroup` constructor. This fixes a crash when resuming playback
after backgrounding the app with an active track override
((#9718)[https://github.com/google/ExoPlayer/issues/9718]).
* Sleep and retry when creating a `MediaCodec` instance fails. This works
around an issue that occurs on some devices when switching a surface
from a secure codec to another codec
(#8696)[https://github.com/google/ExoPlayer/issues/8696].
* Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data
from `MediaCodec`.
([#9766](https://github.com/google/ExoPlayer/issues/9766)).
* Amend logic in `AdaptiveTrackSelection` to allow a quality increase
under sufficient network bandwidth even if playback is very close to the
live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]).
* Fix Maven dependency resolution
((#8353)[https://github.com/google/ExoPlayer/issues/8353]).
* Android 12 compatibility:
* Upgrade the Cast extension to depend on
`com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier
versions of `play-services-cast-framework` are not compatible with apps
targeting Android 12, and will crash with an `IllegalArgumentException`
when creating `PendingIntent`s
([#9528](https://github.com/google/ExoPlayer/issues/9528)).
* Audio:
* Add a `Builder` to `DefaultAudioSink` and deprecate the existing
constructors.
* Change `AudioCapabilities` APIs to require passing explicitly
`AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES` instead of `null`.
* Extractors:
* Fix inconsistency with spec in H.265 SPS nal units parsing
((#9719)[https://github.com/google/ExoPlayer/issues/9719]).
* Text:
* Add a `MediaItem.SubtitleConfiguration#id` field which is propagated to
the `Format#id` field of the subtitle track created from the
configuration
((#9673)[https://github.com/google/ExoPlayer/issues/9673]).
* Rename `DecoderCounters#inputBufferCount` to `queuedInputBufferCount`.
* DRM:
* Remove `playbackLooper` from `DrmSessionManager.(pre)acquireSession`.
When a `DrmSessionManager` is used by an app in a custom `MediaSource`,
the `playbackLooper` needs to be passed to `DrmSessionManager.setPlayer`
instead.
* IMA:
* Add a method to `AdPlaybackState` to allow resetting an ad group so that
it can be played again
([#9615](https://github.com/google/ExoPlayer/issues/9615)).
* DASH:
* Support the `forced-subtitle` track role
([#9727](https://github.com/google/ExoPlayer/issues/9727)).
* Stop interpreting the `main` track role as `C.SELECTION_FLAG_DEFAULT`.
* HLS:
* Use chunkless preparation by default to improve start up time. If your
renditions contain muxed closed-caption tracks that are *not* declared
in the master playlist, you should add them to the master playlist to be
available for playback, or turn off chunkless preparation with
`HlsMediaSource.Factory.setAllowChunklessPreparation(false)`.
* Support key-frame accurate seeking in HLS
([#2882](https://github.com/google/ExoPlayer/issues/2882)).
* Correctly populate `Format.label` for audio only HLS streams
([#9608](https://github.com/google/ExoPlayer/issues/9608)).
* Timestamp adjuster initialization occurs after opening the `DataSource`
([#9777](https://github.com/google/ExoPlayer/pull/9777)).
* UI:
* Fix the color of the numbers in `StyledPlayerView` rewind and
fastforward buttons when using certain themes
([#9765](https://github.com/google/ExoPlayer/issues/9765)).
* Transformer:
* Increase required min API version to 21.
* `TransformationException` is now used to describe errors that occur
during a transformation.
* Add `TransformationRequest` for specifying the transformation options.
* MediaSession extension:
* Remove deprecated call to `onStop(/* reset= */ true)` and provide an
opt-out flag for apps that don't want to clear the playlist on stop.
* RTSP:
* Provide a client API to override the `SocketFactory` used for any server
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
* Prefers DIGEST authentication method over BASIC if both are present.
([#9800](https://github.com/google/ExoPlayer/issues/9800)).
* Cast extension
* Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged`
correctly.
* Remove deprecated symbols:
* Remove `MediaSourceFactory#setDrmSessionManager`,
`MediaSourceFactory#setDrmHttpDataSourceFactory`, and
`MediaSourceFactory#setDrmUserAgent`. Use
`MediaSourceFactory#setDrmSessionManagerProvider` instead.
* Remove `MediaSourceFactory#setStreamKeys`. Use
`MediaItem.Builder#setStreamKeys` instead.
* Remove `MediaSourceFactory#createMediaSource(Uri)`. Use
`MediaSourceFactory#createMediaSource(MediaItem)` instead.
### 2.16.1 (2021-11-18)
* Core Library:
@ -522,8 +624,8 @@
* The most used methods of `Player`'s audio, video, text and metadata
components have been added directly to `Player`.
* Add `Player.getAvailableCommands`, `Player.isCommandAvailable` and
`Listener.onAvailableCommandsChanged` to query which commands
that can be executed on the player.
`Listener.onAvailableCommandsChanged` to query which commands that can
be executed on the player.
* Add a `Player.Listener` interface to receive all player events.
Component listeners and `EventListener` have been deprecated.
* Add `Player.getMediaMetadata`, which returns a combined and structured
@ -532,8 +634,8 @@
* `Player.setPlaybackParameters` no longer accepts null, use
`PlaybackParameters.DEFAULT` instead.
* Report information about the old and the new playback positions to
`Listener.onPositionDiscontinuity`. Add `DISCONTINUITY_REASON_SKIP`
and `DISCONTINUITY_REASON_REMOVE` as discontinuity reasons, and rename
`Listener.onPositionDiscontinuity`. Add `DISCONTINUITY_REASON_SKIP` and
`DISCONTINUITY_REASON_REMOVE` as discontinuity reasons, and rename
`DISCONTINUITY_REASON_PERIOD_TRANSITION` to
`DISCONTINUITY_REASON_AUTO_TRANSITION`. Remove
`DISCONTINUITY_REASON_AD_INSERTION`, for which
@ -588,8 +690,8 @@
dispatched for each track in each period.
* Include the session state in DRM session-acquired listener methods.
* UI:
* Add `PlayerNotificationManager.Builder`, with the ability to
specify which group the notification should belong to.
* Add `PlayerNotificationManager.Builder`, with the ability to specify
which group the notification should belong to.
* Remove `setUseSensorRotation` from `PlayerView` and `StyledPlayerView`.
Instead, cast the view returned by `getVideoSurfaceView` to
`SphericalGLSurfaceView`, and then call `setUseSensorRotation` on the
@ -661,7 +763,8 @@
### 2.13.3 (2021-04-14)
* Published via the Google Maven repository (i.e., google()) rather than JCenter.
* Published via the Google Maven repository (i.e., google()) rather than
JCenter.
* Core:
* Reset playback speed when live playback speed control becomes unused
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
@ -816,8 +919,8 @@
* Remove `Player.setVideoDecoderOutputBufferRenderer` from Player API. Use
`setVideoSurfaceView` and `clearVideoSurfaceView` instead.
* Default `SingleSampleMediaSource.treatLoadErrorsAsEndOfStream` to `true`
so that errors loading external subtitle files do not cause playback
to fail ([#8430](https://github.com/google/ExoPlayer/issues/8430)). A
so that errors loading external subtitle files do not cause playback to
fail ([#8430](https://github.com/google/ExoPlayer/issues/8430)). A
warning will be logged by `SingleSampleMediaPeriod` whenever a load
error is treated as though the end of the stream has been reached.
* Time out on release to prevent ANRs if an underlying platform call is
@ -898,9 +1001,8 @@
([#7847](https://github.com/google/ExoPlayer/issues/7847)).
* Drop key and provision responses if `DefaultDrmSession` is released
while waiting for the response. This prevents harmless log messages of
the form:
`IllegalStateException: sending message to a Handler on a dead thread`
([#8328](https://github.com/google/ExoPlayer/issues/8328)).
the form: `IllegalStateException: sending message to a Handler on a dead
thread` ([#8328](https://github.com/google/ExoPlayer/issues/8328)).
* Allow apps to fully customize DRM behaviour for each `MediaItem` by
passing a `DrmSessionManagerProvider` to `MediaSourceFactory`
([#8466](https://github.com/google/ExoPlayer/issues/8466)).
@ -915,8 +1017,8 @@
existing decoder instance for the new format, and if not then the
reasons why.
* Video:
* Fall back to AVC/HEVC decoders for Dolby Vision streams with level 10
to 13 ([#8530](https://github.com/google/ExoPlayer/issues/8530)).
* Fall back to AVC/HEVC decoders for Dolby Vision streams with level 10 to
13 ([#8530](https://github.com/google/ExoPlayer/issues/8530)).
* Fix VP9 format capability checks on API level 23 and earlier. The
platform does not correctly report the VP9 level supported by the
decoder in this case, so we estimate it based on the decoder's maximum
@ -998,8 +1100,8 @@
* `ExtractorsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
`ExtractorsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
instead.
* `FixedTrackSelection.Factory`. If you need to disable adaptive
selection in `DefaultTrackSelector`, enable the
* `FixedTrackSelection.Factory`. If you need to disable adaptive selection
in `DefaultTrackSelector`, enable the
`DefaultTrackSelector.Parameters.forceHighestSupportedBitrate` flag.
* `HlsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
`HlsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
@ -1012,8 +1114,8 @@
`MappedTrackInfo.getUnmappedTrackGroups()` instead.
* `MappedTrackInfo.length`. Use `MappedTrackInfo.getRendererCount()`
instead.
* `Player.DefaultEventListener.onTimelineChanged(Timeline, Object)`.
Use `Player.EventListener.onTimelineChanged(Timeline, int)` instead.
* `Player.DefaultEventListener.onTimelineChanged(Timeline, Object)`. Use
`Player.EventListener.onTimelineChanged(Timeline, int)` instead.
* `Player.setAudioAttributes(AudioAttributes)`. Use
`Player.AudioComponent.setAudioAttributes(AudioAttributes, boolean)`
instead.
@ -1029,8 +1131,8 @@
`SimpleExoPlayer.removeVideoListener(VideoListener)` instead.
* `SimpleExoPlayer.getAudioStreamType()`. Use
`SimpleExoPlayer.getAudioAttributes()` instead.
* `SimpleExoPlayer.setAudioDebugListener(AudioRendererEventListener)`.
Use `SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
* `SimpleExoPlayer.setAudioDebugListener(AudioRendererEventListener)`. Use
`SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
* `SimpleExoPlayer.setAudioStreamType(int)`. Use
`SimpleExoPlayer.setAudioAttributes(AudioAttributes)` instead.
* `SimpleExoPlayer.setMetadataOutput(MetadataOutput)`. Use
@ -1041,12 +1143,11 @@
* `SimpleExoPlayer.setPlaybackParams(PlaybackParams)`. Use
`SimpleExoPlayer.setPlaybackParameters(PlaybackParameters)` instead.
* `SimpleExoPlayer.setTextOutput(TextOutput)`. Use
`SimpleExoPlayer.addTextOutput(TextOutput)` instead. If your
application is calling `SimpleExoPlayer.setTextOutput(null)`, make sure
to replace this call with a call to
`SimpleExoPlayer.removeTextOutput(TextOutput)`.
* `SimpleExoPlayer.setVideoDebugListener(VideoRendererEventListener)`.
Use `SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
`SimpleExoPlayer.addTextOutput(TextOutput)` instead. If your application
is calling `SimpleExoPlayer.setTextOutput(null)`, make sure to replace
this call with a call to `SimpleExoPlayer.removeTextOutput(TextOutput)`.
* `SimpleExoPlayer.setVideoDebugListener(VideoRendererEventListener)`. Use
`SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
* `SimpleExoPlayer.setVideoListener(VideoListener)`. Use
`SimpleExoPlayer.addVideoListener(VideoListener)` instead. If your
application is calling `SimpleExoPlayer.setVideoListener(null)`, make
@ -1070,7 +1171,7 @@
`SsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
instead.
### 2.12.3 (2021-01-13) ###
### 2.12.3 (2021-01-13)
* Core library:
* Fix `MediaCodecRenderer` issue where empty streams would fail to play in
@ -1107,7 +1208,7 @@
fix a deadlock while creating PlaybackStateCompat internally.
([#8011](https://github.com/google/ExoPlayer/issues/8011)).
### 2.12.2 (2020-12-01) ###
### 2.12.2 (2020-12-01)
* Core library:
* Suppress exceptions from registering and unregistering the stream volume
@ -1168,7 +1269,7 @@
* Allow to remove all playlist items that makes the player reset
([#8047](https://github.com/google/ExoPlayer/issues/8047)).
### 2.12.1 (2020-10-23) ###
### 2.12.1 (2020-10-23)
* Core library:
* Fix issue where `Player.setMediaItems` would ignore its `resetPosition`
@ -1207,7 +1308,7 @@
([#8058](https://github.com/google/ExoPlayer/issues/8058)).
* Extractors:
* MP4:
* Add support for `_mp2` boxes
* Add support for `_mp2` boxes
([#7967](https://github.com/google/ExoPlayer/issues/7967)).
* Fix playback of files containing `pcm_alaw` or `pcm_mulaw` audio
tracks, by enabling sample rechunking for such tracks.
@ -1243,11 +1344,11 @@
([#7961](https://github.com/google/ExoPlayer/issues/7961)).
* Fix incorrect truncation of large cue point positions
([#8067](https://github.com/google/ExoPlayer/issues/8067)).
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for
companion ads rendering when targeting API 29
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for companion
ads rendering when targeting API 29
([#6432](https://github.com/google/ExoPlayer/issues/6432)).
### 2.12.0 (2020-09-11) ###
### 2.12.0 (2020-09-11)
To learn more about what's new in 2.12, read the corresponding
[blog post](https://medium.com/google-exoplayer/exoplayer-2-12-whats-new-e43ef8ff72e7).
@ -1278,8 +1379,7 @@ To learn more about what's new in 2.12, read the corresponding
* Remove `PlaybackParameters.skipSilence`, and replace it with
`AudioComponent.setSkipSilenceEnabled`. This method is also
available on `SimpleExoPlayer`. An
`AudioListener.onSkipSilenceEnabledChanged` callback is also
added.
`AudioListener.onSkipSilenceEnabledChanged` callback is also added.
* Add `TextComponent.getCurrentCues` to get the current cues. This
method is also available on `SimpleExoPlayer`. The current cues are
no longer automatically forwarded to a `TextOutput` when it's added
@ -1607,20 +1707,19 @@ To learn more about what's new in 2.12, read the corresponding
* Add support for downloading DRM-protected content using offline Widevine
licenses.
### 2.11.8 (2020-08-25) ###
### 2.11.8 (2020-08-25)
* Fix distorted playback of floating point audio when samples exceed the
`[-1, 1]` nominal range.
* Fix distorted playback of floating point audio when samples exceed the `[-1,
1]` nominal range.
* MP4:
* Add support for `piff` and `isml` brands
([#7584](https://github.com/google/ExoPlayer/issues/7584)).
* Fix playback of very short MP4 files.
* FMP4:
* Fix `saiz` and `senc` sample count checks, resolving a "length
mismatch" `ParserException` when playing certain protected FMP4 streams
* Fix `saiz` and `senc` sample count checks, resolving a "length mismatch"
`ParserException` when playing certain protected FMP4 streams
([#7592](https://github.com/google/ExoPlayer/issues/7592)).
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd`
boxes.
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd` boxes.
* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than
failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)).
* Better infer the content type of `.ism` and `.isml` streaming URLs.
@ -1633,12 +1732,12 @@ To learn more about what's new in 2.12, read the corresponding
* Demo app: Fix playback of ClearKey protected content on API level 26 and
earlier ([#7735](https://github.com/google/ExoPlayer/issues/7735)).
### 2.11.7 (2020-06-29) ###
### 2.11.7 (2020-06-29)
* IMA extension: Fix the way postroll "content complete" notifications are
handled to avoid repeatedly refreshing the timeline after playback ends.
### 2.11.6 (2020-06-19) ###
### 2.11.6 (2020-06-19)
* UI: Prevent `PlayerView` from temporarily hiding the video surface when
seeking to an unprepared period within the current window. For example when
@ -1653,14 +1752,14 @@ To learn more about what's new in 2.12, read the corresponding
([#7508](https://github.com/google/ExoPlayer/issues/7508)).
* Fix a bug where the number of ads in an ad group couldn't change
([#7477](https://github.com/google/ExoPlayer/issues/7477)).
* Work around unexpected `pauseAd`/`stopAd` for ads that have preloaded
on seeking to another position
* Work around unexpected `pauseAd`/`stopAd` for ads that have preloaded on
seeking to another position
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
* Fix incorrect rounding of ad cue points.
* Fix handling of postrolls preloading
([#7518](https://github.com/google/ExoPlayer/issues/7518)).
### 2.11.5 (2020-06-05) ###
### 2.11.5 (2020-06-05)
* Improve the smoothness of video playback immediately after starting, seeking
or resuming a playback
@ -1668,8 +1767,8 @@ To learn more about what's new in 2.12, read the corresponding
* Add `SilenceMediaSource.Factory` to support tags.
* Enable the configuration of `SilenceSkippingAudioProcessor`
([#6705](https://github.com/google/ExoPlayer/issues/6705)).
* Fix bug where `PlayerMessages` throw an exception after `MediaSources`
are removed from the playlist
* Fix bug where `PlayerMessages` throw an exception after `MediaSources` are
removed from the playlist
([#7278](https://github.com/google/ExoPlayer/issues/7278)).
* Fix "Not allowed to start service" `IllegalStateException` in
`DownloadService`
@ -1701,13 +1800,11 @@ To learn more about what's new in 2.12, read the corresponding
([#7303](https://github.com/google/ExoPlayer/issues/7303)).
* Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`.
* Text:
* Use anti-aliasing and bitmap filtering when displaying bitmap
subtitles.
* Use anti-aliasing and bitmap filtering when displaying bitmap subtitles.
* Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct
color.
* IMA extension:
* Upgrade to IMA SDK version 3.19.0, and migrate to new
preloading APIs
* Upgrade to IMA SDK version 3.19.0, and migrate to new preloading APIs
([#6429](https://github.com/google/ExoPlayer/issues/6429)). This fixes
several issues involving preloading and handling of ad loading error
cases: ([#4140](https://github.com/google/ExoPlayer/issues/4140),

View file

@ -25,7 +25,7 @@ project.ext {
junitVersion = '4.13.2'
// Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
guavaVersion = '27.1-android'
guavaVersion = '31.0.1-android'
mockitoVersion = '3.12.4'
robolectricVersion = '4.6.1'
// Keep this in sync with Google's internal Checker Framework version.

View file

@ -28,7 +28,6 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.gms.cast.framework.CastContext;
@ -67,7 +66,7 @@ import java.util.ArrayList;
*
* @param context A {@link Context}.
* @param listener A {@link Listener} for queue position changes.
* @param playerView The {@link PlayerView} for playback.
* @param playerView The {@link StyledPlayerView} for playback.
* @param castContext The {@link CastContext}.
*/
public PlayerManager(

View file

@ -26,7 +26,6 @@ import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
@ -52,8 +51,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Canvas overlayCanvas;
private GlUtil.@MonotonicNonNull Program program;
@Nullable private GlUtil.Attribute[] attributes;
@Nullable private GlUtil.Uniform[] uniforms;
private float bitmapScaleX;
private float bitmapScaleY;
@ -88,31 +85,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} catch (IOException e) {
throw new IllegalStateException(e);
}
program.use();
GlUtil.Attribute[] attributes = program.getAttributes();
for (GlUtil.Attribute attribute : attributes) {
if (attribute.name.equals("a_position")) {
attribute.setBuffer(
new float[] {
-1, -1, 0, 1,
1, -1, 0, 1,
-1, 1, 0, 1,
1, 1, 0, 1
},
4);
} else if (attribute.name.equals("a_texcoord")) {
attribute.setBuffer(
new float[] {
0, 0, 0, 1,
1, 0, 0, 1,
0, 1, 0, 1,
1, 1, 0, 1
},
4);
}
}
this.attributes = attributes;
this.uniforms = program.getUniforms();
program.setBufferAttribute(
"a_position",
new float[] {
-1, -1, 0, 1,
1, -1, 0, 1,
-1, 1, 0, 1,
1, 1, 0, 1
},
4);
program.setBufferAttribute(
"a_texcoord",
new float[] {
0, 0, 0, 1,
1, 0, 0, 1,
0, 1, 0, 1,
1, 1, 0, 1
},
4);
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);
@ -141,36 +131,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
GlUtil.checkGlError();
// Run the shader program.
GlUtil.Uniform[] uniforms = checkNotNull(this.uniforms);
GlUtil.Attribute[] attributes = checkNotNull(this.attributes);
for (GlUtil.Uniform uniform : uniforms) {
switch (uniform.name) {
case "tex_sampler_0":
uniform.setSamplerTexId(frameTexture, /* unit= */ 0);
break;
case "tex_sampler_1":
uniform.setSamplerTexId(textures[0], /* unit= */ 1);
break;
case "scaleX":
uniform.setFloat(bitmapScaleX);
break;
case "scaleY":
uniform.setFloat(bitmapScaleY);
break;
case "tex_transform":
uniform.setFloats(transformMatrix);
break;
default: // fall out
}
}
for (GlUtil.Attribute copyExternalAttribute : attributes) {
copyExternalAttribute.bind();
}
for (GlUtil.Uniform copyExternalUniform : uniforms) {
copyExternalUniform.bind();
}
GlUtil.Program program = checkNotNull(this.program);
program.setSamplerTexIdUniform("tex_sampler_0", frameTexture, /* unit= */ 0);
program.setSamplerTexIdUniform("tex_sampler_1", textures[0], /* unit= */ 1);
program.setFloatUniform("scaleX", bitmapScaleX);
program.setFloatUniform("scaleY", bitmapScaleY);
program.setFloatsUniform("tex_transform", transformMatrix);
program.bindAttributesAndUniforms();
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
}
@Override
public void release() {
if (program != null) {
program.delete();
}
}
}

View file

@ -34,7 +34,7 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
@ -61,7 +61,7 @@ public final class MainActivity extends Activity {
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
@Nullable private PlayerView playerView;
@Nullable private StyledPlayerView playerView;
@Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView;
@Nullable private ExoPlayer player;
@ -161,12 +161,12 @@ public final class MainActivity extends Activity {
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else {
throw new IllegalStateException();

View file

@ -64,6 +64,9 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
* @param transformMatrix The 4 * 4 transform matrix to be applied to the texture.
*/
void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix);
/** Releases any resources associated with this {@link VideoProcessor}. */
void release();
}
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;

View file

@ -20,7 +20,7 @@
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -237,11 +237,11 @@
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
},
{
"name": "Apple master playlist advanced (TS)",
"name": "Apple multivariant playlist advanced (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
},
{
"name": "Apple master playlist advanced (FMP4)",
"name": "Apple multivariant playlist advanced (FMP4)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
},
{

View file

@ -44,7 +44,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitia
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
@ -234,7 +234,7 @@ public class PlayerActivity extends AppCompatActivity
}
}
// PlayerControlView.VisibilityListener implementation
// StyledPlayerControlView.VisibilityListener implementation
@Override
public void onVisibilityChange(int visibility) {
@ -261,7 +261,7 @@ public class PlayerActivity extends AppCompatActivity
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
MediaSourceFactory mediaSourceFactory =
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory)
.setAdsLoaderProvider(this::getAdsLoader)
.setAdViewProvider(playerView);

View file

@ -206,12 +206,12 @@ public final class MainActivity extends Activity {
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else {
throw new IllegalStateException();

View file

@ -46,10 +46,10 @@ MediaItem mediaItem =
To enable player support for media items that specify ad tags, it's necessary to
build and inject a `DefaultMediaSourceFactory` configured with an
`AdsLoaderProvider` and an `AdViewProvider` when creating the player:
`AdsLoader.Provider` and an `AdViewProvider` when creating the player:
~~~
MediaSourceFactory mediaSourceFactory =
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(context)
.setAdsLoaderProvider(adsLoaderProvider)
.setAdViewProvider(playerView);
@ -61,7 +61,7 @@ ExoPlayer player = new ExoPlayer.Builder(context)
Internally, `DefaultMediaSourceFactory` will wrap the content media source in an
`AdsMediaSource`. The `AdsMediaSource` will obtain an `AdsLoader` from the
`AdsLoaderProvider` and use it to insert ads as defined by the media item's ad
`AdsLoader.Provider` and use it to insert ads as defined by the media item's ad
tag.
ExoPlayer's `StyledPlayerView` and `PlayerView` UI components both implement

View file

@ -14,10 +14,10 @@ Components common to all `ExoPlayer` implementations are:
* `MediaSource` instances that define media to be played, load the media, and
from which the loaded media can be read. `MediaSource` instances are created
from `MediaItem`s by a `MediaSourceFactory` inside the player. They can also
from `MediaItem`s by a `MediaSource.Factory` inside the player. They can also
be passed directly to the player using the [media source based playlist API].
* A `MediaSourceFactory` that converts `MediaItem`s to `MediaSource`s. The
`MediaSourceFactory` is injected when the player is created.
* A `MediaSource.Factory` that converts `MediaItem`s to `MediaSource`s. The
`MediaSource.Factory` is injected when the player is created.
* `Renderer`s that render individual components of the media. `Renderer`s are
injected when the player is created.
* A `TrackSelector` that selects tracks provided by the `MediaSource` to be
@ -245,9 +245,9 @@ required. Some use cases for custom implementations are:
appropriate if you wish to obtain media samples to feed to renderers in a
custom way, or if you wish to implement custom `MediaSource` compositing
behavior.
* `MediaSourceFactory` &ndash; Implementing a custom `MediaSourceFactory` allows
an application to customize the way in which `MediaSource`s are created from
`MediaItem`s.
* `MediaSource.Factory` &ndash; Implementing a custom `MediaSource.Factory`
allows an application to customize the way in which `MediaSource`s are created
from `MediaItem`s.
* `DataSource` &ndash; ExoPlayers upstream package already contains a number of
`DataSource` implementations for different use cases. You may want to
implement you own `DataSource` class to load data in another way, such as over

View file

@ -394,9 +394,7 @@ When building the `MediaItem`, `MediaItem.playbackProperties.streamKeys` must be
set to match those in the `DownloadRequest` so that the player only tries to
play the subset of tracks that have been downloaded. Using
`Download.request.toMediaItem` and `DownloadRequest.toMediaItem` to build the
`MediaItem` will take care of this for you. If building a `MediaSource` to pass
directly to the player, it is similarly important to configure the stream keys
by calling `MediaSourceFactory.setStreamKeys`.
`MediaItem` will take care of this for you.
If you see data being requested from the network when trying to play downloaded
adaptive content, the most likely cause is that the player is trying to adapt to

View file

@ -85,7 +85,7 @@ building the media item.
If an app wants to customise the `DrmSessionManager` used for playback, they can
implement a `DrmSessionManagerProvider` and pass this to the
`MediaSourceFactory` which is [used when building the player]. The provider can
`MediaSource.Factory` which is [used when building the player]. The provider can
choose whether to instantiate a new manager instance each time or not. To always
use the same instance:
@ -93,7 +93,7 @@ use the same instance:
DrmSessionManager customDrmSessionManager =
new CustomDrmSessionManager(/* ... */);
// Pass a drm session manager provider to the media source factory.
MediaSourceFactory mediaSourceFactory =
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory)
.setDrmSessionManagerProvider(mediaItem -> customDrmSessionManager);
~~~

View file

@ -90,7 +90,7 @@ For more information, see the
A file that defines the structure and location of media in
[adaptive streaming](#adaptive-streaming) protocols. Examples include
[DASH](#dash) [MPD](#mpd) files, [HLS](#hls) master playlist files and
[DASH](#dash) [MPD](#mpd) files, [HLS](#hls) multivariant playlist files and
[Smooth Streaming](#smooth-streaming) manifest files. Not to be confused with an
AndroidManifest XML file.

View file

@ -30,8 +30,8 @@ If your URI doesn't end with `.m3u8`, you can pass `MimeTypes.APPLICATION_M3U8`
to `setMimeType` of `MediaItem.Builder` to explicitly indicate the type of the
content.
The URI of the media item may point to either a media playlist or a master
playlist. If the URI points to a master playlist that declares multiple
The URI of the media item may point to either a media playlist or a multivariant
playlist. If the URI points to a multivariant playlist that declares multiple
`#EXT-X-STREAM-INF` tags then ExoPlayer will automatically adapt between
variants, taking into account both available bandwidth and device capabilities.
@ -86,23 +86,29 @@ player.addListener(
ExoPlayer provides multiple ways for you to tailor playback experience to your
app's needs. See the [Customization page][] for examples.
### Enabling faster start-up times ###
### Disabling chunkless preparation ###
You can improve HLS start up times noticeably by enabling chunkless preparation.
When you enable chunkless preparation and `#EXT-X-STREAM-INF` tags contain the
`CODECS` attribute, ExoPlayer will avoid downloading media segments as part of
preparation. The following snippet shows how to enable chunkless preparation.
By default, ExoPlayer will use chunkless preparation. This means that ExoPlayer
will only use the information in the multivariant playlist to prepare the
stream, which works if the `#EXT-X-STREAM-INF` tags contain the `CODECS`
attribute.
You may need to disable this feature if your media segments contain muxed
closed-caption tracks that are not declared in the multivariant playlist with a
`#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS` tag. Otherwise, these closed-caption tracks
won't be detected and played. You can disable chunkless preparation in the
`HlsMediaSource.Factory` as shown in the following snippet. Note that this
will increase start up time as ExoPlayer needs to download a media segment to
discover these additional tracks and it is preferable to declare the
closed-caption tracks in the multivariant playlist instead.
~~~
HlsMediaSource hlsMediaSource =
new HlsMediaSource.Factory(dataSourceFactory)
.setAllowChunklessPreparation(true)
.setAllowChunklessPreparation(false)
.createMediaSource(MediaItem.fromUri(hlsUri));
~~~
{: .language-java}
You can find more details in our [Medium post about chunkless preparation][].
## Creating high quality HLS content ##
In order to get the most out of ExoPlayer, there are certain guidelines you can
@ -114,7 +120,7 @@ ExoPlayer][] for a full explanation. The main points are:
segments.
* Use the `#EXT-X-INDEPENDENT-SEGMENTS` tag.
* Prefer demuxed streams, as opposed to files that include both video and audio.
* Include all information you can in the Master Playlist.
* Include all information you can in the Multivariant Playlist.
The following guidelines apply specifically for live streams:
@ -127,5 +133,4 @@ The following guidelines apply specifically for live streams:
[PlayerView]: {{ site.exo_sdk }}/ui/PlayerView.html
[UI components]: {{ site.baseurl }}/ui-components.html
[Customization page]: {{ site.baseurl }}/customization.html
[Medium post about chunkless preparation]: https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6
[Medium post about HLS playback in ExoPlayer]: https://medium.com/google-exoplayer/hls-playback-in-exoplayer-a33959a47be7

View file

@ -71,7 +71,7 @@ Errors that cause playback to fail can be received by implementing
`onPlayerError(PlaybackException error)` in a registered
`Player.Listener`. When a failure occurs, this method will be called
immediately before the playback state transitions to `Player.STATE_IDLE`.
Failed or stopped playbacks can be retried by calling `ExoPlayer.retry`.
Failed or stopped playbacks can be retried by calling `ExoPlayer.prepare`.
Note that some [`Player`][] implementations pass instances of subclasses of
`PlaybackException` to provide additional information about the failure. For

View file

@ -4,7 +4,7 @@ title: Media items
The [playlist API][] is based on `MediaItem`s, which can be conveniently built
using `MediaItem.Builder`. Inside the player, media items are converted into
playable `MediaSource`s by a `MediaSourceFactory`. Without
playable `MediaSource`s by a `MediaSource.Factory`. Without
[custom configuration]({{ site.baseurl }}/media-sources.html#customizing-media-source-creation),
this conversion is carried out by a `DefaultMediaSourceFactory`, which is
capable of building complex media sources corresponding to the properties of the

View file

@ -6,7 +6,7 @@ redirect_from:
In ExoPlayer every piece of media is represented by a `MediaItem`. However
internally, the player needs `MediaSource` instances to play the content. The
player creates these from media items using a `MediaSourceFactory`.
player creates these from media items using a `MediaSource.Factory`.
By default the player uses a `DefaultMediaSourceFactory`, which can create
instances of the following content `MediaSource` implementations:
@ -27,13 +27,13 @@ customization.
## Customizing media source creation ##
When building the player, a `MediaSourceFactory` can be injected. For example, if
an app wants to insert ads and use a `CacheDataSource.Factory` to support
When building the player, a `MediaSource.Factory` can be injected. For example,
if an app wants to insert ads and use a `CacheDataSource.Factory` to support
caching, an instance of `DefaultMediaSourceFactory` can be configured to match
these requirements and injected during player construction:
~~~
MediaSourceFactory mediaSourceFactory =
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(cacheDataSourceFactory)
.setAdsLoaderProvider(adsLoaderProvider)
.setAdViewProvider(playerView);
@ -47,7 +47,7 @@ The
[`DefaultMediaSourceFactory` JavaDoc]({{ site.baseurl }}/doc/reference/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.html)
describes the available options in more detail.
It's also possible to inject a custom `MediaSourceFactory` implementation, for
It's also possible to inject a custom `MediaSource.Factory` implementation, for
example to support creation of a custom media source type. The factory's
`createMediaSource(MediaItem)` will be called to create a media source for each
media item that is
@ -57,7 +57,7 @@ media item that is
The [`ExoPlayer`] interface defines additional playlist methods that accept
media sources rather than media items. This makes it possible to bypass the
player's internal `MediaSourceFactory` and pass media source instances to the
player's internal `MediaSource.Factory` and pass media source instances to the
player directly:
~~~

View file

@ -6,7 +6,7 @@ title: Retrieving metadata
The metadata of the media can be retrieved during playback in multiple ways. The
most straightforward is to listen for the
`Player.EventListener#onMediaMetadataChanged` event; this will provide a
`Player.Listener#onMediaMetadataChanged` event; this will provide a
[`MediaMetadata`][] object for use, which has fields such as `title` and
`albumArtist`. Alternatively, calling `Player#getMediaMetadata` returns the same
object.

View file

@ -75,3 +75,24 @@ end-of-stream signal under poor network conditions.
RTP/TCP offers better compatibility under some network setups. You can configure
ExoPlayer to use RTP/TCP by default with
`RtspMediaSource.Factory.setForceUseRtpTcp()`.
### Passing a custom SocketFactory
Custom `SocketFactory` instances can be useful when particular routing is
required (e.g. when RTSP traffic needs to pass a specific interface, or the
socket needs additional connectivity flags).
By default, `RtspMediaSource` will use Java's standard socket factory
(`SocketFactory.getDefault()`) to create connections to the remote endpoints.
This behavior can be overridden using
`RtspMediaSource.Factory.setSocketFactory()`.
~~~
// Create an RTSP media source pointing to an RTSP uri and override the socket
// factory.
MediaSource mediaSource =
new RtspMediaSource.Factory()
.setSocketFactory(...)
.createMediaSource(MediaItem.fromUri(rtspUri));
~~~
{: .language-java}

View file

@ -106,9 +106,9 @@ ExoPlayer player =
## Custom `MediaSource` instantiation ##
If your app is using a custom `MediaSourceFactory` and you want
If your app is using a custom `MediaSource.Factory` and you want
`DefaultMediaSourceFactory` to be removed by code stripping, you should pass
your `MediaSourceFactory` directly to the `ExoPlayer.Builder` constructor.
your `MediaSource.Factory` directly to the `ExoPlayer.Builder` constructor.
~~~
ExoPlayer player =
@ -117,13 +117,13 @@ ExoPlayer player =
{: .language-java}
If your app is using `MediaSource`s directly instead of `MediaItem`s you should
pass `MediaSourceFactory.UNSUPPORTED` to the `ExoPlayer.Builder` constructor, to
ensure `DefaultMediaSourceFactory` and `DefaultExtractorsFactory` can be
pass `MediaSource.Factory.UNSUPPORTED` to the `ExoPlayer.Builder` constructor,
to ensure `DefaultMediaSourceFactory` and `DefaultExtractorsFactory` can be
stripped by code shrinking.
~~~
ExoPlayer player =
new ExoPlayer.Builder(context, MediaSourceFactory.UNSUPPORTED).build();
new ExoPlayer.Builder(context, MediaSource.Factory.UNSUPPORTED).build();
ProgressiveMediaSource mediaSource =
new ProgressiveMediaSource.Factory(
dataSourceFactory, customExtractorsFactory)

View file

@ -6,7 +6,6 @@ The [Transformer API][] can be used to convert media streams. It takes an input
media stream, applies changes to it as configured by the app, and produces the
corresponding output file. The available transformations are:
* Transmuxing between container formats.
* Track removal.
* Flattening of slow motion videos or, in other words, their conversion into
normal videos that retain the desired slow motion effects, but can be played
@ -16,7 +15,7 @@ corresponding output file. The available transformations are:
## Starting a transformation ##
To transform media, you need add the following dependency to your apps
To transform media, you need to add the following dependency to your apps
`build.gradle` file:
~~~
@ -28,16 +27,13 @@ where `2.X.X` is your preferred ExoPlayer version.
You can then start a transformation by building a `Transformer` instance and
calling `startTransformation` on it. The code sample below starts a
transformation that removes the audio track from the input and sets the output
container format to WebM:
transformation that removes the audio track from the input:
~~~
// Configure and create a Transformer instance.
Transformer transformer =
new Transformer.Builder()
.setContext(context)
new Transformer.Builder(context)
.setRemoveAudio(true)
.setOutputMimeType(MimeTypes.VIDEO_WEBM)
.setListener(transformerListener)
.build();
// Start the transformation.
@ -45,14 +41,16 @@ transformer.startTransformation(inputMediaItem, outputPath);
~~~
{: .language-java}
Other parameters, such as the `MediaSourceFactory`, can be passed to the
Other parameters, such as the `MediaSource.Factory`, can be passed to the
builder.
`startTransformation` receives a `MediaItem` describing the input, and a path or
a `ParcelFileDescriptor` indicating where the output should be written. The
input can be a progressive or an adaptive stream, but the output is always a
progressive stream. For adaptive inputs, the highest resolution tracks are
always selected for the transformation.
always selected for the transformation. The input can be of any container format
supported by ExoPlayer (see the [Supported formats page][] for details), but the
output is always an MP4 file.
Multiple transformations can be executed sequentially with the same
`Transformer` instance, but concurrent transformations with the same instance
@ -72,7 +70,7 @@ Transformer.Listener transformerListener =
}
@Override
public void onTransformationError(MediaItem inputMediaItem, Exception e) {
public void onTransformationError(MediaItem inputMediaItem, TransformationException e) {
displayError(e);
}
};
@ -121,8 +119,7 @@ method.
~~~
Transformer transformer =
new Transformer.Builder()
.setContext(context)
new Transformer.Builder(context)
.setFlattenForSlowMotion(true)
.setListener(transformerListener)
.build();
@ -137,4 +134,5 @@ flattened version of the video instead of the original one.
Currently, Samsung's slow motion format is the only one supported.
[Transformer API]: {{ site.exo_sdk }}/transformer/Transformer.html
[Supported formats page]: {{ site.baseurl }}/supported-formats.html

View file

@ -190,14 +190,15 @@ Note that overriding these drawables will also affect the appearance of
All of the view components inflate their layouts from corresponding layout
files, which are specified in their Javadoc. For example when a
`PlayerControlView` is instantiated, it inflates its layout from
`exo_player_control_view.xml`. To customize these layouts, an application can
define layout files with the same names in its own `res/layout*` directories.
These layout files will override the ones provided by the ExoPlayer library.
`exo_player_control_view.xml`. To customize these layouts, an application
can define layout files with the same names in its own `res/layout*`
directories. These layout files will override the ones provided by the ExoPlayer
library.
As an example, suppose we want our playback controls to consist of only a
play/pause button positioned in the center of the view. We can achieve this by
creating an `exo_player_control_view.xml` file in the applications `res/layout`
directory, containing:
creating an `exo_player_control_view.xml` file in the applications
`res/layout` directory, containing:
~~~
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

View file

@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
api 'com.google.android.gms:play-services-cast-framework:20.0.0'
api 'com.google.android.gms:play-services-cast-framework:20.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-common')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View file

@ -816,13 +816,7 @@ public final class CastPlayer extends BasePlayer {
!getCurrentTimeline().isEmpty()
? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid
: null;
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
if (wasPlaying != isPlaying) {
listeners.queueEvent(
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
}
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
@ -1058,7 +1052,8 @@ public final class CastPlayer extends BasePlayer {
new TracksInfo.TrackGroupInfo[castMediaTracks.size()];
for (int i = 0; i < castMediaTracks.size(); i++) {
MediaTrack mediaTrack = castMediaTracks.get(i);
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
trackGroups[i] =
new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack));
long id = mediaTrack.getId();
@C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
@ -1092,7 +1087,7 @@ public final class CastPlayer extends BasePlayer {
private void updateAvailableCommandsAndNotifyIfChanged() {
Commands previousAvailableCommands = availableCommands;
availableCommands = getAvailableCommands(PERMANENT_AVAILABLE_COMMANDS);
availableCommands = Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS);
if (!availableCommands.equals(previousAvailableCommands)) {
listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
@ -1215,6 +1210,7 @@ public final class CastPlayer extends BasePlayer {
boolean playWhenReady,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
@Player.State int playbackState) {
boolean wasPlaying = this.playbackState == Player.STATE_READY && this.playWhenReady.value;
boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
boolean playbackStateChanged = this.playbackState != playbackState;
if (playWhenReadyChanged || playbackStateChanged) {
@ -1233,6 +1229,11 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_PLAY_WHEN_READY_CHANGED,
listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason));
}
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady;
if (wasPlaying != isPlaying) {
listeners.queueEvent(
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
}
}
}

View file

@ -34,6 +34,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
private static final String KEY_MEDIA_ITEM = "mediaItem";
private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
private static final String KEY_MEDIA_ID = "mediaId";
private static final String KEY_URI = "uri";
private static final String KEY_TITLE = "title";
private static final String KEY_MIME_TYPE = "mimeType";
@ -77,6 +78,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
MediaItem.Builder builder = new MediaItem.Builder();
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
builder.setMediaId(mediaItemJson.getString(KEY_MEDIA_ID));
if (mediaItemJson.has(KEY_TITLE)) {
com.google.android.exoplayer2.MediaMetadata mediaMetadata =
new com.google.android.exoplayer2.MediaMetadata.Builder()
@ -130,6 +132,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
private static JSONObject getMediaItemJson(MediaItem mediaItem) throws JSONException {
Assertions.checkNotNull(mediaItem.localConfiguration);
JSONObject json = new JSONObject();
json.put(KEY_MEDIA_ID, mediaItem.mediaId);
json.put(KEY_TITLE, mediaItem.mediaMetadata.title);
json.put(KEY_URI, mediaItem.localConfiguration.uri.toString());
json.put(KEY_MIME_TYPE, mediaItem.localConfiguration.mimeType);

View file

@ -52,6 +52,7 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ -138,13 +139,18 @@ public class CastPlayerTest {
@SuppressWarnings("deprecation")
@Test
public void setPlayWhenReady_masksRemoteState() {
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
// Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called.
remoteMediaClientCallback.onStatusUpdated();
reset(mockListener);
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.play();
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
verify(mockListener).onIsPlayingChanged(true);
verify(mockListener)
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
@ -163,13 +169,18 @@ public class CastPlayerTest {
@SuppressWarnings("deprecation")
@Test
public void setPlayWhenReadyMasking_updatesUponResultChange() {
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
// Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called.
remoteMediaClientCallback.onStatusUpdated();
reset(mockListener);
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.play();
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
verify(mockListener).onIsPlayingChanged(true);
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
verify(mockListener)
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
@ -177,38 +188,52 @@ public class CastPlayerTest {
setResultCallbackArgumentCaptor
.getValue()
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
verify(mockListener).onPlayerStateChanged(false, Player.STATE_READY);
verify(mockListener).onIsPlayingChanged(false);
verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
verifyNoMoreInteractions(mockListener);
}
@SuppressWarnings("deprecation")
@Test
public void setPlayWhenReady_correctChangeReasonOnPause() {
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
// Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called.
remoteMediaClientCallback.onStatusUpdated();
reset(mockListener);
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
when(mockRemoteMediaClient.pause()).thenReturn(mockPendingResult);
castPlayer.play();
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
verify(mockListener).onIsPlayingChanged(true);
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
verify(mockListener)
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
castPlayer.pause();
assertThat(castPlayer.getPlayWhenReady()).isFalse();
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
verify(mockListener).onIsPlayingChanged(false);
verify(mockListener)
.onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
verify(mockListener).onPlayerStateChanged(false, Player.STATE_READY);
verifyNoMoreInteractions(mockListener);
}
@SuppressWarnings("deprecation")
@Test
public void playWhenReady_changesOnStatusUpdates() {
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
remoteMediaClientCallback.onStatusUpdated();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
verify(mockListener).onPlaybackStateChanged(Player.STATE_READY);
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onIsPlayingChanged(true);
verifyNoMoreInteractions(mockListener);
}
@Test

View file

@ -64,7 +64,7 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioD
this(
eventHandler,
eventListener,
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors));
new DefaultAudioSink.Builder().setAudioProcessors(audioProcessors).build());
}
/**

View file

@ -42,7 +42,7 @@ public final class FfmpegLibrary {
/**
* Override the names of the FFmpeg native libraries. If an application wishes to call this
* method, it must do so before calling any other method defined by this class, and before
* instantiating a {@link FfmpegAudioRenderer} instance.
* instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance.
*
* @param libraries The names of the FFmpeg native libraries.
*/
@ -140,6 +140,10 @@ public final class FfmpegLibrary {
return "pcm_mulaw";
case MimeTypes.AUDIO_ALAW:
return "pcm_alaw";
case MimeTypes.VIDEO_H264:
return "h264";
case MimeTypes.VIDEO_H265:
return "hevc";
default:
return null;
}

View file

@ -0,0 +1,135 @@
/*
* Copyright (C) 2020 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 com.google.android.exoplayer2.ext.ffmpeg;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.CryptoConfig;
import com.google.android.exoplayer2.decoder.Decoder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.decoder.VideoDecoderOutputBuffer;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.DecoderVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
// TODO: Remove the NOTE below.
/**
* <b>NOTE: This class if under development and is not yet functional.</b>
*
* <p>Decodes and renders video using FFmpeg.
*/
public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
private static final String TAG = "FfmpegVideoRenderer";
/**
* Creates a new instance.
*
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public FfmpegVideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
// TODO: Implement.
}
@Override
public String getName() {
return TAG;
}
@Override
@RendererCapabilities.Capabilities
public final int supportsFormat(Format format) {
// TODO: Remove this line and uncomment the implementation below.
return C.FORMAT_UNSUPPORTED_TYPE;
/*
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
} else if (format.exoMediaCryptoType != null) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
} else {
return RendererCapabilities.create(
FORMAT_HANDLED,
ADAPTIVE_SEAMLESS,
TUNNELING_NOT_SUPPORTED);
}
*/
}
@SuppressWarnings("nullness:return")
@Override
protected Decoder<DecoderInputBuffer, VideoDecoderOutputBuffer, FfmpegDecoderException>
createDecoder(Format format, @Nullable CryptoConfig cryptoConfig)
throws FfmpegDecoderException {
TraceUtil.beginSection("createFfmpegVideoDecoder");
// TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use
// the concrete type of the decoder (probably FfmepgVideoDecoder).
TraceUtil.endSection();
return null;
}
@Override
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws FfmpegDecoderException {
// TODO: Implement.
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
// TODO: Uncomment the implementation below.
/*
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
*/
}
@Override
protected DecoderReuseEvaluation canReuseDecoder(
String decoderName, Format oldFormat, Format newFormat) {
boolean sameMimeType = Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType);
// TODO: Ability to reuse the decoder may be MIME type dependent.
return new DecoderReuseEvaluation(
decoderName,
oldFormat,
newFormat,
sameMimeType ? REUSE_RESULT_YES_WITHOUT_RECONFIGURATION : REUSE_RESULT_NO,
sameMimeType ? 0 : DISCARD_REASON_MIME_TYPE_CHANGED);
}
}

View file

@ -22,7 +22,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}.
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer} and {@link
* FfmpegVideoRenderer}.
*/
@RunWith(AndroidJUnit4.class)
public final class DefaultRenderersFactoryTest {
@ -32,4 +33,10 @@ public final class DefaultRenderersFactoryTest {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
}
@Test
public void createRenderers_instantiatesFfmpegVideoRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO);
}
}

View file

@ -29,7 +29,6 @@ import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@ -67,9 +66,7 @@ public class FlacPlaybackTest {
}
private static void playAndAssertAudioSinkInput(String fileName) throws Exception {
CapturingAudioSink audioSink =
new CapturingAudioSink(
new DefaultAudioSink(/* audioCapabilities= */ null, new AudioProcessor[0]));
CapturingAudioSink audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder().build());
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(

View file

@ -46,7 +46,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.ui.AdViewProvider;
@ -86,7 +86,7 @@ import java.util.Set;
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">IMA
* SDK Open Measurement documentation</a> for more information.
*/
public final class ImaAdsLoader implements Player.Listener, AdsLoader {
public final class ImaAdsLoader implements AdsLoader {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
@ -222,7 +222,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
/**
* Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported
* by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link
* by the {@link MediaSource.Factory adMediaSourceFactory} used to construct the {@link
* AdsMediaSource} will be used.
*
* @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link
@ -386,6 +386,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
private final ImaUtil.Configuration configuration;
private final Context context;
private final ImaUtil.ImaFactory imaFactory;
private final PlayerListenerImpl playerListener;
private final HashMap<Object, AdTagLoader> adTagLoaderByAdsId;
private final HashMap<AdsMediaSource, AdTagLoader> adTagLoaderByAdsMediaSource;
private final Timeline.Period period;
@ -402,6 +403,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
this.context = context.getApplicationContext();
this.configuration = configuration;
this.imaFactory = imaFactory;
playerListener = new PlayerListenerImpl();
supportedMimeTypes = ImmutableList.of();
adTagLoaderByAdsId = new HashMap<>();
adTagLoaderByAdsMediaSource = new HashMap<>();
@ -532,7 +534,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
if (player == null) {
return;
}
player.addListener(this);
player.addListener(playerListener);
}
@Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId);
@ -554,7 +556,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
}
if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) {
player.removeListener(this);
player.removeListener(playerListener);
player = null;
}
}
@ -562,7 +564,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
@Override
public void release() {
if (player != null) {
player.removeListener(this);
player.removeListener(playerListener);
player = null;
maybeUpdateCurrentAdTagLoader();
}
@ -602,37 +604,6 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
}
// Player.Listener implementation.
@Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
if (timeline.isEmpty()) {
// The player is being reset or contains no media.
return;
}
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
maybePreloadNextPeriodAds();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
maybePreloadNextPeriodAds();
}
// Internal methods.
private void maybeUpdateCurrentAdTagLoader() {
@ -672,7 +643,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
}
private void maybePreloadNextPeriodAds() {
@Nullable Player player = this.player;
@Nullable Player player = ImaAdsLoader.this.player;
if (player == null) {
return;
}
@ -700,12 +671,44 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
return;
}
long periodPositionUs =
timeline.getPeriodPosition(
timeline.getPeriodPositionUs(
window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET)
.second;
nextAdTagLoader.maybePreloadAds(Util.usToMs(periodPositionUs), Util.usToMs(period.durationUs));
}
private final class PlayerListenerImpl implements Player.Listener {
@Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
if (timeline.isEmpty()) {
// The player is being reset or contains no media.
return;
}
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
maybePreloadNextPeriodAds();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
maybePreloadNextPeriodAds();
}
}
/**
* Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link
* ImaSdkFactory}.

View file

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.content.Context;
import android.os.Looper;
import android.view.View;
@ -36,15 +39,22 @@ import com.google.ads.interactivemedia.v3.api.UiElement;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.ui.AdOverlayInfo;
import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.DataSchemeDataSource;
import com.google.android.exoplayer2.upstream.DataSourceUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Utilities for working with IMA SDK and IMA extension data types. */
@ -134,6 +144,32 @@ import java.util.Set;
}
}
/** Stores configuration for playing server side ad insertion content. */
public static final class ServerSideAdInsertionConfiguration {
public final AdViewProvider adViewProvider;
public final ImaSdkSettings imaSdkSettings;
@Nullable public final AdEvent.AdEventListener applicationAdEventListener;
@Nullable public final AdErrorEvent.AdErrorListener applicationAdErrorListener;
public final ImmutableList<CompanionAdSlot> companionAdSlots;
public final boolean debugModeEnabled;
public ServerSideAdInsertionConfiguration(
AdViewProvider adViewProvider,
ImaSdkSettings imaSdkSettings,
@Nullable AdEvent.AdEventListener applicationAdEventListener,
@Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener,
List<CompanionAdSlot> companionAdSlots,
boolean debugModeEnabled) {
this.imaSdkSettings = imaSdkSettings;
this.adViewProvider = adViewProvider;
this.applicationAdEventListener = applicationAdEventListener;
this.applicationAdErrorListener = applicationAdErrorListener;
this.companionAdSlots = ImmutableList.copyOf(companionAdSlots);
this.debugModeEnabled = debugModeEnabled;
}
}
public static final int TIMEOUT_UNSET = -1;
public static final int BITRATE_UNSET = -1;
@ -227,5 +263,125 @@ import java.util.Set;
}
}
/**
* Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a
* content timeline. Ad group times are expected to not take previous ad duration into account and
* needs to be translated to the actual position in the {@code contentTimeline} by adding prior ad
* durations.
*
* <p>If a period is enclosed by an ad group, the period is considered an ad period and gets an ad
* playback state assigned with a single ad in a single ad group. The duration of the ad is set to
* the duration of the period. All other periods are considered content periods with an empty ad
* playback state without any ads.
*
* @param adPlaybackState The ad playback state to be split.
* @param contentTimeline The content timeline for each period of which to create an {@link
* AdPlaybackState}.
* @return A map of ad playback states for each period UID in the content timeline.
*/
public static ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStateForPeriods(
AdPlaybackState adPlaybackState, Timeline contentTimeline) {
Timeline.Period period = new Timeline.Period();
if (contentTimeline.getPeriodCount() == 1) {
// A single period gets the entire ad playback state that may contain multiple ad groups.
return ImmutableMap.of(
checkNotNull(
contentTimeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid),
adPlaybackState);
}
int periodIndex = 0;
long totalElapsedContentDurationUs = 0;
Object adsId = checkNotNull(adPlaybackState.adsId);
AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId);
Map<Object, AdPlaybackState> adPlaybackStates = new HashMap<>();
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
if (adGroup.timeUs == C.TIME_END_OF_SOURCE) {
checkState(i == adPlaybackState.adGroupCount - 1);
// The last ad group is a placeholder for a potential post roll. We can just stop here.
break;
}
// The ad group start timeUs is in content position. We need to add the ad
// duration before the ad group to translate the start time to the position in the period.
long adGroupDurationUs = getTotalDurationUs(adGroup.durationsUs);
long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
contentTimeline.getPeriod(j, period, /* setIds= */ true);
if (totalElapsedContentDurationUs < adGroup.timeUs) {
// Period starts before the ad group, so it is a content period.
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
totalElapsedContentDurationUs += period.durationUs;
} else {
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) {
// The period ends before the end of the ad group, so it is an ad period (Note: An ad
// reported by the IMA SDK may span multiple periods).
adPlaybackStates.put(
checkNotNull(period.uid),
splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs));
elapsedAdGroupAdDurationUs += period.durationUs;
} else {
// Period is after the current ad group. Continue with next ad group.
break;
}
}
// Increment the period index to the next unclassified period.
periodIndex++;
}
}
// The remaining periods end after the last ad group, so these are content periods.
for (int i = periodIndex; i < contentTimeline.getPeriodCount(); i++) {
contentTimeline.getPeriod(i, period, /* setIds= */ true);
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
}
return ImmutableMap.copyOf(adPlaybackStates);
}
private static AdPlaybackState splitAdGroupForPeriod(
Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) {
checkState(adGroup.timeUs <= periodStartUs);
AdPlaybackState adPlaybackState =
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
long periodEndUs = periodStartUs + periodDurationUs;
long adDurationsUs = 0;
for (int i = 0; i < adGroup.count; i++) {
adDurationsUs += adGroup.durationsUs[i];
if (periodEndUs == adGroup.timeUs + adDurationsUs) {
// Map the state of the global ad state to the period specific ad state.
switch (adGroup.states[i]) {
case AdPlaybackState.AD_STATE_PLAYED:
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
break;
case AdPlaybackState.AD_STATE_SKIPPED:
adPlaybackState =
adPlaybackState.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
break;
case AdPlaybackState.AD_STATE_ERROR:
adPlaybackState =
adPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
break;
default:
// Do nothing.
break;
}
break;
}
}
return adPlaybackState;
}
private static long getTotalDurationUs(long[] durationsUs) {
long totalDurationUs = 0;
for (long adDurationUs : durationsUs) {
totalDurationUs += adDurationUs;
}
return totalDurationUs;
}
private ImaUtil() {}
}

View file

@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.os.Looper;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
@ -182,6 +185,29 @@ import com.google.android.exoplayer2.util.Util;
}
}
/**
* Sets an error on this player.
*
* <p>This will propagate the error to {@link Player.Listener#onPlayerError(PlaybackException)}
* and {@link Player.Listener#onPlayerErrorChanged(PlaybackException)} and will also update the
* state to {@link Player#STATE_IDLE}.
*
* <p>The player must be in {@link #STATE_BUFFERING} or {@link #STATE_READY}.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener.onPlayerStateChanged()
public void setPlayerError(PlaybackException error) {
checkState(state == STATE_BUFFERING || state == STATE_READY);
this.state = Player.STATE_IDLE;
listeners.sendEvent(
Player.EVENT_PLAYBACK_STATE_CHANGED,
listener -> {
listener.onPlayerError(error);
listener.onPlayerErrorChanged(error);
listener.onPlayerStateChanged(playWhenReady, state);
listener.onPlaybackStateChanged(state);
});
}
// ExoPlayer methods. Other methods are unsupported.
@Override

View file

@ -56,7 +56,6 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@ -276,30 +275,7 @@ public final class ImaAdsLoaderTest {
ExoPlaybackException anException =
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
imaAdsLoader.onPlayerErrorChanged(anException);
imaAdsLoader.onPlayerError(anException);
imaAdsLoader.onPositionDiscontinuity(
new Player.PositionInfo(
/* windowUid= */ new Object(),
/* windowIndex= */ 0,
/* mediaItem= */ MediaItem.fromUri("http://google.com/0"),
/* periodUid= */ new Object(),
/* periodIndex= */ 0,
/* positionMs= */ 10_000,
/* contentPositionMs= */ 0,
/* adGroupIndex= */ -1,
/* adIndexInAdGroup= */ -1),
new Player.PositionInfo(
/* windowUid= */ new Object(),
/* windowIndex= */ 1,
/* mediaItem= */ MediaItem.fromUri("http://google.com/1"),
/* periodUid= */ new Object(),
/* periodIndex= */ 0,
/* positionMs= */ 20_000,
/* contentPositionMs= */ 0,
/* adGroupIndex= */ -1,
/* adIndexInAdGroup= */ -1),
Player.DISCONTINUITY_REASON_SEEK);
fakePlayer.setPlayerError(anException);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
imaAdsLoader.handlePrepareError(
adsMediaSource, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());

View file

@ -0,0 +1,506 @@
/*
* Copyright (C) 2021 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 com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static com.google.common.truth.Truth.assertThat;
import android.util.Pair;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link ImaUtil}. */
@RunWith(AndroidJUnit4.class)
public class ImaUtilTest {
@Test
public void splitAdPlaybackStateForPeriods_emptyTimeline_emptyMapOfAdPlaybackStates() {
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE);
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY);
assertThat(adPlaybackStates).isEmpty();
}
@Test
public void splitAdPlaybackStateForPeriods_singlePeriod_doesNotSplit() {
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE);
FakeTimeline singlePeriodTimeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline);
assertThat(adPlaybackStates).hasSize(1);
assertThat(adPlaybackStates).containsEntry(new Pair<>(0L, 0), adPlaybackState);
}
@Test
public void splitAdPlaybackStateForPeriods_livePlaceholder_isIgnored() {
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "", C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline singlePeriodTimeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline);
assertThat(adPlaybackStates).hasSize(3);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
}
@Test
public void splitAdPlaybackStateForPeriods_noAds_splitToEmptyAdPlaybackStates() {
AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ "adsId");
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 11, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(11);
for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) {
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0);
}
}
@Test
public void splitAdPlaybackStateForPeriods_twoPrerollAds_splitToFirstTwoPeriods() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdDurationsUs(
/* adGroupIndex= */ 0,
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
for (int i = 0; i < 2; i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0);
assertThat(periodAdPlaybackState.getAdGroup(0).isServerSideInserted).isTrue();
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
int adDurationUs = i == 0 ? 125_500_000 : 2_500_000;
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs);
}
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
}
@Test
public void splitAdPlaybackStateForPeriods_onePrerollAdGroup_splitToFirstThreePeriods() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(
/* adGroupIndex= */ 0,
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 3 * periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
for (int i = 0; i < 3; i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
int adDurationUs = i == 0 ? 125_500_000 : 2_500_000;
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs);
}
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
}
@Test
public void splitAdPlaybackStateForPeriods_twoMidrollAds_splitToMiddleTwoPeriods() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
for (int i = 1; i < 3; i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000);
}
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
}
@Test
public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupOneAd_adSpansTwoPeriods() {
int periodCount = 5;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
for (int i = 1; i < 3; i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000);
}
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0);
}
@Test
public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupTwoAds_eachAdSplitsToOnePeriod() {
int periodCount = 5;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
for (int i = 1; i < 3; i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000);
}
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0);
}
@Test
public void splitAdPlaybackStateForPeriods_twoPostrollAds_splitToLastTwoPeriods() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId",
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
for (int i = 2; i < periodCount; i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000);
}
}
@Test
public void splitAdPlaybackStateForPeriods_onePostrollAdGroup_splitToLastThreePeriods() {
int periodCount = 7;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId",
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 4 * periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, 3 * periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
for (int i = 4; i < adPlaybackStates.size(); i++) {
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(periodDurationUs);
}
}
@Test
public void splitAdPlaybackStateForPeriods_preMidAndPostrollAdGroup_splitCorrectly() {
int periodCount = 11;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", 0, (2 * periodDurationUs), (5 * periodDurationUs))
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdDurationsUs(
/* adGroupIndex= */ 0,
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withAdCount(/* adGroupIndex= */ 1, 2)
.withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 1, true)
.withAdCount(/* adGroupIndex= */ 2, 2)
.withAdDurationsUs(/* adGroupIndex= */ 2, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 2, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 9)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 10)).adGroupCount).isEqualTo(1);
}
@Test
public void splitAdPlaybackStateForPeriods_midAndPostrollAdGroup_splitCorrectly() {
int periodCount = 9;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId",
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (2 * periodDurationUs),
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (5 * periodDurationUs))
.withAdCount(/* adGroupIndex= */ 0, 2)
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withAdCount(/* adGroupIndex= */ 1, 2)
.withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 1, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0);
assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(1);
assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(1);
}
@Test
public void splitAdPlaybackStateForPeriods_correctAdsIdInSplitPlaybackStates() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId",
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
for (int i = 0; i < adPlaybackStates.size(); i++) {
assertThat(adPlaybackStates.get(new Pair<>(0L, i)).adsId).isEqualTo("adsId");
}
}
@Test
public void splitAdPlaybackStateForPeriods_correctAdPlaybackStates() {
int periodCount = 7;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", 0)
.withAdCount(/* adGroupIndex= */ 0, periodCount)
.withAdDurationsUs(
/* adGroupIndex= */ 0, /* adDurationsUs...= */
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
periodDurationUs,
periodDurationUs,
periodDurationUs,
periodDurationUs,
periodDurationUs,
periodDurationUs)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_ERROR);
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0])
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
}
@Test
public void splitAdPlaybackStateForPeriods_lateMidrollAdGroupStartTimeUs_adGroupIgnored() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "adsId",
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 1)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) {
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0);
}
}
@Test
public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGroupIgnored() {
int periodCount = 4;
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ "adsId", periodDurationUs - 1)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
FakeTimeline timeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ periodCount, /* id= */ 0L));
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
assertThat(adPlaybackStates).hasSize(periodCount);
for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) {
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0);
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
}
}
}

View file

@ -466,6 +466,8 @@ public final class MediaSessionConnector {
private long enabledPlaybackActions;
private boolean metadataDeduplicationEnabled;
private boolean dispatchUnsupportedActionsEnabled;
private boolean clearMediaItemsOnStop;
private boolean mapIdleToStopped;
/**
* Creates an instance.
@ -486,6 +488,7 @@ public final class MediaSessionConnector {
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
mediaSession.setCallback(componentListener, new Handler(looper));
clearMediaItemsOnStop = true;
}
/**
@ -699,6 +702,23 @@ public final class MediaSessionConnector {
this.dispatchUnsupportedActionsEnabled = dispatchUnsupportedActionsEnabled;
}
/**
* Sets whether media items are cleared from the playlist when a client sends a {@link
* MediaControllerCompat.TransportControls#stop()} command.
*/
public void setClearMediaItemsOnStop(boolean clearMediaItemsOnStop) {
this.clearMediaItemsOnStop = clearMediaItemsOnStop;
}
/**
* Sets whether {@link Player#STATE_IDLE} should be mapped to {@link
* PlaybackStateCompat#STATE_STOPPED}. The default is false {@link Player#STATE_IDLE} which maps
* to {@link PlaybackStateCompat#STATE_NONE}.
*/
public void setMapStateIdleToSessionStateStopped(boolean mapIdleToStopped) {
this.mapIdleToStopped = mapIdleToStopped;
}
/**
* Sets whether {@link MediaMetadataProvider#sameAs(MediaMetadataCompat, MediaMetadataCompat)}
* should be consulted before calling {@link MediaSessionCompat#setMetadata(MediaMetadataCompat)}.
@ -974,7 +994,7 @@ public final class MediaSessionConnector {
player.seekTo(mediaItemIndex, positionMs);
}
private static int getMediaSessionPlaybackState(
private int getMediaSessionPlaybackState(
@Player.State int exoPlayerPlaybackState, boolean playWhenReady) {
switch (exoPlayerPlaybackState) {
case Player.STATE_BUFFERING:
@ -987,7 +1007,9 @@ public final class MediaSessionConnector {
return PlaybackStateCompat.STATE_STOPPED;
case Player.STATE_IDLE:
default:
return PlaybackStateCompat.STATE_NONE;
return mapIdleToStopped
? PlaybackStateCompat.STATE_STOPPED
: PlaybackStateCompat.STATE_NONE;
}
}
@ -1209,7 +1231,9 @@ public final class MediaSessionConnector {
public void onStop() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {
player.stop();
player.clearMediaItems();
if (clearMediaItemsOnStop) {
player.clearMediaItems();
}
}
}

View file

@ -28,7 +28,7 @@ public final class OpusLibrary {
}
private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI");
@C.CryptoType private static int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED;
private static @C.CryptoType int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED;
private OpusLibrary() {}

View file

@ -21,7 +21,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
// Dackka snapshots are listed at https://androidx.dev/dackka/builds.
static final String DACKKA_JAR_URL =
"https://androidx.dev/dackka/builds/7758117/artifacts/dackka-0.0.10.jar"
"https://androidx.dev/dackka/builds/8003564/artifacts/dackka-0.0.14.jar"
@Override
void apply(Project project) {
@ -67,12 +67,12 @@ class CombinedJavadocPlugin implements Plugin<Project> {
}
}
def dackkaOutputDir = project.file("$project.buildDir/docs/dackka")
project.task(DACKKA_TASK_NAME, type: JavaExec) {
doFirst {
// Recreate the output directory to remove any leftover files from a previous run.
def outputDir = project.file("$project.buildDir/docs/dackka")
project.delete outputDir
project.mkdir outputDir
project.delete dackkaOutputDir
project.mkdir dackkaOutputDir
// Download the Dackka JAR.
new URL(DACKKA_JAR_URL).withInputStream {
@ -104,15 +104,27 @@ class CombinedJavadocPlugin implements Plugin<Project> {
.filter({ f -> project.file(f).exists() }).join(";")
def dependenciesString = project.files(dependencies).asPath.replace(':', ';')
args("-moduleName", "",
"-outputDir", "$outputDir",
"-outputDir", "$dackkaOutputDir",
"-globalLinks", "$globalLinksString",
"-loggingLevel", "WARN",
"-sourceSet", "-src $sourcesString -classpath $dependenciesString",
"-offlineMode")
environment("DEVSITE_TENANT", "androidx")
environment("DEVSITE_TENANT", "androidx/media3")
}
description = "Generates combined javadoc for developer.android.com."
classpath = project.files(new File(getTemporaryDir(), "dackka.jar"))
doLast {
libraryModules.each { libraryModule ->
project.copy {
from "${libraryModule.projectDir}/src/main/javadoc"
into "${dackkaOutputDir}/reference/"
}
project.copy {
from "${libraryModule.projectDir}/src/main/javadoc"
into "${dackkaOutputDir}/reference/kotlin/"
}
}
}
}
}
}

View file

@ -27,6 +27,7 @@ dependencies {
// (but declared as runtime deps) [internal b/168188131].
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'

View file

@ -382,37 +382,6 @@ public abstract class BasePlayer implements Player {
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
/**
* Returns the {@link Commands} available in the player.
*
* @param permanentAvailableCommands The commands permanently available in the player.
* @return The available {@link Commands}.
*/
protected Commands getAvailableCommands(Commands permanentAvailableCommands) {
return new Commands.Builder()
.addAll(permanentAvailableCommands)
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd())
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentMediaItemSeekable() && !isPlayingAd())
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem() && !isPlayingAd())
.addIf(
COMMAND_SEEK_TO_PREVIOUS,
!getCurrentTimeline().isEmpty()
&& (hasPreviousMediaItem()
|| !isCurrentMediaItemLive()
|| isCurrentMediaItemSeekable())
&& !isPlayingAd())
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem() && !isPlayingAd())
.addIf(
COMMAND_SEEK_TO_NEXT,
!getCurrentTimeline().isEmpty()
&& (hasNextMediaItem() || (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()))
&& !isPlayingAd())
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd())
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable() && !isPlayingAd())
.addIf(COMMAND_SEEK_FORWARD, isCurrentMediaItemSeekable() && !isPlayingAd())
.build();
}
@RepeatMode
private int getRepeatModeForNavigation() {
@RepeatMode int repeatMode = getRepeatMode();

View file

@ -272,6 +272,20 @@ public final class C {
/** @see AudioFormat#ENCODING_DOLBY_TRUEHD */
public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
/** Represents the behavior affecting whether spatialization will be used. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@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;
/**
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
@ -590,6 +604,7 @@ public final class C {
flag = true,
value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, SELECTION_FLAG_AUTOSELECT})
public @interface SelectionFlags {}
// LINT.IfChange(selection_flags)
/** Indicates that the track should be selected if user preferences do not state otherwise. */
public static final int SELECTION_FLAG_DEFAULT = 1;
/**
@ -1055,6 +1070,7 @@ public final class C {
ROLE_FLAG_TRICK_PLAY
})
public @interface RoleFlags {}
// LINT.IfChange(role_flags)
/** Indicates a main track. */
public static final int ROLE_FLAG_MAIN = 1;
/**

View file

@ -631,7 +631,8 @@ public final class Format implements Bundleable {
* <ul>
* <li>DASH representations: Always {@link Format#NO_VALUE}.
* <li>HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code
* EXT-X-STREAM-INF} tag in the master playlist, or {@link Format#NO_VALUE} if not present.
* EXT-X-STREAM-INF} tag in the multivariant playlist, or {@link Format#NO_VALUE} if not
* present.
* <li>SmoothStreaming track elements: The {@code Bitrate} attribute defined on the
* corresponding {@code TrackElement} in the manifest, or {@link Format#NO_VALUE} if not
* present.
@ -1300,7 +1301,9 @@ public final class Format implements Bundleable {
schemes.add("unknown (" + schemeUuid + ")");
}
}
builder.append(", drm=[").append(Joiner.on(',').join(schemes)).append(']');
builder.append(", drm=[");
Joiner.on(',').appendTo(builder, schemes);
builder.append(']');
}
if (format.width != NO_VALUE && format.height != NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
@ -1320,8 +1323,73 @@ public final class Format implements Bundleable {
if (format.label != null) {
builder.append(", label=").append(format.label);
}
if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
builder.append(", trick-play-track");
if (format.selectionFlags != 0) {
List<String> selectionFlags = new ArrayList<>();
// LINT.IfChange(selection_flags)
if ((format.selectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) {
selectionFlags.add("auto");
}
if ((format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) {
selectionFlags.add("default");
}
if ((format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) {
selectionFlags.add("forced");
}
builder.append(", selectionFlags=[");
Joiner.on(',').appendTo(builder, selectionFlags);
builder.append("]");
}
if (format.roleFlags != 0) {
// LINT.IfChange(role_flags)
List<String> roleFlags = new ArrayList<>();
if ((format.roleFlags & C.ROLE_FLAG_MAIN) != 0) {
roleFlags.add("main");
}
if ((format.roleFlags & C.ROLE_FLAG_ALTERNATE) != 0) {
roleFlags.add("alt");
}
if ((format.roleFlags & C.ROLE_FLAG_SUPPLEMENTARY) != 0) {
roleFlags.add("supplementary");
}
if ((format.roleFlags & C.ROLE_FLAG_COMMENTARY) != 0) {
roleFlags.add("commentary");
}
if ((format.roleFlags & C.ROLE_FLAG_DUB) != 0) {
roleFlags.add("dub");
}
if ((format.roleFlags & C.ROLE_FLAG_EMERGENCY) != 0) {
roleFlags.add("emergency");
}
if ((format.roleFlags & C.ROLE_FLAG_CAPTION) != 0) {
roleFlags.add("caption");
}
if ((format.roleFlags & C.ROLE_FLAG_SUBTITLE) != 0) {
roleFlags.add("subtitle");
}
if ((format.roleFlags & C.ROLE_FLAG_SIGN) != 0) {
roleFlags.add("sign");
}
if ((format.roleFlags & C.ROLE_FLAG_DESCRIBES_VIDEO) != 0) {
roleFlags.add("describes-video");
}
if ((format.roleFlags & C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND) != 0) {
roleFlags.add("describes-music");
}
if ((format.roleFlags & C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY) != 0) {
roleFlags.add("enhanced-intelligibility");
}
if ((format.roleFlags & C.ROLE_FLAG_TRANSCRIBES_DIALOG) != 0) {
roleFlags.add("transcribes-dialog");
}
if ((format.roleFlags & C.ROLE_FLAG_EASY_TO_READ) != 0) {
roleFlags.add("easy-read");
}
if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
roleFlags.add("trick-play");
}
builder.append(", roleFlags=[");
Joiner.on(',').appendTo(builder, roleFlags);
builder.append("]");
}
return builder.toString();
}

View file

@ -1238,6 +1238,7 @@ public final class MediaItem implements Bundleable {
private @C.SelectionFlags int selectionFlags;
private @C.RoleFlags int roleFlags;
@Nullable private String label;
@Nullable private String id;
/**
* Constructs an instance.
@ -1255,6 +1256,7 @@ public final class MediaItem implements Bundleable {
this.selectionFlags = subtitleConfiguration.selectionFlags;
this.roleFlags = subtitleConfiguration.roleFlags;
this.label = subtitleConfiguration.label;
this.id = subtitleConfiguration.id;
}
/** Sets the {@link Uri} to the subtitle file. */
@ -1293,6 +1295,12 @@ public final class MediaItem implements Bundleable {
return this;
}
/** Sets the optional ID for this subtitle track. */
public Builder setId(@Nullable String id) {
this.id = id;
return this;
}
/** Creates a {@link SubtitleConfiguration} from the values of this builder. */
public SubtitleConfiguration build() {
return new SubtitleConfiguration(this);
@ -1315,20 +1323,27 @@ public final class MediaItem implements Bundleable {
public final @C.RoleFlags int roleFlags;
/** The label. */
@Nullable public final String label;
/**
* The ID of the subtitles. This will be propagated to the {@link Format#id} of the subtitle
* track created from this configuration.
*/
@Nullable public final String id;
private SubtitleConfiguration(
Uri uri,
String mimeType,
@Nullable String language,
@C.SelectionFlags int selectionFlags,
@C.RoleFlags int roleFlags,
@Nullable String label) {
int selectionFlags,
int roleFlags,
@Nullable String label,
@Nullable String id) {
this.uri = uri;
this.mimeType = mimeType;
this.language = language;
this.selectionFlags = selectionFlags;
this.roleFlags = roleFlags;
this.label = label;
this.id = id;
}
private SubtitleConfiguration(Builder builder) {
@ -1338,6 +1353,7 @@ public final class MediaItem implements Bundleable {
this.selectionFlags = builder.selectionFlags;
this.roleFlags = builder.roleFlags;
this.label = builder.label;
this.id = builder.id;
}
/** Returns a {@link Builder} initialized with the values of this instance. */
@ -1361,7 +1377,8 @@ public final class MediaItem implements Bundleable {
&& Util.areEqual(language, other.language)
&& selectionFlags == other.selectionFlags
&& roleFlags == other.roleFlags
&& Util.areEqual(label, other.label);
&& Util.areEqual(label, other.label)
&& Util.areEqual(id, other.id);
}
@Override
@ -1372,6 +1389,7 @@ public final class MediaItem implements Bundleable {
result = 31 * result + selectionFlags;
result = 31 * result + roleFlags;
result = 31 * result + (label == null ? 0 : label.hashCode());
result = 31 * result + (id == null ? 0 : id.hashCode());
return result;
}
}
@ -1402,7 +1420,7 @@ public final class MediaItem implements Bundleable {
@C.SelectionFlags int selectionFlags,
@C.RoleFlags int roleFlags,
@Nullable String label) {
super(uri, mimeType, language, selectionFlags, roleFlags, label);
super(uri, mimeType, language, selectionFlags, roleFlags, label, /* id= */ null);
}
private Subtitle(Builder builder) {

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.Math.max;
@ -815,6 +816,21 @@ public abstract class Timeline implements Bundleable {
return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET;
}
/**
* Returns the state of the ad at index {@code adIndexInAdGroup} in the ad group at {@code
* adGroupIndex}, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet known.
*
* @param adGroupIndex The ad group index.
* @return The state of the ad, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet
* known.
*/
public int getAdState(int adGroupIndex, int adIndexInAdGroup) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
return adGroup.count != C.LENGTH_UNSET
? adGroup.states[adIndexInAdGroup]
: AD_STATE_UNAVAILABLE;
}
/**
* Returns the position offset in the first unplayed ad at which to begin playback, in
* microseconds.
@ -1174,13 +1190,13 @@ public abstract class Timeline implements Bundleable {
}
/**
* Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position
* Calls {@link #getPeriodPositionUs(Window, Period, int, long)} with a zero default position
* projection.
*/
public final Pair<Object, Long> getPeriodPositionUs(
Window window, Period period, int windowIndex, long windowPositionUs) {
return Assertions.checkNotNull(
getPeriodPosition(
getPeriodPositionUs(
window, period, windowIndex, windowPositionUs, /* defaultPositionProjectionUs= */ 0));
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio;
import android.os.Bundle;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@ -25,6 +26,7 @@ import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
/**
* Attributes for audio playback, which configure the underlying platform {@link
@ -48,6 +50,7 @@ public final class AudioAttributes implements Bundleable {
private @C.AudioFlags int flags;
private @C.AudioUsage int usage;
private @C.AudioAllowedCapturePolicy int allowedCapturePolicy;
private @C.SpatializationBehavior int spatializationBehavior;
/**
* Creates a new builder for {@link AudioAttributes}.
@ -60,6 +63,7 @@ public final class AudioAttributes implements Bundleable {
flags = 0;
usage = C.USAGE_MEDIA;
allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
spatializationBehavior = C.SPATIALIZATION_BEHAVIOR_AUTO;
}
/** @see android.media.AudioAttributes.Builder#setContentType(int) */
@ -86,9 +90,18 @@ 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 AudioAttributes.Builder#setSpatializationBehavior(int). */
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
this.spatializationBehavior = spatializationBehavior;
return this;
}
/** Creates an {@link AudioAttributes} instance from this builder. */
public AudioAttributes build() {
return new AudioAttributes(contentType, flags, usage, allowedCapturePolicy);
return new AudioAttributes(
contentType, flags, usage, allowedCapturePolicy, spatializationBehavior);
}
}
@ -96,6 +109,7 @@ public final class AudioAttributes implements Bundleable {
public final @C.AudioFlags int flags;
public final @C.AudioUsage int usage;
public final @C.AudioAllowedCapturePolicy int allowedCapturePolicy;
public final @C.SpatializationBehavior int spatializationBehavior;
@Nullable private android.media.AudioAttributes audioAttributesV21;
@ -103,11 +117,13 @@ public final class AudioAttributes implements Bundleable {
@C.AudioContentType int contentType,
@C.AudioFlags int flags,
@C.AudioUsage int usage,
@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
@C.AudioAllowedCapturePolicy int allowedCapturePolicy,
@C.SpatializationBehavior int spatializationBehavior) {
this.contentType = contentType;
this.flags = flags;
this.usage = usage;
this.allowedCapturePolicy = allowedCapturePolicy;
this.spatializationBehavior = spatializationBehavior;
}
/**
@ -124,7 +140,10 @@ public final class AudioAttributes implements Bundleable {
.setFlags(flags)
.setUsage(usage);
if (Util.SDK_INT >= 29) {
builder.setAllowedCapturePolicy(allowedCapturePolicy);
Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, spatializationBehavior);
}
audioAttributesV21 = builder.build();
}
@ -143,7 +162,8 @@ public final class AudioAttributes implements Bundleable {
return this.contentType == other.contentType
&& this.flags == other.flags
&& this.usage == other.usage
&& this.allowedCapturePolicy == other.allowedCapturePolicy;
&& this.allowedCapturePolicy == other.allowedCapturePolicy
&& this.spatializationBehavior == other.spatializationBehavior;
}
@Override
@ -153,6 +173,7 @@ public final class AudioAttributes implements Bundleable {
result = 31 * result + flags;
result = 31 * result + usage;
result = 31 * result + allowedCapturePolicy;
result = 31 * result + spatializationBehavior;
return result;
}
@ -160,13 +181,20 @@ public final class AudioAttributes implements Bundleable {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({FIELD_CONTENT_TYPE, FIELD_FLAGS, FIELD_USAGE, FIELD_ALLOWED_CAPTURE_POLICY})
@IntDef({
FIELD_CONTENT_TYPE,
FIELD_FLAGS,
FIELD_USAGE,
FIELD_ALLOWED_CAPTURE_POLICY,
FIELD_SPATIALIZATION_BEHAVIOR
})
private @interface FieldNumber {}
private static final int FIELD_CONTENT_TYPE = 0;
private static final int FIELD_FLAGS = 1;
private static final int FIELD_USAGE = 2;
private static final int FIELD_ALLOWED_CAPTURE_POLICY = 3;
private static final int FIELD_SPATIALIZATION_BEHAVIOR = 4;
@Override
public Bundle toBundle() {
@ -175,6 +203,7 @@ public final class AudioAttributes implements Bundleable {
bundle.putInt(keyForField(FIELD_FLAGS), flags);
bundle.putInt(keyForField(FIELD_USAGE), usage);
bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy);
bundle.putInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR), spatializationBehavior);
return bundle;
}
@ -194,10 +223,45 @@ public final class AudioAttributes implements Bundleable {
if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) {
builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY)));
}
if (bundle.containsKey(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))) {
builder.setSpatializationBehavior(
bundle.getInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR)));
}
return builder.build();
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
@RequiresApi(29)
private static final class Api29 {
private Api29() {}
@DoNotInline
public static void setAllowedCapturePolicy(
android.media.AudioAttributes.Builder builder,
@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
builder.setAllowedCapturePolicy(allowedCapturePolicy);
}
}
@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.
}
}
}
}

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.os.Bundle;
import androidx.annotation.CheckResult;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Bundleable;
@ -40,6 +41,8 @@ public final class TrackGroup implements Bundleable {
/** The number of tracks in the group. */
public final int length;
/** An identifier for the track group. */
public final String id;
private final Format[] formats;
@ -47,17 +50,39 @@ public final class TrackGroup implements Bundleable {
private int hashCode;
/**
* Constructs an instance {@code TrackGroup} containing the provided {@code formats}.
* Constructs a track group containing the provided {@code formats}.
*
* @param formats Non empty array of format.
* @param formats The list of {@link Format Formats}. Must not be empty.
*/
public TrackGroup(Format... formats) {
this(/* id= */ "", formats);
}
/**
* Constructs a track group with the provided {@code id} and {@code formats}.
*
* @param id The identifier of the track group. May be an empty string.
* @param formats The list of {@link Format Formats}. Must not be empty.
*/
public TrackGroup(String id, Format... formats) {
checkArgument(formats.length > 0);
this.id = id;
this.formats = formats;
this.length = formats.length;
verifyCorrectness();
}
/**
* Returns a copy of this track group with the specified {@code id}.
*
* @param id The identifier for the copy of the track group.
* @return The copied track group.
*/
@CheckResult
public TrackGroup copyWithId(String id) {
return new TrackGroup(id, formats);
}
/**
* Returns the format of the track at a given index.
*
@ -90,6 +115,7 @@ public final class TrackGroup implements Bundleable {
public int hashCode() {
if (hashCode == 0) {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + Arrays.hashCode(formats);
hashCode = result;
}
@ -105,25 +131,25 @@ public final class TrackGroup implements Bundleable {
return false;
}
TrackGroup other = (TrackGroup) obj;
return length == other.length && Arrays.equals(formats, other.formats);
return length == other.length && id.equals(other.id) && Arrays.equals(formats, other.formats);
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_FORMATS,
})
@IntDef({FIELD_FORMATS, FIELD_ID})
private @interface FieldNumber {}
private static final int FIELD_FORMATS = 0;
private static final int FIELD_ID = 1;
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_FORMATS), BundleableUtil.toBundleArrayList(Lists.newArrayList(formats)));
bundle.putString(keyForField(FIELD_ID), id);
return bundle;
}
@ -135,7 +161,8 @@ public final class TrackGroup implements Bundleable {
Format.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)),
ImmutableList.of());
return new TrackGroup(formats.toArray(new Format[0]));
String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ "");
return new TrackGroup(id, formats.toArray(new Format[0]));
};
private static String keyForField(@FieldNumber int field) {

View file

@ -21,32 +21,38 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Bundleable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.BundleableUtil;
import com.google.android.exoplayer2.util.Log;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
/** An immutable array of {@link TrackGroup}s. */
public final class TrackGroupArray implements Bundleable {
private static final String TAG = "TrackGroupArray";
/** The empty array. */
public static final TrackGroupArray EMPTY = new TrackGroupArray();
/** The number of groups in the array. Greater than or equal to zero. */
public final int length;
private final TrackGroup[] trackGroups;
private final ImmutableList<TrackGroup> trackGroups;
// Lazily initialized hashcode.
private int hashCode;
/** Construct a {@code TrackGroupArray} from an array of (possibly empty) trackGroups. */
/**
* Construct a {@code TrackGroupArray} from an array of {@link TrackGroup TrackGroups}.
*
* <p>The groups must not contain duplicates.
*/
public TrackGroupArray(TrackGroup... trackGroups) {
this.trackGroups = trackGroups;
this.trackGroups = ImmutableList.copyOf(trackGroups);
this.length = trackGroups.length;
verifyCorrectness();
}
/**
@ -56,7 +62,7 @@ public final class TrackGroupArray implements Bundleable {
* @return The group.
*/
public TrackGroup get(int index) {
return trackGroups[index];
return trackGroups.get(index);
}
/**
@ -65,16 +71,9 @@ public final class TrackGroupArray implements Bundleable {
* @param group The group.
* @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
*/
@SuppressWarnings("ReferenceEquality")
public int indexOf(TrackGroup group) {
for (int i = 0; i < length; i++) {
// Suppressed reference equality warning because this is looking for the index of a specific
// TrackGroup object, not the index of a potential equal TrackGroup.
if (trackGroups[i] == group) {
return i;
}
}
return C.INDEX_UNSET;
int index = trackGroups.indexOf(group);
return index >= 0 ? index : C.INDEX_UNSET;
}
/** Returns whether this track group array is empty. */
@ -85,7 +84,7 @@ public final class TrackGroupArray implements Bundleable {
@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = Arrays.hashCode(trackGroups);
hashCode = trackGroups.hashCode();
}
return hashCode;
}
@ -99,7 +98,7 @@ public final class TrackGroupArray implements Bundleable {
return false;
}
TrackGroupArray other = (TrackGroupArray) obj;
return length == other.length && Arrays.equals(trackGroups, other.trackGroups);
return length == other.length && trackGroups.equals(other.trackGroups);
}
// Bundleable implementation.
@ -117,8 +116,7 @@ public final class TrackGroupArray implements Bundleable {
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_TRACK_GROUPS),
BundleableUtil.toBundleArrayList(Lists.newArrayList(trackGroups)));
keyForField(FIELD_TRACK_GROUPS), BundleableUtil.toBundleArrayList(trackGroups));
return bundle;
}
@ -133,6 +131,20 @@ public final class TrackGroupArray implements Bundleable {
return new TrackGroupArray(trackGroups.toArray(new TrackGroup[0]));
};
private void verifyCorrectness() {
for (int i = 0; i < trackGroups.size(); i++) {
for (int j = i + 1; j < trackGroups.size(); j++) {
if (trackGroups.get(i).equals(trackGroups.get(j))) {
Log.e(
TAG,
"",
new IllegalArgumentException(
"Multiple identical TrackGroups added to one TrackGroupArray."));
}
}
}
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}

View file

@ -300,6 +300,28 @@ public final class AdPlaybackState implements Bundleable {
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
}
/**
* Returns an instance with all ads in final states (played, skipped, error) reset to either
* available or unavailable, which allows to play them again.
*/
@CheckResult
public AdGroup withAllAdsReset() {
if (count == C.LENGTH_UNSET) {
return this;
}
int count = this.states.length;
@AdState int[] states = Arrays.copyOf(this.states, count);
for (int i = 0; i < count; i++) {
if (states[i] == AD_STATE_PLAYED
|| states[i] == AD_STATE_SKIPPED
|| states[i] == AD_STATE_ERROR) {
states[i] = uris[i] == null ? AD_STATE_UNAVAILABLE : AD_STATE_AVAILABLE;
}
}
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
}
@CheckResult
private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
int oldStateCount = states.length;
@ -783,6 +805,19 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* Returns an instance with all ads in the specified ad group reset from final states (played,
* skipped, error) to either available or unavailable, which allows to play them again.
*/
@CheckResult
public AdPlaybackState withResetAdGroup(@IntRange(from = 0) int adGroupIndex) {
int adjustedIndex = adGroupIndex - removedAdGroupCount;
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
adGroups[adjustedIndex] = adGroups[adjustedIndex].withAllAdsReset();
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {

View file

@ -80,6 +80,7 @@ public class TrackSelectionParameters implements Bundleable {
private int viewportHeight;
private boolean viewportOrientationMayChange;
private ImmutableList<String> preferredVideoMimeTypes;
private @C.RoleFlags int preferredVideoRoleFlags;
// Audio
private ImmutableList<String> preferredAudioLanguages;
private @C.RoleFlags int preferredAudioRoleFlags;
@ -111,6 +112,7 @@ public class TrackSelectionParameters implements Bundleable {
viewportHeight = Integer.MAX_VALUE;
viewportOrientationMayChange = true;
preferredVideoMimeTypes = ImmutableList.of();
preferredVideoRoleFlags = 0;
// Audio
preferredAudioLanguages = ImmutableList.of();
preferredAudioRoleFlags = 0;
@ -183,6 +185,10 @@ public class TrackSelectionParameters implements Bundleable {
firstNonNull(
bundle.getStringArray(keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES)),
new String[0]));
preferredVideoRoleFlags =
bundle.getInt(
keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS),
DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags);
// Audio
String[] preferredAudioLanguages1 =
firstNonNull(
@ -261,6 +267,7 @@ public class TrackSelectionParameters implements Bundleable {
viewportHeight = parameters.viewportHeight;
viewportOrientationMayChange = parameters.viewportOrientationMayChange;
preferredVideoMimeTypes = parameters.preferredVideoMimeTypes;
preferredVideoRoleFlags = parameters.preferredVideoRoleFlags;
// Audio
preferredAudioLanguages = parameters.preferredAudioLanguages;
preferredAudioRoleFlags = parameters.preferredAudioRoleFlags;
@ -441,6 +448,17 @@ public class TrackSelectionParameters implements Bundleable {
return this;
}
/**
* Sets the preferred {@link C.RoleFlags} for video tracks.
*
* @param preferredVideoRoleFlags Preferred video role flags.
* @return This builder.
*/
public Builder setPreferredVideoRoleFlags(@C.RoleFlags int preferredVideoRoleFlags) {
this.preferredVideoRoleFlags = preferredVideoRoleFlags;
return this;
}
// Audio
/**
@ -770,6 +788,11 @@ public class TrackSelectionParameters implements Bundleable {
* no preference. The default is an empty list.
*/
public final ImmutableList<String> preferredVideoMimeTypes;
/**
* The preferred {@link C.RoleFlags} for video tracks. {@code 0} selects the default track if
* there is one, or the first track if there's no default. The default value is {@code 0}.
*/
public final @C.RoleFlags int preferredVideoRoleFlags;
// Audio
/**
* The preferred languages for audio and forced text tracks as IETF BCP 47 conformant tags in
@ -853,6 +876,7 @@ public class TrackSelectionParameters implements Bundleable {
this.viewportHeight = builder.viewportHeight;
this.viewportOrientationMayChange = builder.viewportOrientationMayChange;
this.preferredVideoMimeTypes = builder.preferredVideoMimeTypes;
this.preferredVideoRoleFlags = builder.preferredVideoRoleFlags;
// Audio
this.preferredAudioLanguages = builder.preferredAudioLanguages;
this.preferredAudioRoleFlags = builder.preferredAudioRoleFlags;
@ -898,6 +922,7 @@ public class TrackSelectionParameters implements Bundleable {
&& viewportWidth == other.viewportWidth
&& viewportHeight == other.viewportHeight
&& preferredVideoMimeTypes.equals(other.preferredVideoMimeTypes)
&& preferredVideoRoleFlags == other.preferredVideoRoleFlags
// Audio
&& preferredAudioLanguages.equals(other.preferredAudioLanguages)
&& preferredAudioRoleFlags == other.preferredAudioRoleFlags
@ -930,6 +955,7 @@ public class TrackSelectionParameters implements Bundleable {
result = 31 * result + viewportWidth;
result = 31 * result + viewportHeight;
result = 31 * result + preferredVideoMimeTypes.hashCode();
result = 31 * result + preferredVideoRoleFlags;
// Audio
result = 31 * result + preferredAudioLanguages.hashCode();
result = 31 * result + preferredAudioRoleFlags;
@ -978,6 +1004,7 @@ public class TrackSelectionParameters implements Bundleable {
FIELD_SELECTION_OVERRIDE_KEYS,
FIELD_SELECTION_OVERRIDE_VALUES,
FIELD_DISABLED_TRACK_TYPE,
FIELD_PREFERRED_VIDEO_ROLE_FLAGS
})
private @interface FieldNumber {}
@ -1006,6 +1033,7 @@ public class TrackSelectionParameters implements Bundleable {
private static final int FIELD_SELECTION_OVERRIDE_KEYS = 23;
private static final int FIELD_SELECTION_OVERRIDE_VALUES = 24;
private static final int FIELD_DISABLED_TRACK_TYPE = 25;
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 26;
@Override
public Bundle toBundle() {
@ -1027,6 +1055,7 @@ public class TrackSelectionParameters implements Bundleable {
bundle.putStringArray(
keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES),
preferredVideoMimeTypes.toArray(new String[0]));
bundle.putInt(keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS), preferredVideoRoleFlags);
// Audio
bundle.putStringArray(
keyForField(FIELD_PREFERRED_AUDIO_LANGUAGES),

View file

@ -15,12 +15,19 @@
*/
package com.google.android.exoplayer2.ui;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.view.View;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Provides information about an overlay view shown on top of an ad view group. */
public final class AdOverlayInfo {
@ -31,41 +38,70 @@ public final class AdOverlayInfo {
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({PURPOSE_CONTROLS, PURPOSE_CLOSE_AD, PURPOSE_OTHER, PURPOSE_NOT_VISIBLE})
public @interface Purpose {}
/** Purpose for playback controls overlaying the player. */
public static final int PURPOSE_CONTROLS = 0;
public static final int PURPOSE_CONTROLS = 1;
/** Purpose for ad close buttons overlaying the player. */
public static final int PURPOSE_CLOSE_AD = 1;
public static final int PURPOSE_CLOSE_AD = 2;
/** Purpose for other overlays. */
public static final int PURPOSE_OTHER = 2;
public static final int PURPOSE_OTHER = 3;
/** Purpose for overlays that are not visible. */
public static final int PURPOSE_NOT_VISIBLE = 3;
public static final int PURPOSE_NOT_VISIBLE = 4;
/** A builder for {@link AdOverlayInfo} instances. */
public static final class Builder {
private final View view;
private final @Purpose int purpose;
@Nullable private String detailedReason;
/**
* Creates a new builder.
*
* @param view The view that is overlaying the player.
* @param purpose The purpose of the view.
*/
public Builder(View view, @Purpose int purpose) {
this.view = view;
this.purpose = purpose;
}
/**
* Sets an optional, detailed reason that the view is on top of the player.
*
* @return This builder, for convenience.
*/
public Builder setDetailedReason(@Nullable String detailedReason) {
this.detailedReason = detailedReason;
return this;
}
/** Returns a new {@link AdOverlayInfo} instance with the current builder values. */
// Using deprecated constructor while it still exists.
@SuppressWarnings("deprecation")
public AdOverlayInfo build() {
return new AdOverlayInfo(view, purpose, detailedReason);
}
}
/** The overlay view. */
public final View view;
/** The purpose of the overlay view. */
@Purpose public final int purpose;
public final @Purpose int purpose;
/** An optional, detailed reason that the overlay view is needed. */
@Nullable public final String reasonDetail;
/**
* Creates a new overlay info.
*
* @param view The view that is overlaying the player.
* @param purpose The purpose of the view.
*/
/** @deprecated Use {@link Builder} instead. */
@Deprecated
public AdOverlayInfo(View view, @Purpose int purpose) {
this(view, purpose, /* detailedReason= */ null);
}
/**
* Creates a new overlay info.
*
* @param view The view that is overlaying the player.
* @param purpose The purpose of the view.
* @param detailedReason An optional, detailed reason that the view is on top of the player.
*/
/** @deprecated Use {@link Builder} instead. */
@Deprecated
public AdOverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) {
this.view = view;
this.purpose = purpose;

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.util;
import static android.opengl.GLU.gluErrorString;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.content.Context;
import android.content.pm.PackageManager;
@ -26,7 +27,6 @@ import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.text.TextUtils;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@ -38,9 +38,11 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Map;
import javax.microedition.khronos.egl.EGL10;
/** GL utilities. */
/** OpenGL ES 2.0 utilities. */
public final class GlUtil {
/** Thrown when an OpenGL error occurs and {@link #glAssertionsEnabled} is {@code true}. */
@ -51,28 +53,20 @@ public final class GlUtil {
}
}
/** Thrown when the required EGL version is not supported by the device. */
public static final class UnsupportedEglVersionException extends Exception {}
/** GL program. */
/**
* Represents a GLSL shader program.
*
* <p>After constructing a program, keep a reference for its lifetime and call {@link #delete()}
* (or release the current GL context) when it's no longer needed.
*/
public static final class Program {
/** The identifier of a compiled and linked GLSL shader program. */
private final int programId;
/**
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
*
* @param vertexShaderGlsl The vertex shader program.
* @param fragmentShaderGlsl The fragment shader program.
*/
public Program(String vertexShaderGlsl, String fragmentShaderGlsl) {
programId = GLES20.glCreateProgram();
checkGlError();
// Add the vertex and fragment shaders.
addShader(GLES20.GL_VERTEX_SHADER, vertexShaderGlsl);
addShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderGlsl);
}
private final Attribute[] attributes;
private final Uniform[] uniforms;
private final Map<String, Attribute> attributeByName;
private final Map<String, Uniform> uniformByName;
/**
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
@ -88,267 +82,123 @@ public final class GlUtil {
}
/**
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
* Creates a GL shader program from vertex and fragment shader GLSL GLES20 code.
*
* @param vertexShaderGlsl The vertex shader program as arrays of strings. Strings are joined by
* adding a new line character in between each of them.
* @param fragmentShaderGlsl The fragment shader program as arrays of strings. Strings are
* joined by adding a new line character in between each of them.
* <p>This involves slow steps, like compiling, linking, and switching the GL program, so do not
* call this in fast rendering loops.
*
* @param vertexShaderGlsl The vertex shader program.
* @param fragmentShaderGlsl The fragment shader program.
*/
public Program(String[] vertexShaderGlsl, String[] fragmentShaderGlsl) {
this(TextUtils.join("\n", vertexShaderGlsl), TextUtils.join("\n", fragmentShaderGlsl));
}
public Program(String vertexShaderGlsl, String fragmentShaderGlsl) {
programId = GLES20.glCreateProgram();
checkGlError();
/** Uses the program. */
public void use() {
// Link and check for errors.
// Add the vertex and fragment shaders.
addShader(programId, GLES20.GL_VERTEX_SHADER, vertexShaderGlsl);
addShader(programId, GLES20.GL_FRAGMENT_SHADER, fragmentShaderGlsl);
// Link and use the program, and enumerate attributes/uniforms.
GLES20.glLinkProgram(programId);
int[] linkStatus = new int[] {GLES20.GL_FALSE};
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
throwGlException(
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
}
checkGlError();
GLES20.glUseProgram(programId);
attributeByName = new HashMap<>();
int[] attributeCount = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, /* offset= */ 0);
attributes = new Attribute[attributeCount[0]];
for (int i = 0; i < attributeCount[0]; i++) {
Attribute attribute = Attribute.create(programId, i);
attributes[i] = attribute;
attributeByName.put(attribute.name, attribute);
}
uniformByName = new HashMap<>();
int[] uniformCount = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, /* offset= */ 0);
uniforms = new Uniform[uniformCount[0]];
for (int i = 0; i < uniformCount[0]; i++) {
Uniform uniform = Uniform.create(programId, i);
uniforms[i] = uniform;
uniformByName.put(uniform.name, uniform);
}
checkGlError();
}
/**
* Uses the program.
*
* <p>Call this in the rendering loop to switch between different programs.
*/
public void use() {
// TODO(http://b/205002913): When multiple GL programs are supported by Transformer, make sure
// to call use() to switch between programs.
GLES20.glUseProgram(programId);
checkGlError();
}
/** Deletes the program. Deleted programs cannot be used again. */
public void delete() {
GLES20.glDeleteProgram(programId);
checkGlError();
}
/**
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
* array.
*/
public int getAttributeArrayLocationAndEnable(String attributeName) {
int location = getAttributeLocation(attributeName);
GLES20.glEnableVertexAttribArray(location);
checkGlError();
return location;
}
/** Returns the location of an {@link Attribute}. */
public int getAttribLocation(String attributeName) {
return GLES20.glGetAttribLocation(programId, attributeName);
private int getAttributeLocation(String attributeName) {
return GlUtil.getAttributeLocation(programId, attributeName);
}
/** Returns the location of a {@link Uniform}. */
public int getUniformLocation(String uniformName) {
return GLES20.glGetUniformLocation(programId, uniformName);
return GlUtil.getUniformLocation(programId, uniformName);
}
/** Returns the program's {@link Attribute}s. */
public Attribute[] getAttributes() {
int[] attributeCount = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0);
if (attributeCount[0] != 2) {
throw new IllegalStateException("Expected two attributes.");
/** Sets a float buffer type attribute. */
public void setBufferAttribute(String name, float[] values, int size) {
checkNotNull(attributeByName.get(name)).setBuffer(values, size);
}
/** Sets a texture sampler type uniform. */
public void setSamplerTexIdUniform(String name, int texId, int unit) {
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, unit);
}
/** Sets a float type uniform. */
public void setFloatUniform(String name, float value) {
checkNotNull(uniformByName.get(name)).setFloat(value);
}
/** Sets a float array type uniform. */
public void setFloatsUniform(String name, float[] value) {
checkNotNull(uniformByName.get(name)).setFloats(value);
}
/** Binds all attributes and uniforms in the program. */
public void bindAttributesAndUniforms() {
for (Attribute attribute : attributes) {
attribute.bind();
}
Attribute[] attributes = new Attribute[attributeCount[0]];
for (int i = 0; i < attributeCount[0]; i++) {
attributes[i] = createAttribute(i);
for (Uniform uniform : uniforms) {
uniform.bind();
}
return attributes;
}
/** Returns the program's {@link Uniform}s. */
public Uniform[] getUniforms() {
int[] uniformCount = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0);
Uniform[] uniforms = new Uniform[uniformCount[0]];
for (int i = 0; i < uniformCount[0]; i++) {
uniforms[i] = createUniform(i);
}
return uniforms;
}
private Attribute createAttribute(int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, length, 0);
int[] type = new int[1];
int[] size = new int[1];
byte[] nameBytes = new byte[length[0]];
int[] ignore = new int[1];
GLES20.glGetActiveAttrib(
programId, index, length[0], ignore, 0, size, 0, type, 0, nameBytes, 0);
String name = new String(nameBytes, 0, strlen(nameBytes));
int location = getAttribLocation(name);
return new Attribute(name, index, location);
}
private Uniform createUniform(int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, length, 0);
int[] type = new int[1];
int[] size = new int[1];
byte[] nameBytes = new byte[length[0]];
int[] ignore = new int[1];
GLES20.glGetActiveUniform(
programId, index, length[0], ignore, 0, size, 0, type, 0, nameBytes, 0);
String name = new String(nameBytes, 0, strlen(nameBytes));
int location = getUniformLocation(name);
return new Uniform(name, location, type[0]);
}
private void addShader(int type, String glsl) {
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, 0);
if (result[0] != GLES20.GL_TRUE) {
throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
}
GLES20.glAttachShader(programId, shader);
GLES20.glDeleteShader(shader);
checkGlError();
}
}
/**
* GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
*/
public static final class Attribute {
/** The name of the attribute in the GLSL sources. */
public final String name;
private final int index;
private final int location;
@Nullable private Buffer buffer;
private int size;
/* Creates a new Attribute. */
public Attribute(String name, int index, int location) {
this.name = name;
this.index = index;
this.location = location;
}
/**
* Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
* elements) to this {@link Attribute}.
*
* @param buffer Buffer to bind to this attribute.
* @param size Number of elements per vertex.
*/
public void setBuffer(float[] buffer, int size) {
this.buffer = createBuffer(buffer);
this.size = size;
}
/**
* Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind");
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
GLES20.glVertexAttribPointer(
location,
size, // count
GLES20.GL_FLOAT, // type
false, // normalize
0, // stride
buffer);
GLES20.glEnableVertexAttribArray(index);
checkGlError();
}
}
/**
* GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
*/
public static final class Uniform {
/** The name of the uniform in the GLSL sources. */
public final String name;
private final int location;
private final int type;
private final float[] value;
private int texId;
private int unit;
/** Creates a new uniform. */
public Uniform(String name, int location, int type) {
this.name = name;
this.location = location;
this.type = type;
this.value = new float[16];
}
/**
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
*
* @param texId The GL texture identifier from which to sample.
* @param unit The GL texture unit index.
*/
public void setSamplerTexId(int texId, int unit) {
this.texId = texId;
this.unit = unit;
}
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
public void setFloat(float value) {
this.value[0] = value;
}
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
public void setFloats(float[] value) {
System.arraycopy(value, 0, this.value, 0, value.length);
}
/**
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
* #setFloat(float)} or {@link #setFloats(float[])}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
if (type == GLES20.GL_FLOAT) {
GLES20.glUniform1fv(location, 1, value, 0);
checkGlError();
return;
}
if (type == GLES20.GL_FLOAT_MAT4) {
GLES20.glUniformMatrix4fv(location, 1, false, value, 0);
checkGlError();
return;
}
if (texId == 0) {
throw new IllegalStateException("Call setSamplerTexId before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) {
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
} else if (type == GLES20.GL_SAMPLER_2D) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
} else {
throw new IllegalStateException("Unexpected uniform type: " + type);
}
GLES20.glUniform1i(location, unit);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
}
}
/** Represents an unset texture ID. */
public static final int TEXTURE_ID_UNSET = -1;
/** Whether to throw a {@link GlException} in case of an OpenGL error. */
public static boolean glAssertionsEnabled = false;
@ -406,15 +256,9 @@ public final class GlUtil {
return Api17.createEglDisplay();
}
/**
* Returns a new {@link EGLContext} for the specified {@link EGLDisplay}.
*
* @throws UnsupportedEglVersionException If the device does not support EGL version 2. {@code
* eglDisplay} is terminated before the exception is thrown in this case.
*/
/** Returns a new {@link EGLContext} for the specified {@link EGLDisplay}. */
@RequiresApi(17)
public static EGLContext createEglContext(EGLDisplay eglDisplay)
throws UnsupportedEglVersionException {
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
return Api17.createEglContext(eglDisplay);
}
@ -437,11 +281,11 @@ public final class GlUtil {
int lastError = GLES20.GL_NO_ERROR;
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, "glError " + gluErrorString(error));
Log.e(TAG, "glError: " + gluErrorString(error));
lastError = error;
}
if (lastError != GLES20.GL_NO_ERROR) {
throwGlException("glError " + gluErrorString(lastError));
throwGlException("glError: " + gluErrorString(lastError));
}
}
@ -461,8 +305,7 @@ public final class GlUtil {
* @param textureId The ID of the texture to delete.
*/
public static void deleteTexture(int textureId) {
int[] textures = new int[] {textureId};
GLES20.glDeleteTextures(1, textures, 0);
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
checkGlError();
}
@ -519,7 +362,7 @@ public final class GlUtil {
*/
public static int createExternalTexture() {
int[] texId = new int[1];
GLES20.glGenTextures(1, IntBuffer.wrap(texId));
GLES20.glGenTextures(/* n= */ 1, IntBuffer.wrap(texId));
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
@ -533,6 +376,30 @@ public final class GlUtil {
return texId[0];
}
private static void addShader(int programId, int type, String glsl) {
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) {
throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
}
GLES20.glAttachShader(programId, shader);
GLES20.glDeleteShader(shader);
checkGlError();
}
private static int getAttributeLocation(int programId, String attributeName) {
return GLES20.glGetAttribLocation(programId, attributeName);
}
private static int getUniformLocation(int programId, String uniformName) {
return GLES20.glGetUniformLocation(programId, uniformName);
}
private static void throwGlException(String errorMsg) {
Log.e(TAG, errorMsg);
if (glAssertionsEnabled) {
@ -556,6 +423,190 @@ public final class GlUtil {
return strVal.length;
}
/**
* GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
*/
private static final class Attribute {
/* Returns the attribute at the given index in the program. */
public static Attribute create(int programId, int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, length, /* offset= */ 0);
byte[] nameBytes = new byte[length[0]];
GLES20.glGetActiveAttrib(
programId,
index,
length[0],
/* unusedLength */ new int[1],
/* lengthOffset= */ 0,
/* unusedSize */ new int[1],
/* sizeOffset= */ 0,
/* unusedType */ new int[1],
/* typeOffset= */ 0,
nameBytes,
/* nameOffset= */ 0);
String name = new String(nameBytes, /* offset= */ 0, strlen(nameBytes));
int location = getAttributeLocation(programId, name);
return new Attribute(name, index, location);
}
/** The name of the attribute in the GLSL sources. */
public final String name;
private final int index;
private final int location;
@Nullable private Buffer buffer;
private int size;
private Attribute(String name, int index, int location) {
this.name = name;
this.index = index;
this.location = location;
}
/**
* Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
* elements) to this {@link Attribute}.
*
* @param buffer Buffer to bind to this attribute.
* @param size Number of elements per vertex.
*/
public void setBuffer(float[] buffer, int size) {
this.buffer = createBuffer(buffer);
this.size = size;
}
/**
* Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
GLES20.glVertexAttribPointer(
location, size, GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0, buffer);
GLES20.glEnableVertexAttribArray(index);
checkGlError();
}
}
/**
* GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
*/
private static final class Uniform {
/** Returns the uniform at the given index in the program. */
public static Uniform create(int programId, int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, length, /* offset= */ 0);
int[] type = new int[1];
byte[] nameBytes = new byte[length[0]];
GLES20.glGetActiveUniform(
programId,
index,
length[0],
/* unusedLength */ new int[1],
/* lengthOffset= */ 0,
/* unusedSize */ new int[1],
/*sizeOffset= */ 0,
type,
/* typeOffset= */ 0,
nameBytes,
/* nameOffset= */ 0);
String name = new String(nameBytes, /* offset= */ 0, strlen(nameBytes));
int location = getUniformLocation(programId, name);
return new Uniform(name, location, type[0]);
}
/** The name of the uniform in the GLSL sources. */
public final String name;
private final int location;
private final int type;
private final float[] value;
private int texId;
private int unit;
private Uniform(String name, int location, int type) {
this.name = name;
this.location = location;
this.type = type;
this.value = new float[16];
}
/**
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
*
* @param texId The GL texture identifier from which to sample.
* @param unit The GL texture unit index.
*/
public void setSamplerTexId(int texId, int unit) {
this.texId = texId;
this.unit = unit;
}
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
public void setFloat(float value) {
this.value[0] = value;
}
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
public void setFloats(float[] value) {
System.arraycopy(value, /* srcPos= */ 0, this.value, /* destPos= */ 0, value.length);
}
/**
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
* #setFloat(float)} or {@link #setFloats(float[])}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
if (type == GLES20.GL_FLOAT) {
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
checkGlError();
return;
}
if (type == GLES20.GL_FLOAT_MAT4) {
GLES20.glUniformMatrix4fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
checkGlError();
return;
}
if (texId == 0) {
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) {
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
} else if (type == GLES20.GL_SAMPLER_2D) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
} else {
throw new IllegalStateException("Unexpected uniform type: " + type);
}
GLES20.glUniform1i(location, unit);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
}
}
@RequiresApi(17)
private static final class Api17 {
private Api17() {}
@ -564,9 +615,12 @@ public final class GlUtil {
public static EGLDisplay createEglDisplay() {
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
checkEglException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
int[] major = new int[1];
int[] minor = new int[1];
if (!EGL14.eglInitialize(eglDisplay, major, 0, minor, 0)) {
if (!EGL14.eglInitialize(
eglDisplay,
/* unusedMajor */ new int[1],
/* majorOffset= */ 0,
/* unusedMinor */ new int[1],
/* minorOffset= */ 0)) {
throwGlException("Error in eglInitialize.");
}
checkGlError();
@ -574,15 +628,20 @@ public final class GlUtil {
}
@DoNotInline
public static EGLContext createEglContext(EGLDisplay eglDisplay)
throws UnsupportedEglVersionException {
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
EGLContext eglContext =
EGL14.eglCreateContext(
eglDisplay, getEglConfig(eglDisplay), EGL14.EGL_NO_CONTEXT, contextAttributes, 0);
eglDisplay,
getEglConfig(eglDisplay),
EGL14.EGL_NO_CONTEXT,
contextAttributes,
/* offset= */ 0);
if (eglContext == null) {
EGL14.eglTerminate(eglDisplay);
throw new UnsupportedEglVersionException();
throwGlException(
"eglCreateContext() failed to create a valid context. The device may not support EGL"
+ " version 2");
}
checkGlError();
return eglContext;
@ -591,20 +650,24 @@ public final class GlUtil {
@DoNotInline
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) {
return EGL14.eglCreateWindowSurface(
eglDisplay, getEglConfig(eglDisplay), surface, new int[] {EGL14.EGL_NONE}, 0);
eglDisplay,
getEglConfig(eglDisplay),
surface,
new int[] {EGL14.EGL_NONE},
/* offset= */ 0);
}
@DoNotInline
public static void focusSurface(
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface surface, int width, int height) {
int[] fbos = new int[1];
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, fbos, 0);
int noFbo = 0;
if (fbos[0] != noFbo) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, noFbo);
int[] boundFrameBuffer = new int[1];
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, boundFrameBuffer, /* offset= */ 0);
int defaultFrameBuffer = 0;
if (boundFrameBuffer[0] != defaultFrameBuffer) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, defaultFrameBuffer);
}
EGL14.eglMakeCurrent(eglDisplay, surface, surface, eglContext);
GLES20.glViewport(0, 0, width, height);
GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height);
}
@DoNotInline
@ -632,27 +695,27 @@ public final class GlUtil {
@DoNotInline
private static EGLConfig getEglConfig(EGLDisplay eglDisplay) {
int redSize = 8;
int greenSize = 8;
int blueSize = 8;
int alphaSize = 8;
int depthSize = 0;
int stencilSize = 0;
int[] defaultConfiguration =
new int[] {
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, redSize,
EGL14.EGL_GREEN_SIZE, greenSize,
EGL14.EGL_BLUE_SIZE, blueSize,
EGL14.EGL_ALPHA_SIZE, alphaSize,
EGL14.EGL_DEPTH_SIZE, depthSize,
EGL14.EGL_STENCIL_SIZE, stencilSize,
EGL14.EGL_RED_SIZE, /* redSize= */ 8,
EGL14.EGL_GREEN_SIZE, /* greenSize= */ 8,
EGL14.EGL_BLUE_SIZE, /* blueSize= */ 8,
EGL14.EGL_ALPHA_SIZE, /* alphaSize= */ 8,
EGL14.EGL_DEPTH_SIZE, /* depthSize= */ 0,
EGL14.EGL_STENCIL_SIZE, /* stencilSize= */ 0,
EGL14.EGL_NONE
};
int[] configsCount = new int[1];
EGLConfig[] eglConfigs = new EGLConfig[1];
if (!EGL14.eglChooseConfig(
eglDisplay, defaultConfiguration, 0, eglConfigs, 0, 1, configsCount, 0)) {
eglDisplay,
defaultConfiguration,
/* attrib_listOffset= */ 0,
eglConfigs,
/* configsOffset= */ 0,
/* config_size= */ 1,
/* unusedNumConfig */ new int[1],
/* num_configOffset= */ 0)) {
throwGlException("eglChooseConfig failed.");
}
return eglConfigs[0];

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -78,7 +79,7 @@ public final class Log {
/** @see android.util.Log#d(String, String) */
@Pure
public static void d(String tag, String message) {
public static void d(@Size(max = 23) String tag, String message) {
if (logLevel == LOG_LEVEL_ALL) {
android.util.Log.d(tag, message);
}
@ -86,13 +87,13 @@ public final class Log {
/** @see android.util.Log#d(String, String, Throwable) */
@Pure
public static void d(String tag, String message, @Nullable Throwable throwable) {
public static void d(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
d(tag, appendThrowableString(message, throwable));
}
/** @see android.util.Log#i(String, String) */
@Pure
public static void i(String tag, String message) {
public static void i(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_INFO) {
android.util.Log.i(tag, message);
}
@ -100,13 +101,13 @@ public final class Log {
/** @see android.util.Log#i(String, String, Throwable) */
@Pure
public static void i(String tag, String message, @Nullable Throwable throwable) {
public static void i(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
i(tag, appendThrowableString(message, throwable));
}
/** @see android.util.Log#w(String, String) */
@Pure
public static void w(String tag, String message) {
public static void w(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_WARNING) {
android.util.Log.w(tag, message);
}
@ -114,13 +115,13 @@ public final class Log {
/** @see android.util.Log#w(String, String, Throwable) */
@Pure
public static void w(String tag, String message, @Nullable Throwable throwable) {
public static void w(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
w(tag, appendThrowableString(message, throwable));
}
/** @see android.util.Log#e(String, String) */
@Pure
public static void e(String tag, String message) {
public static void e(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_ERROR) {
android.util.Log.e(tag, message);
}
@ -128,7 +129,7 @@ public final class Log {
/** @see android.util.Log#e(String, String, Throwable) */
@Pure
public static void e(String tag, String message, @Nullable Throwable throwable) {
public static void e(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
e(tag, appendThrowableString(message, throwable));
}

View file

@ -16,6 +16,15 @@
package com.google.android.exoplayer2.util;
import static android.content.Context.UI_MODE_SERVICE;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_BACK;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_FORWARD;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static java.lang.Math.abs;
import static java.lang.Math.max;
@ -63,6 +72,8 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.Commands;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets;
import java.io.ByteArrayOutputStream;
@ -1130,6 +1141,18 @@ public final class Util {
return (timeMs == C.TIME_UNSET || timeMs == C.TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);
}
/**
* Converts a time in seconds to the corresponding time in microseconds.
*
* @param timeSec The time in seconds.
* @return The corresponding time in microseconds.
*/
public static long secToUs(double timeSec) {
return BigDecimal.valueOf(timeSec)
.multiply(BigDecimal.valueOf(C.MICROS_PER_SECOND))
.longValue();
}
/**
* Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
*
@ -2477,6 +2500,43 @@ public final class Util {
}
}
/**
* Returns the {@link Commands} available in the {@link Player}.
*
* @param player The {@link Player}.
* @param permanentAvailableCommands The commands permanently available in the player.
* @return The available {@link Commands}.
*/
public static Commands getAvailableCommands(Player player, Commands permanentAvailableCommands) {
boolean isPlayingAd = player.isPlayingAd();
boolean isCurrentMediaItemSeekable = player.isCurrentMediaItemSeekable();
boolean hasPreviousMediaItem = player.hasPreviousMediaItem();
boolean hasNextMediaItem = player.hasNextMediaItem();
boolean isCurrentMediaItemLive = player.isCurrentMediaItemLive();
boolean isCurrentMediaItemDynamic = player.isCurrentMediaItemDynamic();
boolean isTimelineEmpty = player.getCurrentTimeline().isEmpty();
return new Commands.Builder()
.addAll(permanentAvailableCommands)
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd)
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentMediaItemSeekable && !isPlayingAd)
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem && !isPlayingAd)
.addIf(
COMMAND_SEEK_TO_PREVIOUS,
!isTimelineEmpty
&& (hasPreviousMediaItem || !isCurrentMediaItemLive || isCurrentMediaItemSeekable)
&& !isPlayingAd)
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem && !isPlayingAd)
.addIf(
COMMAND_SEEK_TO_NEXT,
!isTimelineEmpty
&& (hasNextMediaItem || (isCurrentMediaItemLive && isCurrentMediaItemDynamic))
&& !isPlayingAd)
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd)
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable && !isPlayingAd)
.addIf(COMMAND_SEEK_FORWARD, isCurrentMediaItemSeekable && !isPlayingAd)
.build();
}
@Nullable
private static String getSystemProperty(String name) {
try {

View file

@ -278,6 +278,7 @@ public class MediaItemTest {
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build());
MediaItem mediaItem =
@ -619,6 +620,7 @@ public class MediaItemTest {
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build()))
.setTag(new Object())
.build();
@ -675,6 +677,7 @@ public class MediaItemTest {
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build()))
.setTag(new Object())
.build();

View file

@ -34,6 +34,7 @@ public class AudioAttributesTest {
.setFlags(C.FLAG_AUDIBILITY_ENFORCED)
.setUsage(C.USAGE_ALARM)
.setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM)
.setSpatializationBehavior(C.SPATIALIZATION_BEHAVIOR_NEVER)
.build();
assertThat(AudioAttributes.CREATOR.fromBundle(audioAttributes.toBundle()))

View file

@ -32,8 +32,9 @@ public final class TrackGroupTest {
Format.Builder formatBuilder = new Format.Builder();
Format format1 = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build();
Format format2 = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_AAC).build();
String id = "abc";
TrackGroup trackGroupToBundle = new TrackGroup(format1, format2);
TrackGroup trackGroupToBundle = new TrackGroup(id, format1, format2);
TrackGroup trackGroupFromBundle = TrackGroup.CREATOR.fromBundle(trackGroupToBundle.toBundle());

View file

@ -16,7 +16,10 @@
package com.google.android.exoplayer2.source.ads;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_ERROR;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_SKIPPED;
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@ -254,6 +257,60 @@ public class AdPlaybackStateTest {
assertThat(state.getAdGroup(1).count).isEqualTo(0);
}
@Test
public void withResetAdGroup_beforeSetAdCount_doesNothing() {
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
state = state.withResetAdGroup(/* adGroupIndex= */ 1);
assertThat(state.getAdGroup(1).count).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void withResetAdGroup_resetsAdsInFinalStates() {
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 5);
state =
state.withAdDurationsUs(
/* adGroupIndex= */ 1, /* adDurationsUs...= */ 1_000L, 2_000L, 3_000L, 4_000L, 5_000L);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, Uri.EMPTY);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, Uri.EMPTY);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, Uri.EMPTY);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, Uri.EMPTY);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2);
state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3);
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4);
// Verify setup.
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states)
.asList()
.containsExactly(
AD_STATE_UNAVAILABLE,
AD_STATE_AVAILABLE,
AD_STATE_PLAYED,
AD_STATE_SKIPPED,
AD_STATE_ERROR)
.inOrder();
state = state.withResetAdGroup(/* adGroupIndex= */ 1);
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states)
.asList()
.containsExactly(
AD_STATE_UNAVAILABLE,
AD_STATE_AVAILABLE,
AD_STATE_AVAILABLE,
AD_STATE_AVAILABLE,
AD_STATE_AVAILABLE)
.inOrder();
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).uris)
.asList()
.containsExactly(null, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY)
.inOrder();
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).durationsUs)
.asList()
.containsExactly(1_000L, 2_000L, 3_000L, 4_000L, 5_000L);
}
@Test
public void roundTripViaBundle_yieldsEqualFieldsExceptAdsId() {
AdPlaybackState originalState =

View file

@ -47,6 +47,7 @@ dependencies {
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion

View file

@ -9,6 +9,10 @@
-keepclassmembers class com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer {
<init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
}
-dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer {
<init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
}
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
<init>(android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioSink);

View file

@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer.InsufficientCapacityException;
import com.google.android.exoplayer2.source.SampleStream;
@ -26,6 +28,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadFlags;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An abstract base class suitable for most {@link Renderer} implementations. */
public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@ -35,6 +38,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Nullable private RendererConfiguration configuration;
private int index;
private @MonotonicNonNull PlayerId playerId;
private int state;
@Nullable private SampleStream stream;
@Nullable private Format[] streamFormats;
@ -65,8 +69,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
}
@Override
public final void setIndex(int index) {
public final void init(int index, PlayerId playerId) {
this.index = index;
this.playerId = playerId;
}
@Override
@ -328,11 +333,24 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
return Assertions.checkNotNull(configuration);
}
/** Returns the index of the renderer within the player. */
/**
* Returns the index of the renderer within the player.
*
* <p>Must only be used after the renderer has been initialized by the player.
*/
protected final int getIndex() {
return index;
}
/**
* Returns the {@link PlayerId} of the player using this renderer.
*
* <p>Must only be used after the renderer has been initialized by the player.
*/
protected final PlayerId getPlayerId() {
return checkNotNull(playerId);
}
/**
* Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for
* this renderer.

View file

@ -26,7 +26,6 @@ import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink.DefaultAudioProcessorChain;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.mediacodec.DefaultMediaCodecAdapterFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
@ -454,6 +453,32 @@ public class DefaultRenderersFactory implements RenderersFactory {
// The extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating AV1 extension", e);
}
try {
// Full class names used for constructor args so the LINT rule triggers if any of them move.
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer");
Constructor<?> constructor =
clazz.getConstructor(
long.class,
android.os.Handler.class,
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
int.class);
Renderer renderer =
(Renderer)
constructor.newInstance(
allowedVideoJoiningTimeMs,
eventHandler,
eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded FfmpegVideoRenderer.");
} 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 FFmpeg extension", e);
}
}
/**
@ -640,14 +665,15 @@ public class DefaultRenderersFactory implements RenderersFactory {
boolean enableFloatOutput,
boolean enableAudioTrackPlaybackParams,
boolean enableOffload) {
return new DefaultAudioSink(
AudioCapabilities.getCapabilities(context),
new DefaultAudioProcessorChain(),
enableFloatOutput,
enableAudioTrackPlaybackParams,
enableOffload
? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
: DefaultAudioSink.OFFLOAD_MODE_DISABLED);
return new DefaultAudioSink.Builder()
.setAudioCapabilities(AudioCapabilities.getCapabilities(context))
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
.setOffloadMode(
enableOffload
? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
: DefaultAudioSink.OFFLOAD_MODE_DISABLED)
.build();
}
/**

View file

@ -33,7 +33,6 @@ import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
@ -44,7 +43,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
@ -78,7 +76,7 @@ import java.util.List;
* <ul>
* <li><b>{@link MediaSource MediaSources}</b> that define the media to be played, load the media,
* and from which the loaded media can be read. MediaSources are created from {@link MediaItem
* MediaItems} by the {@link MediaSourceFactory} injected into the player {@link
* MediaItems} by the {@link MediaSource.Factory} injected into the player {@link
* Builder#setMediaSourceFactory Builder}, or can be added directly by methods like {@link
* #setMediaSource(MediaSource)}. The library provides a {@link DefaultMediaSourceFactory} for
* progressive media files, DASH, SmoothStreaming and HLS, which also includes functionality
@ -369,7 +367,7 @@ public interface ExoPlayer extends Player {
/* package */ Clock clock;
/* package */ long foregroundModeTimeoutMs;
/* package */ Supplier<RenderersFactory> renderersFactorySupplier;
/* package */ Supplier<MediaSourceFactory> mediaSourceFactorySupplier;
/* package */ Supplier<MediaSource.Factory> mediaSourceFactorySupplier;
/* package */ Supplier<TrackSelector> trackSelectorSupplier;
/* package */ Supplier<LoadControl> loadControlSupplier;
/* package */ Supplier<BandwidthMeter> bandwidthMeterSupplier;
@ -397,7 +395,7 @@ public interface ExoPlayer extends Player {
* Creates a builder.
*
* <p>Use {@link #Builder(Context, RenderersFactory)}, {@link #Builder(Context,
* MediaSourceFactory)} or {@link #Builder(Context, RenderersFactory, MediaSourceFactory)}
* MediaSource.Factory)} or {@link #Builder(Context, RenderersFactory, MediaSource.Factory)}
* instead, if you intend to provide a custom {@link RenderersFactory}, {@link
* ExtractorsFactory} or {@link DefaultMediaSourceFactory}. This is to ensure that ProGuard or
* R8 can remove ExoPlayer's {@link DefaultRenderersFactory}, {@link DefaultExtractorsFactory}
@ -408,7 +406,7 @@ public interface ExoPlayer extends Player {
* <ul>
* <li>{@link RenderersFactory}: {@link DefaultRenderersFactory}
* <li>{@link TrackSelector}: {@link DefaultTrackSelector}
* <li>{@link MediaSourceFactory}: {@link DefaultMediaSourceFactory}
* <li>{@link MediaSource.Factory}: {@link DefaultMediaSourceFactory}
* <li>{@link LoadControl}: {@link DefaultLoadControl}
* <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)}
* <li>{@link LivePlaybackSpeedControl}: {@link DefaultLivePlaybackSpeedControl}
@ -463,7 +461,7 @@ public interface ExoPlayer extends Player {
}
/**
* Creates a builder with a custom {@link MediaSourceFactory}.
* Creates a builder with a custom {@link MediaSource.Factory}.
*
* <p>See {@link #Builder(Context)} for a list of default values.
*
@ -475,12 +473,12 @@ public interface ExoPlayer extends Player {
* @param mediaSourceFactory A factory for creating a {@link MediaSource} from a {@link
* MediaItem}.
*/
public Builder(Context context, MediaSourceFactory mediaSourceFactory) {
public Builder(Context context, MediaSource.Factory mediaSourceFactory) {
this(context, () -> new DefaultRenderersFactory(context), () -> mediaSourceFactory);
}
/**
* Creates a builder with a custom {@link RenderersFactory} and {@link MediaSourceFactory}.
* Creates a builder with a custom {@link RenderersFactory} and {@link MediaSource.Factory}.
*
* <p>See {@link #Builder(Context)} for a list of default values.
*
@ -495,7 +493,9 @@ public interface ExoPlayer extends Player {
* MediaItem}.
*/
public Builder(
Context context, RenderersFactory renderersFactory, MediaSourceFactory mediaSourceFactory) {
Context context,
RenderersFactory renderersFactory,
MediaSource.Factory mediaSourceFactory) {
this(context, () -> renderersFactory, () -> mediaSourceFactory);
}
@ -508,7 +508,7 @@ public interface ExoPlayer extends Player {
* @param context A {@link Context}.
* @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the
* player.
* @param mediaSourceFactory A {@link MediaSourceFactory}.
* @param mediaSourceFactory A {@link MediaSource.Factory}.
* @param trackSelector A {@link TrackSelector}.
* @param loadControl A {@link LoadControl}.
* @param bandwidthMeter A {@link BandwidthMeter}.
@ -517,7 +517,7 @@ public interface ExoPlayer extends Player {
public Builder(
Context context,
RenderersFactory renderersFactory,
MediaSourceFactory mediaSourceFactory,
MediaSource.Factory mediaSourceFactory,
TrackSelector trackSelector,
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
@ -535,7 +535,7 @@ public interface ExoPlayer extends Player {
private Builder(
Context context,
Supplier<RenderersFactory> renderersFactorySupplier,
Supplier<MediaSourceFactory> mediaSourceFactorySupplier) {
Supplier<MediaSource.Factory> mediaSourceFactorySupplier) {
this(
context,
renderersFactorySupplier,
@ -549,7 +549,7 @@ public interface ExoPlayer extends Player {
private Builder(
Context context,
Supplier<RenderersFactory> renderersFactorySupplier,
Supplier<MediaSourceFactory> mediaSourceFactorySupplier,
Supplier<MediaSource.Factory> mediaSourceFactorySupplier,
Supplier<TrackSelector> trackSelectorSupplier,
Supplier<LoadControl> loadControlSupplier,
Supplier<BandwidthMeter> bandwidthMeterSupplier,
@ -608,13 +608,13 @@ public interface ExoPlayer extends Player {
}
/**
* Sets the {@link MediaSourceFactory} that will be used by the player.
* Sets the {@link MediaSource.Factory} that will be used by the player.
*
* @param mediaSourceFactory A {@link MediaSourceFactory}.
* @param mediaSourceFactory A {@link MediaSource.Factory}.
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) {
public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
checkState(!buildCalled);
this.mediaSourceFactorySupplier = () -> mediaSourceFactory;
return this;
@ -1492,8 +1492,7 @@ public interface ExoPlayer extends Player {
* <ul>
* <li>Audio offload rendering is enabled in {@link
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
* DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, int)}.
* DefaultAudioSink.Builder#setOffloadMode}.
* <li>An audio track is playing in a format that the device supports offloading (for example,
* MP3 or AAC).
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.

View file

@ -15,6 +15,39 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static com.google.android.exoplayer2.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static com.google.android.exoplayer2.Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
import static com.google.android.exoplayer2.Player.COMMAND_GET_TIMELINE;
import static com.google.android.exoplayer2.Player.COMMAND_GET_TRACK_INFOS;
import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE;
import static com.google.android.exoplayer2.Player.COMMAND_PREPARE;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static com.google.android.exoplayer2.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
import static com.google.android.exoplayer2.Player.COMMAND_SET_REPEAT_MODE;
import static com.google.android.exoplayer2.Player.COMMAND_SET_SHUFFLE_MODE;
import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH;
import static com.google.android.exoplayer2.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
import static com.google.android.exoplayer2.Player.COMMAND_STOP;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
import static com.google.android.exoplayer2.Player.EVENT_MEDIA_METADATA_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAYLIST_METADATA_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED;
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO;
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT;
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK;
import static com.google.android.exoplayer2.Player.PLAYBACK_SUPPRESSION_REASON_NONE;
import static com.google.android.exoplayer2.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
import static com.google.android.exoplayer2.Player.STATE_BUFFERING;
import static com.google.android.exoplayer2.Player.STATE_ENDED;
import static com.google.android.exoplayer2.Player.STATE_IDLE;
import static com.google.android.exoplayer2.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
import static com.google.android.exoplayer2.Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull;
@ -22,26 +55,34 @@ import static java.lang.Math.max;
import static java.lang.Math.min;
import android.annotation.SuppressLint;
import android.media.metrics.LogSessionId;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.ExoPlayer.AudioOffloadListener;
import com.google.android.exoplayer2.Player.Commands;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.Events;
import com.google.android.exoplayer2.Player.Listener;
import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.Player.State;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
@ -54,15 +95,14 @@ import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoSize;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
/** An {@link ExoPlayer} implementation. */
/* package */ final class ExoPlayerImpl extends BasePlayer {
/** A helper class for the {@link SimpleExoPlayer} implementation of {@link ExoPlayer}. */
/* package */ final class ExoPlayerImpl {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.exoplayer");
@ -80,6 +120,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* package */ final TrackSelectorResult emptyTrackSelectorResult;
/* package */ final Commands permanentAvailableCommands;
private final Player wrappingPlayer;
private final Renderer[] renderers;
private final TrackSelector trackSelector;
private final HandlerWrapper playbackInfoUpdateHandler;
@ -88,10 +129,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final ListenerSet<Player.EventListener> listeners;
private final CopyOnWriteArraySet<AudioOffloadListener> audioOffloadListeners;
private final Timeline.Period period;
private final Timeline.Window window;
private final List<MediaSourceHolderSnapshot> mediaSourceHolderSnapshots;
private final boolean useLazyPreparation;
private final MediaSourceFactory mediaSourceFactory;
@Nullable private final AnalyticsCollector analyticsCollector;
private final MediaSource.Factory mediaSourceFactory;
private final AnalyticsCollector analyticsCollector;
private final Looper applicationLooper;
private final BandwidthMeter bandwidthMeter;
private final long seekBackIncrementMs;
@ -129,7 +171,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
*
* @param renderers The {@link Renderer}s.
* @param trackSelector The {@link TrackSelector}.
* @param mediaSourceFactory The {@link MediaSourceFactory}.
* @param mediaSourceFactory The {@link MediaSource.Factory}.
* @param loadControl The {@link LoadControl}.
* @param bandwidthMeter The {@link BandwidthMeter}.
* @param analyticsCollector The {@link AnalyticsCollector}.
@ -137,16 +179,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
* loads and other initial preparation steps happen immediately. If true, these initial
* preparations are triggered only when the player starts buffering the media.
* @param seekParameters The {@link SeekParameters}.
* @param seekBackIncrementMs The {@link #seekBack()} increment in milliseconds.
* @param seekForwardIncrementMs The {@link #seekForward()} increment in milliseconds.
* @param seekBackIncrementMs The seek back increment in milliseconds.
* @param seekForwardIncrementMs The seek forward increment in milliseconds.
* @param livePlaybackSpeedControl The {@link LivePlaybackSpeedControl}.
* @param releaseTimeoutMs The timeout for calls to {@link #release()} in milliseconds.
* @param pauseAtEndOfMediaItems Whether to pause playback at the end of each media item.
* @param clock The {@link Clock}.
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
* which is used to call listeners on.
* @param wrappingPlayer The {@link Player} wrapping this one if applicable. This player instance
* should be used for all externally visible callbacks.
* @param wrappingPlayer The {@link Player} using this class.
* @param additionalPermanentAvailableCommands The {@link Commands} that are permanently available
* in the wrapping player but that are not in this player.
*/
@ -154,10 +195,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
public ExoPlayerImpl(
Renderer[] renderers,
TrackSelector trackSelector,
MediaSourceFactory mediaSourceFactory,
MediaSource.Factory mediaSourceFactory,
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
@Nullable AnalyticsCollector analyticsCollector,
AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
SeekParameters seekParameters,
long seekBackIncrementMs,
@ -167,7 +208,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
boolean pauseAtEndOfMediaItems,
Clock clock,
Looper applicationLooper,
@Nullable Player wrappingPlayer,
Player wrappingPlayer,
Commands additionalPermanentAvailableCommands) {
Log.i(
TAG,
@ -191,13 +232,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
this.applicationLooper = applicationLooper;
this.clock = clock;
this.wrappingPlayer = wrappingPlayer;
repeatMode = Player.REPEAT_MODE_OFF;
Player playerForListeners = wrappingPlayer != null ? wrappingPlayer : this;
listeners =
new ListenerSet<>(
applicationLooper,
clock,
(listener, flags) -> listener.onEvents(playerForListeners, new Events(flags)));
(listener, flags) -> listener.onEvents(wrappingPlayer, new Events(flags)));
audioOffloadListeners = new CopyOnWriteArraySet<>();
mediaSourceHolderSnapshots = new ArrayList<>();
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
@ -208,6 +249,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
TracksInfo.EMPTY,
/* info= */ null);
period = new Timeline.Period();
window = new Timeline.Window();
permanentAvailableCommands =
new Commands.Builder()
.addAll(
@ -241,11 +283,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
playbackInfoUpdate ->
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
if (analyticsCollector != null) {
analyticsCollector.setPlayer(playerForListeners, applicationLooper);
addListener(analyticsCollector);
bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector);
}
analyticsCollector.setPlayer(wrappingPlayer, applicationLooper);
addListener(analyticsCollector);
bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector);
PlayerId playerId = Util.SDK_INT < 31 ? new PlayerId() : Api31.createPlayerId();
internalPlayer =
new ExoPlayerImplInternal(
renderers,
@ -262,7 +303,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
pauseAtEndOfMediaItems,
applicationLooper,
clock,
playbackInfoUpdateListener);
playbackInfoUpdateListener,
playerId);
}
/**
@ -291,7 +333,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
return internalPlayer.getPlaybackLooper();
}
@Override
public Looper getApplicationLooper() {
return applicationLooper;
}
@ -300,16 +341,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
return clock;
}
@Override
public void addListener(Listener listener) {
addEventListener(listener);
}
@Override
public void removeListener(Listener listener) {
removeEventListener(listener);
}
@SuppressWarnings("deprecation") // Register deprecated EventListener.
public void addEventListener(Player.EventListener eventListener) {
listeners.add(eventListener);
@ -328,24 +363,20 @@ import java.util.concurrent.CopyOnWriteArraySet;
audioOffloadListeners.remove(listener);
}
@Override
public Commands getAvailableCommands() {
return availableCommands;
}
@Override
@State
public int getPlaybackState() {
return playbackInfo.playbackState;
}
@Override
@PlaybackSuppressionReason
public int getPlaybackSuppressionReason() {
return playbackInfo.playbackSuppressionReason;
}
@Override
@Nullable
public ExoPlaybackException getPlayerError() {
return playbackInfo.playbackError;
@ -357,7 +388,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
prepare();
}
@Override
public void prepare() {
if (playbackInfo.playbackState != Player.STATE_IDLE) {
return;
@ -365,7 +395,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackError(null);
playbackInfo =
playbackInfo.copyWithPlaybackState(
playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
playbackInfo.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING);
// Trigger internal prepare first before updating the playback info and notifying external
// listeners to ensure that new operations issued in the listener notifications reach the
// player after this prepare. The internal player can't change the playback info immediately
@ -402,12 +432,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
prepare();
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
setMediaSources(createMediaSources(mediaItems), resetPosition);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
setMediaSources(createMediaSources(mediaItems), startIndex, startPositionMs);
}
@ -443,7 +471,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false);
}
@Override
public void addMediaItems(int index, List<MediaItem> mediaItems) {
index = min(index, mediaSourceHolderSnapshots.size());
addMediaSources(index, createMediaSources(mediaItems));
@ -471,7 +498,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskTimelineAndPosition(
playbackInfo,
newTimeline,
getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline));
getPeriodPositionUsAfterTimelineChanged(oldTimeline, newTimeline));
internalPlayer.addMediaSources(index, holders, shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
@ -484,7 +511,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ C.INDEX_UNSET);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
toIndex = min(toIndex, mediaSourceHolderSnapshots.size());
PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(fromIndex, toIndex);
@ -496,12 +522,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false,
positionDiscontinuity,
Player.DISCONTINUITY_REASON_REMOVE,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET);
}
@Override
public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) {
Assertions.checkArgument(
fromIndex >= 0
@ -517,7 +542,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskTimelineAndPosition(
playbackInfo,
newTimeline,
getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline));
getPeriodPositionUsAfterTimelineChanged(oldTimeline, newTimeline));
internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
@ -536,7 +561,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskTimelineAndPosition(
playbackInfo,
timeline,
getPeriodPositionOrMaskWindowPosition(
maskWindowPositionMsOrGetPeriodPositionUs(
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
pendingOperationAcks++;
this.shuffleOrder = shuffleOrder;
@ -552,14 +577,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ C.INDEX_UNSET);
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
setPlayWhenReady(
playWhenReady,
PLAYBACK_SUPPRESSION_REASON_NONE,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
}
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
if (this.pauseAtEndOfMediaItems == pauseAtEndOfMediaItems) {
return;
@ -595,12 +612,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ C.INDEX_UNSET);
}
@Override
public boolean getPlayWhenReady() {
return playbackInfo.playWhenReady;
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
@ -612,12 +627,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
@Override
public @RepeatMode int getRepeatMode() {
@RepeatMode
public int getRepeatMode() {
return repeatMode;
}
@Override
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled;
@ -630,17 +644,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
@Override
public boolean getShuffleModeEnabled() {
return shuffleModeEnabled;
}
@Override
public boolean isLoading() {
return playbackInfo.isLoading;
}
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
Timeline timeline = playbackInfo.timeline;
if (mediaItemIndex < 0
@ -661,14 +672,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Player.State
int newPlaybackState =
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING;
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING;
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
timeline,
getPeriodPositionOrMaskWindowPosition(timeline, mediaItemIndex, positionMs));
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
@ -681,22 +692,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
oldMaskingMediaItemIndex);
}
@Override
public long getSeekBackIncrement() {
return seekBackIncrementMs;
}
@Override
public long getSeekForwardIncrement() {
return seekForwardIncrementMs;
}
@Override
public long getMaxSeekToPreviousPosition() {
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
if (playbackParameters == null) {
playbackParameters = PlaybackParameters.DEFAULT;
@ -718,7 +725,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ C.INDEX_UNSET);
}
@Override
public PlaybackParameters getPlaybackParameters() {
return playbackInfo.playbackParameters;
}
@ -751,13 +757,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
@Override
public void stop() {
stop(/* reset= */ false);
}
@Deprecated
@Override
public void stop(boolean reset) {
stop(reset, /* error= */ null);
}
@ -800,7 +799,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ C.INDEX_UNSET);
}
@Override
public void release() {
Log.i(
TAG,
@ -825,9 +823,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
listeners.release();
playbackInfoUpdateHandler.removeCallbacksAndMessages(null);
if (analyticsCollector != null) {
bandwidthMeter.removeEventListener(analyticsCollector);
}
bandwidthMeter.removeEventListener(analyticsCollector);
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
@ -844,7 +840,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.getPlaybackLooper());
}
@Override
public int getCurrentPeriodIndex() {
if (playbackInfo.timeline.isEmpty()) {
return maskingPeriodIndex;
@ -853,13 +848,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
@Override
public int getCurrentMediaItemIndex() {
int currentWindowIndex = getCurrentWindowIndexInternal();
return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex;
}
@Override
public long getDuration() {
if (isPlayingAd()) {
MediaPeriodId periodId = playbackInfo.periodId;
@ -870,12 +863,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
return getContentDuration();
}
@Override
private long getContentDuration() {
Timeline timeline = getCurrentTimeline();
return timeline.isEmpty()
? C.TIME_UNSET
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
public long getCurrentPosition() {
return Util.usToMs(getCurrentPositionUsInternal(playbackInfo));
}
@Override
public long getBufferedPosition() {
if (isPlayingAd()) {
return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)
@ -885,27 +883,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
return getContentBufferedPosition();
}
@Override
public long getTotalBufferedDuration() {
return Util.usToMs(playbackInfo.totalBufferedDurationUs);
}
@Override
public boolean isPlayingAd() {
return playbackInfo.periodId.isAd();
}
@Override
public int getCurrentAdGroupIndex() {
return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
}
@Override
public int getCurrentAdIndexInAdGroup() {
return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
}
@Override
public long getContentPosition() {
if (isPlayingAd()) {
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
@ -920,7 +913,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
@Override
public long getContentBufferedPosition() {
if (playbackInfo.timeline.isEmpty()) {
return maskingWindowPositionMs;
@ -952,32 +944,26 @@ import java.util.concurrent.CopyOnWriteArraySet;
return renderers[index].getTrackType();
}
@Nullable
public TrackSelector getTrackSelector() {
return trackSelector;
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return playbackInfo.trackGroups;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return new TrackSelectionArray(playbackInfo.trackSelectorResult.selections);
}
@Override
public TracksInfo getCurrentTracksInfo() {
return playbackInfo.trackSelectorResult.tracksInfo;
}
@Override
public TrackSelectionParameters getTrackSelectionParameters() {
return trackSelector.getParameters();
}
@Override
public void setTrackSelectionParameters(TrackSelectionParameters parameters) {
if (!trackSelector.isSetParametersSupported()
|| parameters.equals(trackSelector.getParameters())) {
@ -989,7 +975,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
listener -> listener.onTrackSelectionParametersChanged(parameters));
}
@Override
public MediaMetadata getMediaMetadata() {
return mediaMetadata;
}
@ -1008,12 +993,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
EVENT_MEDIA_METADATA_CHANGED, listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
@Override
public MediaMetadata getPlaylistMetadata() {
return playlistMetadata;
}
@Override
public void setPlaylistMetadata(MediaMetadata playlistMetadata) {
checkNotNull(playlistMetadata);
if (playlistMetadata.equals(this.playlistMetadata)) {
@ -1025,109 +1008,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
listener -> listener.onPlaylistMetadataChanged(this.playlistMetadata));
}
@Override
public Timeline getCurrentTimeline() {
return playbackInfo.timeline;
}
/** This method is not supported and returns {@link AudioAttributes#DEFAULT}. */
@Override
public AudioAttributes getAudioAttributes() {
return AudioAttributes.DEFAULT;
}
/** This method is not supported and does nothing. */
@Override
public void setVolume(float volume) {}
/** This method is not supported and returns 1. */
@Override
public float getVolume() {
return 1;
}
/** This method is not supported and does nothing. */
@Override
public void clearVideoSurface() {}
/** This method is not supported and does nothing. */
@Override
public void clearVideoSurface(@Nullable Surface surface) {}
/** This method is not supported and does nothing. */
@Override
public void setVideoSurface(@Nullable Surface surface) {}
/** This method is not supported and does nothing. */
@Override
public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
/** This method is not supported and does nothing. */
@Override
public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
/** This method is not supported and does nothing. */
@Override
public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
/** This method is not supported and does nothing. */
@Override
public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
/** This method is not supported and does nothing. */
@Override
public void setVideoTextureView(@Nullable TextureView textureView) {}
/** This method is not supported and does nothing. */
@Override
public void clearVideoTextureView(@Nullable TextureView textureView) {}
/** This method is not supported and returns {@link VideoSize#UNKNOWN}. */
@Override
public VideoSize getVideoSize() {
return VideoSize.UNKNOWN;
}
/** This method is not supported and returns an empty list. */
@Override
public ImmutableList<Cue> getCurrentCues() {
return ImmutableList.of();
}
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
@Override
public DeviceInfo getDeviceInfo() {
return DeviceInfo.UNKNOWN;
}
/** This method is not supported and always returns {@code 0}. */
@Override
public int getDeviceVolume() {
return 0;
}
/** This method is not supported and always returns {@link false}. */
@Override
public boolean isDeviceMuted() {
return false;
}
/** This method is not supported and does nothing. */
@Override
public void setDeviceVolume(int volume) {}
/** This method is not supported and does nothing. */
@Override
public void increaseDeviceVolume() {}
/** This method is not supported and does nothing. */
@Override
public void decreaseDeviceVolume() {}
/** This method is not supported and does nothing. */
@Override
public void setDeviceMuted(boolean muted) {}
private int getCurrentWindowIndexInternal() {
if (playbackInfo.timeline.isEmpty()) {
return maskingWindowIndex;
@ -1310,7 +1194,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (metadataChanged) {
final MediaMetadata finalMediaMetadata = mediaMetadata;
listeners.queueEvent(
Player.EVENT_MEDIA_METADATA_CHANGED,
EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(finalMediaMetadata));
}
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
@ -1518,7 +1402,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private void updateAvailableCommands() {
Commands previousAvailableCommands = availableCommands;
availableCommands = getAvailableCommands(permanentAvailableCommands);
availableCommands = Util.getAvailableCommands(wrappingPlayer, permanentAvailableCommands);
if (!availableCommands.equals(previousAvailableCommands)) {
listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
@ -1556,7 +1440,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskTimelineAndPosition(
playbackInfo,
timeline,
getPeriodPositionOrMaskWindowPosition(timeline, startWindowIndex, startPositionMs));
maskWindowPositionMsOrGetPeriodPositionUs(timeline, startWindowIndex, startPositionMs));
// Mask the playback state.
int maskingPlaybackState = newPlaybackInfo.playbackState;
if (startWindowIndex != C.INDEX_UNSET && newPlaybackInfo.playbackState != STATE_IDLE) {
@ -1580,7 +1464,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false,
/* positionDiscontinuity= */ positionDiscontinuity,
Player.DISCONTINUITY_REASON_REMOVE,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET);
}
@ -1614,7 +1498,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskTimelineAndPosition(
playbackInfo,
newTimeline,
getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline));
getPeriodPositionUsAfterTimelineChanged(oldTimeline, newTimeline));
// Player transitions to STATE_ENDED if the current index is part of the removed tail.
final boolean transitionsToEnded =
newPlaybackInfo.playbackState != STATE_IDLE
@ -1641,8 +1525,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
private PlaybackInfo maskTimelineAndPosition(
PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPosition) {
Assertions.checkArgument(timeline.isEmpty() || periodPosition != null);
PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPositionUs) {
Assertions.checkArgument(timeline.isEmpty() || periodPositionUs != null);
Timeline oldTimeline = playbackInfo.timeline;
// Mask the timeline.
playbackInfo = playbackInfo.copyWithTimeline(timeline);
@ -1667,10 +1551,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
Object oldPeriodUid = playbackInfo.periodId.periodUid;
boolean playingPeriodChanged = !oldPeriodUid.equals(castNonNull(periodPosition).first);
boolean playingPeriodChanged = !oldPeriodUid.equals(castNonNull(periodPositionUs).first);
MediaPeriodId newPeriodId =
playingPeriodChanged ? new MediaPeriodId(periodPosition.first) : playbackInfo.periodId;
long newContentPositionUs = periodPosition.second;
playingPeriodChanged ? new MediaPeriodId(periodPositionUs.first) : playbackInfo.periodId;
long newContentPositionUs = periodPositionUs.second;
long oldContentPositionUs = Util.msToUs(getContentPosition());
if (!oldTimeline.isEmpty()) {
oldContentPositionUs -=
@ -1746,25 +1630,25 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Nullable
private Pair<Object, Long> getPeriodPositionAfterTimelineChanged(
private Pair<Object, Long> getPeriodPositionUsAfterTimelineChanged(
Timeline oldTimeline, Timeline newTimeline) {
long currentPositionMs = getContentPosition();
if (oldTimeline.isEmpty() || newTimeline.isEmpty()) {
boolean isCleared = !oldTimeline.isEmpty() && newTimeline.isEmpty();
return getPeriodPositionOrMaskWindowPosition(
return maskWindowPositionMsOrGetPeriodPositionUs(
newTimeline,
isCleared ? C.INDEX_UNSET : getCurrentWindowIndexInternal(),
isCleared ? C.TIME_UNSET : currentPositionMs);
}
int currentMediaItemIndex = getCurrentMediaItemIndex();
@Nullable
Pair<Object, Long> oldPeriodPosition =
oldTimeline.getPeriodPosition(
Pair<Object, Long> oldPeriodPositionUs =
oldTimeline.getPeriodPositionUs(
window, period, currentMediaItemIndex, Util.msToUs(currentPositionMs));
Object periodUid = castNonNull(oldPeriodPosition).first;
Object periodUid = castNonNull(oldPeriodPositionUs).first;
if (newTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) {
// The old period position is still available in the new timeline.
return oldPeriodPosition;
return oldPeriodPositionUs;
}
// Period uid not found in new timeline. Try to get subsequent period.
@Nullable
@ -1774,19 +1658,19 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (nextPeriodUid != null) {
// Reset position to the default position of the window of the subsequent period.
newTimeline.getPeriodByUid(nextPeriodUid, period);
return getPeriodPositionOrMaskWindowPosition(
return maskWindowPositionMsOrGetPeriodPositionUs(
newTimeline,
period.windowIndex,
newTimeline.getWindow(period.windowIndex, window).getDefaultPositionMs());
} else {
// No subsequent period found and the new timeline is not empty. Use the default position.
return getPeriodPositionOrMaskWindowPosition(
return maskWindowPositionMsOrGetPeriodPositionUs(
newTimeline, /* windowIndex= */ C.INDEX_UNSET, /* windowPositionMs= */ C.TIME_UNSET);
}
}
@Nullable
private Pair<Object, Long> getPeriodPositionOrMaskWindowPosition(
private Pair<Object, Long> maskWindowPositionMsOrGetPeriodPositionUs(
Timeline timeline, int windowIndex, long windowPositionMs) {
if (timeline.isEmpty()) {
// If empty we store the initial seek in the masking variables.
@ -1801,7 +1685,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
windowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
windowPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs();
}
return timeline.getPeriodPosition(window, period, windowIndex, Util.msToUs(windowPositionMs));
return timeline.getPeriodPositionUs(window, period, windowIndex, Util.msToUs(windowPositionMs));
}
private long periodPositionUsToWindowPositionUs(
@ -1819,12 +1703,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
* #onMetadata(Metadata)}) sources.
*/
private MediaMetadata buildUpdatedMediaMetadata() {
@Nullable MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) {
Timeline timeline = getCurrentTimeline();
if (timeline.isEmpty()) {
return staticAndDynamicMediaMetadata;
}
MediaItem mediaItem = timeline.getWindow(getCurrentMediaItemIndex(), window).mediaItem;
// MediaItem metadata is prioritized over metadata within the media.
return staticAndDynamicMediaMetadata.buildUpon().populate(mediaItem.mediaMetadata).build();
}
@ -1856,4 +1739,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
return timeline;
}
}
@RequiresApi(31)
private static final class Api31 {
private Api31() {}
@DoNotInline
public static PlayerId createPlayerId() {
// TODO: Create a MediaMetricsListener and obtain LogSessionId from it.
return new PlayerId(LogSessionId.LOG_SESSION_ID_NONE);
}
}
}

View file

@ -35,6 +35,7 @@ import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
@ -43,6 +44,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@ -229,7 +231,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean pauseAtEndOfWindow,
Looper applicationLooper,
Clock clock,
PlaybackInfoUpdateListener playbackInfoUpdateListener) {
PlaybackInfoUpdateListener playbackInfoUpdateListener,
PlayerId playerId) {
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
this.renderers = renderers;
this.trackSelector = trackSelector;
@ -252,7 +255,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo);
rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
renderers[i].setIndex(i);
renderers[i].init(/* index= */ i, playerId);
rendererCapabilities[i] = renderers[i].getCapabilities();
}
mediaClock = new DefaultMediaClock(this, clock);
@ -266,7 +269,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
Handler eventHandler = new Handler(applicationLooper);
queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler);
mediaSourceList =
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
@ -1119,7 +1123,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean seekPositionAdjusted;
@Nullable
Pair<Object, Long> resolvedSeekPosition =
resolveSeekPosition(
resolveSeekPositionUs(
playbackInfo.timeline,
seekPosition,
/* trySubsequentPeriods= */ true,
@ -1130,10 +1134,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (resolvedSeekPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed or is not ready and a suitable seek position could not be resolved.
Pair<MediaPeriodId, Long> firstPeriodAndPosition =
getPlaceholderFirstMediaPeriodPosition(playbackInfo.timeline);
periodId = firstPeriodAndPosition.first;
periodPositionUs = firstPeriodAndPosition.second;
Pair<MediaPeriodId, Long> firstPeriodAndPositionUs =
getPlaceholderFirstMediaPeriodPositionUs(playbackInfo.timeline);
periodId = firstPeriodAndPositionUs.first;
periodPositionUs = firstPeriodAndPositionUs.second;
requestedContentPositionUs = C.TIME_UNSET;
seekPositionAdjusted = !playbackInfo.timeline.isEmpty();
} else {
@ -1408,10 +1412,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean resetTrackInfo = false;
if (resetPosition) {
pendingInitialSeekPosition = null;
Pair<MediaPeriodId, Long> firstPeriodAndPosition =
getPlaceholderFirstMediaPeriodPosition(playbackInfo.timeline);
mediaPeriodId = firstPeriodAndPosition.first;
startPositionUs = firstPeriodAndPosition.second;
Pair<MediaPeriodId, Long> firstPeriodAndPositionUs =
getPlaceholderFirstMediaPeriodPositionUs(playbackInfo.timeline);
mediaPeriodId = firstPeriodAndPositionUs.first;
startPositionUs = firstPeriodAndPositionUs.second;
requestedContentPositionUs = C.TIME_UNSET;
if (!mediaPeriodId.equals(playbackInfo.periodId)) {
resetTrackInfo = true;
@ -1447,19 +1451,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private Pair<MediaPeriodId, Long> getPlaceholderFirstMediaPeriodPosition(Timeline timeline) {
private Pair<MediaPeriodId, Long> getPlaceholderFirstMediaPeriodPositionUs(Timeline timeline) {
if (timeline.isEmpty()) {
return Pair.create(PlaybackInfo.getDummyPeriodForEmptyTimeline(), 0L);
}
int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
Pair<Object, Long> firstPeriodAndPosition =
timeline.getPeriodPosition(
Pair<Object, Long> firstPeriodAndPositionUs =
timeline.getPeriodPositionUs(
window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET);
// Add ad metadata if any and propagate the window sequence number to new period id.
MediaPeriodId firstPeriodId =
queue.resolveMediaPeriodIdForAds(
timeline, firstPeriodAndPosition.first, /* positionUs= */ 0);
long positionUs = firstPeriodAndPosition.second;
timeline, firstPeriodAndPositionUs.first, /* positionUs= */ 0);
long positionUs = firstPeriodAndPositionUs.second;
if (firstPeriodId.isAd()) {
timeline.getPeriodByUid(firstPeriodId.periodUid, period);
positionUs =
@ -2539,7 +2543,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Resolve initial seek position.
@Nullable
Pair<Object, Long> periodPosition =
resolveSeekPosition(
resolveSeekPositionUs(
timeline,
pendingInitialSeekPosition,
/* trySubsequentPeriods= */ true,
@ -2604,10 +2608,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
// at position 0 and don't need to be resolved.
long windowPositionUs = oldContentPositionUs + period.getPositionInWindowUs();
int windowIndex = timeline.getPeriodByUid(newPeriodUid, period).windowIndex;
Pair<Object, Long> periodPosition =
timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
newPeriodUid = periodPosition.first;
newContentPositionUs = periodPosition.second;
Pair<Object, Long> periodPositionUs =
timeline.getPeriodPositionUs(window, period, windowIndex, windowPositionUs);
newPeriodUid = periodPositionUs.first;
newContentPositionUs = periodPositionUs.second;
}
// Use an explicitly requested content position as new target live offset.
setTargetLiveOffset = true;
@ -2616,14 +2620,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Set period uid for default positions and resolve position for ad resolution.
long contentPositionForAdResolutionUs = newContentPositionUs;
if (startAtDefaultPositionWindowIndex != C.INDEX_UNSET) {
Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition(
Pair<Object, Long> defaultPositionUs =
timeline.getPeriodPositionUs(
window,
period,
startAtDefaultPositionWindowIndex,
/* windowPositionUs= */ C.TIME_UNSET);
newPeriodUid = defaultPosition.first;
contentPositionForAdResolutionUs = defaultPosition.second;
newPeriodUid = defaultPositionUs.first;
contentPositionForAdResolutionUs = defaultPositionUs.second;
newContentPositionUs = C.TIME_UNSET;
}
@ -2645,15 +2649,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
&& earliestCuePointIsUnchangedOrLater;
// Drop update if the change is from/to server-side inserted ads at the same content position to
// avoid any unintentional renderer reset.
timeline.getPeriodByUid(newPeriodUid, period);
boolean isInStreamAdChange =
sameOldAndNewPeriodUid
&& !isUsingPlaceholderPeriod
&& oldContentPositionUs == newContentPositionUs
&& ((periodIdWithAds.isAd()
&& period.isServerSideInsertedAdGroup(periodIdWithAds.adGroupIndex))
|| (oldPeriodId.isAd()
&& period.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)));
isIgnorableServerSideAdInsertionPeriodChange(
isUsingPlaceholderPeriod,
oldPeriodId,
oldContentPositionUs,
periodIdWithAds,
timeline.getPeriodByUid(newPeriodUid, period),
newContentPositionUs);
MediaPeriodId newPeriodId =
onlyNextAdGroupIndexIncreased || isInStreamAdChange ? oldPeriodId : periodIdWithAds;
@ -2679,6 +2682,30 @@ import java.util.concurrent.atomic.AtomicBoolean;
setTargetLiveOffset);
}
private static boolean isIgnorableServerSideAdInsertionPeriodChange(
boolean isUsingPlaceholderPeriod,
MediaPeriodId oldPeriodId,
long oldContentPositionUs,
MediaPeriodId newPeriodId,
Timeline.Period newPeriod,
long newContentPositionUs) {
if (isUsingPlaceholderPeriod
|| oldContentPositionUs != newContentPositionUs
|| !oldPeriodId.periodUid.equals(newPeriodId.periodUid)) {
// The period position changed.
return false;
}
if (oldPeriodId.isAd() && newPeriod.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)) {
// Whether the old period was a server side ad that doesn't need skipping to the content.
return newPeriod.getAdState(oldPeriodId.adGroupIndex, oldPeriodId.adIndexInAdGroup)
!= AdPlaybackState.AD_STATE_ERROR
&& newPeriod.getAdState(oldPeriodId.adGroupIndex, oldPeriodId.adIndexInAdGroup)
!= AdPlaybackState.AD_STATE_SKIPPED;
}
// If the new period is a server side inserted ad, we can just continue playing.
return newPeriodId.isAd() && newPeriod.isServerSideInsertedAdGroup(newPeriodId.adGroupIndex);
}
private static boolean isUsingPlaceholderPeriod(
PlaybackInfo playbackInfo, Timeline.Period period) {
MediaPeriodId periodId = playbackInfo.periodId;
@ -2714,7 +2741,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
: Util.msToUs(pendingMessageInfo.message.getPositionMs());
@Nullable
Pair<Object, Long> periodPosition =
resolveSeekPosition(
resolveSeekPositionUs(
newTimeline,
new SeekPosition(
pendingMessageInfo.message.getTimeline(),
@ -2759,12 +2786,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs();
int windowIndex =
newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex;
Pair<Object, Long> periodPosition =
newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
Pair<Object, Long> periodPositionUs =
newTimeline.getPeriodPositionUs(window, period, windowIndex, windowPositionUs);
pendingMessageInfo.setResolvedPosition(
/* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first),
/* periodTimeUs= */ periodPosition.second,
/* periodUid= */ periodPosition.first);
/* periodIndex= */ newTimeline.getIndexOfPeriod(periodPositionUs.first),
/* periodTimeUs= */ periodPositionUs.second,
/* periodUid= */ periodPositionUs.first);
}
return true;
}
@ -2793,7 +2820,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* bounds of the timeline.
*/
@Nullable
private static Pair<Object, Long> resolveSeekPosition(
private static Pair<Object, Long> resolveSeekPositionUs(
Timeline timeline,
SeekPosition seekPosition,
boolean trySubsequentPeriods,
@ -2812,10 +2839,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
seekTimeline = timeline;
}
// Map the SeekPosition to a position in the corresponding timeline.
Pair<Object, Long> periodPosition;
Pair<Object, Long> periodPositionUs;
try {
periodPosition =
seekTimeline.getPeriodPosition(
periodPositionUs =
seekTimeline.getPeriodPositionUs(
window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);
} catch (IndexOutOfBoundsException e) {
// The window index of the seek position was outside the bounds of the timeline.
@ -2823,24 +2850,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
if (timeline.equals(seekTimeline)) {
// Our internal timeline is the seek timeline, so the mapped position is correct.
return periodPosition;
return periodPositionUs;
}
// Attempt to find the mapped period in the internal timeline.
int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);
int periodIndex = timeline.getIndexOfPeriod(periodPositionUs.first);
if (periodIndex != C.INDEX_UNSET) {
// We successfully located the period in the internal timeline.
if (seekTimeline.getPeriodByUid(periodPosition.first, period).isPlaceholder
if (seekTimeline.getPeriodByUid(periodPositionUs.first, period).isPlaceholder
&& seekTimeline.getWindow(period.windowIndex, window).firstPeriodIndex
== seekTimeline.getIndexOfPeriod(periodPosition.first)) {
== seekTimeline.getIndexOfPeriod(periodPositionUs.first)) {
// The seek timeline was using a placeholder, so we need to re-resolve using the updated
// timeline in case the resolved position changed. Only resolve the first period in a window
// because subsequent periods must start at position 0 and don't need to be resolved.
int newWindowIndex = timeline.getPeriodByUid(periodPosition.first, period).windowIndex;
periodPosition =
timeline.getPeriodPosition(
int newWindowIndex = timeline.getPeriodByUid(periodPositionUs.first, period).windowIndex;
periodPositionUs =
timeline.getPeriodPositionUs(
window, period, newWindowIndex, seekPosition.windowPositionUs);
}
return periodPosition;
return periodPositionUs;
}
if (trySubsequentPeriods) {
// Try and find a subsequent period from the seek timeline in the internal timeline.
@ -2851,12 +2878,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
period,
repeatMode,
shuffleModeEnabled,
periodPosition.first,
periodPositionUs.first,
seekTimeline,
timeline);
if (periodUid != null) {
// We found one. Use the default position of the corresponding window.
return timeline.getPeriodPosition(
return timeline.getPeriodPositionUs(
window,
period,
timeline.getPeriodByUid(periodUid, period).windowIndex,

View file

@ -661,18 +661,18 @@ import com.google.common.collect.ImmutableList;
// forward by the duration of the buffer, and start buffering from this point.
contentPositionUs = C.TIME_UNSET;
@Nullable
Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition(
Pair<Object, Long> defaultPositionUs =
timeline.getPeriodPositionUs(
window,
period,
nextWindowIndex,
/* windowPositionUs= */ C.TIME_UNSET,
/* defaultPositionProjectionUs= */ max(0, bufferedDurationUs));
if (defaultPosition == null) {
if (defaultPositionUs == null) {
return null;
}
nextPeriodUid = defaultPosition.first;
startPositionUs = defaultPosition.second;
nextPeriodUid = defaultPositionUs.first;
startPositionUs = defaultPositionUs.second;
MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {
windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;
@ -716,17 +716,17 @@ import com.google.common.collect.ImmutableList;
// If we're transitioning from an ad group to content starting from its default position,
// project the start position forward as if this were a transition to a new window.
@Nullable
Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition(
Pair<Object, Long> defaultPositionUs =
timeline.getPeriodPositionUs(
window,
period,
period.windowIndex,
/* windowPositionUs= */ C.TIME_UNSET,
/* defaultPositionProjectionUs= */ max(0, bufferedDurationUs));
if (defaultPosition == null) {
if (defaultPositionUs == null) {
return null;
}
startPositionUs = defaultPosition.second;
startPositionUs = defaultPositionUs.second;
}
long minStartPositionUs =
getMinStartPositionAfterAdGroupUs(

View file

@ -21,6 +21,7 @@ import static java.lang.Math.min;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.source.LoadEventInfo;
@ -70,6 +71,7 @@ import java.util.Set;
private static final String TAG = "MediaSourceList";
private final PlayerId playerId;
private final List<MediaSourceHolder> mediaSourceHolders;
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
@ -93,11 +95,14 @@ import java.util.Set;
* source events.
* @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
* on.
* @param playerId The {@link PlayerId} of the player using this list.
*/
public MediaSourceList(
MediaSourceListInfoRefreshListener listener,
@Nullable AnalyticsCollector analyticsCollector,
Handler analyticsCollectorHandler) {
Handler analyticsCollectorHandler,
PlayerId playerId) {
this.playerId = playerId;
mediaSourceListInfoListener = listener;
shuffleOrder = new DefaultShuffleOrder(0);
mediaSourceByMediaPeriod = new IdentityHashMap<>();
@ -440,7 +445,7 @@ import java.util.Set;
childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
mediaSource.addEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
mediaSource.addDrmEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
mediaSource.prepareSource(caller, mediaTransferListener);
mediaSource.prepareSource(caller, mediaTransferListener, playerId);
}
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {

View file

@ -23,13 +23,13 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
@ -48,7 +48,7 @@ public final class MetadataRetriever {
/**
* Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}.
*
* <p>This is equivalent to using {@link #retrieveMetadata(MediaSourceFactory, MediaItem)} with a
* <p>This is equivalent to using {@link #retrieveMetadata(MediaSource.Factory, MediaItem)} with a
* {@link DefaultMediaSourceFactory} and a {@link DefaultExtractorsFactory} with {@link
* Mp4Extractor#FLAG_READ_MOTION_PHOTO_METADATA} and {@link Mp4Extractor#FLAG_READ_SEF_DATA} set.
*
@ -66,13 +66,13 @@ public final class MetadataRetriever {
*
* <p>This method is thread-safe.
*
* @param mediaSourceFactory mediaSourceFactory The {@link MediaSourceFactory} to use to read the
* @param mediaSourceFactory mediaSourceFactory The {@link MediaSource.Factory} to use to read the
* data.
* @param mediaItem The {@link MediaItem} whose metadata should be retrieved.
* @return A {@link ListenableFuture} of the result.
*/
public static ListenableFuture<TrackGroupArray> retrieveMetadata(
MediaSourceFactory mediaSourceFactory, MediaItem mediaItem) {
MediaSource.Factory mediaSourceFactory, MediaItem mediaItem) {
return retrieveMetadata(mediaSourceFactory, mediaItem, Clock.DEFAULT);
}
@ -83,13 +83,13 @@ public final class MetadataRetriever {
new DefaultExtractorsFactory()
.setMp4ExtractorFlags(
Mp4Extractor.FLAG_READ_MOTION_PHOTO_METADATA | Mp4Extractor.FLAG_READ_SEF_DATA);
MediaSourceFactory mediaSourceFactory =
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(context, extractorsFactory);
return retrieveMetadata(mediaSourceFactory, mediaItem, clock);
}
private static ListenableFuture<TrackGroupArray> retrieveMetadata(
MediaSourceFactory mediaSourceFactory, MediaItem mediaItem, Clock clock) {
MediaSource.Factory mediaSourceFactory, MediaItem mediaItem, Clock clock) {
// Recreate thread and handler every time this method is called so that it can be used
// concurrently.
return new MetadataRetrieverInternal(mediaSourceFactory, clock).retrieveMetadata(mediaItem);
@ -102,12 +102,12 @@ public final class MetadataRetriever {
private static final int MESSAGE_CONTINUE_LOADING = 2;
private static final int MESSAGE_RELEASE = 3;
private final MediaSourceFactory mediaSourceFactory;
private final MediaSource.Factory mediaSourceFactory;
private final HandlerThread mediaSourceThread;
private final HandlerWrapper mediaSourceHandler;
private final SettableFuture<TrackGroupArray> trackGroupsFuture;
public MetadataRetrieverInternal(MediaSourceFactory mediaSourceFactory, Clock clock) {
public MetadataRetrieverInternal(MediaSource.Factory mediaSourceFactory, Clock clock) {
this.mediaSourceFactory = mediaSourceFactory;
mediaSourceThread = new HandlerThread("ExoPlayer:MetadataRetriever");
mediaSourceThread.start();
@ -140,7 +140,8 @@ public final class MetadataRetriever {
case MESSAGE_PREPARE_SOURCE:
MediaItem mediaItem = (MediaItem) msg.obj;
mediaSource = mediaSourceFactory.createMediaSource(mediaItem);
mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null);
mediaSource.prepareSource(
mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET);
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
return true;
case MESSAGE_CHECK_FOR_FAILURE:

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock;
@ -45,7 +46,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
}
@Override
public final void setIndex(int index) {
public final void init(int index, PlayerId playerId) {
this.index = index;
}

View file

@ -20,6 +20,7 @@ import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.source.SampleStream;
@ -248,11 +249,12 @@ public interface Renderer extends PlayerMessage.Target {
RendererCapabilities getCapabilities();
/**
* Sets the index of this renderer within the player.
* Initializes the renderer for playback with a player.
*
* @param index The renderer index.
* @param index The renderer index within the player.
* @param playerId The {@link PlayerId} of the player.
*/
void setIndex(int index);
void init(int index, PlayerId playerId);
/**
* If the renderer advances its own playback position then this method returns a corresponding

View file

@ -60,16 +60,16 @@ public interface RendererCapabilities {
@interface AdaptiveSupport {}
/** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */
int ADAPTIVE_SUPPORT_MASK = 0b11000;
int ADAPTIVE_SUPPORT_MASK = 0b11 << 3;
/** The {@link Renderer} can seamlessly adapt between formats. */
int ADAPTIVE_SEAMLESS = 0b10000;
int ADAPTIVE_SEAMLESS = 0b10 << 3;
/**
* The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
* (~50-100ms) when adaptation occurs.
*/
int ADAPTIVE_NOT_SEAMLESS = 0b01000;
int ADAPTIVE_NOT_SEAMLESS = 0b01 << 3;
/** The {@link Renderer} does not support adaptation between formats. */
int ADAPTIVE_NOT_SUPPORTED = 0b00000;
int ADAPTIVE_NOT_SUPPORTED = 0;
/**
* Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link
@ -80,20 +80,62 @@ public interface RendererCapabilities {
@IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED})
@interface TunnelingSupport {}
/** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */
int TUNNELING_SUPPORT_MASK = 0b100000;
/** A mask to apply to {@link Capabilities} to obtain {@link TunnelingSupport} only. */
int TUNNELING_SUPPORT_MASK = 0b1 << 5;
/** The {@link Renderer} supports tunneled output. */
int TUNNELING_SUPPORTED = 0b100000;
int TUNNELING_SUPPORTED = 0b1 << 5;
/** The {@link Renderer} does not support tunneled output. */
int TUNNELING_NOT_SUPPORTED = 0b000000;
int TUNNELING_NOT_SUPPORTED = 0;
/**
* Level of renderer support for hardware acceleration. One of {@link
* #HARDWARE_ACCELERATION_SUPPORTED} and {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}.
*
* <p>For video renderers, the level of support is indicated for non-tunneled output.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
HARDWARE_ACCELERATION_SUPPORTED,
HARDWARE_ACCELERATION_NOT_SUPPORTED,
})
@interface HardwareAccelerationSupport {}
/** A mask to apply to {@link Capabilities} to obtain {@link HardwareAccelerationSupport} only. */
int HARDWARE_ACCELERATION_SUPPORT_MASK = 0b1 << 6;
/** The renderer is able to use hardware acceleration. */
int HARDWARE_ACCELERATION_SUPPORTED = 0b1 << 6;
/** The renderer is not able to use hardware acceleration. */
int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0;
/**
* Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY} and {@link
* #DECODER_SUPPORT_FALLBACK}.
*
* <p>For video renderers, the level of support is indicated for non-tunneled output.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DECODER_SUPPORT_PRIMARY,
DECODER_SUPPORT_FALLBACK,
})
@interface DecoderSupport {}
/** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */
int MODE_SUPPORT_MASK = 0b1 << 7;
/** The renderer is able to use the primary decoder for the format's MIME type. */
int DECODER_SUPPORT_PRIMARY = 0b1 << 7;
/** The renderer will use a fallback decoder. */
int DECODER_SUPPORT_FALLBACK = 0;
/**
* Combined renderer capabilities.
*
* <p>This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport} and {@link
* TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or
* {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)}
* or {@link #create(int, int, int)} to create the combined capabilities.
* <p>This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport}, {@link
* TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link DecoderSupport}. Use {@link
* #getFormatSupport}, {@link #getAdaptiveSupport}, {@link #getTunnelingSupport}, {@link
* #getHardwareAccelerationSupport} and {@link #getDecoderSupport} to obtain individual
* components. Use {@link #create(int)}, {@link #create(int, int, int)} or {@link #create(int,
* int, int, int, int)} to create combined capabilities from individual components.
*
* <p>Possible values:
*
@ -111,6 +153,11 @@ public interface RendererCapabilities {
* #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of
* support for the format itself is {@link C#FORMAT_HANDLED} or {@link
* C#FORMAT_EXCEEDS_CAPABILITIES}.
* <li>{@link HardwareAccelerationSupport}: The level of support for hardware acceleration. One
* of {@link #HARDWARE_ACCELERATION_SUPPORTED} and {@link
* #HARDWARE_ACCELERATION_NOT_SUPPORTED}.
* <li>{@link DecoderSupport}: The level of decoder support. One of {@link
* #DECODER_SUPPORT_PRIMARY} and {@link #DECODER_SUPPORT_FALLBACK}.
* </ul>
*/
@Documented
@ -122,8 +169,10 @@ public interface RendererCapabilities {
/**
* Returns {@link Capabilities} for the given {@link C.FormatSupport}.
*
* <p>The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link
* TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}.
* <p>{@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED}, {@link TunnelingSupport}
* is set to {@link #TUNNELING_NOT_SUPPORTED}, {@link HardwareAccelerationSupport} is set to
* {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} and {@link DecoderSupport} is set to {@link
* #DECODER_SUPPORT_PRIMARY}.
*
* @param formatSupport The {@link C.FormatSupport}.
* @return The combined {@link Capabilities} of the given {@link C.FormatSupport}, {@link
@ -138,19 +187,53 @@ public interface RendererCapabilities {
* Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link
* AdaptiveSupport} and {@link TunnelingSupport}.
*
* <p>{@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}
* and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}.
*
* @param formatSupport The {@link C.FormatSupport}.
* @param adaptiveSupport The {@link AdaptiveSupport}.
* @param tunnelingSupport The {@link TunnelingSupport}.
* @return The combined {@link Capabilities}.
*/
@Capabilities
static int create(
@C.FormatSupport int formatSupport,
@AdaptiveSupport int adaptiveSupport,
@TunnelingSupport int tunnelingSupport) {
return create(
formatSupport,
adaptiveSupport,
tunnelingSupport,
HARDWARE_ACCELERATION_NOT_SUPPORTED,
DECODER_SUPPORT_PRIMARY);
}
/**
* Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link
* AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link
* DecoderSupport}.
*
* @param formatSupport The {@link C.FormatSupport}.
* @param adaptiveSupport The {@link AdaptiveSupport}.
* @param tunnelingSupport The {@link TunnelingSupport}.
* @param hardwareAccelerationSupport The {@link HardwareAccelerationSupport}.
* @param decoderSupport The {@link DecoderSupport}.
* @return The combined {@link Capabilities}.
*/
// Suppression needed for IntDef casting.
@SuppressLint("WrongConstant")
@Capabilities
static int create(
@C.FormatSupport int formatSupport,
@AdaptiveSupport int adaptiveSupport,
@TunnelingSupport int tunnelingSupport) {
return formatSupport | adaptiveSupport | tunnelingSupport;
@TunnelingSupport int tunnelingSupport,
@HardwareAccelerationSupport int hardwareAccelerationSupport,
@DecoderSupport int decoderSupport) {
return formatSupport
| adaptiveSupport
| tunnelingSupport
| hardwareAccelerationSupport
| decoderSupport;
}
/**
@ -192,6 +275,32 @@ public interface RendererCapabilities {
return supportFlags & TUNNELING_SUPPORT_MASK;
}
/**
* Returns the {@link HardwareAccelerationSupport} from the combined {@link Capabilities}.
*
* @param supportFlags The combined {@link Capabilities}.
* @return The {@link HardwareAccelerationSupport} only.
*/
// Suppression needed for IntDef casting.
@SuppressLint("WrongConstant")
@HardwareAccelerationSupport
static int getHardwareAccelerationSupport(@Capabilities int supportFlags) {
return supportFlags & HARDWARE_ACCELERATION_SUPPORT_MASK;
}
/**
* Returns the {@link DecoderSupport} from the combined {@link Capabilities}.
*
* @param supportFlags The combined {@link Capabilities}.
* @return The {@link DecoderSupport} only.
*/
// Suppression needed for IntDef casting.
@SuppressLint("WrongConstant")
@DecoderSupport
static int getDecoderSupport(@Capabilities int supportFlags) {
return supportFlags & MODE_SUPPORT_MASK;
}
/** Returns the name of the {@link Renderer}. */
String getName();

View file

@ -58,7 +58,6 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
@ -113,7 +112,7 @@ public class SimpleExoPlayer extends BasePlayer
}
/**
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, MediaSourceFactory)} and {@link
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, MediaSource.Factory)} and {@link
* DefaultMediaSourceFactory#DefaultMediaSourceFactory(Context, ExtractorsFactory)} instead.
*/
@Deprecated
@ -124,7 +123,7 @@ public class SimpleExoPlayer extends BasePlayer
/**
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, RenderersFactory,
* MediaSourceFactory)} and {@link
* MediaSource.Factory)} and {@link
* DefaultMediaSourceFactory#DefaultMediaSourceFactory(Context, ExtractorsFactory)} instead.
*/
@Deprecated
@ -137,7 +136,7 @@ public class SimpleExoPlayer extends BasePlayer
/**
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, RenderersFactory,
* MediaSourceFactory, TrackSelector, LoadControl, BandwidthMeter, AnalyticsCollector)}
* MediaSource.Factory, TrackSelector, LoadControl, BandwidthMeter, AnalyticsCollector)}
* instead.
*/
@Deprecated
@ -145,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer
Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
MediaSourceFactory mediaSourceFactory,
MediaSource.Factory mediaSourceFactory,
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
AnalyticsCollector analyticsCollector) {
@ -178,10 +177,10 @@ public class SimpleExoPlayer extends BasePlayer
}
/**
* @deprecated Use {@link ExoPlayer.Builder#setMediaSourceFactory(MediaSourceFactory)} instead.
* @deprecated Use {@link ExoPlayer.Builder#setMediaSourceFactory(MediaSource.Factory)} instead.
*/
@Deprecated
public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) {
public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
wrappedBuilder.setMediaSourceFactory(mediaSourceFactory);
return this;
}
@ -400,7 +399,7 @@ public class SimpleExoPlayer extends BasePlayer
Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
MediaSourceFactory mediaSourceFactory,
MediaSource.Factory mediaSourceFactory,
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
AnalyticsCollector analyticsCollector,
@ -1351,7 +1350,6 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
@Nullable
public TrackSelector getTrackSelector() {
verifyApplicationThread();
return player.getTrackSelector();

View file

@ -0,0 +1,858 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.analytics;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.DeniedByServerException;
import android.media.MediaCodec;
import android.media.MediaDrm;
import android.media.MediaDrmResetException;
import android.media.NotProvisionedException;
import android.media.metrics.LogSessionId;
import android.media.metrics.MediaMetricsManager;
import android.media.metrics.NetworkEvent;
import android.media.metrics.PlaybackErrorEvent;
import android.media.metrics.PlaybackMetrics;
import android.media.metrics.PlaybackSession;
import android.media.metrics.PlaybackStateEvent;
import android.media.metrics.TrackChangeEvent;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.TracksInfo.TrackGroupInfo;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.UdpDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NetworkTypeObserver;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoSize;
import com.google.common.collect.ImmutableList;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.UUID;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* An {@link AnalyticsListener} that interacts with the Android {@link MediaMetricsManager}.
*
* <p>It listens to playback events and forwards them to a {@link PlaybackSession}. The {@link
* LogSessionId} of the playback session can be obtained with {@link #getLogSessionId()}.
*/
@RequiresApi(31)
public final class MediaMetricsListener
implements AnalyticsListener, PlaybackSessionManager.Listener {
/**
* Creates a media metrics listener.
*
* @param context A context.
* @return The {@link MediaMetricsListener}, or null if the {@link Context#MEDIA_METRICS_SERVICE
* media metrics service} isn't available.
*/
@Nullable
public static MediaMetricsListener create(Context context) {
@Nullable
MediaMetricsManager mediaMetricsManager =
(MediaMetricsManager) context.getSystemService(Context.MEDIA_METRICS_SERVICE);
return mediaMetricsManager == null
? null
: new MediaMetricsListener(context, mediaMetricsManager.createPlaybackSession());
}
private final Context context;
private final PlaybackSessionManager sessionManager;
private final PlaybackSession playbackSession;
private final long startTimeMs;
private final Timeline.Window window;
private final Timeline.Period period;
@Nullable private PlaybackMetrics.Builder metricsBuilder;
@Player.DiscontinuityReason private int discontinuityReason;
private int currentPlaybackState;
private int currentNetworkType;
@Nullable private PlaybackException pendingPlayerError;
@Nullable private PendingFormatUpdate pendingVideoFormat;
@Nullable private PendingFormatUpdate pendingAudioFormat;
@Nullable private PendingFormatUpdate pendingTextFormat;
@Nullable private Format currentVideoFormat;
@Nullable private Format currentAudioFormat;
@Nullable private Format currentTextFormat;
private boolean isSeeking;
private int ioErrorType;
private boolean hasFatalError;
private int droppedFrames;
private int playedFrames;
private long bandwidthTimeMs;
private long bandwidthBytes;
private int audioUnderruns;
/**
* Creates the listener.
*
* @param context A {@link Context}.
*/
private MediaMetricsListener(Context context, PlaybackSession playbackSession) {
context = context.getApplicationContext();
this.context = context;
this.playbackSession = playbackSession;
window = new Timeline.Window();
period = new Timeline.Period();
startTimeMs = SystemClock.elapsedRealtime();
currentPlaybackState = PlaybackStateEvent.STATE_NOT_STARTED;
currentNetworkType = NetworkEvent.NETWORK_TYPE_UNKNOWN;
sessionManager = new DefaultPlaybackSessionManager();
sessionManager.setListener(this);
}
/** Returns the {@link LogSessionId} used by this listener. */
public LogSessionId getLogSessionId() {
return playbackSession.getSessionId();
}
// PlaybackSessionManager.Listener implementation.
@Override
public void onSessionCreated(EventTime eventTime, String sessionId) {}
@Override
public void onSessionActive(EventTime eventTime, String sessionId) {
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
// Ignore ad sessions.
return;
}
finishCurrentSession();
metricsBuilder =
new PlaybackMetrics.Builder()
.setPlayerName(ExoPlayerLibraryInfo.TAG)
.setPlayerVersion(ExoPlayerLibraryInfo.VERSION);
maybeUpdateTimelineMetadata(eventTime.timeline, eventTime.mediaPeriodId);
}
@Override
public void onAdPlaybackStarted(
EventTime eventTime, String contentSessionId, String adSessionId) {}
@Override
public void onSessionFinished(
EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback) {
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
// Ignore ad sessions.
return;
}
finishCurrentSession();
}
// AnalyticsListener implementation.
@Override
public void onPositionDiscontinuity(
EventTime eventTime,
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = true;
}
discontinuityReason = reason;
}
@Override
public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
// TODO(b/181122234): DecoderCounters are not re-reported at period boundaries.
droppedFrames += decoderCounters.droppedBufferCount;
playedFrames += decoderCounters.renderedOutputBufferCount;
}
@Override
public void onBandwidthEstimate(
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
bandwidthTimeMs += totalLoadTimeMs;
bandwidthBytes += totalBytesLoaded;
}
@Override
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
PendingFormatUpdate update =
new PendingFormatUpdate(
checkNotNull(mediaLoadData.trackFormat),
mediaLoadData.trackSelectionReason,
sessionManager.getSessionForMediaPeriodId(
eventTime.timeline, checkNotNull(eventTime.mediaPeriodId)));
switch (mediaLoadData.trackType) {
case C.TRACK_TYPE_VIDEO:
case C.TRACK_TYPE_DEFAULT:
pendingVideoFormat = update;
break;
case C.TRACK_TYPE_AUDIO:
pendingAudioFormat = update;
break;
case C.TRACK_TYPE_TEXT:
pendingTextFormat = update;
break;
default:
// Other track type. Ignore.
}
}
@Override
public void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) {
@Nullable PendingFormatUpdate pendingVideoFormat = this.pendingVideoFormat;
if (pendingVideoFormat != null && pendingVideoFormat.format.height == Format.NO_VALUE) {
Format formatWithHeightAndWidth =
pendingVideoFormat
.format
.buildUpon()
.setWidth(videoSize.width)
.setHeight(videoSize.height)
.build();
this.pendingVideoFormat =
new PendingFormatUpdate(
formatWithHeightAndWidth,
pendingVideoFormat.selectionReason,
pendingVideoFormat.sessionId);
}
}
@Override
public void onLoadError(
EventTime eventTime,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
ioErrorType = mediaLoadData.dataType;
}
@Override
public void onPlayerError(EventTime eventTime, PlaybackException error) {
pendingPlayerError = error;
}
@Override
public void onEvents(Player player, Events events) {
if (events.size() == 0) {
return;
}
maybeAddSessions(events);
long realtimeMs = SystemClock.elapsedRealtime();
maybeUpdateMetricsBuilderValues(player, events);
maybeReportPlaybackError(realtimeMs);
maybeReportTrackChanges(player, events, realtimeMs);
maybeReportNetworkChange(realtimeMs);
maybeReportPlaybackStateChange(player, events, realtimeMs);
if (events.contains(AnalyticsListener.EVENT_PLAYER_RELEASED)) {
sessionManager.finishAllSessions(events.getEventTime(EVENT_PLAYER_RELEASED));
}
}
private void maybeAddSessions(Events events) {
for (int i = 0; i < events.size(); i++) {
@EventFlags int event = events.get(i);
EventTime eventTime = events.getEventTime(event);
if (event == EVENT_TIMELINE_CHANGED) {
sessionManager.updateSessionsWithTimelineChange(eventTime);
} else if (event == EVENT_POSITION_DISCONTINUITY) {
sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason);
} else {
sessionManager.updateSessions(eventTime);
}
}
}
private void maybeUpdateMetricsBuilderValues(Player player, Events events) {
if (events.contains(EVENT_TIMELINE_CHANGED)) {
EventTime eventTime = events.getEventTime(EVENT_TIMELINE_CHANGED);
if (metricsBuilder != null) {
maybeUpdateTimelineMetadata(eventTime.timeline, eventTime.mediaPeriodId);
}
}
if (events.contains(EVENT_TRACKS_CHANGED) && metricsBuilder != null) {
@Nullable
DrmInitData drmInitData = getDrmInitData(player.getCurrentTracksInfo().getTrackGroupInfos());
if (drmInitData != null) {
castNonNull(metricsBuilder).setDrmType(getDrmType(drmInitData));
}
}
if (events.contains(EVENT_AUDIO_UNDERRUN)) {
audioUnderruns++;
}
}
private void maybeReportPlaybackError(long realtimeMs) {
@Nullable PlaybackException error = pendingPlayerError;
if (error == null) {
return;
}
ErrorInfo errorInfo =
getErrorInfo(
error, context, /* lastIoErrorForManifest= */ ioErrorType == C.DATA_TYPE_MANIFEST);
playbackSession.reportPlaybackErrorEvent(
new PlaybackErrorEvent.Builder()
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.setErrorCode(errorInfo.errorCode)
.setSubErrorCode(errorInfo.subErrorCode)
.setException(error)
.build());
pendingPlayerError = null;
}
private void maybeReportTrackChanges(Player player, Events events, long realtimeMs) {
if (events.contains(EVENT_TRACKS_CHANGED)) {
TracksInfo tracksInfo = player.getCurrentTracksInfo();
boolean isVideoSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO);
boolean isAudioSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO);
boolean isTextSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_TEXT);
if (isVideoSelected || isAudioSelected || isTextSelected) {
// Ignore updates with insufficient information where no tracks are selected.
if (!isVideoSelected) {
maybeUpdateVideoFormat(realtimeMs, /* videoFormat= */ null, C.SELECTION_REASON_UNKNOWN);
}
if (!isAudioSelected) {
maybeUpdateAudioFormat(realtimeMs, /* audioFormat= */ null, C.SELECTION_REASON_UNKNOWN);
}
if (!isTextSelected) {
maybeUpdateTextFormat(realtimeMs, /* textFormat= */ null, C.SELECTION_REASON_UNKNOWN);
}
}
}
if (canReportPendingFormatUpdate(pendingVideoFormat)
&& pendingVideoFormat.format.height != Format.NO_VALUE) {
maybeUpdateVideoFormat(
realtimeMs, pendingVideoFormat.format, pendingVideoFormat.selectionReason);
pendingVideoFormat = null;
}
if (canReportPendingFormatUpdate(pendingAudioFormat)) {
maybeUpdateAudioFormat(
realtimeMs, pendingAudioFormat.format, pendingAudioFormat.selectionReason);
pendingAudioFormat = null;
}
if (canReportPendingFormatUpdate(pendingTextFormat)) {
maybeUpdateTextFormat(
realtimeMs, pendingTextFormat.format, pendingTextFormat.selectionReason);
pendingTextFormat = null;
}
}
@EnsuresNonNullIf(result = true, expression = "#1")
private boolean canReportPendingFormatUpdate(@Nullable PendingFormatUpdate pendingFormatUpdate) {
return pendingFormatUpdate != null
&& pendingFormatUpdate.sessionId.equals(sessionManager.getActiveSessionId());
}
private void maybeReportNetworkChange(long realtimeMs) {
int networkType = getNetworkType(context);
if (networkType != currentNetworkType) {
currentNetworkType = networkType;
playbackSession.reportNetworkEvent(
new NetworkEvent.Builder()
.setNetworkType(networkType)
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.build());
}
}
private void maybeReportPlaybackStateChange(Player player, Events events, long realtimeMs) {
if (player.getPlaybackState() != Player.STATE_BUFFERING) {
isSeeking = false;
}
if (player.getPlayerError() == null) {
hasFatalError = false;
} else if (events.contains(EVENT_PLAYER_ERROR)) {
hasFatalError = true;
}
int newPlaybackState = resolveNewPlaybackState(player);
if (currentPlaybackState != newPlaybackState) {
currentPlaybackState = newPlaybackState;
playbackSession.reportPlaybackStateEvent(
new PlaybackStateEvent.Builder()
.setState(currentPlaybackState)
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.build());
}
}
private int resolveNewPlaybackState(Player player) {
@Player.State int playerPlaybackState = player.getPlaybackState();
if (isSeeking) {
// Seeking takes precedence over errors such that we report a seek while in error state.
return PlaybackStateEvent.STATE_SEEKING;
} else if (hasFatalError) {
return PlaybackStateEvent.STATE_FAILED;
} else if (playerPlaybackState == Player.STATE_ENDED) {
return PlaybackStateEvent.STATE_ENDED;
} else if (playerPlaybackState == Player.STATE_BUFFERING) {
if (currentPlaybackState == PlaybackStateEvent.STATE_NOT_STARTED
|| currentPlaybackState == PlaybackStateEvent.STATE_JOINING_FOREGROUND) {
return PlaybackStateEvent.STATE_JOINING_FOREGROUND;
}
if (!player.getPlayWhenReady()) {
return PlaybackStateEvent.STATE_PAUSED_BUFFERING;
}
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
? PlaybackStateEvent.STATE_SUPPRESSED_BUFFERING
: PlaybackStateEvent.STATE_BUFFERING;
} else if (playerPlaybackState == Player.STATE_READY) {
if (!player.getPlayWhenReady()) {
return PlaybackStateEvent.STATE_PAUSED;
}
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
? PlaybackStateEvent.STATE_SUPPRESSED
: PlaybackStateEvent.STATE_PLAYING;
} else if (playerPlaybackState == Player.STATE_IDLE
&& currentPlaybackState != PlaybackStateEvent.STATE_NOT_STARTED) {
// This case only applies for calls to player.stop(). All other IDLE cases are handled by
// !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.
return PlaybackStateEvent.STATE_STOPPED;
}
return currentPlaybackState;
}
private void maybeUpdateVideoFormat(
long realtimeMs, @Nullable Format videoFormat, @C.SelectionReason int trackSelectionReason) {
if (Util.areEqual(currentVideoFormat, videoFormat)) {
return;
}
if (currentVideoFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
trackSelectionReason = C.SELECTION_REASON_INITIAL;
}
currentVideoFormat = videoFormat;
reportTrackChangeEvent(
TrackChangeEvent.TRACK_TYPE_VIDEO, realtimeMs, videoFormat, trackSelectionReason);
}
private void maybeUpdateAudioFormat(
long realtimeMs, @Nullable Format audioFormat, @C.SelectionReason int trackSelectionReason) {
if (Util.areEqual(currentAudioFormat, audioFormat)) {
return;
}
if (currentAudioFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
trackSelectionReason = C.SELECTION_REASON_INITIAL;
}
currentAudioFormat = audioFormat;
reportTrackChangeEvent(
TrackChangeEvent.TRACK_TYPE_AUDIO, realtimeMs, audioFormat, trackSelectionReason);
}
private void maybeUpdateTextFormat(
long realtimeMs, @Nullable Format textFormat, @C.SelectionReason int trackSelectionReason) {
if (Util.areEqual(currentTextFormat, textFormat)) {
return;
}
if (currentTextFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
trackSelectionReason = C.SELECTION_REASON_INITIAL;
}
currentTextFormat = textFormat;
reportTrackChangeEvent(
TrackChangeEvent.TRACK_TYPE_TEXT, realtimeMs, textFormat, trackSelectionReason);
}
private void reportTrackChangeEvent(
int type,
long realtimeMs,
@Nullable Format format,
@C.SelectionReason int trackSelectionReason) {
TrackChangeEvent.Builder builder =
new TrackChangeEvent.Builder(type).setTimeSinceCreatedMillis(realtimeMs - startTimeMs);
if (format != null) {
builder.setTrackState(TrackChangeEvent.TRACK_STATE_ON);
builder.setTrackChangeReason(getTrackChangeReason(trackSelectionReason));
if (format.containerMimeType != null) {
// TODO(b/181121074): Progressive container mime type is not filled in by MediaSource.
builder.setContainerMimeType(format.containerMimeType);
}
if (format.sampleMimeType != null) {
builder.setSampleMimeType(format.sampleMimeType);
}
if (format.codecs != null) {
builder.setCodecName(format.codecs);
}
if (format.bitrate != Format.NO_VALUE) {
builder.setBitrate(format.bitrate);
}
if (format.width != Format.NO_VALUE) {
builder.setWidth(format.width);
}
if (format.height != Format.NO_VALUE) {
builder.setHeight(format.height);
}
if (format.channelCount != Format.NO_VALUE) {
builder.setChannelCount(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
builder.setAudioSampleRate(format.sampleRate);
}
if (format.language != null) {
Pair<String, @NullableType String> languageAndRegion =
getLanguageAndRegion(format.language);
builder.setLanguage(languageAndRegion.first);
if (languageAndRegion.second != null) {
builder.setLanguageRegion(languageAndRegion.second);
}
}
if (format.frameRate != Format.NO_VALUE) {
builder.setVideoFrameRate(format.frameRate);
}
} else {
builder.setTrackState(TrackChangeEvent.TRACK_STATE_OFF);
}
playbackSession.reportTrackChangeEvent(builder.build());
}
@RequiresNonNull("metricsBuilder")
private void maybeUpdateTimelineMetadata(
Timeline timeline, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
PlaybackMetrics.Builder metricsBuilder = this.metricsBuilder;
if (mediaPeriodId == null) {
return;
}
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
if (periodIndex == C.INDEX_UNSET) {
return;
}
timeline.getPeriod(periodIndex, period);
timeline.getWindow(period.windowIndex, window);
metricsBuilder.setStreamType(getStreamType(window.mediaItem));
if (window.durationUs != C.TIME_UNSET
&& !window.isPlaceholder
&& !window.isDynamic
&& !window.isLive()) {
metricsBuilder.setMediaDurationMillis(window.getDurationMs());
}
metricsBuilder.setPlaybackType(
window.isLive() ? PlaybackMetrics.PLAYBACK_TYPE_LIVE : PlaybackMetrics.PLAYBACK_TYPE_VOD);
}
private void finishCurrentSession() {
if (metricsBuilder == null) {
return;
}
metricsBuilder.setAudioUnderrunCount(audioUnderruns);
metricsBuilder.setVideoFramesDropped(droppedFrames);
metricsBuilder.setVideoFramesPlayed(playedFrames);
metricsBuilder.setNetworkTransferDurationMillis(bandwidthTimeMs);
// TODO(b/181121847): Report localBytesRead. This requires additional callbacks or plumbing.
metricsBuilder.setNetworkBytesRead(bandwidthBytes);
// TODO(b/181121847): Detect stream sources mixed and local depending on localBytesRead.
metricsBuilder.setStreamSource(
bandwidthBytes > 0
? PlaybackMetrics.STREAM_SOURCE_NETWORK
: PlaybackMetrics.STREAM_SOURCE_UNKNOWN);
playbackSession.reportPlaybackMetrics(metricsBuilder.build());
metricsBuilder = null;
}
private static int getTrackChangeReason(@C.SelectionReason int trackSelectionReason) {
switch (trackSelectionReason) {
case C.SELECTION_REASON_INITIAL:
return TrackChangeEvent.TRACK_CHANGE_REASON_INITIAL;
case C.SELECTION_REASON_ADAPTIVE:
return TrackChangeEvent.TRACK_CHANGE_REASON_ADAPTIVE;
case C.SELECTION_REASON_MANUAL:
return TrackChangeEvent.TRACK_CHANGE_REASON_MANUAL;
case C.SELECTION_REASON_TRICK_PLAY:
case C.SELECTION_REASON_UNKNOWN:
default:
return TrackChangeEvent.TRACK_CHANGE_REASON_OTHER;
}
}
private static Pair<String, @NullableType String> getLanguageAndRegion(String languageCode) {
String[] parts = Util.split(languageCode, "-");
return Pair.create(parts[0], parts.length >= 2 ? parts[1] : null);
}
private static int getNetworkType(Context context) {
switch (NetworkTypeObserver.getInstance(context).getNetworkType()) {
case C.NETWORK_TYPE_WIFI:
return NetworkEvent.NETWORK_TYPE_WIFI;
case C.NETWORK_TYPE_2G:
return NetworkEvent.NETWORK_TYPE_2G;
case C.NETWORK_TYPE_3G:
return NetworkEvent.NETWORK_TYPE_3G;
case C.NETWORK_TYPE_4G:
return NetworkEvent.NETWORK_TYPE_4G;
case C.NETWORK_TYPE_5G_SA:
return NetworkEvent.NETWORK_TYPE_5G_SA;
case C.NETWORK_TYPE_5G_NSA:
return NetworkEvent.NETWORK_TYPE_5G_NSA;
case C.NETWORK_TYPE_ETHERNET:
return NetworkEvent.NETWORK_TYPE_ETHERNET;
case C.NETWORK_TYPE_OFFLINE:
return NetworkEvent.NETWORK_TYPE_OFFLINE;
case C.NETWORK_TYPE_UNKNOWN:
return NetworkEvent.NETWORK_TYPE_UNKNOWN;
default:
return NetworkEvent.NETWORK_TYPE_OTHER;
}
}
private static int getStreamType(MediaItem mediaItem) {
if (mediaItem.localConfiguration == null || mediaItem.localConfiguration.mimeType == null) {
return PlaybackMetrics.STREAM_TYPE_UNKNOWN;
}
String mimeType = mediaItem.localConfiguration.mimeType;
switch (mimeType) {
case MimeTypes.APPLICATION_M3U8:
return PlaybackMetrics.STREAM_TYPE_HLS;
case MimeTypes.APPLICATION_MPD:
return PlaybackMetrics.STREAM_TYPE_DASH;
case MimeTypes.APPLICATION_SS:
return PlaybackMetrics.STREAM_TYPE_SS;
default:
return PlaybackMetrics.STREAM_TYPE_PROGRESSIVE;
}
}
private static ErrorInfo getErrorInfo(
PlaybackException error, Context context, boolean lastIoErrorForManifest) {
if (error.errorCode == PlaybackException.ERROR_CODE_REMOTE_ERROR) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_PLAYER_REMOTE, /* subErrorCode= */ 0);
}
// Unpack the PlaybackException.
// TODO(b/190203080): Use error codes instead of the Exception's cause where possible.
boolean isRendererExoPlaybackException = false;
int rendererFormatSupport = C.FORMAT_UNSUPPORTED_TYPE;
if (error instanceof ExoPlaybackException) {
ExoPlaybackException exoPlaybackException = (ExoPlaybackException) error;
isRendererExoPlaybackException =
exoPlaybackException.type == ExoPlaybackException.TYPE_RENDERER;
rendererFormatSupport = exoPlaybackException.rendererFormatSupport;
}
Throwable cause = checkNotNull(error.getCause());
if (cause instanceof IOException) {
if (cause instanceof HttpDataSource.InvalidResponseCodeException) {
int responseCode = ((HttpDataSource.InvalidResponseCodeException) cause).responseCode;
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_BAD_HTTP_STATUS, /* subErrorCode= */ responseCode);
} else if (cause instanceof HttpDataSource.InvalidContentTypeException
|| cause instanceof ParserException) {
return new ErrorInfo(
lastIoErrorForManifest
? PlaybackErrorEvent.ERROR_PARSING_MANIFEST_MALFORMED
: PlaybackErrorEvent.ERROR_PARSING_CONTAINER_MALFORMED,
/* subErrorCode= */ 0);
} else if (cause instanceof HttpDataSource.HttpDataSourceException
|| cause instanceof UdpDataSource.UdpDataSourceException) {
if (NetworkTypeObserver.getInstance(context).getNetworkType() == C.NETWORK_TYPE_OFFLINE) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_NETWORK_UNAVAILABLE, /* subErrorCode= */ 0);
} else {
@Nullable Throwable detailedCause = cause.getCause();
if (detailedCause instanceof UnknownHostException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_DNS_FAILED, /* subErrorCode= */ 0);
} else if (detailedCause instanceof SocketTimeoutException) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_CONNECTION_TIMEOUT, /* subErrorCode= */ 0);
} else if (cause instanceof HttpDataSource.HttpDataSourceException
&& ((HttpDataSource.HttpDataSourceException) cause).type
== HttpDataSource.HttpDataSourceException.TYPE_OPEN) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_NETWORK_CONNECTION_FAILED, /* subErrorCode= */ 0);
} else {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_CONNECTION_CLOSED, /* subErrorCode= */ 0);
}
}
} else if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_PLAYER_BEHIND_LIVE_WINDOW, /* subErrorCode= */ 0);
} else if (cause instanceof DrmSession.DrmSessionException) {
// Unpack DrmSessionException.
cause = checkNotNull(cause.getCause());
if (Util.SDK_INT >= 21 && cause instanceof MediaDrm.MediaDrmStateException) {
String diagnosticsInfo = ((MediaDrm.MediaDrmStateException) cause).getDiagnosticInfo();
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
int errorCode = getDrmErrorCode(subErrorCode);
return new ErrorInfo(errorCode, subErrorCode);
} else if (Util.SDK_INT >= 23 && cause instanceof MediaDrmResetException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_SYSTEM_ERROR, /* subErrorCode= */ 0);
} else if (Util.SDK_INT >= 18 && cause instanceof NotProvisionedException) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DRM_PROVISIONING_FAILED, /* subErrorCode= */ 0);
} else if (Util.SDK_INT >= 18 && cause instanceof DeniedByServerException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_DEVICE_REVOKED, /* subErrorCode= */ 0);
} else if (cause instanceof UnsupportedDrmException) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DRM_SCHEME_UNSUPPORTED, /* subErrorCode= */ 0);
} else if (cause instanceof DefaultDrmSessionManager.MissingSchemeDataException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_CONTENT_ERROR, /* subErrorCode= */ 0);
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_OTHER, /* subErrorCode= */ 0);
}
} else if (cause instanceof FileDataSource.FileDataSourceException
&& cause.getCause() instanceof FileNotFoundException) {
@Nullable Throwable notFoundCause = checkNotNull(cause.getCause()).getCause();
if (Util.SDK_INT >= 21
&& notFoundCause instanceof ErrnoException
&& ((ErrnoException) notFoundCause).errno == OsConstants.EACCES) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_NO_PERMISSION, /* subErrorCode= */ 0);
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_FILE_NOT_FOUND, /* subErrorCode= */ 0);
}
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_OTHER, /* subErrorCode= */ 0);
}
} else if (isRendererExoPlaybackException
&& (rendererFormatSupport == C.FORMAT_UNSUPPORTED_TYPE
|| rendererFormatSupport == C.FORMAT_UNSUPPORTED_SUBTYPE)) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DECODING_FORMAT_UNSUPPORTED, /* subErrorCode= */ 0);
} else if (isRendererExoPlaybackException
&& rendererFormatSupport == C.FORMAT_EXCEEDS_CAPABILITIES) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DECODING_FORMAT_EXCEEDS_CAPABILITIES, /* subErrorCode= */ 0);
} else if (isRendererExoPlaybackException
&& rendererFormatSupport == C.FORMAT_UNSUPPORTED_DRM) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_SCHEME_UNSUPPORTED, /* subErrorCode= */ 0);
} else if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
@Nullable
String diagnosticsInfo =
((MediaCodecRenderer.DecoderInitializationException) cause).diagnosticInfo;
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODER_INIT_FAILED, subErrorCode);
} else if (cause instanceof MediaCodecDecoderException) {
@Nullable String diagnosticsInfo = ((MediaCodecDecoderException) cause).diagnosticInfo;
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODING_FAILED, subErrorCode);
} else if (cause instanceof OutOfMemoryError) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODING_FAILED, /* subErrorCode= */ 0);
} else if (cause instanceof AudioSink.InitializationException) {
int subErrorCode = ((AudioSink.InitializationException) cause).audioTrackState;
return new ErrorInfo(PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, subErrorCode);
} else if (cause instanceof AudioSink.WriteException) {
int subErrorCode = ((AudioSink.WriteException) cause).errorCode;
return new ErrorInfo(PlaybackErrorEvent.ERROR_AUDIO_TRACK_WRITE_FAILED, subErrorCode);
} else if (Util.SDK_INT >= 16 && cause instanceof MediaCodec.CryptoException) {
int subErrorCode = ((MediaCodec.CryptoException) cause).getErrorCode();
int errorCode = getDrmErrorCode(subErrorCode);
return new ErrorInfo(errorCode, subErrorCode);
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_PLAYER_OTHER, /* subErrorCode= */ 0);
}
}
@Nullable
private static DrmInitData getDrmInitData(ImmutableList<TrackGroupInfo> trackGroupInfos) {
for (TrackGroupInfo trackGroupInfo : trackGroupInfos) {
TrackGroup trackGroup = trackGroupInfo.getTrackGroup();
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
if (trackGroupInfo.isTrackSelected(trackIndex)) {
@Nullable DrmInitData drmInitData = trackGroup.getFormat(trackIndex).drmInitData;
if (drmInitData != null) {
return drmInitData;
}
}
}
}
return null;
}
private static int getDrmType(DrmInitData drmInitData) {
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
UUID uuid = drmInitData.get(i).uuid;
if (uuid.equals(C.WIDEVINE_UUID)) {
// TODO(b/77625596): Forward MediaDrm metrics to distinguish between L1 and L3 and to set
// the drm session id.
return PlaybackMetrics.DRM_TYPE_WIDEVINE_L1;
}
if (uuid.equals(C.PLAYREADY_UUID)) {
return PlaybackMetrics.DRM_TYPE_PLAY_READY;
}
if (uuid.equals(C.CLEARKEY_UUID)) {
return PlaybackMetrics.DRM_TYPE_CLEARKEY;
}
}
return PlaybackMetrics.DRM_TYPE_OTHER;
}
@SuppressLint("SwitchIntDef") // Only DRM error codes are relevant here.
private static int getDrmErrorCode(int mediaDrmErrorCode) {
switch (Util.getErrorCodeForMediaDrmErrorCode(mediaDrmErrorCode)) {
case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED:
return PlaybackErrorEvent.ERROR_DRM_PROVISIONING_FAILED;
case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
return PlaybackErrorEvent.ERROR_DRM_LICENSE_ACQUISITION_FAILED;
case PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION:
return PlaybackErrorEvent.ERROR_DRM_DISALLOWED_OPERATION;
case PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR:
return PlaybackErrorEvent.ERROR_DRM_CONTENT_ERROR;
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
default:
return PlaybackErrorEvent.ERROR_DRM_SYSTEM_ERROR;
}
}
private static final class ErrorInfo {
public final int errorCode;
public final int subErrorCode;
public ErrorInfo(int errorCode, int subErrorCode) {
this.errorCode = errorCode;
this.subErrorCode = subErrorCode;
}
}
private static final class PendingFormatUpdate {
public final Format format;
@C.SelectionReason public final int selectionReason;
public final String sessionId;
public PendingFormatUpdate(
Format format, @C.SelectionReason int selectionReason, String sessionId) {
this.format = format;
this.selectionReason = selectionReason;
this.sessionId = sessionId;
}
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.analytics;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.media.metrics.LogSessionId;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.Util;
/** Identifier for a player instance. */
public final class PlayerId {
/**
* A player identifier with unset default values that can be used as a placeholder or for testing.
*/
public static final PlayerId UNSET =
Util.SDK_INT < 31 ? new PlayerId() : new PlayerId(LogSessionIdApi31.UNSET);
@Nullable private final LogSessionIdApi31 logSessionIdApi31;
/** Creates an instance for API &lt; 31. */
public PlayerId() {
this(/* logSessionIdApi31= */ (LogSessionIdApi31) null);
checkState(Util.SDK_INT < 31);
}
/**
* Creates an instance for API &ge; 31.
*
* @param logSessionId The {@link LogSessionId} used for this player.
*/
@RequiresApi(31)
public PlayerId(LogSessionId logSessionId) {
this(new LogSessionIdApi31(logSessionId));
}
private PlayerId(@Nullable LogSessionIdApi31 logSessionIdApi31) {
this.logSessionIdApi31 = logSessionIdApi31;
}
/** Returns the {@link LogSessionId} for this player instance. */
@RequiresApi(31)
public LogSessionId getLogSessionId() {
return checkNotNull(logSessionIdApi31).logSessionId;
}
@RequiresApi(31)
private static final class LogSessionIdApi31 {
public static final LogSessionIdApi31 UNSET =
new LogSessionIdApi31(LogSessionId.LOG_SESSION_ID_NONE);
public final LogSessionId logSessionId;
public LogSessionIdApi31(LogSessionId logSessionId) {
this.logSessionId = logSessionId;
}
}
}

View file

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.analytics.PlayerId;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -281,6 +282,13 @@ public interface AudioSink {
*/
void setListener(Listener listener);
/**
* Sets the {@link PlayerId} of the player using this audio sink.
*
* @param playerId The {@link PlayerId}, or null to clear a previously set id.
*/
default void setPlayerId(@Nullable PlayerId playerId) {}
/**
* Returns whether the sink supports a given {@link Format}.
*

View file

@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCA
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_REUSE_NOT_IMPLEMENTED;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static com.google.android.exoplayer2.source.SampleStream.FLAG_REQUIRE_FORMAT;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.lang.Math.max;
import android.os.Handler;
@ -162,16 +163,24 @@ public abstract class DecoderAudioRenderer<
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioCapabilities The audio capabilities for playback on this device. Use {@link
* AudioCapabilities#DEFAULT_AUDIO_CAPABILITIES} if default capabilities (no encoded audio
* passthrough support) should be assumed.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public DecoderAudioRenderer(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
@Nullable AudioCapabilities audioCapabilities,
AudioCapabilities audioCapabilities,
AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors));
this(
eventHandler,
eventListener,
new DefaultAudioSink.Builder()
.setAudioCapabilities( // For backward compatibility, null == default.
firstNonNull(audioCapabilities, AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES))
.setAudioProcessors(audioProcessors)
.build());
}
/**
@ -463,7 +472,7 @@ public abstract class DecoderAudioRenderer<
onQueueInputBuffer(inputBuffer);
decoder.queueInputBuffer(inputBuffer);
decoderReceivedBuffers = true;
decoderCounters.inputBufferCount++;
decoderCounters.queuedInputBufferCount++;
inputBuffer = null;
return true;
default:
@ -530,6 +539,7 @@ public abstract class DecoderAudioRenderer<
} else {
audioSink.disableTunneling();
}
audioSink.setPlayerId(getPlayerId());
}
@Override

View file

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.audio.AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -23,21 +26,26 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.PlaybackParams;
import android.media.metrics.LogSessionId;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.errorprone.annotations.InlineMe;
import com.google.errorprone.annotations.InlineMeValidationDisabled;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -47,6 +55,7 @@ import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback
@ -200,6 +209,108 @@ public final class DefaultAudioSink implements AudioSink {
}
}
/** A builder to create {@link DefaultAudioSink} instances. */
public static final class Builder {
private AudioCapabilities audioCapabilities;
@Nullable private AudioProcessorChain audioProcessorChain;
private boolean enableFloatOutput;
private boolean enableAudioTrackPlaybackParams;
private int offloadMode;
/** Creates a new builder. */
public Builder() {
audioCapabilities = DEFAULT_AUDIO_CAPABILITIES;
offloadMode = OFFLOAD_MODE_DISABLED;
}
/**
* Sets audio capabilities for playback on this device. May be {@code null} if the default
* capabilities (no encoded audio passthrough support) should be assumed.
*
* <p>Default is {@link AudioCapabilities#DEFAULT_AUDIO_CAPABILITIES}.
*/
public Builder setAudioCapabilities(AudioCapabilities audioCapabilities) {
checkNotNull(audioCapabilities);
this.audioCapabilities = audioCapabilities;
return this;
}
/**
* Sets an array of {@link AudioProcessor AudioProcessors}s that will process PCM audio before
* output. May be empty. Equivalent of {@code setAudioProcessorChain(new
* DefaultAudioProcessorChain(audioProcessors)}.
*
* <p>The default value is an empty array.
*/
public Builder setAudioProcessors(AudioProcessor[] audioProcessors) {
checkNotNull(audioProcessors);
return setAudioProcessorChain(new DefaultAudioProcessorChain(audioProcessors));
}
/**
* Sets the {@link AudioProcessorChain} to process audio before playback. The instance passed in
* must not be reused in other sinks. Processing chains are only supported for PCM playback (not
* passthrough or offload).
*
* <p>By default, no processing will be applied.
*/
public Builder setAudioProcessorChain(AudioProcessorChain audioProcessorChain) {
checkNotNull(audioProcessorChain);
this.audioProcessorChain = audioProcessorChain;
return this;
}
/**
* Sets whether to enable 32-bit float output or integer output. Where possible, 32-bit float
* output will be used if the input is 32-bit float, and also if the input is high resolution
* (24-bit or 32-bit) integer PCM. Float output is supported from API level 21. Audio processing
* (for example, speed adjustment) will not be available when float output is in use.
*
* <p>The default value is {@code false}.
*/
public Builder setEnableFloatOutput(boolean enableFloatOutput) {
this.enableFloatOutput = enableFloatOutput;
return this;
}
/**
* Sets whether to control the playback speed using the platform implementation (see {@link
* AudioTrack#setPlaybackParams(PlaybackParams)}), if supported. If set to {@code false}, speed
* up/down of the audio will be done by ExoPlayer (see {@link SonicAudioProcessor}). Platform
* speed adjustment is lower latency, but less reliable.
*
* <p>The default value is {@code false}.
*/
public Builder setEnableAudioTrackPlaybackParams(boolean enableAudioTrackPlaybackParams) {
this.enableAudioTrackPlaybackParams = enableAudioTrackPlaybackParams;
return this;
}
/**
* Sets the offload mode. If an audio format can be both played with offload and encoded audio
* passthrough, it will be played in offload. Audio offload is supported from API level 29. Most
* Android devices can only support one offload {@link AudioTrack} at a time and can invalidate
* it at any time. Thus an app can never be guaranteed that it will be able to play in offload.
* Audio processing (for example, speed adjustment) will not be available when offload is in
* use.
*
* <p>The default value is {@link #OFFLOAD_MODE_DISABLED}.
*/
public Builder setOffloadMode(@OffloadMode int offloadMode) {
this.offloadMode = offloadMode;
return this;
}
/** Builds the {@link DefaultAudioSink}. Must only be called once per Builder instance. */
public DefaultAudioSink build() {
if (audioProcessorChain == null) {
audioProcessorChain = new DefaultAudioProcessorChain();
}
return new DefaultAudioSink(this);
}
}
/** The default playback speed. */
public static final float DEFAULT_PLAYBACK_SPEED = 1f;
/** The minimum allowed playback speed. Lower values will be constrained to fall in range. */
@ -306,7 +417,7 @@ public final class DefaultAudioSink implements AudioSink {
*/
public static boolean failOnSpuriousAudioTimestamp = false;
@Nullable private final AudioCapabilities audioCapabilities;
private final AudioCapabilities audioCapabilities;
private final AudioProcessorChain audioProcessorChain;
private final boolean enableFloatOutput;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
@ -323,6 +434,7 @@ public final class DefaultAudioSink implements AudioSink {
initializationExceptionPendingExceptionHolder;
private final PendingExceptionHolder<WriteException> writeExceptionPendingExceptionHolder;
@Nullable private PlayerId playerId;
@Nullable private Listener listener;
@Nullable private Configuration pendingConfiguration;
@MonotonicNonNull private Configuration configuration;
@ -366,76 +478,81 @@ public final class DefaultAudioSink implements AudioSink {
private boolean offloadDisabledUntilNextConfiguration;
private boolean isWaitingForOffloadEndOfStreamHandled;
/**
* Creates a new default audio sink.
*
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
* output. May be empty.
*/
/** @deprecated Use {@link Builder}. */
@Deprecated
@InlineMeValidationDisabled("Migrate constructor to Builder")
@InlineMe(
replacement =
"new DefaultAudioSink.Builder()"
+ ".setAudioCapabilities(audioCapabilities)"
+ ".setAudioProcessors(audioProcessors)"
+ ".build()",
imports = "com.google.android.exoplayer2.audio.DefaultAudioSink")
public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) {
this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false);
this(
new Builder()
.setAudioCapabilities(firstNonNull(audioCapabilities, DEFAULT_AUDIO_CAPABILITIES))
.setAudioProcessors(audioProcessors));
}
/**
* Creates a new default audio sink, optionally using float output for high resolution PCM.
*
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
* output. May be empty.
* @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float
* output will be used if the input is 32-bit float, and also if the input is high resolution
* (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not
* be available when float output is in use.
*/
/** @deprecated Use {@link Builder}. */
@Deprecated
@InlineMeValidationDisabled("Migrate constructor to Builder")
@InlineMe(
replacement =
"new DefaultAudioSink.Builder()"
+ ".setAudioCapabilities(audioCapabilities)"
+ ".setAudioProcessors(audioProcessors)"
+ ".setEnableFloatOutput(enableFloatOutput)"
+ ".build()",
imports = "com.google.android.exoplayer2.audio.DefaultAudioSink")
public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities,
AudioProcessor[] audioProcessors,
boolean enableFloatOutput) {
this(
audioCapabilities,
new DefaultAudioProcessorChain(audioProcessors),
enableFloatOutput,
/* enableAudioTrackPlaybackParams= */ false,
OFFLOAD_MODE_DISABLED);
new Builder()
.setAudioCapabilities(firstNonNull(audioCapabilities, DEFAULT_AUDIO_CAPABILITIES))
.setAudioProcessors(audioProcessors)
.setEnableFloatOutput(enableFloatOutput));
}
/**
* Creates a new default audio sink, optionally using float output for high resolution PCM and
* with the specified {@code audioProcessorChain}.
*
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback
* parameters adjustments. The instance passed in must not be reused in other sinks.
* @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float
* output will be used if the input is 32-bit float, and also if the input is high resolution
* (24-bit or 32-bit) integer PCM. Float output is supported from API level 21. Audio
* processing (for example, speed adjustment) will not be available when float output is in
* use.
* @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
* android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
* @param offloadMode Audio offload configuration. If an audio format can be both played with
* offload and encoded audio passthrough, it will be played in offload. Audio offload is
* supported from API level 29. Most Android devices can only support one offload {@link
* android.media.AudioTrack} at a time and can invalidate it at any time. Thus an app can
* never be guaranteed that it will be able to play in offload. Audio processing (for example,
* speed adjustment) will not be available when offload is in use.
*/
/** @deprecated Use {@link Builder}. */
@Deprecated
@InlineMeValidationDisabled("Migrate constructor to Builder")
@InlineMe(
replacement =
"new DefaultAudioSink.Builder()"
+ ".setAudioCapabilities(audioCapabilities)"
+ ".setAudioProcessorChain(audioProcessorChain)"
+ ".setEnableFloatOutput(enableFloatOutput)"
+ ".setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)"
+ ".setOffloadMode(offloadMode)"
+ ".build()",
imports = "com.google.android.exoplayer2.audio.DefaultAudioSink")
public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities,
AudioProcessorChain audioProcessorChain,
boolean enableFloatOutput,
boolean enableAudioTrackPlaybackParams,
@OffloadMode int offloadMode) {
this.audioCapabilities = audioCapabilities;
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput;
this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams;
this.offloadMode = Util.SDK_INT >= 29 ? offloadMode : OFFLOAD_MODE_DISABLED;
this(
new Builder()
.setAudioCapabilities(firstNonNull(audioCapabilities, DEFAULT_AUDIO_CAPABILITIES))
.setAudioProcessorChain(audioProcessorChain)
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
.setOffloadMode(offloadMode));
}
@RequiresNonNull("#1.audioProcessorChain")
private DefaultAudioSink(Builder builder) {
audioCapabilities = builder.audioCapabilities;
audioProcessorChain = builder.audioProcessorChain;
enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput;
enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams;
offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED;
releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
@ -477,6 +594,11 @@ public final class DefaultAudioSink implements AudioSink {
this.listener = listener;
}
@Override
public void setPlayerId(@Nullable PlayerId playerId) {
this.playerId = playerId;
}
@Override
public boolean supportsFormat(Format format) {
return getFormatSupport(format) != SINK_FORMAT_UNSUPPORTED;
@ -578,8 +700,7 @@ public final class DefaultAudioSink implements AudioSink {
if (useOffloadedPlayback(inputFormat, audioAttributes)) {
outputMode = OUTPUT_MODE_OFFLOAD;
outputEncoding =
MimeTypes.getEncoding(
Assertions.checkNotNull(inputFormat.sampleMimeType), inputFormat.codecs);
MimeTypes.getEncoding(checkNotNull(inputFormat.sampleMimeType), inputFormat.codecs);
outputChannelConfig = Util.getAudioTrackChannelConfig(inputFormat.channelCount);
} else {
outputMode = OUTPUT_MODE_PASSTHROUGH;
@ -665,6 +786,9 @@ public final class DefaultAudioSink implements AudioSink {
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
}
}
if (Util.SDK_INT >= 31 && playerId != null) {
Api31.setLogSessionIdOnAudioTrack(audioTrack, playerId);
}
audioSessionId = audioTrack.getAudioSessionId();
audioTrackPositionTracker.setAudioTrack(
audioTrack,
@ -850,7 +974,7 @@ public final class DefaultAudioSink implements AudioSink {
private AudioTrack buildAudioTrack() throws InitializationException {
try {
return Assertions.checkNotNull(configuration)
return checkNotNull(configuration)
.buildAudioTrack(tunneling, audioAttributes, audioSessionId);
} catch (InitializationException e) {
maybeDisableOffload();
@ -1195,7 +1319,7 @@ public final class DefaultAudioSink implements AudioSink {
audioTrack.pause();
}
if (isOffloadedPlayback(audioTrack)) {
Assertions.checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack);
checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack);
}
// AudioTrack.release can take some time, so we call it on a background thread.
final AudioTrack toRelease = audioTrack;
@ -1475,7 +1599,7 @@ public final class DefaultAudioSink implements AudioSink {
}
private static boolean isPassthroughPlaybackSupported(
Format format, @Nullable AudioCapabilities audioCapabilities) {
Format format, AudioCapabilities audioCapabilities) {
return getEncodingAndChannelConfigForPassthrough(format, audioCapabilities) != null;
}
@ -1491,14 +1615,9 @@ public final class DefaultAudioSink implements AudioSink {
*/
@Nullable
private static Pair<Integer, Integer> getEncodingAndChannelConfigForPassthrough(
Format format, @Nullable AudioCapabilities audioCapabilities) {
if (audioCapabilities == null) {
return null;
}
Format format, AudioCapabilities audioCapabilities) {
@C.Encoding
int encoding =
MimeTypes.getEncoding(Assertions.checkNotNull(format.sampleMimeType), format.codecs);
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).
@ -1609,8 +1728,7 @@ public final class DefaultAudioSink implements AudioSink {
return false;
}
@C.Encoding
int encoding =
MimeTypes.getEncoding(Assertions.checkNotNull(format.sampleMimeType), format.codecs);
int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs);
if (encoding == C.ENCODING_INVALID) {
return false;
}
@ -2094,6 +2212,7 @@ public final class DefaultAudioSink implements AudioSink {
audioSessionId);
}
@SuppressWarnings("deprecation") // Using deprecated AudioTrack constructor.
private AudioTrack createAudioTrackV9(AudioAttributes audioAttributes, int audioSessionId) {
int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
@ -2216,4 +2335,17 @@ public final class DefaultAudioSink implements AudioSink {
pendingException = null;
}
}
@RequiresApi(31)
private static final class Api31 {
private Api31() {}
@DoNotInline
public static void setLogSessionIdOnAudioTrack(AudioTrack audioTrack, PlayerId playerId) {
LogSessionId logSessionId = playerId.getLogSessionId();
if (!logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE)) {
audioTrack.setLogSessionId(logSessionId);
}
}
}
}

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.analytics.PlayerId;
import java.nio.ByteBuffer;
/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */
@ -34,6 +35,11 @@ public class ForwardingAudioSink implements AudioSink {
sink.setListener(listener);
}
@Override
public void setPlayerId(@Nullable PlayerId playerId) {
sink.setPlayerId(playerId);
}
@Override
public boolean supportsFormat(Format format) {
return sink.supportsFormat(format);

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.lang.Math.max;
import android.annotation.SuppressLint;
@ -129,7 +130,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
MediaCodecSelector mediaCodecSelector,
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener) {
this(context, mediaCodecSelector, eventHandler, eventListener, (AudioCapabilities) null);
this(
context,
mediaCodecSelector,
eventHandler,
eventListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES);
}
/**
@ -138,8 +144,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioCapabilities The audio capabilities for playback on this device. Use {@link
* AudioCapabilities#DEFAULT_AUDIO_CAPABILITIES} if default capabilities (no encoded audio
* passthrough support) should be assumed.
* @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
* output.
*/
@ -148,14 +155,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
MediaCodecSelector mediaCodecSelector,
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
@Nullable AudioCapabilities audioCapabilities,
AudioCapabilities audioCapabilities,
AudioProcessor... audioProcessors) {
this(
context,
mediaCodecSelector,
eventHandler,
eventListener,
new DefaultAudioSink(audioCapabilities, audioProcessors));
new DefaultAudioSink.Builder()
.setAudioCapabilities( // For backward compatibility, null == default.
firstNonNull(audioCapabilities, AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES))
.setAudioProcessors(audioProcessors)
.build());
}
/**
@ -292,30 +303,82 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
}
List<MediaCodecInfo> decoderInfos =
getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false);
getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false, audioSink);
if (decoderInfos.isEmpty()) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
}
if (!supportsFormatDrm) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM);
}
// Check capabilities for the first decoder in the list, which takes priority.
// Check whether the first decoder supports the format. This is the preferred decoder for the
// format's MIME type, according to the MediaCodecSelector.
MediaCodecInfo decoderInfo = decoderInfos.get(0);
boolean isFormatSupported = decoderInfo.isFormatSupported(format);
boolean isPreferredDecoder = true;
if (!isFormatSupported) {
// Check whether any of the other decoders support the format.
for (int i = 1; i < decoderInfos.size(); i++) {
MediaCodecInfo otherDecoderInfo = decoderInfos.get(i);
if (otherDecoderInfo.isFormatSupported(format)) {
decoderInfo = otherDecoderInfo;
isFormatSupported = true;
isPreferredDecoder = false;
break;
}
}
}
@C.FormatSupport
int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES;
@AdaptiveSupport
int adaptiveSupport =
isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format)
? ADAPTIVE_SEAMLESS
: ADAPTIVE_NOT_SEAMLESS;
@C.FormatSupport
int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES;
return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport);
@HardwareAccelerationSupport
int hardwareAccelerationSupport =
decoderInfo.hardwareAccelerated
? HARDWARE_ACCELERATION_SUPPORTED
: HARDWARE_ACCELERATION_NOT_SUPPORTED;
@DecoderSupport
int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK;
return RendererCapabilities.create(
formatSupport,
adaptiveSupport,
tunnelingSupport,
hardwareAccelerationSupport,
decoderSupport);
}
@Override
protected List<MediaCodecInfo> getDecoderInfos(
MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfosSortedByFormatSupport(
getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, audioSink), format);
}
/**
* Returns a list of decoders that can decode media in the specified format, in the priority order
* specified by the {@link MediaCodecSelector}. Note that since the {@link MediaCodecSelector}
* only has access to {@link Format#sampleMimeType}, the list is not ordered to account for
* whether each decoder supports the details of the format (e.g., taking into account the format's
* profile, level, channel count and so on). {@link
* MediaCodecUtil#getDecoderInfosSortedByFormatSupport} can be used to further sort the list into
* an order where decoders that fully support the format come first.
*
* @param mediaCodecSelector The decoder selector.
* @param format The {@link Format} for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required.
* @param audioSink The {@link AudioSink} to which audio will be output.
* @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
private static List<MediaCodecInfo> getDecoderInfos(
MediaCodecSelector mediaCodecSelector,
Format format,
boolean requiresSecureDecoder,
AudioSink audioSink)
throws DecoderQueryException {
@Nullable String mimeType = format.sampleMimeType;
if (mimeType == null) {
return Collections.emptyList();
@ -330,7 +393,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
List<MediaCodecInfo> decoderInfos =
mediaCodecSelector.getDecoderInfos(
mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);
decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format);
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {
// E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
List<MediaCodecInfo> decoderInfosWithEac3 = new ArrayList<>(decoderInfos);
@ -452,10 +514,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} else {
// If the format is anything other than PCM then we assume that the audio decoder will
// output 16-bit PCM.
pcmEncoding =
MimeTypes.AUDIO_RAW.equals(format.sampleMimeType)
? format.pcmEncoding
: C.ENCODING_PCM_16BIT;
pcmEncoding = C.ENCODING_PCM_16BIT;
}
audioSinkInputFormat =
new Format.Builder()
@ -500,6 +559,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} else {
audioSink.disableTunneling();
}
audioSink.setPlayerId(getPlayerId());
}
@Override
@ -787,6 +847,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
== AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY) {
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_FLOAT);
}
if (Util.SDK_INT >= 32) {
// Disable down-mixing in the decoder (for decoders that read the max-output-channel-count
// key).
// TODO[b/190759307]: Update key to use MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT once the
// compile SDK target is set to 32.
mediaFormat.setInteger("max-output-channel-count", 99);
}
return mediaFormat;
}

View file

@ -0,0 +1,217 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Exposes the android.media.Spatializer API via reflection. This is so that we can use the
* Spatializer while the compile SDK target is set to 31.
*/
@RequiresApi(31)
/* package */ final class SpatializerDelegate {
/** Level of support for audio spatialization. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({
SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL,
SPATIALIZER_IMMERSIVE_LEVEL_NONE,
SPATIALIZER_IMMERSIVE_LEVEL_OTHER
})
@interface ImmersiveAudioLevel {}
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0;
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_OTHER */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1;
/** Wrapper for Spatializer.OnSpatializerStateChangedListener */
public interface Listener {
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerEnabledChanged */
void onSpatializerEnabledChanged(SpatializerDelegate spatializer, boolean enabled);
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerAvailableChanged */
void onSpatializerAvailableChanged(SpatializerDelegate spatializer, boolean available);
}
private final Object spatializer;
private final Class<?> spatializerClass;
private final Class<?> spatializerListenerClass;
private final Method isEnabled;
private final Method isAvailable;
private final Method getImmersiveAudioLevel;
private final Method canBeSpatialized;
private final Method addListener;
private final Method removeListener;
private final Map<Listener, Object> listeners;
/** Creates an instance. */
public SpatializerDelegate(Context context)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Method getSpatializerMethod = AudioManager.class.getMethod("getSpatializer");
AudioManager manager =
Assertions.checkNotNull(
(AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE));
spatializer = checkStateNotNull(getSpatializerMethod.invoke(manager));
spatializerClass = Class.forName("android.media.Spatializer");
spatializerListenerClass =
Class.forName("android.media.Spatializer$OnSpatializerStateChangedListener");
isEnabled = spatializerClass.getMethod("isEnabled");
isAvailable = spatializerClass.getMethod("isAvailable");
getImmersiveAudioLevel = spatializerClass.getMethod("getImmersiveAudioLevel");
canBeSpatialized =
spatializerClass.getMethod(
"canBeSpatialized", android.media.AudioAttributes.class, AudioFormat.class);
addListener =
spatializerClass.getMethod(
"addOnSpatializerStateChangedListener", Executor.class, spatializerListenerClass);
removeListener =
spatializerClass.getMethod(
"removeOnSpatializerStateChangedListener", spatializerListenerClass);
listeners = new HashMap<>();
}
/** Delegates to Spatializer.isEnabled() */
public boolean isEnabled() {
try {
return (boolean) Util.castNonNull(isEnabled.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.isAvailable() */
public boolean isAvailable() {
try {
return (boolean) Util.castNonNull(isAvailable.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.getImmersiveAudioLevel() */
@ImmersiveAudioLevel
public int getImmersiveAudioLevel() {
try {
return (int) Util.castNonNull(getImmersiveAudioLevel.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.canBeSpatialized() */
public boolean canBeSpatialized(AudioAttributes attributes, AudioFormat format) {
try {
return (boolean) Util.castNonNull(canBeSpatialized.invoke(spatializer, attributes, format));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.addOnSpatializerStateChangedListener() */
public void addOnSpatializerStateChangedListener(Executor executor, Listener listener) {
if (listeners.containsKey(listener)) {
return;
}
Object listenerProxy = createSpatializerListenerProxy(listener);
try {
addListener.invoke(spatializer, executor, listenerProxy);
listeners.put(listener, listenerProxy);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.removeOnSpatializerStateChangedListener() */
public void removeOnSpatializerStateChangedListener(Listener listener) {
@Nullable Object proxy = listeners.get(listener);
if (proxy == null) {
return;
}
try {
removeListener.invoke(spatializer, proxy);
listeners.remove(listener);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
private Object createSpatializerListenerProxy(Listener listener) {
return Proxy.newProxyInstance(
spatializerListenerClass.getClassLoader(),
new Class<?>[] {spatializerListenerClass},
new ProxySpatializerListener(this, listener));
}
/** Proxy-based implementation of Spatializer.OnSpatializerStateChangedListener. */
private static final class ProxySpatializerListener implements InvocationHandler {
private final SpatializerDelegate spatializerDelegate;
private final Listener listener;
private ProxySpatializerListener(SpatializerDelegate spatializerDelegate, Listener listener) {
this.spatializerDelegate = spatializerDelegate;
this.listener = listener;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (methodName.equals("onSpatializerAvailableChanged")
&& parameterTypes.length == 2
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
&& parameterTypes[1].equals(Boolean.TYPE)) {
listener.onSpatializerAvailableChanged(spatializerDelegate, (boolean) objects[1]);
} else if (methodName.equals("onSpatializerEnabledChanged")
&& parameterTypes.length == 2
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
&& parameterTypes[1].equals(Boolean.TYPE)) {
listener.onSpatializerEnabledChanged(spatializerDelegate, (boolean) objects[1]);
}
return this;
}
}
}

View file

@ -17,6 +17,8 @@ package com.google.android.exoplayer2.decoder;
import static java.lang.Math.max;
import com.google.android.exoplayer2.util.Util;
/**
* Maintains decoder event counts, for debugging purposes only.
*
@ -30,12 +32,12 @@ public final class DecoderCounters {
public int decoderInitCount;
/** The number of times a decoder has been released. */
public int decoderReleaseCount;
/** The number of queued input buffers. */
public int inputBufferCount;
/** The number of input buffers queued to the decoder. */
public int queuedInputBufferCount;
/**
* The number of skipped input buffers.
*
* <p>A skipped input buffer is an input buffer that was deliberately not sent to the decoder.
* <p>A skipped input buffer is an input buffer that was deliberately not queued to the decoder.
*/
public int skippedInputBufferCount;
/** The number of rendered output buffers. */
@ -43,28 +45,47 @@ public final class DecoderCounters {
/**
* The number of skipped output buffers.
*
* <p>A skipped output buffer is an output buffer that was deliberately not rendered.
* <p>A skipped output buffer is an output buffer that was deliberately not rendered. This
* includes buffers that were never dequeued from the decoder and instead skipped while 'inside'
* the codec due to a flush.
*/
public int skippedOutputBufferCount;
/**
* The number of dropped buffers.
*
* <p>A dropped buffer is an buffer that was supposed to be decoded/rendered, but was instead
* <p>A dropped buffer is a buffer that was supposed to be decoded/rendered, but was instead
* dropped because it could not be rendered in time.
*
* <p>This includes all of {@link #droppedInputBufferCount} in addition to buffers dropped after
* being queued to the decoder.
*/
public int droppedBufferCount;
/**
* The number of input buffers dropped.
*
* <p>A dropped input buffer is a buffer that was not queued to the decoder because it would not
* be rendered in time.
*/
public int droppedInputBufferCount;
/**
* The maximum number of dropped buffers without an interleaving rendered output buffer.
*
* <p>Skipped output buffers are ignored for the purposes of calculating this value.
* <p>Skipped buffers are ignored for the purposes of calculating this value.
*/
public int maxConsecutiveDroppedBufferCount;
/**
* The number of times all buffers to a keyframe were dropped.
*
* <p>Each time buffers to a keyframe are dropped, this counter is increased by one, and the
* dropped buffer counters are increased by one (for the current output buffer) plus the number of
* buffers dropped from the source to advance to the keyframe.
* <p>Each time buffers to a keyframe are dropped:
*
* <ul>
* <li>This counter is incremented by one.
* <li>{@link #droppedInputBufferCount} is incremented by the number of buffers dropped from the
* source to advance to the keyframe.
* <li>{@link #droppedBufferCount} is incremented by the sum of the number of buffers dropped
* from the source to advance to the keyframe and the number of buffers 'inside' the
* decoder.
* </ul>
*/
public int droppedToKeyframeCount;
/**
@ -106,11 +127,12 @@ public final class DecoderCounters {
public void merge(DecoderCounters other) {
decoderInitCount += other.decoderInitCount;
decoderReleaseCount += other.decoderReleaseCount;
inputBufferCount += other.inputBufferCount;
queuedInputBufferCount += other.queuedInputBufferCount;
skippedInputBufferCount += other.skippedInputBufferCount;
renderedOutputBufferCount += other.renderedOutputBufferCount;
skippedOutputBufferCount += other.skippedOutputBufferCount;
droppedBufferCount += other.droppedBufferCount;
droppedInputBufferCount += other.droppedInputBufferCount;
maxConsecutiveDroppedBufferCount =
max(maxConsecutiveDroppedBufferCount, other.maxConsecutiveDroppedBufferCount);
droppedToKeyframeCount += other.droppedToKeyframeCount;
@ -134,4 +156,34 @@ public final class DecoderCounters {
totalVideoFrameProcessingOffsetUs += totalProcessingOffsetUs;
videoFrameProcessingOffsetCount += count;
}
@Override
public String toString() {
return Util.formatInvariant(
"DecoderCounters {\n "
+ "decoderInits=%s,\n "
+ "decoderReleases=%s\n "
+ "queuedInputBuffers=%s\n "
+ "skippedInputBuffers=%s\n "
+ "renderedOutputBuffers=%s\n "
+ "skippedOutputBuffers=%s\n "
+ "droppedBuffers=%s\n "
+ "droppedInputBuffers=%s\n "
+ "maxConsecutiveDroppedBuffers=%s\n "
+ "droppedToKeyframeEvents=%s\n "
+ "totalVideoFrameProcessingOffsetUs=%s\n "
+ "videoFrameProcessingOffsetCount=%s\n}",
decoderInitCount,
decoderReleaseCount,
queuedInputBufferCount,
skippedInputBufferCount,
renderedOutputBufferCount,
skippedOutputBufferCount,
droppedBufferCount,
droppedInputBufferCount,
maxConsecutiveDroppedBufferCount,
droppedToKeyframeCount,
totalVideoFrameProcessingOffsetUs,
videoFrameProcessingOffsetCount);
}
}

View file

@ -31,6 +31,7 @@ import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.decoder.CryptoConfig;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
@ -133,6 +134,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final HashMap<String, String> keyRequestParameters;
private final CopyOnWriteMultiset<DrmSessionEventListener.EventDispatcher> eventDispatchers;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final PlayerId playerId;
/* package */ final MediaDrmCallback callback;
/* package */ final UUID uuid;
@ -182,7 +184,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
HashMap<String, String> keyRequestParameters,
MediaDrmCallback callback,
Looper playbackLooper,
LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
PlayerId playerId) {
if (mode == DefaultDrmSessionManager.MODE_QUERY
|| mode == DefaultDrmSessionManager.MODE_RELEASE) {
Assertions.checkNotNull(offlineLicenseKeySetId);
@ -204,6 +207,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.callback = callback;
this.eventDispatchers = new CopyOnWriteMultiset<>();
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.playerId = playerId;
state = STATE_OPENING;
responseHandler = new ResponseHandler(playbackLooper);
}
@ -370,6 +374,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
try {
sessionId = mediaDrm.openSession();
mediaDrm.setPlayerIdForSession(sessionId, playerId);
cryptoConfig = mediaDrm.createCryptoConfig(sessionId);
state = STATE_OPENED;
// Capture state into a local so a consistent value is seen by the lambda.

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.annotation.SuppressLint;
import android.media.ResourceBusyException;
@ -31,6 +32,7 @@ import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
@ -59,7 +61,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}.
*
* <p>This implementation supports pre-acquisition of sessions using {@link
* #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
* #preacquireSession(DrmSessionEventListener.EventDispatcher, Format)}.
*/
@RequiresApi(18)
public class DefaultDrmSessionManager implements DrmSessionManager {
@ -302,6 +304,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private @MonotonicNonNull Handler playbackHandler;
private int mode;
@Nullable private byte[] offlineLicenseKeySetId;
private @MonotonicNonNull PlayerId playerId;
/* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;
@ -419,8 +422,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
/**
* Sets the mode, which determines the role of sessions acquired from the instance. This must be
* called before {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}
* is called.
* called before {@link #acquireSession(DrmSessionEventListener.EventDispatcher, Format)} is
* called.
*
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
* required.
@ -488,12 +491,16 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
}
@Override
public DrmSessionReference preacquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
checkState(prepareCallsCount > 0);
public void setPlayer(Looper playbackLooper, PlayerId playerId) {
initPlaybackLooper(playbackLooper);
this.playerId = playerId;
}
@Override
public DrmSessionReference preacquireSession(
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
checkState(prepareCallsCount > 0);
checkStateNotNull(playbackLooper);
PreacquiredSessionReference preacquiredSessionReference =
new PreacquiredSessionReference(eventDispatcher);
preacquiredSessionReference.acquire(format);
@ -503,11 +510,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Override
@Nullable
public DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
checkState(prepareCallsCount > 0);
initPlaybackLooper(playbackLooper);
checkStateNotNull(playbackLooper);
return acquireSession(
playbackLooper,
eventDispatcher,
@ -774,7 +779,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
keyRequestParameters,
callback,
checkNotNull(playbackLooper),
loadErrorHandlingPolicy);
loadErrorHandlingPolicy,
checkNotNull(playerId));
// Acquire the session once on behalf of the caller to DrmSessionManager - this is the
// reference 'assigned' to the caller which they're responsible for releasing. Do this first,
// to ensure that eventDispatcher receives all events related to the initial
@ -977,7 +983,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* Constructs an instance.
*
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link
* #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
* #acquireSession(DrmSessionEventListener.EventDispatcher, Format)}.
*/
public PreacquiredSessionReference(
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {

View file

@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.analytics.PlayerId;
/** Manages a DRM session. */
public interface DrmSessionManager {
@ -45,12 +46,13 @@ public interface DrmSessionManager {
DrmSessionManager DRM_UNSUPPORTED =
new DrmSessionManager() {
@Override
public void setPlayer(Looper playbackLooper, PlayerId playerId) {}
@Override
@Nullable
public DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
if (format.drmInitData == null) {
return null;
} else {
@ -100,25 +102,33 @@ public interface DrmSessionManager {
// Do nothing.
}
/**
* Sets information about the player using this DRM session manager.
*
* @param playbackLooper The {@link Looper} associated with the player's playback thread.
* @param playerId The {@link PlayerId} of the player.
*/
void setPlayer(Looper playbackLooper, PlayerId playerId);
/**
* Pre-acquires a DRM session for the specified {@link Format}.
*
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(Looper,
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(
* DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely,
* allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready
* in the background.
*
* <p>The caller must call {@link DrmSessionReference#release()} on the returned instance when
* they no longer require the pre-acquisition (i.e. they know they won't be making a matching call
* to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near
* to {@link #acquireSession(DrmSessionEventListener.EventDispatcher, Format)} in the near
* future).
*
* <p>This manager may silently release the underlying session in order to allow another operation
* to complete. This will result in a subsequent call to {@link #acquireSession(Looper,
* to complete. This will result in a subsequent call to {@link #acquireSession(
* DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including
* repeating key loads and other async initialization steps.
*
* <p>The caller must separately call {@link #acquireSession(Looper,
* <p>The caller must separately call {@link #acquireSession(
* DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for
* playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances
* are distinct. The caller must release both, and can release the {@link DrmSessionReference}
@ -129,19 +139,15 @@ public interface DrmSessionManager {
* <p>Implementations that do not support pre-acquisition always return an empty {@link
* DrmSessionReference} instance.
*
* @param playbackLooper The looper associated with the media playback thread.
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
* events, and passed on to {@link
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
* @param format The {@link Format} for which to pre-acquire a {@link DrmSession}.
* @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching
* {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would
* return null.
* {@link #acquireSession(DrmSessionEventListener.EventDispatcher, Format)} would return null.
*/
default DrmSessionReference preacquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
return DrmSessionReference.EMPTY;
}
@ -158,7 +164,6 @@ public interface DrmSessionManager {
* used to configure secure decoders for playback of clear content periods, which can reduce the
* cost of transitioning between clear and encrypted content.
*
* @param playbackLooper The looper associated with the media playback thread.
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
* events, and passed on to {@link
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
@ -167,9 +172,7 @@ public interface DrmSessionManager {
*/
@Nullable
DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format);
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format);
/**
* Returns the {@link C.CryptoType} that the DRM session manager will use for a given {@link

View file

@ -26,6 +26,7 @@ import android.os.PersistableBundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.decoder.CryptoConfig;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import java.lang.annotation.Documented;
@ -395,6 +396,14 @@ public interface ExoMediaDrm {
*/
void closeSession(byte[] sessionId);
/**
* Sets the {@link PlayerId} of the player using a session.
*
* @param sessionId The ID of the session.
* @param playerId The {@link PlayerId} of the player using the session.
*/
default void setPlayerIdForSession(byte[] sessionId, PlayerId playerId) {}
/**
* Generates a key request.
*

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.annotation.SuppressLint;
import android.media.DeniedByServerException;
import android.media.MediaCrypto;
@ -23,12 +25,14 @@ import android.media.MediaDrm;
import android.media.MediaDrmException;
import android.media.NotProvisionedException;
import android.media.UnsupportedSchemeException;
import android.media.metrics.LogSessionId;
import android.os.PersistableBundle;
import android.text.TextUtils;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
@ -182,6 +186,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
mediaDrm.closeSession(sessionId);
}
@Override
public void setPlayerIdForSession(byte[] sessionId, PlayerId playerId) {
if (Util.SDK_INT >= 31) {
Api31.setLogSessionIdOnMediaDrmSession(mediaDrm, sessionId, playerId);
}
}
// Return values of MediaDrm.KeyRequest.getRequestType are equal to KeyRequest.RequestType.
@SuppressLint("WrongConstant")
@Override
@ -504,9 +515,22 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
@RequiresApi(31)
private static class Api31 {
private Api31() {}
@DoNotInline
public static boolean requiresSecureDecoder(MediaDrm mediaDrm, String mimeType) {
return mediaDrm.requiresSecureDecoder(mimeType);
}
@DoNotInline
public static void setLogSessionIdOnMediaDrmSession(
MediaDrm mediaDrm, byte[] drmSessionId, PlayerId playerId) {
LogSessionId logSessionId = playerId.getLogSessionId();
if (!logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE)) {
MediaDrm.PlaybackComponent playbackComponent =
checkNotNull(mediaDrm.getPlaybackComponent(drmSessionId));
playbackComponent.setLogSessionId(logSessionId);
}
}
}
}

View file

@ -23,6 +23,7 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
@ -235,6 +236,7 @@ public final class OfflineLicenseHelper {
public synchronized Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
throws DrmSessionException {
Assertions.checkNotNull(offlineLicenseKeySetId);
drmSessionManager.setPlayer(handlerThread.getLooper(), PlayerId.UNSET);
drmSessionManager.prepare();
DrmSession drmSession =
openBlockingKeyRequest(
@ -263,6 +265,7 @@ public final class OfflineLicenseHelper {
private byte[] blockingKeyRequest(
@Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format)
throws DrmSessionException {
drmSessionManager.setPlayer(handlerThread.getLooper(), PlayerId.UNSET);
drmSessionManager.prepare();
DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, format);
DrmSessionException error = drmSession.getError();
@ -280,8 +283,7 @@ public final class OfflineLicenseHelper {
Assertions.checkNotNull(format.drmInitData);
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
conditionVariable.close();
DrmSession drmSession =
drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, format);
DrmSession drmSession = drmSessionManager.acquireSession(eventDispatcher, format);
// Block current thread until key loading is finished
conditionVariable.block();
return Assertions.checkNotNull(drmSession);

View file

@ -22,6 +22,7 @@ import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PersistableBundle;
import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@ -306,6 +307,13 @@ import java.nio.ByteBuffer;
codec.signalEndOfInputStream();
}
@Override
@RequiresApi(26)
public PersistableBundle getMetrics() {
maybeBlockOnQueueing();
return codec.getMetrics();
}
@VisibleForTesting
/* package */ void onError(MediaCodec.CodecException error) {
asynchronousMediaCodecCallback.onError(codec, error);

View file

@ -43,7 +43,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
private static final int MODE_ENABLED = 1;
private static final int MODE_DISABLED = 2;
private static final String TAG = "DefaultMediaCodecAdapterFactory";
private static final String TAG = "DMCodecAdapterFactory";
@Mode private int asynchronousMode;
private boolean enableSynchronizeCodecInteractionsWithQueueing;

View file

@ -20,6 +20,7 @@ import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.PersistableBundle;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@ -336,4 +337,12 @@ public interface MediaCodecAdapter {
*/
@RequiresApi(18)
void signalEndOfInputStream();
/**
* Returns metrics data about the current codec instance.
*
* @see MediaCodec#getMetrics()
*/
@RequiresApi(26)
PersistableBundle getMetrics();
}

View file

@ -37,10 +37,12 @@ import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaFormat;
import android.media.metrics.LogSessionId;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.CallSuper;
import androidx.annotation.CheckResult;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@ -50,6 +52,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.decoder.CryptoConfig;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@ -973,13 +976,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
}
MediaCodecInfo preferredCodecInfo = availableCodecInfos.peekFirst();
while (codec == null) {
MediaCodecInfo codecInfo = availableCodecInfos.peekFirst();
if (!shouldInitCodec(codecInfo)) {
return;
}
try {
initCodec(codecInfo, crypto);
try {
initCodec(codecInfo, crypto);
} catch (Exception e) {
if (codecInfo == preferredCodecInfo) {
// If creating the preferred decoder failed then sleep briefly before retrying.
// Workaround for [internal b/191966399].
// See also https://github.com/google/ExoPlayer/issues/8696.
Log.w(TAG, "Preferred decoder instantiation failed. Sleeping for 50ms then retrying.");
Thread.sleep(/* millis= */ 50);
initCodec(codecInfo, crypto);
} else {
throw e;
}
}
} catch (Exception e) {
Log.w(TAG, "Failed to initialize decoder: " + codecInfo, e);
// This codec failed to initialize, so fall back to the next codec in the list (if any). We
@ -1057,10 +1074,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
}
codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createCodec:" + codecName);
MediaCodecAdapter.Configuration configuration =
getMediaCodecConfiguration(codecInfo, inputFormat, crypto, codecOperatingRate);
codec = codecAdapterFactory.createAdapter(configuration);
if (Util.SDK_INT >= 31) {
Api31.setLogSessionIdToMediaCodecFormat(configuration, getPlayerId());
}
try {
TraceUtil.beginSection("createCodec:" + codecName);
codec = codecAdapterFactory.createAdapter(configuration);
} finally {
TraceUtil.endSection();
}
codecInitializedTimestamp = SystemClock.elapsedRealtime();
this.codecInfo = codecInfo;
@ -1318,7 +1342,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
resetInputBuffer();
codecReceivedBuffers = true;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
decoderCounters.inputBufferCount++;
decoderCounters.queuedInputBufferCount++;
return true;
}
@ -2421,4 +2445,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& format.channelCount == 1
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
}
@RequiresApi(31)
private static final class Api31 {
private Api31() {}
@DoNotInline
public static void setLogSessionIdToMediaCodecFormat(
MediaCodecAdapter.Configuration codecConfiguration, PlayerId playerId) {
LogSessionId logSessionId = playerId.getLogSessionId();
if (!logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE)) {
codecConfiguration.mediaFormat.setString("log-session-id", logSessionId.getStringId());
}
}
}
}

View file

@ -27,6 +27,7 @@ import androidx.annotation.CheckResult;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Log;
@ -105,11 +106,8 @@ public final class MediaCodecUtil {
}
}
/**
* Clears the codec cache.
*
* <p>This method should only be called in tests.
*/
/* Clears the codec cache.*/
@VisibleForTesting
public static synchronized void clearDecoderInfoCache() {
decoderInfosCache.clear();
}
@ -322,8 +320,8 @@ public final class MediaCodecUtil {
if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) {
continue;
}
boolean hardwareAccelerated = isHardwareAccelerated(codecInfo);
boolean softwareOnly = isSoftwareOnly(codecInfo);
boolean hardwareAccelerated = isHardwareAccelerated(codecInfo, mimeType);
boolean softwareOnly = isSoftwareOnly(codecInfo, mimeType);
boolean vendor = isVendor(codecInfo);
if ((secureDecodersExplicit && key.secure == secureSupported)
|| (!secureDecodersExplicit && !key.secure)) {
@ -514,8 +512,10 @@ public final class MediaCodecUtil {
return false;
}
// MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
// MTK AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].
if (Util.SDK_INT <= 23
&& MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
return false;
}
@ -603,13 +603,14 @@ public final class MediaCodecUtil {
* The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+,
* or a best-effort approximation for lower levels.
*/
private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) {
private static boolean isHardwareAccelerated(
android.media.MediaCodecInfo codecInfo, String mimeType) {
if (Util.SDK_INT >= 29) {
return isHardwareAcceleratedV29(codecInfo);
}
// codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true.
// However, we assume this to be true as an approximation.
return !isSoftwareOnly(codecInfo);
return !isSoftwareOnly(codecInfo, mimeType);
}
@RequiresApi(29)
@ -621,12 +622,17 @@ public final class MediaCodecUtil {
* The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a
* best-effort approximation for lower levels.
*/
private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) {
private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo, String mimeType) {
if (Util.SDK_INT >= 29) {
return isSoftwareOnlyV29(codecInfo);
}
if (MimeTypes.isAudio(mimeType)) {
// Assume audio decoders are software only.
return true;
}
String codecName = Ascii.toLowerCase(codecInfo.getName());
if (codecName.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs
if (codecName.startsWith("arc.")) {
// App Runtime for Chrome (ARC) codecs
return false;
}
return codecName.startsWith("omx.google.")

View file

@ -23,6 +23,7 @@ import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.PersistableBundle;
import android.view.Surface;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
@ -37,7 +38,7 @@ import java.nio.ByteBuffer;
/**
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode.
*/
public class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
/** A factory for {@link SynchronousMediaCodecAdapter} instances. */
public static class Factory implements MediaCodecAdapter.Factory {
@ -231,8 +232,16 @@ public class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
codec.setVideoScalingMode(scalingMode);
}
@Override
@RequiresApi(26)
public PersistableBundle getMetrics() {
return codec.getMetrics();
}
@RequiresApi(18)
private static final class Api18 {
private Api18() {}
@DoNotInline
public static Surface createCodecInputSurface(MediaCodec codec) {
return codec.createInputSurface();

View file

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
@ -831,8 +832,6 @@ public final class DownloadHelper {
* Runs the track selection for a given period index with the current parameters. The selected
* tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.
*/
// Intentional reference comparison of track group instances.
@SuppressWarnings("ReferenceEquality")
@RequiresNonNull({
"trackGroupArrays",
"trackSelectionsByPeriodAndRenderer",
@ -857,7 +856,7 @@ public final class DownloadHelper {
boolean mergedWithExistingSelection = false;
for (int j = 0; j < existingSelectionList.size(); j++) {
ExoTrackSelection existingSelection = existingSelectionList.get(j);
if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {
if (existingSelection.getTrackGroup().equals(newSelection.getTrackGroup())) {
// Merge with existing selection.
scratchSet.clear();
for (int k = 0; k < existingSelection.length(); k++) {
@ -891,8 +890,10 @@ public final class DownloadHelper {
MediaItem mediaItem,
DataSource.Factory dataSourceFactory,
@Nullable DrmSessionManager drmSessionManager) {
return new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY)
.setDrmSessionManager(drmSessionManager)
return new DefaultMediaSourceFactory(
dataSourceFactory, ExtractorsFactory.EMPTY, /* serverSideDaiMediaSourceFactory= */ null)
.setDrmSessionManagerProvider(
drmSessionManager != null ? unusedMediaItem -> drmSessionManager : null)
.createMediaSource(mediaItem);
}
@ -955,7 +956,8 @@ public final class DownloadHelper {
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_PREPARE_SOURCE:
mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null);
mediaSource.prepareSource(
/* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET);
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
return true;
case MESSAGE_CHECK_FOR_FAILURE:

View file

@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@ -41,6 +44,7 @@ public abstract class BaseMediaSource implements MediaSource {
@Nullable private Looper looper;
@Nullable private Timeline timeline;
@Nullable private PlayerId playerId;
public BaseMediaSource() {
mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1);
@ -51,7 +55,7 @@ public abstract class BaseMediaSource implements MediaSource {
/**
* Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller,
* TransferListener)}. This method is called at most once until the next call to {@link
* TransferListener, PlayerId)}. This method is called at most once until the next call to {@link
* #releaseSourceInternal()}.
*
* @param mediaTransferListener The transfer listener which should be informed of any media data
@ -160,6 +164,16 @@ public abstract class BaseMediaSource implements MediaSource {
return !enabledMediaSourceCallers.isEmpty();
}
/**
* Returns the {@link PlayerId} of the player using this media source.
*
* <p>Must only be used when the media source is {@link #prepareSourceInternal(TransferListener)
* prepared}.
*/
protected final PlayerId getPlayerId() {
return checkStateNotNull(playerId);
}
@Override
public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
Assertions.checkNotNull(handler);
@ -186,9 +200,12 @@ public abstract class BaseMediaSource implements MediaSource {
@Override
public final void prepareSource(
MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) {
MediaSourceCaller caller,
@Nullable TransferListener mediaTransferListener,
PlayerId playerId) {
Looper looper = Looper.myLooper();
Assertions.checkArgument(this.looper == null || this.looper == looper);
this.playerId = playerId;
@Nullable Timeline timeline = this.timeline;
mediaSourceCallers.add(caller);
if (this.looper == null) {
@ -226,6 +243,7 @@ public abstract class BaseMediaSource implements MediaSource {
if (mediaSourceCallers.isEmpty()) {
looper = null;
timeline = null;
playerId = null;
enabledMediaSourceCallers.clear();
releaseSourceInternal();
} else {

View file

@ -117,7 +117,7 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
childSources.put(id, new MediaSourceAndListener<>(mediaSource, caller, eventListener));
mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);
mediaSource.addDrmEventListener(Assertions.checkNotNull(eventHandler), eventListener);
mediaSource.prepareSource(caller, mediaTransferListener);
mediaSource.prepareSource(caller, mediaTransferListener, getPlayerId());
if (!isEnabled()) {
mediaSource.disable(caller);
}

View file

@ -24,7 +24,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
@ -34,7 +33,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.text.SubtitleDecoderFactory;
@ -43,8 +41,8 @@ import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@ -60,7 +58,7 @@ import java.util.Set;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* The default {@link MediaSourceFactory} implementation.
* The default {@link MediaSource.Factory} implementation.
*
* <p>This implementation delegates calls to {@link #createMediaSource(MediaItem)} to the following
* factories:
@ -94,32 +92,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* configuration}, {@link #setAdsLoaderProvider} and {@link #setAdViewProvider} need to be called to
* configure the factory with the required providers.
*/
@SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility.
public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
* Provides {@link AdsLoader} instances for media items that have {@link
* MediaItem.LocalConfiguration#adsConfiguration ad tag URIs}.
*/
public interface AdsLoaderProvider {
/** @deprecated Use {@link AdsLoader.Provider} instead. */
@Deprecated
public interface AdsLoaderProvider extends AdsLoader.Provider {}
/**
* Returns an {@link AdsLoader} for the given {@link
* MediaItem.LocalConfiguration#adsConfiguration ads configuration}, or {@code null} if no ads
* loader is available for the given ads configuration.
*
* <p>This method is called each time a {@link MediaSource} is created from a {@link MediaItem}
* that defines an {@link MediaItem.LocalConfiguration#adsConfiguration ads configuration}.
*/
@Nullable
AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration);
}
private static final String TAG = "DefaultMediaSourceFactory";
private static final String TAG = "DMediaSourceFactory";
private final DataSource.Factory dataSourceFactory;
private final DelegateFactoryLoader delegateFactoryLoader;
@Nullable private AdsLoaderProvider adsLoaderProvider;
@Nullable private final MediaSource.Factory serverSideDaiMediaSourceFactory;
@Nullable private AdsLoader.Provider adsLoaderProvider;
@Nullable private AdViewProvider adViewProvider;
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long liveTargetOffsetMs;
@ -146,7 +132,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
* its container.
*/
public DefaultMediaSourceFactory(Context context, ExtractorsFactory extractorsFactory) {
this(new DefaultDataSource.Factory(context), extractorsFactory);
this(
new DefaultDataSource.Factory(context),
extractorsFactory,
/* serverSideDaiMediaSourceFactory= */ null);
}
/**
@ -156,7 +145,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
* for requesting media data.
*/
public DefaultMediaSourceFactory(DataSource.Factory dataSourceFactory) {
this(dataSourceFactory, new DefaultExtractorsFactory());
this(
dataSourceFactory,
new DefaultExtractorsFactory(),
/* serverSideDaiMediaSourceFactory= */ null);
}
/**
@ -166,10 +158,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
* for requesting media data.
* @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from
* its container.
* @param serverSideDaiMediaSourceFactory A {@link MediaSource.Factory} for creating server side
* inserted ad media sources.
*/
public DefaultMediaSourceFactory(
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
@Nullable MediaSource.Factory serverSideDaiMediaSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
// Temporary until factory registration is agreed upon.
this.serverSideDaiMediaSourceFactory = serverSideDaiMediaSourceFactory;
delegateFactoryLoader = new DelegateFactoryLoader(dataSourceFactory, extractorsFactory);
liveTargetOffsetMs = C.TIME_UNSET;
liveMinOffsetMs = C.TIME_UNSET;
@ -196,14 +195,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
/**
* Sets the {@link AdsLoaderProvider} that provides {@link AdsLoader} instances for media items
* Sets the {@link AdsLoader.Provider} that provides {@link AdsLoader} instances for media items
* that have {@link MediaItem.LocalConfiguration#adsConfiguration ads configurations}.
*
* @param adsLoaderProvider A provider for {@link AdsLoader} instances.
* @return This factory, for convenience.
*/
public DefaultMediaSourceFactory setAdsLoaderProvider(
@Nullable AdsLoaderProvider adsLoaderProvider) {
@Nullable AdsLoader.Provider adsLoaderProvider) {
this.adsLoaderProvider = adsLoaderProvider;
return this;
}
@ -279,29 +278,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return this;
}
@Deprecated
@Override
public DefaultMediaSourceFactory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
delegateFactoryLoader.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
return this;
}
@Deprecated
@Override
public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
delegateFactoryLoader.setDrmUserAgent(userAgent);
return this;
}
@Deprecated
@Override
public DefaultMediaSourceFactory setDrmSessionManager(
@Nullable DrmSessionManager drmSessionManager) {
delegateFactoryLoader.setDrmSessionManager(drmSessionManager);
return this;
}
@Override
public DefaultMediaSourceFactory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
@ -317,18 +293,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return this;
}
/**
* @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link
* #createMediaSource(MediaItem)} instead.
*/
@SuppressWarnings("deprecation") // Calling through to the same deprecated method.
@Deprecated
@Override
public DefaultMediaSourceFactory setStreamKeys(@Nullable List<StreamKey> streamKeys) {
delegateFactoryLoader.setStreamKeys(streamKeys);
return this;
}
@Override
public int[] getSupportedTypes() {
return delegateFactoryLoader.getSupportedTypes();
@ -336,13 +300,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
checkNotNull(mediaItem.localConfiguration);
Assertions.checkNotNull(mediaItem.localConfiguration);
@Nullable String scheme = mediaItem.localConfiguration.uri.getScheme();
if (scheme != null && scheme.equals("imadai")) {
return checkNotNull(serverSideDaiMediaSourceFactory).createMediaSource(mediaItem);
}
@C.ContentType
int type =
Util.inferContentTypeForUriAndMimeType(
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
@Nullable
MediaSourceFactory mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type);
MediaSource.Factory mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type);
checkStateNotNull(
mediaSourceFactory, "No suitable media source factory found for content type: " + type);
@ -385,6 +353,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
.setSelectionFlags(subtitleConfigurations.get(i).selectionFlags)
.setRoleFlags(subtitleConfigurations.get(i).roleFlags)
.setLabel(subtitleConfigurations.get(i).label)
.setId(subtitleConfigurations.get(i).id)
.build();
ExtractorsFactory extractorsFactory =
() ->
@ -437,7 +406,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
if (adsConfiguration == null) {
return mediaSource;
}
@Nullable AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
@Nullable AdsLoader.Provider adsLoaderProvider = this.adsLoaderProvider;
@Nullable AdViewProvider adViewProvider = this.adViewProvider;
if (adsLoaderProvider == null || adViewProvider == null) {
Log.w(
@ -467,17 +436,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
private static final class DelegateFactoryLoader {
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final Map<Integer, @NullableType Supplier<MediaSourceFactory>>
private final Map<Integer, @NullableType Supplier<MediaSource.Factory>>
mediaSourceFactorySuppliers;
private final Set<Integer> supportedTypes;
private final Map<Integer, MediaSourceFactory> mediaSourceFactories;
private final Map<Integer, MediaSource.Factory> mediaSourceFactories;
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
@Nullable private String userAgent;
@Nullable private DrmSessionManager drmSessionManager;
@Nullable private DrmSessionManagerProvider drmSessionManagerProvider;
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@Nullable private List<StreamKey> streamKeys;
public DelegateFactoryLoader(
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
@ -496,69 +461,32 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
@SuppressWarnings("deprecation") // Forwarding to deprecated methods.
@Nullable
public MediaSourceFactory getMediaSourceFactory(@C.ContentType int contentType) {
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(contentType);
public MediaSource.Factory getMediaSourceFactory(@C.ContentType int contentType) {
@Nullable MediaSource.Factory mediaSourceFactory = mediaSourceFactories.get(contentType);
if (mediaSourceFactory != null) {
return mediaSourceFactory;
}
@Nullable
Supplier<MediaSourceFactory> mediaSourceFactorySupplier = maybeLoadSupplier(contentType);
Supplier<MediaSource.Factory> mediaSourceFactorySupplier = maybeLoadSupplier(contentType);
if (mediaSourceFactorySupplier == null) {
return null;
}
mediaSourceFactory = mediaSourceFactorySupplier.get();
if (drmHttpDataSourceFactory != null) {
mediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
}
if (userAgent != null) {
mediaSourceFactory.setDrmUserAgent(userAgent);
}
if (drmSessionManager != null) {
mediaSourceFactory.setDrmSessionManager(drmSessionManager);
}
if (drmSessionManagerProvider != null) {
mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
}
if (loadErrorHandlingPolicy != null) {
mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
}
if (streamKeys != null) {
mediaSourceFactory.setStreamKeys(streamKeys);
}
mediaSourceFactories.put(contentType, mediaSourceFactory);
return mediaSourceFactory;
}
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
this.drmHttpDataSourceFactory = drmHttpDataSourceFactory;
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
}
}
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void setDrmUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setDrmUserAgent(userAgent);
}
}
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
this.drmSessionManager = drmSessionManager;
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setDrmSessionManager(drmSessionManager);
}
}
public void setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
this.drmSessionManagerProvider = drmSessionManagerProvider;
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
}
}
@ -566,19 +494,11 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
public void setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
}
}
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void setStreamKeys(@Nullable List<StreamKey> streamKeys) {
this.streamKeys = streamKeys;
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setStreamKeys(streamKeys);
}
}
private void ensureAllSuppliersAreLoaded() {
maybeLoadSupplier(C.TYPE_DASH);
maybeLoadSupplier(C.TYPE_SS);
@ -588,38 +508,38 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
@Nullable
private Supplier<MediaSourceFactory> maybeLoadSupplier(@C.ContentType int contentType) {
private Supplier<MediaSource.Factory> maybeLoadSupplier(@C.ContentType int contentType) {
if (mediaSourceFactorySuppliers.containsKey(contentType)) {
return mediaSourceFactorySuppliers.get(contentType);
}
@Nullable Supplier<MediaSourceFactory> mediaSourceFactorySupplier = null;
@Nullable Supplier<MediaSource.Factory> mediaSourceFactorySupplier = null;
try {
Class<? extends MediaSourceFactory> clazz;
Class<? extends MediaSource.Factory> clazz;
switch (contentType) {
case C.TYPE_DASH:
clazz =
Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory")
.asSubclass(MediaSourceFactory.class);
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.TYPE_SS:
clazz =
Class.forName(
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory")
.asSubclass(MediaSourceFactory.class);
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.TYPE_HLS:
clazz =
Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory")
.asSubclass(MediaSourceFactory.class);
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.TYPE_RTSP:
clazz =
Class.forName("com.google.android.exoplayer2.source.rtsp.RtspMediaSource$Factory")
.asSubclass(MediaSourceFactory.class);
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz);
break;
case C.TYPE_OTHER:
@ -681,8 +601,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
public void release() {}
}
private static MediaSourceFactory newInstance(
Class<? extends MediaSourceFactory> clazz, DataSource.Factory dataSourceFactory) {
private static MediaSource.Factory newInstance(
Class<? extends MediaSource.Factory> clazz, DataSource.Factory dataSourceFactory) {
try {
return clazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory);
} catch (Exception e) {
@ -690,7 +610,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
}
private static MediaSourceFactory newInstance(Class<? extends MediaSourceFactory> clazz) {
private static MediaSource.Factory newInstance(Class<? extends MediaSource.Factory> clazz) {
try {
return clazz.getConstructor().newInstance();
} catch (Exception e) {

View file

@ -179,11 +179,11 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
windowStartPositionUs = windowPreparePositionUs;
}
}
Pair<Object, Long> periodPosition =
newTimeline.getPeriodPosition(
Pair<Object, Long> periodUidAndPositionUs =
newTimeline.getPeriodPositionUs(
window, period, /* windowIndex= */ 0, windowStartPositionUs);
Object periodUid = periodPosition.first;
long periodPositionUs = periodPosition.second;
Object periodUid = periodUidAndPositionUs.first;
long periodPositionUs = periodUidAndPositionUs.second;
timeline =
hasRealTimeline
? timeline.cloneWithUpdatedTimeline(newTimeline)

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