Merge branch 'dev-v2' into trackSelectionView

This commit is contained in:
Yo Ob 2020-08-28 11:36:59 +02:00 committed by GitHub
commit 869981a360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2548 additions and 1487 deletions

View File

@ -1,4 +1,4 @@
# ExoPlayer #
# ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/> #
ExoPlayer is an application level media player for Android. It provides an
alternative to Androids MediaPlayer API for playing audio and video both

View File

@ -2,138 +2,137 @@
### dev-v2 (not yet released)
* New release notes go here!
### 2.12.0 (not yet released - targeted for 2020-08-TBD) ###
### 2.12.0 (not yet released - targeted for 2020-09-03) ###
* Core library:
* Implement getTag for SilenceMediaSource.
* Added `TextComponent.getCurrentCues` because the current cues are no
longer forwarded to a new `TextOutput` in `SimpleExoPlayer`
automatically.
* Add additional options to `SimpleExoPlayer.Builder` that were previously
only accessible via setters.
* Add opt-in to verify correct thread usage with
`SimpleExoPlayer.setThrowsWhenUsingWrongThread(true)`
([#4463](https://github.com/google/ExoPlayer/issues/4463)).
* Add playbackPositionUs parameter to 'LoadControl.shouldContinueLoading'.
* The `DefaultLoadControl` default minimum buffer is set to 50 seconds,
equal to the default maximum buffer. `DefaultLoadControl` applies the
same behavior for audio and video.
* Add API in `AnalyticsListener` to report video frame processing offset.
`MediaCodecVideoRenderer` reports the event.
* Add fields `videoFrameProcessingOffsetUsSum` and
`videoFrameProcessingOffsetUsCount` in `DecoderCounters` to compute the
average video frame processing offset.
* Add playlist API
([#6161](https://github.com/google/ExoPlayer/issues/6161)).
* Attach an identifier and extra information to load error events passed
to `LoadErrorHandlingPolicy`. `LoadErrorHandlingPolicy` implementations
must migrate to overriding the non-deprecated methods of the interface
in preparation for deprecated methods' removal in a future ExoPlayer
version ([#7309](https://github.com/google/ExoPlayer/issues/7309)).
* Add `play` and `pause` methods to `Player`.
* Add `Player.getCurrentLiveOffset` to conveniently return the live
offset.
* Add `Player.EventListener.onPlayWhenReadyChanged` with reasons.
* Add `Player.EventListener.onPlaybackStateChanged` and deprecate
`Player.EventListener.onPlayerStateChanged`.
* Add `Player.EventListener.onMediaItemTransition` with reasons.
* Add `Player.setAudioSessionId` to set the session ID attached to the
`AudioTrack`.
* Add `Player.getTrackSelector`.
* `Player`:
* Add a top level playlist API based on a new `MediaItem` class
([#6161](https://github.com/google/ExoPlayer/issues/6161)). The
new methods for playlist manipulation are `setMediaItem(s)`,
`addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and
`clearMediaItems`. This API should be used instead of
`ConcatenatingMediaSource` in most cases.
* Add `getCurrentMediaItem` for getting the currently playing item
in the playlist.
* Add `EventListener.onMediaItemTransition` to report when
playback transitions from one item to another in the playlist.
* Add `play` and `pause` convenience methods. They are equivalent to
`setPlayWhenReady(true)` and `setPlayWhenReady(false)` respectively.
* Add `getCurrentLiveOffset` for getting the offset of the current
playback position from the live edge of a live stream.
* Add `getTrackSelector` for getting the `TrackSelector` used by the
player.
* Add `AudioComponent.setAudioSessionId` to set the audio session ID.
This method is also available on `SimpleExoPlayer`.
* 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
to a `SimpleExoPlayer`.
* Add `Player.DeviceComponent` to query and control the device volume.
`SimpleExoPlayer` implements this interface.
* Deprecate and rename `getPlaybackError` to `getPlayerError` for
consistency.
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
consistency.
* Deprecate `onSeekProcessed` because all seek changes happen instantly
now and listening to `onPositionDiscontinuity` is sufficient.
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the
end of each media item
* Deprecate `EventListener.onPlayerStateChanged`, replacing it with
`EventListener.onPlayWhenReadyChanged` and
`EventListener.onPlaybackStateChanged`.
* Deprecate `EventListener.onSeekProcessed` because seek changes now
happen instantly and listening to `onPositionDiscontinuity` is
sufficient.
* `ExoPlayer`:
* Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for
adding `MediaSource` instances directly to the playlist.
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at
the end of each media item
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
* Split `setPlaybackParameter` into `setPlaybackSpeed` and
`AudioComponent.setSkipSilenceEnabled` with callbacks
`onPlaybackSpeedChanged` and
`AudioListener.onSkipSilenceEnabledChanged`.
* Make `MediaSourceEventListener.LoadEventInfo` and
`MediaSourceEventListener.MediaLoadData` top-level classes.
* Rename `MediaCodecRenderer.onOutputFormatChanged` to
`MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the
distinction between `Format` and `MediaFormat`.
* Improve `Format` propagation within the media codec renderer
([#6646](https://github.com/google/ExoPlayer/issues/6646)).
* Move player message-related constants from `C` to `Renderer`, to avoid
having the constants class depend on player/renderer classes.
* Split out `common` and `extractor` submodules.
* Allow to explicitly send `PlayerMessage`s at the end of a stream.
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
* Add `DataSpec.customData` to allow applications to pass custom data
through `DataSource` chains.
* Add a `Format.Builder` and deprecate all `Format.create*` methods and
most `Format.copyWith*` methods.
* Split `Format.bitrate` into `Format.averageBitrate` and
`Format.peakBitrate`
* Allow passing `C.TIME_END_OF_SOURCE` to `PlayerMessage.setPosition`
to send a `PlayerMessage` at the end of a stream.
* `SimpleExoPlayer`:
* `SimpleExoPlayer` implements the new `MediaItem` based playlist API,
using a `MediaSourceFactory` to convert `MediaItem` instances to
playable `MediaSource` instances. A `DefaultMediaSourceFactory` is
used by default. `Builder.setMediaSourceFactory` allows setting a
custom factory.
* Add additional options to `Builder` that were previously only
accessible via setters.
* Add opt-in to verify correct thread usage with
`setThrowsWhenUsingWrongThread(true)`
([#4463](https://github.com/google/ExoPlayer/issues/4463)).
* `Format`:
* Add a `Builder` and deprecate all `create` methods and most
`Format.copyWith` methods.
* Split `bitrate` into `averageBitrate` and `peakBitrate`
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
* Add option to `MergingMediaSource` to adjust the time offsets between
the merged sources
* `LoadControl`:
* Add a `playbackPositionUs` parameter to `shouldContinueLoading`.
* Set the default minimum buffer duration in `DefaultLoadControl` to
50 seconds (equal to the default maximum buffer), and treat audio
and video the same.
* Add a `MetadataRetriever` API for retrieving track information and
static metadata for a media item
([#3609](https://github.com/google/ExoPlayer/issues/3609)).
* Attach an identifier and extra information to load error events passed
to `LoadErrorHandlingPolicy`
([#7309](https://github.com/google/ExoPlayer/issues/7309)).
`LoadErrorHandlingPolicy` implementations should migrate to implementing
the non-deprecated methods of the interface.
* Add an option to `MergingMediaSource` to adjust the time offsets
between the merged sources
([#6103](https://github.com/google/ExoPlayer/issues/6103)).
* `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` renamed to
* Move `MediaSourceEventListener.LoadEventInfo` and
`MediaSourceEventListener.MediaLoadData` to be top-level classes in
`com.google.android.exoplayer2.source`.
* Move `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` to
`DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and
generalized to work with `Decoder` rather than `SimpleDecoder`.
* Add media item based playlist API to `Player`.
* Add `getCurrentMediaItem` to `Player`.
* Remove deprecated members in `DefaultTrackSelector`.
* Add `DefaultTrackSelector` constraints for minimum video resolution,
bitrate and frame rate
([#4511](https://github.com/google/ExoPlayer/issues/4511)).
* Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so
that the device volume can be controlled by player.
* Parse track titles from Matroska files
([#7247](https://github.com/google/ExoPlayer/pull/7247)).
* Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
`CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
* Extend `EventTime` with more details about the current player state for
easier access
([#7332](https://github.com/google/ExoPlayer/issues/7332)).
* Add `HttpDataSource.InvalidResponseCodeException#responseBody` field
([#6853](https://github.com/google/ExoPlayer/issues/6853)).
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
ongoing load should be canceled. Only supported by HLS streams so far.
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
* Remove throws clause from Renderer.stop.
* Don't clear `exception` in `SimpleDecoder#flush()`
([#7590](https://github.com/google/ExoPlayer/issues/7590)).
* Remove `AdaptiveTrackSelection.minTimeBetweenBufferReevaluationMs`
parameter ([#7582](https://github.com/google/ExoPlayer/issues/7582)).
* Fix wrong `MediaPeriodId` for some renderer errors reported by
`AnalyticsListener.onPlayerError`.
* Remove onMediaPeriodCreated/Released/ReadingStarted from
`MediaSourceEventListener` and `AnalyticsListener`.
* Dispatch previous, next, fast forward and rewind actions through
`ControlDispatcher`
([#6926](https://github.com/google/ExoPlayer/issues/6926)).
* Add Guava dependency.
* Add MetadataRetriever API to retrieve the static metadata of a media
item ([#3609](https://github.com/google/ExoPlayer/issues/3609)).
* Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices.
generalize them to work with `Decoder` rather than `SimpleDecoder`.
* Deprecate `C.MSG_*` constants, replacing them with constants in
`Renderer`.
* Split the `library-core` module into `library-core`,
`library-common` and `library-extractor`. The `library-core` module
has an API dependency on both of the new modules, so this change
should be transparent to developers including ExoPlayer using Gradle
dependencies.
* Add a dependency on Guava.
* Video:
* Pass frame rate hint to `Surface.setFrameRate` on Android 11.
* Fix incorrect aspect ratio when transitioning from one video to another
with the same resolution, but a different pixel aspect ratio
([#6646](https://github.com/google/ExoPlayer/issues/6646)).
* Audio:
* Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer`
and `AudioSink.handleBuffer` to allow batching multiple encoded frames
in one buffer.
* No longer use a `MediaCodec` in audio passthrough mode.
* Add experimental support for power efficient playback using audio
offload.
* Add support for using framework audio speed adjustment instead of
ExoPlayer's implementation
([#7502](https://github.com/google/ExoPlayer/issues/7502)). This option
can be set using
`DefaultRenderersFactory.setEnableAudioTrackPlaybackParams`.
* Add an event for the audio position starting to advance, to make it
easier for apps to determine when audio playout started
([#7577](https://github.com/google/ExoPlayer/issues/7577)).
* Generalize support for floating point audio.
* Add an option to `DefaultAudioSink` for enabling floating point
output. This option can also be set using
`DefaultRenderersFactory.setEnableAudioFloatOutput`.
* Add floating point output capability to `MediaCodecAudioRenderer`
and `LibopusAudioRenderer`, which is enabled automatically if the
audio sink supports floating point output and if it makes sense for
the content being played.
* Enable the floating point output capability of `FfmpegAudioRenderer`
automatically if the audio sink supports floating point output and
if it makes sense for the content being played. The option to
manually enable floating point output has been removed, since this
now done with the generalized option on `DefaultAudioSink`.
* In `MediaCodecAudioRenderer`, stop passing audio samples through
`MediaCodec` when playing PCM audio or encoded audio using passthrough
mode.
* Reuse audio decoders when transitioning through playlists of gapless
audio, rather than reinstantiating them.
* Check `DefaultAudioSink` supports passthrough, in addition to checking
the `AudioCapabilities`
* Add an experimental scheduling mode to save power in offload.
([#7404](https://github.com/google/ExoPlayer/issues/7404)).
* Adjust input timestamps in `MediaCodecRenderer` to account for the
Codec2 MP3 decoder having lower timestamps on the output side.
* Propagate gapless audio metadata without the need to recreate the audio
decoders.
* Add floating point PCM output capability in `MediaCodecAudioRenderer`,
and `LibopusAudioRenderer`.
* Do not use a MediaCodec for PCM formats if AudioTrack supports it.
* Add optional support for using framework audio speed adjustment instead
of application-level audio speed adjustment
([#7502](https://github.com/google/ExoPlayer/issues/7502)).
* Text:
* Add a WebView-based output option to `SubtitleView`. This can display
some features not supported by the existing Canvas-based output such as
@ -209,21 +208,19 @@
`Mp3Extractor`. A significant portion of the file may need to be scanned
when a seek is performed, which may be costly for large files.
* MP4: Fix playback of MP4 streams that contain Opus audio.
* FMP4:
* Add support for partially fragmented MP4s
* FMP4: Add support for partially fragmented MP4s
([#7308](https://github.com/google/ExoPlayer/issues/7308)).
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd`
boxes ([#7716](https://github.com/google/ExoPlayer/issues/7716)).
* Matroska: Remove support for the `Invisible` block header flag.
* Matroska:
* Support Dolby Vision
([#7267](https://github.com/google/ExoPlayer/issues/7267).
* Populate `Format.label` with track titles.
* Remove support for the `Invisible` block header flag.
* MPEG-TS: Add support for MPEG-4 Part 2 and H.263
([#1603](https://github.com/google/ExoPlayer/issues/1603),
[#5107](https://github.com/google/ExoPlayer/issues/5107)).
* Ogg: Fix handling of non-contiguous pages
([#7230](https://github.com/google/ExoPlayer/issues/7230)).
* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than
failing playback
([#7675](https://github.com/google/ExoPlayer/issues/7675)).
* UI
* UI:
* Add `StyledPlayerView` and `StyledPlayerControlView`, which provide a
more polished user experience than `PlayerView` and `PlayerControlView`
at the cost of decreased customizability.
@ -238,6 +235,9 @@
* Update `TrackSelectionDialogBuilder` to use the AndroidX app compat
`AlertDialog` rather than the platform version, if available
([#7357](https://github.com/google/ExoPlayer/issues/7357)).
* Make UI components dispatch previous, next, fast forward and rewind
actions via their `ControlDispatcher`
([#6926](https://github.com/google/ExoPlayer/issues/6926)).
* Downloads and caching:
* Add `DownloadRequest.Builder`.
* Add `DownloadRequest.keySetId` to make it easier to store an offline
@ -269,7 +269,39 @@
([#7011](https://github.com/google/ExoPlayer/issues/7011),
[#6725](https://github.com/google/ExoPlayer/issues/6725),
[#7066](https://github.com/google/ExoPlayer/issues/7066)).
* Remove support for `cbc1` and `cens` encrytion schemes. Support for
these schemes was removed from the Android platform from API level 30,
and the range of API levels for which they are supported is too small to
be useful.
* Remove generic types from DRM components.
* Track selection:
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
ongoing load should be canceled
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
* Add `DefaultTrackSelector` constraints for minimum video resolution,
bitrate and frame rate
([#4511](https://github.com/google/ExoPlayer/issues/4511)).
* Remove previously deprecated `DefaultTrackSelector` members.
* Data sources:
* Add `HttpDataSource.InvalidResponseCodeException#responseBody` field
([#6853](https://github.com/google/ExoPlayer/issues/6853)).
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
* Add `DataSpec.customData` to allow applications to pass custom data
through `DataSource` chains.
* Deprecate `CacheDataSinkFactory` and `CacheDataSourceFactory`, which are
replaced by `CacheDataSink.Factory` and `CacheDataSource.Factory`
respectively.
* Analytics:
* Extend `EventTime` with more details about the current player state
([#7332](https://github.com/google/ExoPlayer/issues/7332)).
* Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how
early or late video frames are processed relative to them needing to be
presented. Video frame processing offset fields are also added to
`DecoderCounters`.
* Fix incorrect `MediaPeriodId` for some renderer errors reported by
`AnalyticsListener.onPlayerError`.
* Remove `onMediaPeriodCreated`, `onMediaPeriodReleased` and
`onReadingStarted` from `AnalyticsListener`.
* Test utils: Add `TestExoPlayer`, a utility class with APIs to create
`SimpleExoPlayer` instances with fake components for testing.
* Media2 extension: This is a new extension that makes it easy to use
@ -277,8 +309,6 @@
* Cast extension: Implement playlist API and deprecate the old queue
manipulation API.
* IMA extension:
* Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the media load
timeout ([#7170](https://github.com/google/ExoPlayer/issues/7170)).
* Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to
register a purpose and detail reason for overlay views via
`AdsLoader.AdViewProvider`.
@ -304,8 +334,32 @@
* Remove support for media tunneling, random ABR and playback of
spherical video. Developers wishing to experiment with these features
can enable them by modifying the demo app source code.
* Fix playback of ClearKey protected content on API level 26 and earlier
([#7735](https://github.com/google/ExoPlayer/issues/7735)).
### 2.11.8 (2020-08-25) ###
* 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
([#7592](https://github.com/google/ExoPlayer/issues/7592)).
* 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.
* Workaround an issue on Broadcom based devices where playbacks would not
transition to `STATE_ENDED` when using video tunneling mode
([#7647](https://github.com/google/ExoPlayer/issues/7647)).
* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the
media load timeout
([#7170](https://github.com/google/ExoPlayer/issues/7170)).
* 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) ###

View File

@ -124,18 +124,6 @@
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "Secure (cbc1)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "Secure UHD (cbc1)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "Secure (cbcs)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",

View File

@ -458,32 +458,16 @@ public final class CastPlayer extends BasePlayer {
flushNotifications();
}
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
@Override
public float getPlaybackSpeed() {
return Player.DEFAULT_PLAYBACK_SPEED;
}
@Override
public void stop(boolean reset) {
playbackState = STATE_IDLE;

View File

@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.os.Looper;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
@ -46,13 +45,6 @@ public class MediaSessionUtilTest {
@Test
public void getSessionCompatToken_withMediaControllerCompat_returnsValidToken() throws Exception {
// Workaround to instantiate MediaSession with public androidx.media dependency.
// TODO(b/146536708): Remove this workaround when the relevant change is released via
// androidx.media 1.2.0.
if (Looper.myLooper() == null) {
Looper.prepare();
}
Context context = ApplicationProvider.getApplicationContext();
SessionPlayerConnector sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import android.content.Context;
@ -55,15 +54,23 @@ import org.junit.rules.ExternalResource;
@Override
protected void before() {
// Workaround limitation in androidx.media2.session:1.0.3 which session can only be instantiated
// on thread with prepared Looper.
// TODO: Remove when androidx.media2.session:1.1.0 is released without the limitation
// [Internal: b/146536708]
if (Looper.myLooper() == null) {
Looper.prepare();
}
context = ApplicationProvider.getApplicationContext();
executor = Executors.newFixedThreadPool(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
// Initialize AudioManager on the main thread to workaround b/78617702 that
// Initialize AudioManager on the main thread to workaround that
// audio focus listener is called on the thread where the AudioManager was
// originally initialized.
// originally initialized. [Internal: b/78617702]
// Without posting this, audio focus listeners wouldn't be called because the
// listeners would be posted to the test thread (here) where it waits until the
// tests are finished.
@ -75,8 +82,7 @@ import org.junit.rules.ExternalResource;
.setLooper(Looper.myLooper())
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, null))
.build();
sessionPlayerConnector =
new SessionPlayerConnector(exoPlayer, new DefaultMediaItemConverter());
sessionPlayerConnector = new SessionPlayerConnector(exoPlayer);
});
}

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import static com.google.android.exoplayer2.ext.media2.TestUtils.assertPlayerResultSuccess;
@ -29,6 +28,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.UriMediaItem;
@ -136,7 +136,7 @@ public class SessionCallbackBuilderTest {
SessionResult.RESULT_ERROR_BAD_VALUE)
.setRewindIncrementMs(testRewindIncrementMs)
.setFastForwardIncrementMs(testFastForwardIncrementMs)
.setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider())
.setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider())
.build())) {
assertPlayerResultSuccess(sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem()));
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@ -179,7 +179,7 @@ public class SessionCallbackBuilderTest {
SessionResult.RESULT_ERROR_BAD_VALUE)
.setRewindIncrementMs(testRewindIncrementMs)
.setFastForwardIncrementMs(testFastForwardIncrementMs)
.setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider())
.setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider())
.build())) {
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null));
@ -455,13 +455,13 @@ public class SessionCallbackBuilderTest {
Uri testMediaUri = RawResourceDataSource.buildRawResourceUri(R.raw.audio);
CountDownLatch providerLatch = new CountDownLatch(1);
SessionCallbackBuilder.DefaultMediaItemProvider defaultMediaItemProvider =
new SessionCallbackBuilder.DefaultMediaItemProvider();
SessionCallbackBuilder.MediaIdMediaItemProvider mediaIdMediaItemProvider =
new SessionCallbackBuilder.MediaIdMediaItemProvider();
SessionCallbackBuilder.MediaItemProvider provider =
(session, controllerInfo, mediaId) -> {
assertThat(mediaId).isEqualTo(testMediaUri.toString());
providerLatch.countDown();
return defaultMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId);
return mediaIdMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId);
};
CountDownLatch currentMediaItemChangedLatch = new CountDownLatch(1);
@ -471,7 +471,9 @@ public class SessionCallbackBuilderTest {
@Override
public void onCurrentMediaItemChanged(
@NonNull SessionPlayer player, @NonNull MediaItem item) {
assertThat(((UriMediaItem) item).getUri()).isEqualTo(testMediaUri);
MediaMetadata metadata = item.getMetadata();
assertThat(metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID))
.isEqualTo(testMediaUri.toString());
currentMediaItemChangedLatch.countDown();
}
});

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED;
@ -33,6 +32,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
@ -61,6 +61,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -273,17 +274,17 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getCurrentPosition_whenIdleState_returnsUnknownTime() {
public void getCurrentPosition_whenIdleState_returnsDefaultPosition() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getBufferedPosition_whenIdleState_returnsUnknownTime() {
public void getBufferedPosition_whenIdleState_returnsDefaultPosition() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0);
}
@Test
@ -795,6 +796,73 @@ public class SessionPlayerConnectorTest {
assertThat(onPlaylistChangedLatch.getCount()).isEqualTo(1);
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged()
throws Exception {
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
for (MediaItem mediaItem : playlistToExoPlayer) {
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
}
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor,
new SessionPlayer.PlayerCallback() {
@Override
public void onPlaylistChanged(
@NonNull SessionPlayer player,
@Nullable List<MediaItem> list,
@Nullable MediaMetadata metadata) {
if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
onPlaylistChangedLatch.countDown();
}
}
});
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged()
throws Exception {
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
for (MediaItem mediaItem : playlistToExoPlayer) {
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
}
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor,
new SessionPlayer.PlayerCallback() {
@Override
public void onPlaylistChanged(
@NonNull SessionPlayer player,
@Nullable List<MediaItem> list,
@Nullable MediaMetadata metadata) {
if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
onPlaylistChangedLatch.countDown();
}
}
});
sessionPlayerConnector.prepare();
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
@ -862,7 +930,7 @@ public class SessionPlayerConnectorTest {
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
int replaceIndex = 2;
MediaItem newMediaItem = TestUtils.createMediaItem();
MediaItem newMediaItem = TestUtils.createMediaItem(R.raw.video_big_buck_bunny);
playlist.set(replaceIndex, newMediaItem);
sessionPlayerConnector.registerPlayerCallback(
executor,
@ -1185,6 +1253,32 @@ public class SessionPlayerConnectorTest {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PLAYING);
}
@Test
@LargeTest
public void getPlaylist_returnsPlaylistInUnderlyingPlayer() {
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
for (MediaItem mediaItem : playlistToExoPlayer) {
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
}
AtomicReference<List<MediaItem>> playlistFromSessionPlayer = new AtomicReference<>();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
simpleExoPlayer.setMediaItems(exoMediaItems);
try (SessionPlayerConnector sessionPlayer =
new SessionPlayerConnector(simpleExoPlayer)) {
List<MediaItem> playlist = sessionPlayer.getPlaylist();
playlistFromSessionPlayer.set(playlist);
}
});
assertThat(playlistFromSessionPlayer.get()).isEqualTo(playlistToExoPlayer);
}
private class PlayerCallbackForPlaylist extends SessionPlayer.PlayerCallback {
private List<MediaItem> playlist;
private CountDownLatch onCurrentMediaItemChangedLatch;

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS;

View File

@ -13,79 +13,125 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_DISPLAY_TITLE;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_ID;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_URI;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_TITLE;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media2.common.CallbackMediaItem;
import androidx.media2.common.FileMediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.UriMediaItem;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.Assertions;
/** Default implementation of {@link MediaItemConverter}. */
public final class DefaultMediaItemConverter implements MediaItemConverter {
/**
* Default implementation of {@link MediaItemConverter}.
*
* <p>Note that {@link #getMetadata} can be overridden to fill in additional metadata when
* converting {@link MediaItem ExoPlayer MediaItems} to their AndroidX equivalents.
*/
public class DefaultMediaItemConverter implements MediaItemConverter {
@Override
public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem) {
if (androidXMediaItem instanceof FileMediaItem) {
public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
if (media2MediaItem instanceof FileMediaItem) {
throw new IllegalStateException("FileMediaItem isn't supported");
}
if (androidXMediaItem instanceof CallbackMediaItem) {
if (media2MediaItem instanceof CallbackMediaItem) {
throw new IllegalStateException("CallbackMediaItem isn't supported");
}
MediaItem.Builder exoplayerMediaItemBuilder = new MediaItem.Builder();
// Set mediaItem as tag for creating MediaSource via MediaSourceFactory methods.
exoplayerMediaItemBuilder.setTag(androidXMediaItem);
// Media ID or URI must be present. Get it from androidx.MediaItem if possible.
@Nullable Uri uri = null;
@Nullable String mediaId = null;
if (androidXMediaItem instanceof UriMediaItem) {
UriMediaItem uriMediaItem = (UriMediaItem) androidXMediaItem;
@Nullable String title = null;
if (media2MediaItem instanceof UriMediaItem) {
UriMediaItem uriMediaItem = (UriMediaItem) media2MediaItem;
uri = uriMediaItem.getUri();
}
@Nullable MediaMetadata metadata = androidXMediaItem.getMetadata();
@Nullable androidx.media2.common.MediaMetadata metadata = media2MediaItem.getMetadata();
if (metadata != null) {
mediaId = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
@Nullable String uriString = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_URI);
if (uri == null && uriString != null) {
@Nullable String uriString = metadata.getString(METADATA_KEY_MEDIA_URI);
mediaId = metadata.getString(METADATA_KEY_MEDIA_ID);
if (uri == null) {
if (uriString != null) {
uri = Uri.parse(uriString);
} else if (mediaId != null) {
uri = Uri.parse("media2:///" + mediaId);
}
}
title = metadata.getString(METADATA_KEY_DISPLAY_TITLE);
if (title == null) {
title = metadata.getString(METADATA_KEY_TITLE);
}
}
if (uri == null) {
// Generate a Uri to make it non-null. If not, tag will be ignored.
uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode());
// Generate a URI to make it non-null. If not, then the tag passed to setTag will be ignored.
uri = Uri.parse("media2:///");
}
exoplayerMediaItemBuilder.setUri(uri);
exoplayerMediaItemBuilder.setMediaId(mediaId);
if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
exoplayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition());
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
long startPositionMs = media2MediaItem.getStartPosition();
if (startPositionMs == androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
startPositionMs = 0;
}
if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
exoplayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition());
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
long endPositionMs = media2MediaItem.getEndPosition();
if (endPositionMs == androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
endPositionMs = C.TIME_END_OF_SOURCE;
}
return exoplayerMediaItemBuilder.build();
return new MediaItem.Builder()
.setUri(uri)
.setMediaId(mediaId)
.setMediaMetadata(
new com.google.android.exoplayer2.MediaMetadata.Builder().setTitle(title).build())
.setTag(media2MediaItem)
.setClipStartPositionMs(startPositionMs)
.setClipEndPositionMs(endPositionMs)
.build();
}
@Override
public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem) {
Assertions.checkNotNull(exoplayerMediaItem);
public androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem) {
Assertions.checkNotNull(exoPlayerMediaItem);
MediaItem.PlaybackProperties playbackProperties =
Assertions.checkNotNull(exoplayerMediaItem.playbackProperties);
Assertions.checkNotNull(exoPlayerMediaItem.playbackProperties);
@Nullable Object tag = playbackProperties.tag;
if (!(tag instanceof androidx.media2.common.MediaItem)) {
throw new IllegalStateException(
"MediaItem tag must be an instance of androidx.media2.common.MediaItem");
}
if (tag instanceof androidx.media2.common.MediaItem) {
return (androidx.media2.common.MediaItem) tag;
}
androidx.media2.common.MediaMetadata metadata = getMetadata(exoPlayerMediaItem);
long startPositionMs = exoPlayerMediaItem.clippingProperties.startPositionMs;
long endPositionMs = exoPlayerMediaItem.clippingProperties.endPositionMs;
if (endPositionMs == C.TIME_END_OF_SOURCE) {
endPositionMs = androidx.media2.common.MediaItem.POSITION_UNKNOWN;
}
return new androidx.media2.common.MediaItem.Builder()
.setMetadata(metadata)
.setStartPosition(startPositionMs)
.setEndPosition(endPositionMs)
.build();
}
/**
* Returns a {@link androidx.media2.common.MediaMetadata} corresponding to the given {@link
* MediaItem ExoPlayer MediaItem}.
*/
protected androidx.media2.common.MediaMetadata getMetadata(MediaItem exoPlayerMediaItem) {
@Nullable String title = exoPlayerMediaItem.mediaMetadata.title;
androidx.media2.common.MediaMetadata.Builder metadataBuilder =
new androidx.media2.common.MediaMetadata.Builder()
.putString(METADATA_KEY_MEDIA_ID, exoPlayerMediaItem.mediaId);
if (title != null) {
metadataBuilder.putString(METADATA_KEY_TITLE, title);
metadataBuilder.putString(METADATA_KEY_DISPLAY_TITLE, title);
}
return metadataBuilder.build();
}
}

View File

@ -13,25 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import com.google.android.exoplayer2.MediaItem;
/**
* Converter for between {@link MediaItem AndroidX MediaItem} and {@link
* com.google.android.exoplayer2.MediaItem ExoPlayer MediaItem}.
* Converts between {@link androidx.media2.common.MediaItem Media2 MediaItem} and {@link MediaItem
* ExoPlayer MediaItem}.
*/
public interface MediaItemConverter {
/**
* Converts {@link androidx.media2.common.MediaItem AndroidX MediaItem} to {@link MediaItem
* Converts an {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem
* ExoPlayer MediaItem}.
*/
MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem);
MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem);
/**
* Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.MediaItem
* AndroidX MediaItem}.
* Converts an {@link MediaItem ExoPlayer MediaItem} to an {@link androidx.media2.common.MediaItem
* Media2 MediaItem}.
*/
androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem);
androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem);
}

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import androidx.annotation.IntRange;
@ -27,6 +26,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes;
@ -56,44 +56,44 @@ import java.util.List;
void onPlayerStateChanged(/* @SessionPlayer.PlayerState */ int playerState);
/** Called when the player is prepared. */
void onPrepared(androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage);
void onPrepared(androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
/** Called when a seek request has completed. */
void onSeekCompleted();
/** Called when the player rebuffers. */
void onBufferingStarted(androidx.media2.common.MediaItem androidXMediaItem);
void onBufferingStarted(androidx.media2.common.MediaItem media2MediaItem);
/** Called when the player becomes ready again after rebuffering. */
void onBufferingEnded(
androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage);
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
/** Called periodically with the player's buffered position as a percentage. */
void onBufferingUpdate(
androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage);
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
/** Called when current media item is changed. */
void onCurrentMediaItemChanged(androidx.media2.common.MediaItem androidXMediaItem);
void onCurrentMediaItemChanged(androidx.media2.common.MediaItem media2MediaItem);
/** Called when playback of the item list has ended. */
void onPlaybackEnded();
/** Called when the player encounters an error. */
void onError(@Nullable androidx.media2.common.MediaItem androidXMediaItem);
void onError(@Nullable androidx.media2.common.MediaItem media2MediaItem);
/** Called when the playlist is changed */
/** Called when the playlist is changed. */
void onPlaylistChanged();
/** Called when the shuffle mode is changed */
/** Called when the shuffle mode is changed. */
void onShuffleModeChanged(int shuffleMode);
/** Called when the repeat mode is changed */
/** Called when the repeat mode is changed. */
void onRepeatModeChanged(int repeatMode);
/** Called when the audio attributes is changed */
/** Called when the audio attributes is changed. */
void onAudioAttributesChanged(AudioAttributesCompat audioAttributes);
/** Called when the playback speed is changed */
/** Called when the playback speed is changed. */
void onPlaybackSpeedChanged(float playbackSpeed);
}
@ -108,14 +108,15 @@ import java.util.List;
private final ControlDispatcher controlDispatcher;
private final ComponentListener componentListener;
private final List<androidx.media2.common.MediaItem> cachedPlaylist;
@Nullable private MediaMetadata playlistMetadata;
private final List<MediaItem> cachedMediaItems;
// These should be only updated in TimelineChanges.
private final List<androidx.media2.common.MediaItem> media2Playlist;
private final List<MediaItem> exoPlayerPlaylist;
private boolean prepared;
private boolean rebuffering;
private int currentWindowIndex;
private boolean loggedUnexpectedTimelineChanges;
private boolean ignoreTimelineUpdates;
/**
@ -146,79 +147,69 @@ import java.util.List;
handler = new PlayerHandler(player.getApplicationLooper());
pollBufferRunnable = new PollBufferRunnable();
cachedPlaylist = new ArrayList<>();
cachedMediaItems = new ArrayList<>();
media2Playlist = new ArrayList<>();
exoPlayerPlaylist = new ArrayList<>();
currentWindowIndex = C.INDEX_UNSET;
prepared = player.getPlaybackState() != Player.STATE_IDLE;
rebuffering = player.getPlaybackState() == Player.STATE_BUFFERING;
updatePlaylist(player.getCurrentTimeline());
}
public boolean setMediaItem(androidx.media2.common.MediaItem androidXMediaItem) {
return setPlaylist(Collections.singletonList(androidXMediaItem), /* metadata= */ null);
public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null);
}
public boolean setPlaylist(
List<androidx.media2.common.MediaItem> playlist, @Nullable MediaMetadata metadata) {
// Check for duplication.
for (int i = 0; i < playlist.size(); i++) {
androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i);
Assertions.checkArgument(playlist.indexOf(androidXMediaItem) == i);
androidx.media2.common.MediaItem media2MediaItem = playlist.get(i);
Assertions.checkArgument(playlist.indexOf(media2MediaItem) == i);
}
this.cachedPlaylist.clear();
this.cachedPlaylist.addAll(playlist);
this.playlistMetadata = metadata;
this.cachedMediaItems.clear();
List<MediaItem> exoplayerMediaItems = new ArrayList<>();
List<MediaItem> exoPlayerMediaItems = new ArrayList<>();
for (int i = 0; i < playlist.size(); i++) {
androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i);
MediaItem exoplayerMediaItem =
Assertions.checkNotNull(
mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem));
exoplayerMediaItems.add(exoplayerMediaItem);
androidx.media2.common.MediaItem media2MediaItem = playlist.get(i);
MediaItem exoPlayerMediaItem =
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem));
exoPlayerMediaItems.add(exoPlayerMediaItem);
}
this.cachedMediaItems.addAll(exoplayerMediaItems);
player.setMediaItems(exoplayerMediaItems, /* resetPosition= */ true);
player.setMediaItems(exoPlayerMediaItems, /* resetPosition= */ true);
currentWindowIndex = getCurrentMediaItemIndex();
return true;
}
public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem androidXMediaItem) {
Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem));
index = Util.constrainValue(index, 0, cachedPlaylist.size());
public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem media2MediaItem) {
Assertions.checkArgument(!media2Playlist.contains(media2MediaItem));
index = Util.constrainValue(index, 0, media2Playlist.size());
cachedPlaylist.add(index, androidXMediaItem);
MediaItem exoplayerMediaItem =
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem));
cachedMediaItems.add(index, exoplayerMediaItem);
player.addMediaItem(index, exoplayerMediaItem);
MediaItem exoPlayerMediaItem =
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem));
player.addMediaItem(index, exoPlayerMediaItem);
return true;
}
public boolean removePlaylistItem(@IntRange(from = 0) int index) {
androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.remove(index);
releaseMediaItem(androidXMediaItemToRemove);
cachedMediaItems.remove(index);
player.removeMediaItem(index);
return true;
}
public boolean replacePlaylistItem(
int index, androidx.media2.common.MediaItem androidXMediaItem) {
Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem));
index = Util.constrainValue(index, 0, cachedPlaylist.size());
public boolean replacePlaylistItem(int index, androidx.media2.common.MediaItem media2MediaItem) {
Assertions.checkArgument(!media2Playlist.contains(media2MediaItem));
index = Util.constrainValue(index, 0, media2Playlist.size());
androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.get(index);
cachedPlaylist.set(index, androidXMediaItem);
releaseMediaItem(androidXMediaItemToRemove);
MediaItem exoplayerMediaItemToAdd =
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem));
cachedMediaItems.set(index, exoplayerMediaItemToAdd);
MediaItem exoPlayerMediaItemToAdd =
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem));
ignoreTimelineUpdates = true;
player.removeMediaItem(index);
ignoreTimelineUpdates = false;
player.addMediaItem(index, exoplayerMediaItemToAdd);
player.addMediaItem(index, exoPlayerMediaItemToAdd);
return true;
}
@ -272,8 +263,8 @@ import java.util.List;
}
@Nullable
public List<androidx.media2.common.MediaItem> getCachedPlaylist() {
return new ArrayList<>(cachedPlaylist);
public List<androidx.media2.common.MediaItem> getPlaylist() {
return new ArrayList<>(media2Playlist);
}
@Nullable
@ -290,7 +281,7 @@ import java.util.List;
}
public int getCurrentMediaItemIndex() {
return cachedPlaylist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex();
return media2Playlist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex();
}
public int getPreviousMediaItemIndex() {
@ -304,7 +295,7 @@ import java.util.List;
@Nullable
public androidx.media2.common.MediaItem getCurrentMediaItem() {
int index = getCurrentMediaItemIndex();
return (index != C.INDEX_UNSET) ? cachedPlaylist.get(index) : null;
return index == C.INDEX_UNSET ? null : media2Playlist.get(index);
}
public boolean prepare() {
@ -317,9 +308,9 @@ import java.util.List;
public boolean play() {
if (player.getPlaybackState() == Player.STATE_ENDED) {
int currentWindowIndex = getCurrentMediaItemIndex();
boolean seekHandled =
controlDispatcher.dispatchSeekTo(player, currentWindowIndex, /* positionMs= */ 0);
controlDispatcher.dispatchSeekTo(
player, player.getCurrentWindowIndex(), /* positionMs= */ 0);
if (!seekHandled) {
return false;
}
@ -342,23 +333,19 @@ import java.util.List;
}
public boolean seekTo(long position) {
int currentWindowIndex = getCurrentMediaItemIndex();
return controlDispatcher.dispatchSeekTo(player, currentWindowIndex, position);
return controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), position);
}
public long getCurrentPosition() {
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
return Math.max(0, player.getCurrentPosition());
return player.getCurrentPosition();
}
public long getDuration() {
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
long duration = player.getDuration();
return duration == C.TIME_UNSET ? -1 : duration;
return duration == C.TIME_UNSET ? SessionPlayer.UNKNOWN_TIME : duration;
}
public long getBufferedPosition() {
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
return player.getBufferedPosition();
}
@ -397,11 +384,11 @@ import java.util.List;
}
public void setPlaybackSpeed(float playbackSpeed) {
player.setPlaybackSpeed(playbackSpeed);
player.setPlaybackParameters(new PlaybackParameters(playbackSpeed));
}
public float getPlaybackSpeed() {
return player.getPlaybackSpeed();
return player.getPlaybackParameters().speed;
}
public void reset() {
@ -427,7 +414,7 @@ import java.util.List;
}
public boolean canSkipToPlaylistItem() {
@Nullable List<androidx.media2.common.MediaItem> playlist = getCachedPlaylist();
@Nullable List<androidx.media2.common.MediaItem> playlist = getPlaylist();
return playlist != null && playlist.size() > 1;
}
@ -497,57 +484,57 @@ import java.util.List;
listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled));
}
private void handlePlaybackSpeedChanged(float playbackSpeed) {
listener.onPlaybackSpeedChanged(playbackSpeed);
private void handlePlaybackParametersChanged(PlaybackParameters playbackParameters) {
listener.onPlaybackSpeedChanged(playbackParameters.speed);
}
private void handleTimelineChanged(Timeline timeline) {
if (ignoreTimelineUpdates) {
return;
}
updateCachedPlaylistAndMediaItems(timeline);
if (!isExoPlayerMediaItemsChanged(timeline)) {
return;
}
updatePlaylist(timeline);
listener.onPlaylistChanged();
}
// Update cached playlist, if the ExoPlayer Player's Timeline is unexpectedly changed without
// using SessionPlayer interface.
private void updateCachedPlaylistAndMediaItems(Timeline currentTimeline) {
// Check whether ExoPlayer media items are the same as expected.
// Check whether Timeline is changed by media item changes or not
private boolean isExoPlayerMediaItemsChanged(Timeline timeline) {
if (exoPlayerPlaylist.size() != timeline.getWindowCount()) {
return true;
}
Timeline.Window window = new Timeline.Window();
int windowCount = currentTimeline.getWindowCount();
int windowCount = timeline.getWindowCount();
for (int i = 0; i < windowCount; i++) {
currentTimeline.getWindow(i, window);
if (i >= cachedMediaItems.size()
|| !ObjectsCompat.equals(cachedMediaItems.get(i), window.mediaItem)) {
if (!loggedUnexpectedTimelineChanges) {
Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt.");
loggedUnexpectedTimelineChanges = true;
timeline.getWindow(i, window);
if (!ObjectsCompat.equals(exoPlayerPlaylist.get(i), window.mediaItem)) {
return true;
}
}
return false;
}
androidx.media2.common.MediaItem oldAndroidXMediaItem = cachedPlaylist.get(i);
releaseMediaItem(oldAndroidXMediaItem);
private void updatePlaylist(Timeline timeline) {
List<androidx.media2.common.MediaItem> media2MediaItemToBeRemoved =
new ArrayList<>(media2Playlist);
media2Playlist.clear();
exoPlayerPlaylist.clear();
androidx.media2.common.MediaItem androidXMediaItem =
Assertions.checkNotNull(
mediaItemConverter.convertToAndroidXMediaItem(window.mediaItem));
if (i < cachedMediaItems.size()) {
cachedMediaItems.set(i, window.mediaItem);
cachedPlaylist.set(i, androidXMediaItem);
} else {
cachedMediaItems.add(window.mediaItem);
cachedPlaylist.add(androidXMediaItem);
}
}
}
if (cachedMediaItems.size() > windowCount) {
if (!loggedUnexpectedTimelineChanges) {
Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt.");
loggedUnexpectedTimelineChanges = true;
}
while (cachedMediaItems.size() > windowCount) {
cachedMediaItems.remove(windowCount);
cachedPlaylist.remove(windowCount);
Timeline.Window window = new Timeline.Window();
int windowCount = timeline.getWindowCount();
for (int i = 0; i < windowCount; i++) {
timeline.getWindow(i, window);
MediaItem exoPlayerMediaItem = window.mediaItem;
androidx.media2.common.MediaItem media2MediaItem =
Assertions.checkNotNull(mediaItemConverter.convertToMedia2MediaItem(exoPlayerMediaItem));
exoPlayerPlaylist.add(exoPlayerMediaItem);
media2Playlist.add(media2MediaItem);
media2MediaItemToBeRemoved.remove(media2MediaItem);
}
for (androidx.media2.common.MediaItem item : media2MediaItemToBeRemoved) {
releaseMediaItem(item);
}
}
@ -556,35 +543,35 @@ import java.util.List;
}
private void updateBufferingAndScheduleNextPollBuffer() {
androidx.media2.common.MediaItem androidXMediaItem =
androidx.media2.common.MediaItem media2MediaItem =
Assertions.checkNotNull(getCurrentMediaItem());
listener.onBufferingUpdate(androidXMediaItem, player.getBufferedPercentage());
listener.onBufferingUpdate(media2MediaItem, player.getBufferedPercentage());
handler.removeCallbacks(pollBufferRunnable);
handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS);
}
private void maybeNotifyBufferingEvents() {
androidx.media2.common.MediaItem androidXMediaItem =
androidx.media2.common.MediaItem media2MediaItem =
Assertions.checkNotNull(getCurrentMediaItem());
if (prepared && !rebuffering) {
rebuffering = true;
listener.onBufferingStarted(androidXMediaItem);
listener.onBufferingStarted(media2MediaItem);
}
}
private void maybeNotifyReadyEvents() {
androidx.media2.common.MediaItem androidXMediaItem =
androidx.media2.common.MediaItem media2MediaItem =
Assertions.checkNotNull(getCurrentMediaItem());
boolean prepareComplete = !prepared;
if (prepareComplete) {
prepared = true;
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED);
listener.onPrepared(androidXMediaItem, player.getBufferedPercentage());
listener.onPrepared(media2MediaItem, player.getBufferedPercentage());
}
if (rebuffering) {
rebuffering = false;
listener.onBufferingEnded(androidXMediaItem, player.getBufferedPercentage());
listener.onBufferingEnded(media2MediaItem, player.getBufferedPercentage());
}
}
@ -596,13 +583,13 @@ import java.util.List;
}
}
private void releaseMediaItem(androidx.media2.common.MediaItem androidXMediaItem) {
private void releaseMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
try {
if (androidXMediaItem instanceof CallbackMediaItem) {
((CallbackMediaItem) androidXMediaItem).getDataSourceCallback().close();
if (media2MediaItem instanceof CallbackMediaItem) {
((CallbackMediaItem) media2MediaItem).getDataSourceCallback().close();
}
} catch (IOException e) {
Log.w(TAG, "Error releasing media item " + androidXMediaItem, e);
Log.w(TAG, "Error releasing media item " + media2MediaItem, e);
}
}
@ -641,8 +628,8 @@ import java.util.List;
}
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
handlePlaybackSpeedChanged(playbackSpeed);
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
handlePlaybackParametersChanged(playbackParameters);
}
@Override

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

View File

@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
@ -31,7 +29,6 @@ import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.UriMediaItem;
import androidx.media2.session.MediaController;
import androidx.media2.session.MediaSession;
import androidx.media2.session.MediaSession.ControllerInfo;
@ -39,13 +36,11 @@ import androidx.media2.session.SessionCommand;
import androidx.media2.session.SessionCommandGroup;
import androidx.media2.session.SessionResult;
import com.google.android.exoplayer2.util.Assertions;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
/**
* Builds {@link MediaSession.SessionCallback} with various collaborators.
* Builds a {@link MediaSession.SessionCallback} with various collaborators.
*
* @see MediaSession.SessionCallback
*/
@ -351,10 +346,8 @@ public final class SessionCallbackBuilder {
}
}
/**
* Default implementation of {@link MediaItemProvider} that assumes the media id is a URI string.
*/
public static final class DefaultMediaItemProvider implements MediaItemProvider {
/** A {@link MediaItemProvider} that creates media items containing only a media ID. */
public static final class MediaIdMediaItemProvider implements MediaItemProvider {
@Override
@Nullable
public MediaItem onCreateMediaItem(
@ -362,17 +355,11 @@ public final class SessionCallbackBuilder {
if (TextUtils.isEmpty(mediaId)) {
return null;
}
try {
new URI(mediaId);
} catch (URISyntaxException e) {
// Ignore if mediaId isn't a URI.
return null;
}
MediaMetadata metadata =
new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, mediaId)
.build();
return new UriMediaItem.Builder(Uri.parse(mediaId)).setMetadata(metadata).build();
return new MediaItem.Builder().setMetadata(metadata).build();
}
}

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import androidx.annotation.FloatRange;
@ -23,6 +22,7 @@ import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Pair;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.CallbackMediaItem;
import androidx.media2.common.FileMediaItem;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
@ -41,25 +41,11 @@ import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* An implementation of {@link SessionPlayer} that wraps a given ExoPlayer {@link Player} instance.
*
* <h3>Ownership</h3>
*
* <p>{@code SessionPlayerConnector} takes ownership of the provided ExoPlayer {@link Player}
* instance between when it's constructed and when it's {@link #close() closed}. No other components
* should interact with the wrapped player (otherwise, unexpected event callbacks from the wrapped
* player may put the session player in an inconsistent state).
*
* <p>Call {@link SessionPlayer#close()} when the {@code SessionPlayerConnector} is no longer needed
* to regain ownership of the wrapped player. It is the caller's responsibility to release the
* wrapped player via {@link Player#release()}.
*
* <h3>Threading model</h3>
*
* <p>Internally this implementation posts operations to and receives callbacks on the thread
* associated with {@link Player#getApplicationLooper()}, so it is important not to block this
* thread. In particular, when awaiting the result of an asynchronous session player operation, apps
@ -95,16 +81,15 @@ public final class SessionPlayerConnector extends SessionPlayer {
// Should be only accessed on the executor, which is currently single-threaded.
@Nullable private MediaItem currentMediaItem;
@Nullable private List<MediaItem> currentPlaylist;
/**
* Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands.
* Creates an instance using {@link DefaultMediaItemConverter} to convert between ExoPlayer and
* media2 MediaItems and {@link DefaultControlDispatcher} to dispatch player commands.
*
* @param player The player to wrap.
* @param mediaItemConverter The {@link MediaItemConverter}.
*/
public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) {
this(player, mediaItemConverter, new DefaultControlDispatcher());
public SessionPlayerConnector(Player player) {
this(player, new DefaultMediaItemConverter(), new DefaultControlDispatcher());
}
/**
@ -124,19 +109,8 @@ public final class SessionPlayerConnector extends SessionPlayer {
taskHandler = new PlayerHandler(player.getApplicationLooper());
taskHandlerExecutor = taskHandler::postOrRun;
ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener();
PlayerWrapper playerWrapper =
new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
this.player = playerWrapper;
this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
@SuppressWarnings("assignment.type.incompatible")
@Initialized
SessionPlayerConnector initializedThis = this;
initializedThis.<Void>runPlayerCallableBlocking(
/* callable= */ () -> {
playerWrapper.reset();
return null;
});
}
@Override
@ -251,17 +225,27 @@ public final class SessionPlayerConnector extends SessionPlayer {
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getAudioAttributes);
}
/**
* {@inheritDoc}
*
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
*/
@Override
public ListenableFuture<PlayerResult> setMediaItem(MediaItem item) {
Assertions.checkNotNull(item);
Assertions.checkArgument(!(item instanceof FileMediaItem));
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
/**
* {@inheritDoc}
*
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
*/
@Override
public ListenableFuture<PlayerResult> setPlaylist(
final List<MediaItem> playlist, @Nullable MediaMetadata metadata) {
@ -271,6 +255,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
MediaItem item = playlist.get(i);
Assertions.checkNotNull(item);
Assertions.checkArgument(!(item instanceof FileMediaItem));
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
for (int j = 0; j < i; j++) {
Assertions.checkArgument(
item != playlist.get(j),
@ -281,20 +266,24 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST,
/* command= */ () -> player.setPlaylist(playlist, metadata));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
/**
* {@inheritDoc}
*
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
*/
@Override
public ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem item) {
Assertions.checkArgument(index >= 0);
Assertions.checkNotNull(item);
Assertions.checkArgument(!(item instanceof FileMediaItem));
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
/* command= */ () -> player.addPlaylistItem(index, item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
@ -305,20 +294,24 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
/* command= */ () -> player.removePlaylistItem(index));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
/**
* {@inheritDoc}
*
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
*/
@Override
public ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem item) {
Assertions.checkArgument(index >= 0);
Assertions.checkNotNull(item);
Assertions.checkArgument(!(item instanceof FileMediaItem));
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
/* command= */ () -> player.replacePlaylistItem(index, item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
@ -385,7 +378,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
@Override
@Nullable
public List<MediaItem> getPlaylist() {
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getCachedPlaylist);
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist);
}
@Override
@ -447,7 +440,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
}
reset();
this.<Void>runPlayerCallableBlockingInternal(
this.<Void>runPlayerCallableBlocking(
/* callable= */ () -> {
player.close();
return null;
@ -511,7 +504,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
state = PLAYER_STATE_IDLE;
mediaItemToBuffState.clear();
}
this.<Void>runPlayerCallableBlockingInternal(
this.<Void>runPlayerCallableBlocking(
/* callable= */ () -> {
player.reset();
return null;
@ -558,25 +551,18 @@ public final class SessionPlayerConnector extends SessionPlayer {
}
private void handlePlaylistChangedOnHandler() {
List<MediaItem> currentPlaylist = player.getCachedPlaylist();
boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist);
this.currentPlaylist = currentPlaylist;
List<MediaItem> currentPlaylist = player.getPlaylist();
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
MediaItem currentMediaItem = player.getCurrentMediaItem();
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
this.currentMediaItem = currentMediaItem;
if (!notifyCurrentMediaItem && !notifyCurrentPlaylist) {
return;
}
long currentPosition = getCurrentPosition();
notifySessionPlayerCallback(
callback -> {
if (notifyCurrentPlaylist) {
callback.onPlaylistChanged(
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
}
if (notifyCurrentMediaItem) {
Assertions.checkNotNull(
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
@ -610,13 +596,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
}
private <T> T runPlayerCallableBlocking(Callable<T> callable) {
synchronized (stateLock) {
Assertions.checkState(!closed);
}
return runPlayerCallableBlockingInternal(callable);
}
private <T> T runPlayerCallableBlockingInternal(Callable<T> callable) {
SettableFuture<T> future = SettableFuture.create();
boolean success =
taskHandler.postOrRun(

View File

@ -38,6 +38,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
@ -127,8 +128,8 @@ public final class MediaSessionConnector {
@PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;
/**
* The name of the {@link PlaybackStateCompat} float extra with the value of {@link
* Player#getPlaybackSpeed()}.
* The name of the {@link PlaybackStateCompat} float extra with the value of {@code
* Player.getPlaybackParameters().speed}.
*/
public static final String EXTRAS_SPEED = "EXO_SPEED";
@ -765,7 +766,7 @@ public final class MediaSessionConnector {
queueNavigator != null
? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
float playbackSpeed = player.getPlaybackSpeed();
float playbackSpeed = player.getPlaybackParameters().speed;
extras.putFloat(EXTRAS_SPEED, playbackSpeed);
float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f;
builder
@ -1134,7 +1135,7 @@ public final class MediaSessionConnector {
}
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
invalidateMediaSessionPlaybackState();
}

View File

@ -592,37 +592,7 @@ public final class Format implements Parcelable {
// Build.
public Format build() {
return new Format(
id,
label,
language,
selectionFlags,
roleFlags,
averageBitrate,
peakBitrate,
codecs,
metadata,
containerMimeType,
sampleMimeType,
maxInputSize,
initializationData,
drmInitData,
subsampleOffsetUs,
width,
height,
frameRate,
rotationDegrees,
pixelWidthHeightRatio,
projectionData,
stereoMode,
colorInfo,
channelCount,
sampleRate,
pcmEncoding,
encoderDelay,
encoderPadding,
accessibilityChannel,
exoMediaCryptoType);
return new Format(/* builder= */ this);
}
}
@ -1211,86 +1181,51 @@ public final class Format implements Parcelable {
return new Builder().setId(id).setSampleMimeType(sampleMimeType).build();
}
// Some fields are deprecated but they're still assigned below.
/* package */ Format(
@Nullable String id,
@Nullable String label,
@Nullable String language,
@C.SelectionFlags int selectionFlags,
@C.RoleFlags int roleFlags,
int averageBitrate,
int peakBitrate,
@Nullable String codecs,
@Nullable Metadata metadata,
private Format(Builder builder) {
id = builder.id;
label = builder.label;
language = Util.normalizeLanguageCode(builder.language);
selectionFlags = builder.selectionFlags;
roleFlags = builder.roleFlags;
averageBitrate = builder.averageBitrate;
peakBitrate = builder.peakBitrate;
bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate;
codecs = builder.codecs;
metadata = builder.metadata;
// Container specific.
@Nullable String containerMimeType,
containerMimeType = builder.containerMimeType;
// Sample specific.
@Nullable String sampleMimeType,
int maxInputSize,
@Nullable List<byte[]> initializationData,
@Nullable DrmInitData drmInitData,
long subsampleOffsetUs,
sampleMimeType = builder.sampleMimeType;
maxInputSize = builder.maxInputSize;
initializationData =
builder.initializationData == null ? Collections.emptyList() : builder.initializationData;
drmInitData = builder.drmInitData;
subsampleOffsetUs = builder.subsampleOffsetUs;
// Video specific.
int width,
int height,
float frameRate,
int rotationDegrees,
float pixelWidthHeightRatio,
@Nullable byte[] projectionData,
@C.StereoMode int stereoMode,
@Nullable ColorInfo colorInfo,
width = builder.width;
height = builder.height;
frameRate = builder.frameRate;
rotationDegrees = builder.rotationDegrees == NO_VALUE ? 0 : builder.rotationDegrees;
pixelWidthHeightRatio =
builder.pixelWidthHeightRatio == NO_VALUE ? 1 : builder.pixelWidthHeightRatio;
projectionData = builder.projectionData;
stereoMode = builder.stereoMode;
colorInfo = builder.colorInfo;
// Audio specific.
int channelCount,
int sampleRate,
@C.PcmEncoding int pcmEncoding,
int encoderDelay,
int encoderPadding,
channelCount = builder.channelCount;
sampleRate = builder.sampleRate;
pcmEncoding = builder.pcmEncoding;
encoderDelay = builder.encoderDelay == NO_VALUE ? 0 : builder.encoderDelay;
encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding;
// Text specific.
int accessibilityChannel,
accessibilityChannel = builder.accessibilityChannel;
// Provided by source.
@Nullable Class<? extends ExoMediaCrypto> exoMediaCryptoType) {
this.id = id;
this.label = label;
this.language = Util.normalizeLanguageCode(language);
this.selectionFlags = selectionFlags;
this.roleFlags = roleFlags;
this.averageBitrate = averageBitrate;
this.peakBitrate = peakBitrate;
this.bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate;
this.codecs = codecs;
this.metadata = metadata;
// Container specific.
this.containerMimeType = containerMimeType;
// Sample specific.
this.sampleMimeType = sampleMimeType;
this.maxInputSize = maxInputSize;
this.initializationData =
initializationData == null ? Collections.emptyList() : initializationData;
this.drmInitData = drmInitData;
this.subsampleOffsetUs = subsampleOffsetUs;
// Video specific.
this.width = width;
this.height = height;
this.frameRate = frameRate;
this.rotationDegrees = rotationDegrees == NO_VALUE ? 0 : rotationDegrees;
this.pixelWidthHeightRatio = pixelWidthHeightRatio == NO_VALUE ? 1 : pixelWidthHeightRatio;
this.projectionData = projectionData;
this.stereoMode = stereoMode;
this.colorInfo = colorInfo;
// Audio specific.
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.pcmEncoding = pcmEncoding;
this.encoderDelay = encoderDelay == NO_VALUE ? 0 : encoderDelay;
this.encoderPadding = encoderPadding == NO_VALUE ? 0 : encoderPadding;
// Text specific.
this.accessibilityChannel = accessibilityChannel;
// Provided by source.
if (exoMediaCryptoType == null && drmInitData != null) {
if (builder.exoMediaCryptoType == null && drmInitData != null) {
// Encrypted content must always have a non-null exoMediaCryptoType.
exoMediaCryptoType = UnsupportedMediaCrypto.class;
} else {
exoMediaCryptoType = builder.exoMediaCryptoType;
}
this.exoMediaCryptoType = exoMediaCryptoType;
}
// Some fields are deprecated but they're still assigned below.

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.util;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -219,11 +220,12 @@ public final class NalUnitUtil {
* Returns whether the NAL unit with the specified header contains supplemental enhancement
* information.
*
* @param mimeType The sample MIME type.
* @param mimeType The sample MIME type, or {@code null} if unknown.
* @param nalUnitHeaderFirstByte The first byte of nal_unit().
* @return Whether the NAL unit with the specified header is an SEI NAL unit.
* @return Whether the NAL unit with the specified header is an SEI NAL unit. False is returned if
* the {@code MimeType} is {@code null}.
*/
public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) {
public static boolean isNalUnitSei(@Nullable String mimeType, byte nalUnitHeaderFirstByte) {
return (MimeTypes.VIDEO_H264.equals(mimeType)
&& (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI)
|| (MimeTypes.VIDEO_H265.equals(mimeType)

View File

@ -405,6 +405,21 @@ public final class Util {
return concatenation;
}
/**
* Copies the contents of {@code list} into {@code array}.
*
* <p>{@code list.size()} must be the same as {@code array.length} to ensure the contents can be
* copied into {@code array} without leaving any nulls at the end.
*
* @param list The list to copy items from.
* @param array The array to copy items to.
*/
@SuppressWarnings("nullness:toArray.nullable.elements.not.newarray")
public static <T> void nullSafeListToArray(List<T> list, T[] array) {
Assertions.checkState(list.size() == array.length);
list.toArray(array);
}
/**
* Creates a {@link Handler} on the current {@link Looper} thread.
*
@ -2362,7 +2377,7 @@ public final class Util {
case TelephonyManager.NETWORK_TYPE_LTE:
return C.NETWORK_TYPE_4G;
case TelephonyManager.NETWORK_TYPE_NR:
return C.NETWORK_TYPE_5G;
return SDK_INT >= 29 ? C.NETWORK_TYPE_5G : C.NETWORK_TYPE_UNKNOWN;
case TelephonyManager.NETWORK_TYPE_IWLAN:
return C.NETWORK_TYPE_WIFI;
case TelephonyManager.NETWORK_TYPE_GSM:

View File

@ -90,37 +90,38 @@ public final class FormatTest {
C.COLOR_TRANSFER_SDR,
new byte[] {1, 2, 3, 4, 5, 6, 7});
return new Format(
"id",
"label",
"language",
C.SELECTION_FLAG_DEFAULT,
C.ROLE_FLAG_MAIN,
/* averageBitrate= */ 1024,
/* peakBitrate= */ 2048,
"codec",
metadata,
/* containerMimeType= */ MimeTypes.VIDEO_MP4,
/* sampleMimeType= */ MimeTypes.VIDEO_H264,
/* maxInputSize= */ 5000,
initializationData,
drmInitData,
Format.OFFSET_SAMPLE_RELATIVE,
/* width= */ 1920,
/* height= */ 1080,
/* frameRate= */ 24,
/* rotationDegrees= */ 90,
/* pixelWidthHeightRatio= */ 4,
projectionData,
C.STEREO_MODE_TOP_BOTTOM,
colorInfo,
/* channelCount= */ 6,
/* sampleRate= */ 44100,
C.ENCODING_PCM_24BIT,
/* encoderDelay= */ 1001,
/* encoderPadding= */ 1002,
/* accessibilityChannel= */ 2,
/* exoMediaCryptoType= */ ExoMediaCrypto.class);
return new Format.Builder()
.setId("id")
.setLabel("label")
.setLanguage("language")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setRoleFlags(C.ROLE_FLAG_MAIN)
.setAverageBitrate(1024)
.setPeakBitrate(2048)
.setCodecs("codec")
.setMetadata(metadata)
.setContainerMimeType(MimeTypes.VIDEO_MP4)
.setSampleMimeType(MimeTypes.VIDEO_H264)
.setMaxInputSize(5000)
.setInitializationData(initializationData)
.setDrmInitData(drmInitData)
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.setWidth(1920)
.setHeight(1080)
.setFrameRate(24)
.setRotationDegrees(90)
.setPixelWidthHeightRatio(4)
.setProjectionData(projectionData)
.setStereoMode(C.STEREO_MODE_TOP_BOTTOM)
.setColorInfo(colorInfo)
.setChannelCount(6)
.setSampleRate(44100)
.setPcmEncoding(C.ENCODING_PCM_24BIT)
.setEncoderDelay(1001)
.setEncoderPadding(1002)
.setAccessibilityChannel(2)
.setExoMediaCryptoType(ExoMediaCrypto.class)
.build();
}
/** Generates an array of random bytes with the specified length. */

View File

@ -27,20 +27,20 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
*/
/* package */ final class DefaultMediaClock implements MediaClock {
/** Listener interface to be notified of changes to the active playback speed. */
public interface PlaybackSpeedListener {
/** Listener interface to be notified of changes to the active playback parameters. */
public interface PlaybackParametersListener {
/**
* Called when the active playback speed changed. Will not be called for {@link
* #setPlaybackSpeed(float)}.
* Called when the active playback parameters changed. Will not be called for {@link
* #setPlaybackParameters(PlaybackParameters)}.
*
* @param newPlaybackSpeed The newly active playback speed.
* @param newPlaybackParameters The newly active playback parameters.
*/
void onPlaybackSpeedChanged(float newPlaybackSpeed);
void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);
}
private final StandaloneMediaClock standaloneClock;
private final PlaybackSpeedListener listener;
private final PlaybackParametersListener listener;
@Nullable private Renderer rendererClockSource;
@Nullable private MediaClock rendererClock;
@ -48,13 +48,13 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
private boolean standaloneClockIsStarted;
/**
* Creates a new instance with listener for playback speed changes and a {@link Clock} to use for
* the standalone clock implementation.
* Creates a new instance with a listener for playback parameters changes and a {@link Clock} to
* use for the standalone clock implementation.
*
* @param listener A {@link PlaybackSpeedListener} to listen for playback speed changes.
* @param listener A {@link PlaybackParametersListener} to listen for playback parameters changes.
* @param clock A {@link Clock}.
*/
public DefaultMediaClock(PlaybackSpeedListener listener, Clock clock) {
public DefaultMediaClock(PlaybackParametersListener listener, Clock clock) {
this.listener = listener;
this.standaloneClock = new StandaloneMediaClock(clock);
isUsingStandaloneClock = true;
@ -102,7 +102,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
}
this.rendererClock = rendererMediaClock;
this.rendererClockSource = renderer;
rendererClock.setPlaybackSpeed(standaloneClock.getPlaybackSpeed());
rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());
}
}
@ -140,19 +140,19 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
if (rendererClock != null) {
rendererClock.setPlaybackSpeed(playbackSpeed);
playbackSpeed = rendererClock.getPlaybackSpeed();
rendererClock.setPlaybackParameters(playbackParameters);
playbackParameters = rendererClock.getPlaybackParameters();
}
standaloneClock.setPlaybackSpeed(playbackSpeed);
standaloneClock.setPlaybackParameters(playbackParameters);
}
@Override
public float getPlaybackSpeed() {
public PlaybackParameters getPlaybackParameters() {
return rendererClock != null
? rendererClock.getPlaybackSpeed()
: standaloneClock.getPlaybackSpeed();
? rendererClock.getPlaybackParameters()
: standaloneClock.getPlaybackParameters();
}
private void syncClocks(boolean isReadingAhead) {
@ -180,10 +180,10 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
}
// Continuously sync stand-alone clock to renderer clock so that it can take over if needed.
standaloneClock.resetPosition(rendererClockPositionUs);
float playbackSpeed = rendererClock.getPlaybackSpeed();
if (playbackSpeed != standaloneClock.getPlaybackSpeed()) {
standaloneClock.setPlaybackSpeed(playbackSpeed);
listener.onPlaybackSpeedChanged(playbackSpeed);
PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();
if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) {
standaloneClock.setPlaybackParameters(playbackParameters);
listener.onPlaybackParametersChanged(playbackParameters);
}
}

View File

@ -619,32 +619,17 @@ import java.util.concurrent.TimeoutException;
/* seekProcessed= */ true);
}
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
setPlaybackSpeed(
playbackParameters != null ? playbackParameters.speed : Player.DEFAULT_PLAYBACK_SPEED);
if (playbackParameters == null) {
playbackParameters = PlaybackParameters.DEFAULT;
}
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public PlaybackParameters getPlaybackParameters() {
return new PlaybackParameters(playbackInfo.playbackSpeed);
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
checkState(playbackSpeed > 0);
if (playbackInfo.playbackSpeed == playbackSpeed) {
if (playbackInfo.playbackParameters.equals(playbackParameters)) {
return;
}
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackSpeed(playbackSpeed);
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters);
pendingOperationAcks++;
internalPlayer.setPlaybackSpeed(playbackSpeed);
internalPlayer.setPlaybackParameters(playbackParameters);
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ false,
@ -655,8 +640,8 @@ import java.util.concurrent.TimeoutException;
}
@Override
public float getPlaybackSpeed() {
return playbackInfo.playbackSpeed;
public PlaybackParameters getPlaybackParameters() {
return playbackInfo.playbackParameters;
}
@Override
@ -1366,7 +1351,7 @@ import java.util.concurrent.TimeoutException;
private final boolean playWhenReadyChanged;
private final boolean playbackSuppressionReasonChanged;
private final boolean isPlayingChanged;
private final boolean playbackSpeedChanged;
private final boolean playbackParametersChanged;
private final boolean offloadSchedulingEnabledChanged;
public PlaybackInfoUpdate(
@ -1405,7 +1390,8 @@ import java.util.concurrent.TimeoutException;
playbackSuppressionReasonChanged =
previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason;
isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo);
playbackSpeedChanged = previousPlaybackInfo.playbackSpeed != playbackInfo.playbackSpeed;
playbackParametersChanged =
!previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters);
offloadSchedulingEnabledChanged =
previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled;
}
@ -1473,13 +1459,11 @@ import java.util.concurrent.TimeoutException;
invokeAll(
listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo)));
}
if (playbackSpeedChanged) {
PlaybackParameters playbackParameters = new PlaybackParameters(playbackInfo.playbackSpeed);
if (playbackParametersChanged) {
invokeAll(
listenerSnapshot,
listener -> {
listener.onPlaybackSpeedChanged(playbackInfo.playbackSpeed);
listener.onPlaybackParametersChanged(playbackParameters);
listener.onPlaybackParametersChanged(playbackInfo.playbackParameters);
});
}
if (seekProcessed) {

View File

@ -27,7 +27,7 @@ import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParametersListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
@ -61,7 +61,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriod.Callback,
TrackSelector.InvalidationListener,
MediaSourceList.MediaSourceListInfoRefreshListener,
PlaybackSpeedListener,
PlaybackParametersListener,
PlayerMessage.Sender {
private static final String TAG = "ExoPlayerImplInternal";
@ -121,7 +121,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int MSG_SET_PLAY_WHEN_READY = 1;
private static final int MSG_DO_SOME_WORK = 2;
private static final int MSG_SEEK_TO = 3;
private static final int MSG_SET_PLAYBACK_SPEED = 4;
private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
private static final int MSG_SET_SEEK_PARAMETERS = 5;
private static final int MSG_STOP = 6;
private static final int MSG_RELEASE = 7;
@ -133,7 +133,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int MSG_SET_FOREGROUND_MODE = 13;
private static final int MSG_SEND_MESSAGE = 14;
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
private static final int MSG_PLAYBACK_SPEED_CHANGED_INTERNAL = 16;
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
private static final int MSG_SET_MEDIA_SOURCES = 17;
private static final int MSG_ADD_MEDIA_SOURCES = 18;
private static final int MSG_MOVE_MEDIA_SOURCES = 19;
@ -163,6 +163,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final BandwidthMeter bandwidthMeter;
private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
private final Looper playbackLooper;
private final Timeline.Window window;
private final Timeline.Period period;
private final long backBufferDurationUs;
@ -252,7 +253,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
// not normally change to this priority" is incorrect.
internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
playbackLooper = internalPlaybackThread.getLooper();
handler = clock.createHandler(playbackLooper, this);
}
public void experimentalSetReleaseTimeoutMs(long releaseTimeoutMs) {
@ -301,8 +303,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
.sendToTarget();
}
public void setPlaybackSpeed(float playbackSpeed) {
handler.obtainMessage(MSG_SET_PLAYBACK_SPEED, playbackSpeed).sendToTarget();
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
}
public void setSeekParameters(SeekParameters seekParameters) {
@ -403,7 +405,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
public Looper getPlaybackLooper() {
return internalPlaybackThread.getLooper();
return playbackLooper;
}
// Playlist.PlaylistInfoRefreshListener implementation.
@ -432,11 +434,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
}
// DefaultMediaClock.PlaybackSpeedListener implementation.
// DefaultMediaClock.PlaybackParametersListener implementation.
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
sendPlaybackSpeedChangedInternal(playbackSpeed, /* acknowledgeCommand= */ false);
public void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters) {
sendPlaybackParametersChangedInternal(newPlaybackParameters, /* acknowledgeCommand= */ false);
}
// Handler.Callback implementation.
@ -467,8 +469,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
case MSG_SEEK_TO:
seekToInternal((SeekPosition) msg.obj);
break;
case MSG_SET_PLAYBACK_SPEED:
setPlaybackSpeedInternal((Float) msg.obj);
case MSG_SET_PLAYBACK_PARAMETERS:
setPlaybackParametersInternal((PlaybackParameters) msg.obj);
break;
case MSG_SET_SEEK_PARAMETERS:
setSeekParametersInternal((SeekParameters) msg.obj);
@ -489,8 +491,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
case MSG_TRACK_SELECTION_INVALIDATED:
reselectTracksInternal();
break;
case MSG_PLAYBACK_SPEED_CHANGED_INTERNAL:
handlePlaybackSpeed((Float) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:
handlePlaybackParameters(
(PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
break;
case MSG_SEND_MESSAGE:
sendMessageInternal((PlayerMessage) msg.obj);
@ -733,12 +736,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow)
throws ExoPlaybackException {
this.pauseAtEndOfWindow = pauseAtEndOfWindow;
if (queue.getReadingPeriod() != queue.getPlayingPeriod()) {
seekToCurrentPosition(/* sendDiscontinuity= */ true);
}
resetPendingPauseAtEndOfPeriod();
if (pendingPauseAtEndOfPeriod && queue.getReadingPeriod() != queue.getPlayingPeriod()) {
// When pausing is required, we need to set the streams of the playing period final. If we
// already started reading the next period, we need to flush the renderers.
seekToCurrentPosition(/* sendDiscontinuity= */ true);
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
}
}
private void setOffloadSchedulingEnabledInternal(boolean offloadSchedulingEnabled) {
if (offloadSchedulingEnabled == this.offloadSchedulingEnabled) {
@ -1182,9 +1187,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
notifyTrackSelectionDiscontinuity();
}
private void setPlaybackSpeedInternal(float playbackSpeed) {
mediaClock.setPlaybackSpeed(playbackSpeed);
sendPlaybackSpeedChangedInternal(mediaClock.getPlaybackSpeed(), /* acknowledgeCommand= */ true);
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
mediaClock.setPlaybackParameters(playbackParameters);
sendPlaybackParametersChangedInternal(
mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);
}
private void setSeekParametersInternal(SeekParameters seekParameters) {
@ -1301,7 +1307,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
mediaPeriodId,
playbackInfo.playWhenReady,
playbackInfo.playbackSuppressionReason,
playbackInfo.playbackSpeed,
playbackInfo.playbackParameters,
startPositionUs,
/* totalBufferedDurationUs= */ 0,
startPositionUs,
@ -1361,7 +1367,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
if (message.getHandler().getLooper() == handler.getLooper()) {
if (message.getHandler().getLooper() == playbackLooper) {
deliverMessage(message);
if (playbackInfo.playbackState == Player.STATE_READY
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
@ -1506,7 +1512,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void reselectTracksInternal() throws ExoPlaybackException {
float playbackSpeed = mediaClock.getPlaybackSpeed();
float playbackSpeed = mediaClock.getPlaybackParameters().speed;
// Reselect tracks on each period in turn, until the selection changes.
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
@ -1624,7 +1630,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
return bufferedToEnd
|| loadControl.shouldStartPlayback(
getTotalBufferedDurationUs(), mediaClock.getPlaybackSpeed(), rebuffering);
getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering);
}
private boolean isTimelineReady() {
@ -1960,7 +1966,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
return;
}
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackSpeed(), playbackInfo.timeline);
loadingPeriodHolder.handlePrepared(
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
updateLoadControlTrackSelection(
loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());
if (loadingPeriodHolder == queue.getPlayingPeriod()) {
@ -1985,14 +1992,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
maybeContinueLoading();
}
private void handlePlaybackSpeed(float playbackSpeed, boolean acknowledgeCommand)
private void handlePlaybackParameters(
PlaybackParameters playbackParameters, boolean acknowledgeCommand)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeCommand ? 1 : 0);
playbackInfo = playbackInfo.copyWithPlaybackSpeed(playbackSpeed);
updateTrackSelectionPlaybackSpeed(playbackSpeed);
playbackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters);
updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
for (Renderer renderer : renderers) {
if (renderer != null) {
renderer.setOperatingRate(playbackSpeed);
renderer.setOperatingRate(playbackParameters.speed);
}
}
}
@ -2018,7 +2026,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
: loadingPeriodHolder.toPeriodTime(rendererPositionUs)
- loadingPeriodHolder.info.startPositionUs;
return loadControl.shouldContinueLoading(
playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackSpeed());
playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
}
private boolean isLoadingPossible() {
@ -2194,10 +2202,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
}
private void sendPlaybackSpeedChangedInternal(float playbackSpeed, boolean acknowledgeCommand) {
private void sendPlaybackParametersChangedInternal(
PlaybackParameters playbackParameters, boolean acknowledgeCommand) {
handler
.obtainMessage(
MSG_PLAYBACK_SPEED_CHANGED_INTERNAL, acknowledgeCommand ? 1 : 0, 0, playbackSpeed)
MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL,
acknowledgeCommand ? 1 : 0,
0,
playbackParameters)
.sendToTarget();
}

View File

@ -63,8 +63,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public final boolean playWhenReady;
/** Reason why playback is suppressed even though {@link #playWhenReady} is {@code true}. */
@PlaybackSuppressionReason public final int playbackSuppressionReason;
/** The playback speed. */
public final float playbackSpeed;
/** The playback parameters. */
public final PlaybackParameters playbackParameters;
/** Whether offload scheduling is enabled for the main player loop. */
public final boolean offloadSchedulingEnabled;
@ -105,7 +105,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
PLACEHOLDER_MEDIA_PERIOD_ID,
/* playWhenReady= */ false,
Player.PLAYBACK_SUPPRESSION_REASON_NONE,
Player.DEFAULT_PLAYBACK_SPEED,
PlaybackParameters.DEFAULT,
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
@ -119,10 +119,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
* @param periodId See {@link #periodId}.
* @param requestedContentPositionUs See {@link #requestedContentPositionUs}.
* @param playbackState See {@link #playbackState}.
* @param playbackError See {@link #playbackError}.
* @param isLoading See {@link #isLoading}.
* @param trackGroups See {@link #trackGroups}.
* @param trackSelectorResult See {@link #trackSelectorResult}.
* @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}.
* @param playWhenReady See {@link #playWhenReady}.
* @param playbackSuppressionReason See {@link #playbackSuppressionReason}.
* @param playbackParameters See {@link #playbackParameters}.
* @param bufferedPositionUs See {@link #bufferedPositionUs}.
* @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}.
* @param positionUs See {@link #positionUs}.
@ -140,7 +144,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
MediaPeriodId loadingMediaPeriodId,
boolean playWhenReady,
@PlaybackSuppressionReason int playbackSuppressionReason,
float playbackSpeed,
PlaybackParameters playbackParameters,
long bufferedPositionUs,
long totalBufferedDurationUs,
long positionUs,
@ -156,7 +160,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
this.loadingMediaPeriodId = loadingMediaPeriodId;
this.playWhenReady = playWhenReady;
this.playbackSuppressionReason = playbackSuppressionReason;
this.playbackSpeed = playbackSpeed;
this.playbackParameters = playbackParameters;
this.bufferedPositionUs = bufferedPositionUs;
this.totalBufferedDurationUs = totalBufferedDurationUs;
this.positionUs = positionUs;
@ -201,7 +205,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -228,7 +232,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -255,7 +259,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -282,7 +286,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -309,7 +313,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -336,7 +340,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -367,7 +371,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -375,13 +379,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
}
/**
* Copies playback info with new playback speed.
* Copies playback info with new playback parameters.
*
* @param playbackSpeed New playback speed. See {@link #playbackSpeed}.
* @return Copied playback info with new playback speed.
* @param playbackParameters New playback parameters. See {@link #playbackParameters}.
* @return Copied playback info with new playback parameters.
*/
@CheckResult
public PlaybackInfo copyWithPlaybackSpeed(float playbackSpeed) {
public PlaybackInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) {
return new PlaybackInfo(
timeline,
periodId,
@ -394,7 +398,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
@ -422,7 +426,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackSpeed,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,

View File

@ -17,13 +17,9 @@ package com.google.android.exoplayer2;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
* @deprecated Use {@link Player#setPlaybackSpeed(float)} and {@link
* Player.AudioComponent#setSkipSilenceEnabled(boolean)} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
/** Parameters that apply to playback, including speed setting. */
public final class PlaybackParameters {
/** The default playback parameters: real-time playback with no silence skipping. */
@ -32,16 +28,34 @@ public final class PlaybackParameters {
/** The factor by which playback will be sped up. */
public final float speed;
/** The factor by which pitch will be shifted. */
public final float pitch;
private final int scaledUsPerMs;
/**
* Creates new playback parameters that set the playback speed.
* Creates new playback parameters that set the playback speed. The pitch of audio will not be
* adjusted, so the effect is to time-stretch the audio.
*
* @param speed The factor by which playback will be sped up. Must be greater than zero.
*/
public PlaybackParameters(float speed) {
this(speed, /* pitch= */ 1f);
}
/**
* Creates new playback parameters that set the playback speed/pitch.
*
* @param speed The factor by which playback will be sped up. Must be greater than zero.
* @param pitch The factor by which the pitch of audio will be adjusted. Must be greater than
* zero. Useful values are {@code 1} (to time-stretch audio) and the same value as passed in
* as the {@code speed} (to resample audio, which is useful for slow-motion videos).
*/
public PlaybackParameters(float speed, float pitch) {
Assertions.checkArgument(speed > 0);
Assertions.checkArgument(pitch > 0);
this.speed = speed;
this.pitch = pitch;
scaledUsPerMs = Math.round(speed * 1000f);
}
@ -65,11 +79,19 @@ public final class PlaybackParameters {
return false;
}
PlaybackParameters other = (PlaybackParameters) obj;
return this.speed == other.speed;
return this.speed == other.speed && this.pitch == other.pitch;
}
@Override
public int hashCode() {
return Float.floatToRawIntBits(speed);
int result = 17;
result = 31 * result + Float.floatToRawIntBits(speed);
result = 31 * result + Float.floatToRawIntBits(pitch);
return result;
}
@Override
public String toString() {
return Util.formatInvariant("PlaybackParameters(speed=%.2f, pitch=%.2f)", speed, pitch);
}
}

View File

@ -583,21 +583,15 @@ public interface Player {
default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
/**
* @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link
* AudioListener#onSkipSilenceEnabledChanged(boolean)} instead.
* Called when the current playback parameters change. The playback parameters may change due to
* a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change
* them (for example, if audio playback switches to passthrough or offload mode, where speed
* adjustment is no longer possible).
*
* @param playbackParameters The playback parameters.
*/
@SuppressWarnings("deprecation")
@Deprecated
default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}
/**
* Called when the current playback speed changes. The normal playback speed is 1. The speed may
* change due to a call to {@link #setPlaybackSpeed(float)}, or the player itself may change it
* (for example, if audio playback switches to passthrough mode, where speed adjustment is no
* longer possible).
*/
default void onPlaybackSpeedChanged(float playbackSpeed) {}
/**
* @deprecated Seeks are processed without delay. Listen to {@link
* #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead.
@ -810,9 +804,6 @@ public interface Player {
*/
int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3;
/** The default playback speed. */
float DEFAULT_PLAYBACK_SPEED = 1.0f;
/** Returns the component of this player for audio output, or null if audio is not supported. */
@Nullable
AudioComponent getAudioComponent();
@ -1161,39 +1152,24 @@ public interface Player {
void next();
/**
* @deprecated Use {@link #setPlaybackSpeed(float)} or {@link
* AudioComponent#setSkipSilenceEnabled(boolean)} instead.
* Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
* default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
*
* <p>Playback parameters changes may cause the player to buffer. {@link
* EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the
* currently active playback parameters change.
*
* @param playbackParameters The playback parameters, or {@code null} to use the defaults.
*/
@SuppressWarnings("deprecation")
@Deprecated
void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
/**
* @deprecated Use {@link #getPlaybackSpeed()} or {@link AudioComponent#getSkipSilenceEnabled()}
* instead.
* Returns the currently active playback parameters.
*
* @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
*/
@SuppressWarnings("deprecation")
@Deprecated
PlaybackParameters getPlaybackParameters();
/**
* Attempts to set the playback speed.
*
* <p>Playback speed changes may cause the player to buffer. {@link
* EventListener#onPlaybackSpeedChanged(float)} will be called whenever the currently active
* playback speed change.
*
* @param playbackSpeed The playback speed.
*/
void setPlaybackSpeed(float playbackSpeed);
/**
* Returns the currently active playback speed.
*
* @see EventListener#onPlaybackSpeedChanged(float)
*/
float getPlaybackSpeed();
/**
* Stops playback without resetting the player. Use {@link #pause()} rather than this method if
* the intention is to pause playback.

View File

@ -1030,18 +1030,23 @@ public class SimpleExoPlayer extends BasePlayer
this.priorityTaskManager = priorityTaskManager;
}
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
/**
* Sets the {@link PlaybackParams} governing audio playback.
*
* @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
* @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.
*/
@Deprecated
@RequiresApi(23)
public void setPlaybackParams(@Nullable PlaybackParams params) {
float playbackSpeed;
PlaybackParameters playbackParameters;
if (params != null) {
params.allowDefaults();
playbackSpeed = params.getSpeed();
playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());
} else {
playbackSpeed = 1.0f;
playbackParameters = null;
}
setPlaybackSpeed(playbackSpeed);
setPlaybackParameters(playbackParameters);
}
/** Returns the video format currently being played, or null if no video is being played. */
@ -1623,39 +1628,18 @@ public class SimpleExoPlayer extends BasePlayer
player.seekTo(windowIndex, positionMs);
}
/**
* @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)}
* instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
verifyApplicationThread();
player.setPlaybackParameters(playbackParameters);
}
/** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public PlaybackParameters getPlaybackParameters() {
verifyApplicationThread();
return player.getPlaybackParameters();
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
verifyApplicationThread();
player.setPlaybackSpeed(playbackSpeed);
}
@Override
public float getPlaybackSpeed() {
verifyApplicationThread();
return player.getPlaybackSpeed();
}
@Override
public void setSeekParameters(@Nullable SeekParameters seekParameters) {
verifyApplicationThread();
@ -2231,6 +2215,13 @@ public class SimpleExoPlayer extends BasePlayer
}
}
@Override
public void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioPositionAdvancing(playoutStartSystemTimeMs);
}
}
@Override
public void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {

View File

@ -205,6 +205,14 @@ public class AnalyticsCollector
}
}
@Override
public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs);
}
}
@Override
public final void onAudioUnderrun(
int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
@ -544,12 +552,6 @@ public class AnalyticsCollector
}
}
/**
* @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link
* #onSkipSilenceEnabledChanged(boolean)} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
@Override
public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
@ -558,14 +560,6 @@ public class AnalyticsCollector
}
}
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlaybackSpeedChanged(eventTime, playbackSpeed);
}
}
@SuppressWarnings("deprecation")
@Override
public final void onSeekProcessed() {

View File

@ -279,21 +279,13 @@ public interface AnalyticsListener {
default void onSeekProcessed(EventTime eventTime) {}
/**
* @deprecated Use {@link #onPlaybackSpeedChanged(EventTime, float)} and {@link
* #onSkipSilenceEnabledChanged(EventTime, boolean)} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
default void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {}
/**
* Called when the playback speed changes.
* Called when the playback parameters changed.
*
* @param eventTime The event time.
* @param playbackSpeed The playback speed.
* @param playbackParameters The new playback parameters.
*/
default void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {}
default void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {}
/**
* Called when the repeat mode changed.
@ -479,6 +471,16 @@ public interface AnalyticsListener {
*/
default void onAudioInputFormatChanged(EventTime eventTime, Format format) {}
/**
* Called when the audio position has increased for the first time since the last pause or
* position reset.
*
* @param eventTime The event time.
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
* which playout started.
*/
default void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {}
/**
* Called when an audio underrun occurs.
*

View File

@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
@ -334,8 +335,9 @@ public final class PlaybackStatsListener
}
@Override
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
this.playbackSpeed = playbackSpeed;
public void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {
playbackSpeed = playbackParameters.speed;
maybeAddSession(eventTime);
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);

View File

@ -65,6 +65,15 @@ public interface AudioRendererEventListener {
*/
default void onAudioInputFormatChanged(Format format) {}
/**
* Called when the audio position has increased for the first time since the last pause or
* position reset.
*
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
* which playout started.
*/
default void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {}
/**
* Called when an audio underrun occurs.
*
@ -89,7 +98,7 @@ public interface AudioRendererEventListener {
*/
default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
/** Dispatches events to a {@link AudioRendererEventListener}. */
/** Dispatches events to an {@link AudioRendererEventListener}. */
final class EventDispatcher {
@Nullable private final Handler handler;
@ -106,20 +115,16 @@ public interface AudioRendererEventListener {
this.listener = listener;
}
/**
* Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.
*/
public void enabled(final DecoderCounters decoderCounters) {
/** Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */
public void enabled(DecoderCounters decoderCounters) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters));
}
}
/**
* Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.
*/
public void decoderInitialized(final String decoderName,
final long initializedTimestampMs, final long initializationDurationMs) {
/** Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}. */
public void decoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
if (handler != null) {
handler.post(
() ->
@ -129,18 +134,23 @@ public interface AudioRendererEventListener {
}
}
/**
* Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.
*/
public void inputFormatChanged(final Format format) {
/** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */
public void inputFormatChanged(Format format) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format));
}
}
/** Invokes {@link AudioRendererEventListener#onAudioPositionAdvancing(long)}. */
public void positionAdvancing(long playoutStartSystemTimeMs) {
if (handler != null) {
handler.post(
() -> castNonNull(listener).onAudioPositionAdvancing(playoutStartSystemTimeMs));
}
}
/** Invokes {@link AudioRendererEventListener#onAudioUnderrun(int, long, long)}. */
public void underrun(
final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) {
public void underrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
if (handler != null) {
handler.post(
() ->
@ -149,10 +159,8 @@ public interface AudioRendererEventListener {
}
}
/**
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
*/
public void disabled(final DecoderCounters counters) {
/** Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. */
public void disabled(DecoderCounters counters) {
counters.ensureUpdated();
if (handler != null) {
handler.post(
@ -163,17 +171,15 @@ public interface AudioRendererEventListener {
}
}
/**
* Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.
*/
public void audioSessionId(final int audioSessionId) {
/** Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */
public void audioSessionId(int audioSessionId) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId));
}
}
/** Invokes {@link AudioRendererEventListener#onSkipSilenceEnabledChanged(boolean)}. */
public void skipSilenceEnabledChanged(final boolean skipSilenceEnabled) {
public void skipSilenceEnabledChanged(boolean skipSilenceEnabled) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled));
}

View File

@ -20,6 +20,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -72,10 +73,19 @@ public interface AudioSink {
*/
void onPositionDiscontinuity();
/**
* Called when the audio sink's position has increased for the first time since it was last
* paused or flushed.
*
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
* which playout started. Only valid if the audio track has not underrun.
*/
default void onPositionAdvancing(long playoutStartSystemTimeMs) {}
/**
* Called when the audio sink runs out of data.
* <p>
* An audio sink implementation may never call this method (for example, if audio data is
*
* <p>An audio sink implementation may never call this method (for example, if audio data is
* consumed in batches rather than based on the sink's own clock).
*
* @param bufferSize The size of the sink's buffer, in bytes.
@ -297,16 +307,21 @@ public interface AudioSink {
*/
boolean hasPendingData();
/** Sets the playback speed. */
void setPlaybackSpeed(float playbackSpeed);
/**
* Attempts to set the playback parameters. The audio sink may override these parameters if they
* are not supported.
*
* @param playbackParameters The new playback parameters to attempt to set.
*/
void setPlaybackParameters(PlaybackParameters playbackParameters);
/** Gets the playback speed. */
float getPlaybackSpeed();
/** Returns the active {@link PlaybackParameters}. */
PlaybackParameters getPlaybackParameters();
/** Sets whether silences should be skipped in the audio stream. */
void setSkipSilenceEnabled(boolean skipSilenceEnabled);
/** Gets whether silences are skipped in the audio stream. */
/** Returns whether silences are skipped in the audio stream. */
boolean getSkipSilenceEnabled();
/**

View File

@ -48,6 +48,15 @@ import java.lang.reflect.Method;
/** Listener for position tracker events. */
public interface Listener {
/**
* Called when the position tracker's position has increased for the first time since it was
* last paused or reset.
*
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
* which playout started.
*/
void onPositionAdvancing(long playoutStartSystemTimeMs);
/**
* Called when the frame position is too far from the expected frame position.
*
@ -145,6 +154,7 @@ import java.lang.reflect.Method;
private boolean needsPassthroughWorkarounds;
private long bufferSizeUs;
private float audioTrackPlaybackSpeed;
private boolean notifiedPositionIncreasing;
private long smoothedPlayheadOffsetUs;
private long lastPlayheadSampleTimeUs;
@ -287,9 +297,21 @@ import java.lang.reflect.Method;
positionUs /= 1000;
}
if (!notifiedPositionIncreasing && positionUs > lastPositionUs) {
notifiedPositionIncreasing = true;
long mediaDurationSinceLastPositionUs = C.usToMs(positionUs - lastPositionUs);
long playoutDurationSinceLastPositionUs =
Util.getPlayoutDurationForMediaDuration(
mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed);
long playoutStartSystemTimeMs =
System.currentTimeMillis() - C.usToMs(playoutDurationSinceLastPositionUs);
listener.onPositionAdvancing(playoutStartSystemTimeMs);
}
lastSystemTimeUs = systemTimeUs;
lastPositionUs = positionUs;
lastSampleUsedGetTimestampMode = useGetTimestampMode;
return positionUs;
}
@ -512,6 +534,7 @@ import java.lang.reflect.Method;
lastPlayheadSampleTimeUs = 0;
lastSystemTimeUs = 0;
previousModeSystemTimeUs = 0;
notifiedPositionIncreasing = false;
}
/**

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
@ -498,13 +499,13 @@ public abstract class DecoderAudioRenderer<
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
audioSink.setPlaybackSpeed(playbackSpeed);
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
audioSink.setPlaybackParameters(playbackParameters);
}
@Override
public float getPlaybackSpeed() {
return audioSink.getPlaybackSpeed();
public PlaybackParameters getPlaybackParameters() {
return audioSink.getPlaybackParameters();
}
@Override
@ -708,6 +709,11 @@ public abstract class DecoderAudioRenderer<
DecoderAudioRenderer.this.onPositionDiscontinuity();
}
@Override
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);

View File

@ -32,6 +32,7 @@ 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.audio.AudioProcessor.UnhandledAudioFormatException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
@ -92,14 +93,14 @@ public final class DefaultAudioSink implements AudioSink {
AudioProcessor[] getAudioProcessors();
/**
* Configures audio processors to apply the specified playback speed immediately, returning the
* new playback speed, which may differ from the speed passed in. Only called when processors
* have no input pending.
* Configures audio processors to apply the specified playback parameters immediately, returning
* the new playback parameters, which may differ from those passed in. Only called when
* processors have no input pending.
*
* @param playbackSpeed The playback speed to try to apply.
* @return The playback speed that was actually applied.
* @param playbackParameters The playback parameters to try to apply.
* @return The playback parameters that were actually applied.
*/
float applyPlaybackSpeed(float playbackSpeed);
PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters);
/**
* Configures audio processors to apply whether to skip silences immediately, returning the new
@ -170,8 +171,10 @@ public final class DefaultAudioSink implements AudioSink {
}
@Override
public float applyPlaybackSpeed(float playbackSpeed) {
return sonicAudioProcessor.setSpeed(playbackSpeed);
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
float speed = sonicAudioProcessor.setSpeed(playbackParameters.speed);
float pitch = sonicAudioProcessor.setPitch(playbackParameters.pitch);
return new PlaybackParameters(speed, pitch);
}
@Override
@ -197,6 +200,10 @@ public final class DefaultAudioSink implements AudioSink {
public static final float MIN_PLAYBACK_SPEED = 0.1f;
/** The maximum allowed playback speed. Higher values will be constrained to fall in range. */
public static final float MAX_PLAYBACK_SPEED = 8f;
/** The minimum allowed pitch factor. Lower values will be constrained to fall in range. */
public static final float MIN_PITCH = 0.1f;
/** The maximum allowed pitch factor. Higher values will be constrained to fall in range. */
public static final float MAX_PITCH = 8f;
/** The default skip silence flag. */
private static final boolean DEFAULT_SKIP_SILENCE = false;
@ -296,7 +303,7 @@ public final class DefaultAudioSink implements AudioSink {
private AudioAttributes audioAttributes;
@Nullable private MediaPositionParameters afterDrainParameters;
private MediaPositionParameters mediaPositionParameters;
private float audioTrackPlaybackSpeed;
private PlaybackParameters audioTrackPlaybackParameters;
@Nullable private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync;
@ -418,11 +425,11 @@ public final class DefaultAudioSink implements AudioSink {
auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);
mediaPositionParameters =
new MediaPositionParameters(
DEFAULT_PLAYBACK_SPEED,
PlaybackParameters.DEFAULT,
DEFAULT_SKIP_SILENCE,
/* mediaTimeUs= */ 0,
/* audioTrackPositionUs= */ 0);
audioTrackPlaybackSpeed = 1f;
audioTrackPlaybackParameters = PlaybackParameters.DEFAULT;
drainingAudioProcessorIndex = C.INDEX_UNSET;
activeAudioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0];
@ -707,7 +714,7 @@ public final class DefaultAudioSink implements AudioSink {
}
}
// Re-apply playback parameters.
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
}
if (!isAudioTrackInitialized()) {
@ -720,9 +727,9 @@ public final class DefaultAudioSink implements AudioSink {
startMediaTimeUsNeedsInit = false;
if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
setAudioTrackPlaybackSpeedV23(audioTrackPlaybackSpeed);
setAudioTrackPlaybackParametersV23(audioTrackPlaybackParameters);
}
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
if (playing) {
play();
@ -758,7 +765,7 @@ public final class DefaultAudioSink implements AudioSink {
// Don't process any more input until draining completes.
return false;
}
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
afterDrainParameters = null;
}
@ -789,7 +796,7 @@ public final class DefaultAudioSink implements AudioSink {
startMediaTimeUs += adjustmentUs;
startMediaTimeUsNeedsSync = false;
// Re-apply playback parameters because the startMediaTimeUs changed.
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
if (listener != null && adjustmentUs != 0) {
listener.onPositionDiscontinuity();
}
@ -1011,26 +1018,30 @@ public final class DefaultAudioSink implements AudioSink {
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
playbackSpeed = Util.constrainValue(playbackSpeed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED);
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
playbackParameters =
new PlaybackParameters(
Util.constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED),
Util.constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH));
if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
setAudioTrackPlaybackSpeedV23(playbackSpeed);
setAudioTrackPlaybackParametersV23(playbackParameters);
} else {
setAudioProcessorPlaybackSpeedAndSkipSilence(playbackSpeed, getSkipSilenceEnabled());
setAudioProcessorPlaybackParametersAndSkipSilence(
playbackParameters, getSkipSilenceEnabled());
}
}
@Override
public float getPlaybackSpeed() {
// We use either audio processor speed adjustment or AudioTrack playback parameters, so one of
// the operands is always 1f.
return getAudioProcessorPlaybackSpeed() * audioTrackPlaybackSpeed;
public PlaybackParameters getPlaybackParameters() {
return enableAudioTrackPlaybackParams
? audioTrackPlaybackParameters
: getAudioProcessorPlaybackParameters();
}
@Override
public void setSkipSilenceEnabled(boolean skipSilenceEnabled) {
setAudioProcessorPlaybackSpeedAndSkipSilence(
getAudioProcessorPlaybackSpeed(), skipSilenceEnabled);
setAudioProcessorPlaybackParametersAndSkipSilence(
getAudioProcessorPlaybackParameters(), skipSilenceEnabled);
}
@Override
@ -1212,7 +1223,7 @@ public final class DefaultAudioSink implements AudioSink {
framesPerEncodedSample = 0;
mediaPositionParameters =
new MediaPositionParameters(
getAudioProcessorPlaybackSpeed(),
getAudioProcessorPlaybackParameters(),
getSkipSilenceEnabled(),
/* mediaTimeUs= */ 0,
/* audioTrackPositionUs= */ 0);
@ -1249,12 +1260,13 @@ public final class DefaultAudioSink implements AudioSink {
}
@RequiresApi(23)
private void setAudioTrackPlaybackSpeedV23(float audioTrackPlaybackSpeed) {
private void setAudioTrackPlaybackParametersV23(PlaybackParameters audioTrackPlaybackParameters) {
if (isAudioTrackInitialized()) {
PlaybackParams playbackParams =
new PlaybackParams()
.allowDefaults()
.setSpeed(audioTrackPlaybackSpeed)
.setSpeed(audioTrackPlaybackParameters.speed)
.setPitch(audioTrackPlaybackParameters.pitch)
.setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL);
try {
audioTrack.setPlaybackParams(playbackParams);
@ -1262,20 +1274,22 @@ public final class DefaultAudioSink implements AudioSink {
Log.w(TAG, "Failed to set playback params", e);
}
// Update the speed using the actual effective speed from the audio track.
audioTrackPlaybackSpeed = audioTrack.getPlaybackParams().getSpeed();
audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackSpeed);
audioTrackPlaybackParameters =
new PlaybackParameters(
audioTrack.getPlaybackParams().getSpeed(), audioTrack.getPlaybackParams().getPitch());
audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackParameters.speed);
}
this.audioTrackPlaybackSpeed = audioTrackPlaybackSpeed;
this.audioTrackPlaybackParameters = audioTrackPlaybackParameters;
}
private void setAudioProcessorPlaybackSpeedAndSkipSilence(
float playbackSpeed, boolean skipSilence) {
private void setAudioProcessorPlaybackParametersAndSkipSilence(
PlaybackParameters playbackParameters, boolean skipSilence) {
MediaPositionParameters currentMediaPositionParameters = getMediaPositionParameters();
if (playbackSpeed != currentMediaPositionParameters.playbackSpeed
if (!playbackParameters.equals(currentMediaPositionParameters.playbackParameters)
|| skipSilence != currentMediaPositionParameters.skipSilence) {
MediaPositionParameters mediaPositionParameters =
new MediaPositionParameters(
playbackSpeed,
playbackParameters,
skipSilence,
/* mediaTimeUs= */ C.TIME_UNSET,
/* audioTrackPositionUs= */ C.TIME_UNSET);
@ -1291,8 +1305,8 @@ public final class DefaultAudioSink implements AudioSink {
}
}
private float getAudioProcessorPlaybackSpeed() {
return getMediaPositionParameters().playbackSpeed;
private PlaybackParameters getAudioProcessorPlaybackParameters() {
return getMediaPositionParameters().playbackParameters;
}
private MediaPositionParameters getMediaPositionParameters() {
@ -1304,18 +1318,18 @@ public final class DefaultAudioSink implements AudioSink {
: mediaPositionParameters;
}
private void applyAudioProcessorPlaybackSpeedAndSkipSilence(long presentationTimeUs) {
float playbackSpeed =
private void applyAudioProcessorPlaybackParametersAndSkipSilence(long presentationTimeUs) {
PlaybackParameters playbackParameters =
configuration.canApplyPlaybackParameters
? audioProcessorChain.applyPlaybackSpeed(getAudioProcessorPlaybackSpeed())
: DEFAULT_PLAYBACK_SPEED;
? audioProcessorChain.applyPlaybackParameters(getAudioProcessorPlaybackParameters())
: PlaybackParameters.DEFAULT;
boolean skipSilenceEnabled =
configuration.canApplyPlaybackParameters
? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled())
: DEFAULT_SKIP_SILENCE;
mediaPositionParametersCheckpoints.add(
new MediaPositionParameters(
playbackSpeed,
playbackParameters,
skipSilenceEnabled,
/* mediaTimeUs= */ max(0, presentationTimeUs),
/* audioTrackPositionUs= */ configuration.framesToDurationUs(getWrittenFrames())));
@ -1340,7 +1354,7 @@ public final class DefaultAudioSink implements AudioSink {
long playoutDurationSinceLastCheckpoint =
positionUs - mediaPositionParameters.audioTrackPositionUs;
if (mediaPositionParameters.playbackSpeed != 1f) {
if (!mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
if (mediaPositionParametersCheckpoints.isEmpty()) {
playoutDurationSinceLastCheckpoint =
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint);
@ -1348,7 +1362,8 @@ public final class DefaultAudioSink implements AudioSink {
// Playing data at a previous playback speed, so fall back to multiplying by the speed.
playoutDurationSinceLastCheckpoint =
Util.getMediaDurationForPlayoutDuration(
playoutDurationSinceLastCheckpoint, mediaPositionParameters.playbackSpeed);
playoutDurationSinceLastCheckpoint,
mediaPositionParameters.playbackParameters.speed);
}
}
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint;
@ -1692,8 +1707,8 @@ public final class DefaultAudioSink implements AudioSink {
/** Stores parameters used to calculate the current media position. */
private static final class MediaPositionParameters {
/** The playback speed. */
public final float playbackSpeed;
/** The playback parameters. */
public final PlaybackParameters playbackParameters;
/** Whether to skip silences. */
public final boolean skipSilence;
/** The media time from which the playback parameters apply, in microseconds. */
@ -1702,8 +1717,11 @@ public final class DefaultAudioSink implements AudioSink {
public final long audioTrackPositionUs;
private MediaPositionParameters(
float playbackSpeed, boolean skipSilence, long mediaTimeUs, long audioTrackPositionUs) {
this.playbackSpeed = playbackSpeed;
PlaybackParameters playbackParameters,
boolean skipSilence,
long mediaTimeUs,
long audioTrackPositionUs) {
this.playbackParameters = playbackParameters;
this.skipSilence = skipSilence;
this.mediaTimeUs = mediaTimeUs;
this.audioTrackPositionUs = audioTrackPositionUs;
@ -1776,6 +1794,13 @@ public final class DefaultAudioSink implements AudioSink {
Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
}
@Override
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
if (listener != null) {
listener.onPositionAdvancing(playoutStartSystemTimeMs);
}
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs) {
if (listener != null) {

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import java.nio.ByteBuffer;
/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */
@ -88,13 +89,13 @@ public class ForwardingAudioSink implements AudioSink {
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
sink.setPlaybackSpeed(playbackSpeed);
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
sink.setPlaybackParameters(playbackParameters);
}
@Override
public float getPlaybackSpeed() {
return sink.getPlaybackSpeed();
public PlaybackParameters getPlaybackParameters() {
return sink.getPlaybackParameters();
}
@Override

View File

@ -33,6 +33,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
@ -545,13 +546,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
audioSink.setPlaybackSpeed(playbackSpeed);
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
audioSink.setPlaybackParameters(playbackParameters);
}
@Override
public float getPlaybackSpeed() {
return audioSink.getPlaybackSpeed();
public PlaybackParameters getPlaybackParameters() {
return audioSink.getPlaybackParameters();
}
@Override
@ -828,6 +829,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
MediaCodecAudioRenderer.this.onPositionDiscontinuity();
}
@Override
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);

View File

@ -37,6 +37,7 @@ import java.util.Arrays;
private final int inputSampleRateHz;
private final int channelCount;
private final float speed;
private final float pitch;
private final float rate;
private final int minPeriod;
private final int maxPeriod;
@ -63,12 +64,15 @@ import java.util.Arrays;
* @param inputSampleRateHz The sample rate of input audio, in hertz.
* @param channelCount The number of channels in the input audio.
* @param speed The speedup factor for output audio.
* @param pitch The pitch factor for output audio.
* @param outputSampleRateHz The sample rate for output audio, in hertz.
*/
public Sonic(int inputSampleRateHz, int channelCount, float speed, int outputSampleRateHz) {
public Sonic(
int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) {
this.inputSampleRateHz = inputSampleRateHz;
this.channelCount = channelCount;
this.speed = speed;
this.pitch = pitch;
rate = (float) inputSampleRateHz / outputSampleRateHz;
minPeriod = inputSampleRateHz / MAXIMUM_PITCH;
maxPeriod = inputSampleRateHz / MINIMUM_PITCH;
@ -118,8 +122,10 @@ import java.util.Arrays;
*/
public void queueEndOfStream() {
int remainingFrameCount = inputFrameCount;
float s = speed / pitch;
float r = rate * pitch;
int expectedOutputFrames =
outputFrameCount + (int) ((remainingFrameCount / speed + pitchFrameCount) / rate + 0.5f);
outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f);
// Add enough silence to flush both input and pitch buffers.
inputBuffer =
@ -464,14 +470,16 @@ import java.util.Arrays;
private void processStreamInput() {
// Resample as many pitch periods as we have buffered on the input.
int originalOutputFrameCount = outputFrameCount;
if (speed > 1.00001 || speed < 0.99999) {
changeSpeed(speed);
float s = speed / pitch;
float r = rate * pitch;
if (s > 1.00001 || s < 0.99999) {
changeSpeed(s);
} else {
copyToOutput(inputBuffer, 0, inputFrameCount);
inputFrameCount = 0;
}
if (rate != 1.0f) {
adjustRate(rate, originalOutputFrameCount);
if (r != 1.0f) {
adjustRate(r, originalOutputFrameCount);
}
}

View File

@ -43,6 +43,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
private int pendingOutputSampleRate;
private float speed;
private float pitch;
private AudioFormat pendingInputAudioFormat;
private AudioFormat pendingOutputAudioFormat;
@ -61,6 +62,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
/** Creates a new Sonic audio processor. */
public SonicAudioProcessor() {
speed = 1f;
pitch = 1f;
pendingInputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputAudioFormat = AudioFormat.NOT_SET;
@ -87,6 +89,22 @@ public final class SonicAudioProcessor implements AudioProcessor {
return speed;
}
/**
* Sets the playback pitch. This method may only be called after draining data through the
* processor. The value returned by {@link #isActive()} may change, and the processor must be
* {@link #flush() flushed} before queueing more data.
*
* @param pitch The requested new pitch.
* @return The actual new pitch.
*/
public float setPitch(float pitch) {
if (this.pitch != pitch) {
this.pitch = pitch;
pendingSonicRecreation = true;
}
return pitch;
}
/**
* Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output
* audio at the same sample rate as the input. After calling this method, call {@link
@ -140,6 +158,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
public boolean isActive() {
return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE
&& (Math.abs(speed - 1f) >= CLOSE_THRESHOLD
|| Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
|| pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);
}
@ -200,6 +219,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
inputAudioFormat.sampleRate,
inputAudioFormat.channelCount,
speed,
pitch,
outputAudioFormat.sampleRate);
} else if (sonic != null) {
sonic.flush();
@ -214,6 +234,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public void reset() {
speed = 1f;
pitch = 1f;
pendingInputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputAudioFormat = AudioFormat.NOT_SET;

View File

@ -409,7 +409,7 @@ 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)}
* or {@link #acquirePlaceholderSession} is called.
* is called.
*
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
* required.
@ -469,34 +469,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Override
@Nullable
public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) {
initPlaybackLooper(playbackLooper);
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
boolean avoidPlaceholderDrmSessions =
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
// Avoid attaching a session to sparse formats.
if (avoidPlaceholderDrmSessions
|| Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
|| exoMediaDrm.getExoMediaCryptoType() == null) {
return null;
}
maybeCreateMediaDrmHandler(playbackLooper);
if (placeholderDrmSession == null) {
DefaultDrmSession placeholderDrmSession =
createAndAcquireSessionWithRetry(
/* schemeDatas= */ ImmutableList.of(),
/* isPlaceholderSession= */ true,
/* eventDispatcher= */ null);
sessions.add(placeholderDrmSession);
this.placeholderDrmSession = placeholderDrmSession;
} else {
placeholderDrmSession.acquire(/* eventDispatcher= */ null);
}
return placeholderDrmSession;
}
@Override
public DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
@ -504,6 +476,11 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
initPlaybackLooper(playbackLooper);
maybeCreateMediaDrmHandler(playbackLooper);
if (format.drmInitData == null) {
// Content is not encrypted.
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType));
}
@Nullable List<SchemeData> schemeDatas = null;
if (offlineLicenseKeySetId == null) {
schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false);
@ -565,6 +542,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Internal methods.
@Nullable
private DrmSession maybeAcquirePlaceholderSession(int trackType) {
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
boolean avoidPlaceholderDrmSessions =
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
// Avoid attaching a session to sparse formats.
if (avoidPlaceholderDrmSessions
|| Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
|| UnsupportedMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())) {
return null;
}
if (placeholderDrmSession == null) {
DefaultDrmSession placeholderDrmSession =
createAndAcquireSessionWithRetry(
/* schemeDatas= */ ImmutableList.of(),
/* isPlaceholderSession= */ true,
/* eventDispatcher= */ null);
sessions.add(placeholderDrmSession);
this.placeholderDrmSession = placeholderDrmSession;
} else {
placeholderDrmSession.acquire(/* eventDispatcher= */ null);
}
return placeholderDrmSession;
}
private boolean canAcquireSession(DrmInitData drmInitData) {
if (offlineLicenseKeySetId != null) {
// An offline license can be restored so a session can always be acquired.
@ -585,12 +588,16 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
// If there is no scheme information, assume patternless AES-CTR.
return true;
} else if (C.CENC_TYPE_cbc1.equals(schemeType)
|| C.CENC_TYPE_cbcs.equals(schemeType)
|| C.CENC_TYPE_cens.equals(schemeType)) {
// API support for AES-CBC and pattern encryption was added in API 24. However, the
} else if (C.CENC_TYPE_cbcs.equals(schemeType)) {
// Support for cbcs (AES-CBC with pattern encryption) was added in API 24. However, the
// implementation was not stable until API 25.
return Util.SDK_INT >= 25;
} else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType)) {
// Support for cbc1 (AES-CTR with pattern encryption) and cens (AES-CBC without pattern
// encryption) was also added in API 24 and made stable from API 25, however support was
// removed from API 30. Since the range of API levels for which these modes are usable is too
// small to be useful, we don't indicate support on any API level.
return false;
}
// Unknown schemes, assume one of them is supported.
return true;

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
/** Manages a DRM session. */
@ -33,13 +32,19 @@ public interface DrmSessionManager {
new DrmSessionManager() {
@Override
@Nullable
public DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
if (format.drmInitData == null) {
return null;
} else {
return new ErrorStateDrmSession(
new DrmSession.DrmSessionException(
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));
new UnsupportedDrmException(
UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));
}
}
@Override
@ -64,39 +69,27 @@ public interface DrmSessionManager {
// Do nothing.
}
/**
* Returns a {@link DrmSession} that does not execute key requests, with an incremented reference
* count. When the caller no longer needs to use the instance, it must call {@link
* DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count.
*
* <p>Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for
* playback of clear content periods. This can reduce the cost of transitioning between clear and
* encrypted content periods.
*
* @param playbackLooper The looper associated with the media playback thread.
* @param trackType The type of the track to acquire a placeholder session for. Must be one of the
* {@link C}{@code .TRACK_TYPE_*} constants.
* @return The placeholder DRM session, or null if this DRM session manager does not support
* placeholder sessions.
*/
@Nullable
default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) {
return null;
}
/**
* Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference
* count. When the caller no longer needs to use the instance, it must call {@link
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is
* not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs
* to use a returned {@link DrmSession}, it must call {@link
* DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count.
*
* <p>If the provided {@link Format} contains a null {@link Format#drmInitData}, the returned
* {@link DrmSession} (if not null) will be a placeholder session which does not execute key
* requests, and cannot be used to handle encrypted content. However, a placeholder session may be
* 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)}.
* @param format The {@link Format} for which to acquire a {@link DrmSession}. Must contain a
* non-null {@link Format#drmInitData}.
* @return The DRM session.
* @param format The {@link Format} for which to acquire a {@link DrmSession}.
* @return The DRM session. May be null if the given {@link Format#drmInitData} is null.
*/
@Nullable
DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
@ -105,16 +98,16 @@ public interface DrmSessionManager {
/**
* Returns the {@link ExoMediaCrypto} type associated to sessions acquired for the given {@link
* Format}. Returns the {@link UnsupportedMediaCrypto} type if this DRM session manager does not
* support any of the DRM schemes defined in the given {@link Format}. If the {@link Format}
* describes unencrypted content, returns an {@link ExoMediaCrypto} type if this DRM session
* manager would associate a {@link #acquirePlaceholderSession placeholder session} to the given
* {@link Format}, or null otherwise.
* support any of the DRM schemes defined in the given {@link Format}. Returns null if {@link
* Format#drmInitData} is null and {@link #acquireSession} would return null for the given {@link
* Format}.
*
* @param format The {@link Format} for which to return the {@link ExoMediaCrypto} type.
* @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given
* parameters, or the {@link UnsupportedMediaCrypto} type if the provided {@code drmInitData}
* is not supported, or {@code null} if {@code drmInitData} is null and no DRM session will be
* associated to the given {@code trackType}.
* @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given {@link
* Format}, or {@link UnsupportedMediaCrypto} if this DRM session manager does not support any
* of the DRM schemes defined in the given {@link Format}. May be null if {@link
* Format#drmInitData} is null and {@link #acquireSession} would return null for the given
* {@link Format}.
*/
@Nullable
Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format);

View File

@ -184,7 +184,8 @@ public final class OfflineLicenseHelper {
/**
* Downloads an offline license.
*
* @param format The {@link Format} of the content whose license is to be downloaded.
* @param format The {@link Format} of the content whose license is to be downloaded. Must contain
* a non-null {@link Format#drmInitData}.
* @return The key set id for the downloaded license.
* @throws DrmSessionException Thrown when a DRM session error occurs.
*/
@ -278,13 +279,14 @@ public final class OfflineLicenseHelper {
private DrmSession openBlockingKeyRequest(
@Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) {
Assertions.checkNotNull(format.drmInitData);
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
conditionVariable.close();
DrmSession drmSession =
drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, format);
// Block current thread until key loading is finished
conditionVariable.block();
return drmSession;
return Assertions.checkNotNull(drmSession);
}
}

View File

@ -15,28 +15,19 @@
*/
package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaItem.DrmConfiguration;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.MediaDrmCallback;
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.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
@ -44,10 +35,8 @@ 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.common.primitives.Ints;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* The default {@link MediaSourceFactory} implementation.
@ -79,21 +68,6 @@ import java.util.Map;
* the stream.
* </ul>
*
* <h3>DrmSessionManager creation for protected content</h3>
*
* <p>For a media item with a {@link DrmConfiguration}, a {@link DefaultDrmSessionManager} is
* created based on that configuration. The following setter can be used to optionally configure the
* creation:
*
* <ul>
* <li>{@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory
* to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link
* DefaultHttpDataSourceFactory}).
* </ul>
*
* <p>For media items without a {@link DrmConfiguration}, the {@link DrmSessionManager} passed to
* {@link #setDrmSessionManager(DrmSessionManager)} will be used.
*
* <h3>Ad support for media items with ad tag uri</h3>
*
* <p>For a media item with an ad tag uri, an {@link AdSupportProvider} needs to be passed to {@link
@ -167,21 +141,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
private static final String TAG = "DefaultMediaSourceFactory";
private static final String DEFAULT_USER_AGENT =
ExoPlayerLibraryInfo.VERSION_SLASHY
+ " (Linux;Android "
+ Build.VERSION.RELEASE
+ ") "
+ ExoPlayerLibraryInfo.VERSION_SLASHY;
private final MediaSourceDrmHelper mediaSourceDrmHelper;
private final DataSource.Factory dataSourceFactory;
@Nullable private final AdSupportProvider adSupportProvider;
private final SparseArray<MediaSourceFactory> mediaSourceFactories;
@C.ContentType private final int[] supportedTypes;
private DrmSessionManager drmSessionManager;
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
private String userAgent;
@Nullable private DrmSessionManager drmSessionManager;
@Nullable private List<StreamKey> streamKeys;
/**
@ -196,8 +163,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
DataSource.Factory dataSourceFactory, @Nullable AdSupportProvider adSupportProvider) {
this.dataSourceFactory = dataSourceFactory;
this.adSupportProvider = adSupportProvider;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
userAgent = DEFAULT_USER_AGENT;
mediaSourceDrmHelper = new MediaSourceDrmHelper();
mediaSourceFactories = loadDelegates(dataSourceFactory);
supportedTypes = new int[mediaSourceFactories.size()];
for (int i = 0; i < mediaSourceFactories.size(); i++) {
@ -205,49 +171,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
}
/**
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
* is passed the {@link DefaultHttpDataSourceFactory} is used.
*
* @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link
* DefaultHttpDataSourceFactory}.
* @return This factory, for convenience.
*/
@Override
public DefaultMediaSourceFactory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
this.drmHttpDataSourceFactory = drmHttpDataSourceFactory;
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
return this;
}
/**
* Sets the optional user agent to be used for DRM requests.
*
* <p>In case a factory has been set by {@link
* #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored.
*
* @param userAgent The user agent to be used for DRM requests.
* @return This factory, for convenience.
*/
@Override
public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent != null ? userAgent : DEFAULT_USER_AGENT;
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
return this;
}
/**
* Sets the {@link DrmSessionManager} to use for media items that do not specify a {@link
* DrmConfiguration}. The default value is {@link DrmSessionManager#DUMMY}.
*
* @param drmSessionManager The {@link DrmSessionManager}.
* @return This factory, for convenience.
*/
@Override
public DefaultMediaSourceFactory setDrmSessionManager(
@Nullable DrmSessionManager drmSessionManager) {
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
this.drmSessionManager = drmSessionManager;
return this;
}
@ -292,7 +232,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type);
Assertions.checkNotNull(
mediaSourceFactory, "No suitable media source factory found for content type: " + type);
mediaSourceFactory.setDrmSessionManager(createDrmSessionManager(mediaItem));
mediaSourceFactory.setDrmSessionManager(
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem));
mediaSourceFactory.setStreamKeys(
!mediaItem.playbackProperties.streamKeys.isEmpty()
? mediaItem.playbackProperties.streamKeys
@ -318,46 +259,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
// internal methods
private DrmSessionManager createDrmSessionManager(MediaItem mediaItem) {
Assertions.checkNotNull(mediaItem.playbackProperties);
if (mediaItem.playbackProperties.drmConfiguration == null
|| mediaItem.playbackProperties.drmConfiguration.licenseUri == null
|| Util.SDK_INT < 18) {
return drmSessionManager;
}
DefaultDrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(
mediaItem.playbackProperties.drmConfiguration.uuid,
FrameworkMediaDrm.DEFAULT_PROVIDER)
.setMultiSession(mediaItem.playbackProperties.drmConfiguration.multiSession)
.setPlayClearSamplesWithoutKeys(
mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey)
.setUseDrmSessionsForClearContent(
Ints.toArray(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes))
.build(createHttpMediaDrmCallback(mediaItem.playbackProperties.drmConfiguration));
drmSessionManager.setMode(
MODE_PLAYBACK, mediaItem.playbackProperties.drmConfiguration.getKeySetId());
return drmSessionManager;
}
private MediaDrmCallback createHttpMediaDrmCallback(MediaItem.DrmConfiguration drmConfiguration) {
Assertions.checkNotNull(drmConfiguration.licenseUri);
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
drmHttpDataSourceFactory != null
? drmHttpDataSourceFactory
: new DefaultHttpDataSourceFactory(userAgent));
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
drmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue());
}
return drmCallback;
}
private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
if (mediaItem.clippingProperties.startPositionMs == 0
&& mediaItem.clippingProperties.endPositionMs == C.TIME_END_OF_SOURCE

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@ -168,6 +169,23 @@ public final class ExtractorMediaSource extends CompositeMediaSource<Void> {
throw new UnsupportedOperationException();
}
/**
* @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmHttpDataSourceFactory} instead.
*/
@Deprecated
@Override
public MediaSourceFactory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
throw new UnsupportedOperationException();
}
/** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmUserAgent} instead. */
@Deprecated
@Override
public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
throw new UnsupportedOperationException();
}
/** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */
@SuppressWarnings("deprecation")
@Deprecated

View File

@ -0,0 +1,105 @@
/*
* 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.source;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Build;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.primitives.Ints;
import java.util.Map;
/** A helper to create a {@link DrmSessionManager} from a {@link MediaItem}. */
public final class MediaSourceDrmHelper {
private static final String DEFAULT_USER_AGENT =
ExoPlayerLibraryInfo.VERSION_SLASHY
+ " (Linux;Android "
+ Build.VERSION.RELEASE
+ ") "
+ ExoPlayerLibraryInfo.VERSION_SLASHY;
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
@Nullable private String userAgent;
/**
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
* is passed the {@link DefaultHttpDataSourceFactory} is used.
*
* @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link
* DefaultHttpDataSourceFactory}.
*/
public void setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
this.drmHttpDataSourceFactory = drmHttpDataSourceFactory;
}
/**
* Sets the optional user agent to be used for DRM requests.
*
* <p>In case a factory has been set by {@link
* #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored.
*
* @param userAgent The user agent to be used for DRM requests.
*/
public void setDrmUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
}
/** Creates a {@link DrmSessionManager} for the given media item. */
public DrmSessionManager create(MediaItem mediaItem) {
Assertions.checkNotNull(mediaItem.playbackProperties);
@Nullable
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
if (drmConfiguration == null || drmConfiguration.licenseUri == null || Util.SDK_INT < 18) {
return DrmSessionManager.getDummyDrmSessionManager();
}
HttpDataSource.Factory dataSourceFactory =
drmHttpDataSourceFactory != null
? drmHttpDataSourceFactory
: new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT);
HttpMediaDrmCallback httpDrmCallback =
new HttpMediaDrmCallback(
castNonNull(drmConfiguration.licenseUri).toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory);
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
httpDrmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue());
}
DefaultDrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(
drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.setMultiSession(drmConfiguration.multiSession)
.setPlayClearSamplesWithoutKeys(drmConfiguration.playClearContentWithoutKey)
.setUseDrmSessionsForClearContent(Ints.toArray(drmConfiguration.sessionForClearTypes))
.build(httpDrmCallback);
drmSessionManager.setMode(MODE_PLAYBACK, drmConfiguration.getKeySetId());
return drmSessionManager;
}
}

View File

@ -19,13 +19,34 @@ import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import java.util.List;
/** Factory for creating {@link MediaSource}s from URIs. */
/**
* Factory for creating {@link MediaSource}s from URIs.
*
* <h3>DrmSessionManager creation for protected content</h3>
*
* <p>In case a {@link DrmSessionManager} is passed to {@link
* #setDrmSessionManager(DrmSessionManager)}, it will be used regardless of the drm configuration of
* the media item.
*
* <p>For a media item with a {@link MediaItem.DrmConfiguration}, a {@link DefaultDrmSessionManager}
* is created based on that configuration. The following setter can be used to optionally configure
* the creation:
*
* <ul>
* <li>{@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory
* to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link
* DefaultHttpDataSourceFactory}).
* </ul>
*/
public interface MediaSourceFactory {
/** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */
@ -35,13 +56,40 @@ public interface MediaSourceFactory {
}
/**
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}.
* Sets the {@link DrmSessionManager} to use for all media items regardless of their {@link
* MediaItem.DrmConfiguration}.
*
* @param drmSessionManager The {@link DrmSessionManager}.
* @return This factory, for convenience.
*/
MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager);
/**
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
* HttpMediaDrmCallbacks} to execute key and provisioning requests over HTTP.
*
* <p>In case a {@link DrmSessionManager} has been set by {@link
* #setDrmSessionManager(DrmSessionManager)}, this data source factory is ignored.
*
* @param drmHttpDataSourceFactory The HTTP data source factory, or {@code null} to use {@link
* DefaultHttpDataSourceFactory}.
* @return This factory, for convenience.
*/
MediaSourceFactory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory);
/**
* Sets the optional user agent to be used for DRM requests.
*
* <p>In case a factory has been set by {@link
* #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} or a {@link DrmSessionManager} has been
* set by {@link #setDrmSessionManager(DrmSessionManager)}, this user agent is ignored.
*
* @param userAgent The user agent to be used for DRM requests.
* @return This factory, for convenience.
*/
MediaSourceFactory setDrmUserAgent(@Nullable String userAgent);
/**
* Sets an optional {@link LoadErrorHandlingPolicy}.
*

View File

@ -22,7 +22,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
@ -30,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
@ -51,9 +51,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
public static final class Factory implements MediaSourceFactory {
private final DataSource.Factory dataSourceFactory;
private final MediaSourceDrmHelper mediaSourceDrmHelper;
private ExtractorsFactory extractorsFactory;
private DrmSessionManager drmSessionManager;
@Nullable private DrmSessionManager drmSessionManager;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private int continueLoadingCheckIntervalBytes;
@Nullable private String customCacheKey;
@ -78,7 +79,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
mediaSourceDrmHelper = new MediaSourceDrmHelper();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
}
@ -146,19 +147,22 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return this;
}
/**
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
* default value is {@link DrmSessionManager#DUMMY}.
*
* @param drmSessionManager The {@link DrmSessionManager}.
* @return This factory, for convenience.
*/
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
this.drmSessionManager = drmSessionManager;
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
return this;
}
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
return this;
}
@ -194,7 +198,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
mediaItem,
dataSourceFactory,
extractorsFactory,
drmSessionManager,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes);
}

View File

@ -327,13 +327,7 @@ public class SampleQueue implements TrackOutput {
* Attempts to read from the queue.
*
* <p>{@link Format Formats} read from this method may be associated to a {@link DrmSession}
* through {@link FormatHolder#drmSession}, which is populated in two scenarios:
*
* <ul>
* <li>The {@link Format} has a non-null {@link Format#drmInitData}.
* <li>The {@link DrmSessionManager} provides placeholder sessions for this queue's track type.
* See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}.
* </ul>
* through {@link FormatHolder#drmSession}.
*
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
@ -842,10 +836,7 @@ public class SampleQueue implements TrackOutput {
// is being used for both DrmInitData.
@Nullable DrmSession previousSession = currentDrmSession;
currentDrmSession =
newDrmInitData != null
? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat)
: drmSessionManager.acquirePlaceholderSession(
playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType));
drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat);
outputFormatHolder.drmSession = currentDrmSession;
if (previousSession != null) {

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.cache;
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 java.lang.Math.max;
import static java.lang.Math.min;
@ -219,9 +220,9 @@ import java.util.TreeSet;
public SimpleCacheSpan setLastTouchTimestamp(
SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) {
checkState(cachedSpans.remove(cacheSpan));
File file = cacheSpan.file;
File file = checkNotNull(cacheSpan.file);
if (updateFile) {
File directory = file.getParentFile();
File directory = checkNotNull(file.getParentFile());
long position = cacheSpan.position;
File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp);
if (file.renameTo(newFile)) {
@ -244,7 +245,9 @@ import java.util.TreeSet;
/** Removes the given span from cache. */
public boolean removeSpan(CacheSpan span) {
if (cachedSpans.remove(span)) {
if (span.file != null) {
span.file.delete();
}
return true;
}
return false;

View File

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.upstream.cache;
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;
import static java.lang.Math.min;
import android.annotation.SuppressLint;
@ -61,6 +64,7 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Maintains the index of cached content. */
/* package */ class CachedContentIndex {
@ -155,13 +159,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Nullable byte[] legacyStorageSecretKey,
boolean legacyStorageEncrypt,
boolean preferLegacyStorage) {
Assertions.checkState(databaseProvider != null || legacyStorageDir != null);
checkState(databaseProvider != null || legacyStorageDir != null);
keyToContent = new HashMap<>();
idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
newIds = new SparseBooleanArray();
@Nullable
Storage databaseStorage =
databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;
@Nullable
Storage legacyStorage =
legacyStorageDir != null
? new LegacyStorage(
@ -170,7 +176,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
legacyStorageEncrypt)
: null;
if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) {
storage = legacyStorage;
storage = castNonNull(legacyStorage);
previousStorage = databaseStorage;
} else {
storage = databaseStorage;
@ -325,7 +331,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Returns a {@link ContentMetadata} for the given key. */
public ContentMetadata getContentMetadata(String key) {
CachedContent cachedContent = get(key);
@Nullable CachedContent cachedContent = get(key);
return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
}
@ -358,7 +364,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* returns the smallest unused non-negative integer.
*/
@VisibleForTesting
/* package */ static int getNewId(SparseArray<String> idToKey) {
/* package */ static int getNewId(SparseArray<@NullableType String> idToKey) {
int size = idToKey.size();
int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);
if (id < 0) { // In case if we pass max int value.
@ -512,8 +518,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Nullable private ReusableBufferedOutputStream bufferedOutputStream;
public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) {
Cipher cipher = null;
SecretKeySpec secretKeySpec = null;
checkState(secretKey != null || !encrypt);
@Nullable Cipher cipher = null;
@Nullable SecretKeySpec secretKeySpec = null;
if (secretKey != null) {
Assertions.checkArgument(secretKey.length == 16);
try {
@ -550,7 +557,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public void load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
Assertions.checkState(!changed);
checkState(!changed);
if (!readFile(content, idToKey)) {
content.clear();
idToKey.clear();
@ -588,7 +595,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return true;
}
DataInputStream input = null;
@Nullable DataInputStream input = null;
try {
InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
input = new DataInputStream(inputStream);
@ -606,7 +613,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
input.readFully(initializationVector);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
try {
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
cipher.init(Cipher.DECRYPT_MODE, castNonNull(secretKeySpec), ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalStateException(e);
}
@ -647,6 +654,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} else {
bufferedOutputStream.reset(outputStream);
}
ReusableBufferedOutputStream bufferedOutputStream = this.bufferedOutputStream;
output = new DataOutputStream(bufferedOutputStream);
output.writeInt(VERSION);
@ -655,11 +663,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (encrypt) {
byte[] initializationVector = new byte[16];
random.nextBytes(initializationVector);
castNonNull(random).nextBytes(initializationVector);
output.write(initializationVector);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
castNonNull(cipher)
.init(Cipher.ENCRYPT_MODE, castNonNull(secretKeySpec), ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalStateException(e); // Should never happen.
}
@ -762,16 +771,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
+ " BLOB NOT NULL)";
private final DatabaseProvider databaseProvider;
private final SparseArray<CachedContent> pendingUpdates;
private final SparseArray<@NullableType CachedContent> pendingUpdates;
private String hexUid;
private String tableName;
private @MonotonicNonNull String hexUid;
private @MonotonicNonNull String tableName;
public static void delete(DatabaseProvider databaseProvider, long uid)
throws DatabaseIOException {
delete(databaseProvider, Long.toHexString(uid));
}
@SuppressWarnings("nullness:initialization.fields.uninitialized")
public DatabaseStorage(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
pendingUpdates = new SparseArray<>();
@ -788,26 +798,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return VersionTable.getVersion(
databaseProvider.getReadableDatabase(),
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
hexUid)
checkNotNull(hexUid))
!= VersionTable.VERSION_UNSET;
}
@Override
public void delete() throws DatabaseIOException {
delete(databaseProvider, hexUid);
delete(databaseProvider, checkNotNull(hexUid));
}
@Override
public void load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)
throws IOException {
Assertions.checkState(pendingUpdates.size() == 0);
checkState(pendingUpdates.size() == 0);
try {
int version =
VersionTable.getVersion(
databaseProvider.getReadableDatabase(),
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
hexUid);
checkNotNull(hexUid));
if (version != TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransactionNonExclusive();
@ -871,7 +881,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
writableDatabase.beginTransactionNonExclusive();
try {
for (int i = 0; i < pendingUpdates.size(); i++) {
CachedContent cachedContent = pendingUpdates.valueAt(i);
@Nullable CachedContent cachedContent = pendingUpdates.valueAt(i);
if (cachedContent == null) {
deleteRow(writableDatabase, pendingUpdates.keyAt(i));
} else {
@ -906,7 +916,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return databaseProvider
.getReadableDatabase()
.query(
tableName,
checkNotNull(tableName),
COLUMNS,
/* selection= */ null,
/* selectionArgs= */ null,
@ -917,13 +927,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException {
VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION);
dropTable(writableDatabase, tableName);
writableDatabase,
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
checkNotNull(hexUid),
TABLE_VERSION);
dropTable(writableDatabase, checkNotNull(tableName));
writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
}
private void deleteRow(SQLiteDatabase writableDatabase, int key) {
writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
writableDatabase.delete(
checkNotNull(tableName), WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
}
private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)
@ -936,7 +950,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
values.put(COLUMN_ID, cachedContent.id);
values.put(COLUMN_KEY, cachedContent.key);
values.put(COLUMN_METADATA, data);
writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
writableDatabase.replaceOrThrow(checkNotNull(tableName), /* nullColumnHack= */ null, values);
}
private static void delete(DatabaseProvider databaseProvider, String hexUid)

View File

@ -25,6 +25,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
import com.google.android.exoplayer2.RendererCapabilities;
@ -147,8 +148,9 @@ public class EventLogger implements AnalyticsListener {
}
@Override
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
logd(eventTime, "playbackSpeed", Float.toString(playbackSpeed));
public void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {
logd(eventTime, "playbackParameters", playbackParameters.toString());
}
@Override
@ -316,13 +318,19 @@ public class EventLogger implements AnalyticsListener {
logd(eventTime, "audioInputFormat", Format.toLogString(format));
}
@Override
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs;
logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs);
}
@Override
public void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
loge(
eventTime,
"audioTrackUnderrun",
bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]",
bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs,
/* throwable= */ null);
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.util;
import com.google.android.exoplayer2.PlaybackParameters;
/**
* Tracks the progression of media time.
*/
@ -26,13 +28,13 @@ public interface MediaClock {
long getPositionUs();
/**
* Attempts to set the playback speed. The media clock may override the speed if changing the
* speed is not supported.
* Attempts to set the playback parameters. The media clock may override the speed if changing the
* playback parameters is not supported.
*
* @param playbackSpeed The playback speed to attempt to set.
* @param playbackParameters The playback parameters to attempt to set.
*/
void setPlaybackSpeed(float playbackSpeed);
void setPlaybackParameters(PlaybackParameters playbackParameters);
/** Returns the active playback speed. */
float getPlaybackSpeed();
/** Returns the active playback parameters. */
PlaybackParameters getPlaybackParameters();
}

View File

@ -16,7 +16,7 @@
package com.google.android.exoplayer2.util;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.PlaybackParameters;
/**
* A {@link MediaClock} whose position advances with real time based on the playback parameters when
@ -29,8 +29,7 @@ public final class StandaloneMediaClock implements MediaClock {
private boolean started;
private long baseUs;
private long baseElapsedMs;
private float playbackSpeed;
private int scaledUsPerMs;
private PlaybackParameters playbackParameters;
/**
* Creates a new standalone media clock using the given {@link Clock} implementation.
@ -39,8 +38,7 @@ public final class StandaloneMediaClock implements MediaClock {
*/
public StandaloneMediaClock(Clock clock) {
this.clock = clock;
playbackSpeed = Player.DEFAULT_PLAYBACK_SPEED;
scaledUsPerMs = getScaledUsPerMs(playbackSpeed);
playbackParameters = PlaybackParameters.DEFAULT;
}
/**
@ -80,33 +78,29 @@ public final class StandaloneMediaClock implements MediaClock {
long positionUs = baseUs;
if (started) {
long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs;
if (playbackSpeed == 1f) {
if (playbackParameters.speed == 1f) {
positionUs += C.msToUs(elapsedSinceBaseMs);
} else {
// Add the media time in microseconds that will elapse in elapsedSinceBaseMs milliseconds of
// wallclock time
positionUs += elapsedSinceBaseMs * scaledUsPerMs;
positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs);
}
}
return positionUs;
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
// Store the current position as the new base, in case the playback speed has changed.
if (started) {
resetPosition(getPositionUs());
}
this.playbackSpeed = playbackSpeed;
scaledUsPerMs = getScaledUsPerMs(playbackSpeed);
this.playbackParameters = playbackParameters;
}
@Override
public float getPlaybackSpeed() {
return playbackSpeed;
public PlaybackParameters getPlaybackParameters() {
return playbackParameters;
}
private static int getScaledUsPerMs(float playbackSpeed) {
return Math.round(playbackSpeed * 1000f);
}
}

View File

@ -1611,6 +1611,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "ELUGA_Prim":
case "ELUGA_Ray_X":
case "EverStar_S":
case "F02H":
case "F03H":
case "F3111":
case "F3113":
case "F3116":

View File

@ -22,7 +22,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.MockitoAnnotations.initMocks;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParametersListener;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
import org.junit.Before;
@ -36,9 +36,10 @@ public class DefaultMediaClockTest {
private static final long TEST_POSITION_US = 123456789012345678L;
private static final long SLEEP_TIME_MS = 1_000;
private static final float TEST_PLAYBACK_SPEED = 2f;
private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS =
new PlaybackParameters(/* speed= */ 2f);
@Mock private PlaybackSpeedListener listener;
@Mock private PlaybackParametersListener listener;
private FakeClock fakeClock;
private DefaultMediaClock mediaClock;
@ -109,44 +110,44 @@ public class DefaultMediaClockTest {
}
@Test
public void standaloneGetPlaybackSpeed_initializedWithDefaultPlaybackSpeed() {
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() {
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
}
@Test
public void standaloneSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() {
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() {
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
}
@Test
public void standaloneSetPlaybackSpeed_shouldNotTriggerCallback() {
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
public void standaloneSetPlaybackParameters_shouldNotTriggerCallback() {
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
verifyNoMoreInteractions(listener);
}
@Test
public void standaloneSetPlaybackSpeed_shouldApplyNewPlaybackSpeed() {
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackParameters() {
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
mediaClock.start();
// Asserts that clock is running with speed declared in getPlaybackSpeed().
// Asserts that clock is running with speed declared in getPlaybackParameters().
assertClockIsRunning(/* isReadingAhead= */ false);
}
@Test
public void standaloneSetOtherPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() {
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
mediaClock.setPlaybackSpeed(Player.DEFAULT_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() {
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
}
@Test
public void enableRendererMediaClock_shouldOverwriteRendererPlaybackSpeedIfPossible()
public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible()
throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true);
mediaClock.onRendererEnabled(mediaClockRenderer);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
verifyNoMoreInteractions(listener);
}
@ -154,26 +155,27 @@ public class DefaultMediaClockTest {
public void enableRendererMediaClockWithFixedPlaybackSpeed_usesRendererPlaybackSpeed()
throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);
mediaClock.onRendererEnabled(mediaClockRenderer);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
}
@Test
public void enableRendererMediaClockWithFixedPlaybackSpeed_shouldTriggerCallback()
throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);
mediaClock.onRendererEnabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED);
verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);
}
@Test
public void enableRendererMediaClockWithFixedButSamePlaybackSpeed_shouldNotTriggerCallback()
throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
new MediaClockRenderer(
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false);
mediaClock.onRendererEnabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
verifyNoMoreInteractions(listener);
@ -182,44 +184,47 @@ public class DefaultMediaClockTest {
@Test
public void disableRendererMediaClock_shouldKeepPlaybackSpeed() throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);
mediaClock.onRendererEnabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
mediaClock.onRendererDisabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
}
@Test
public void rendererClockSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue()
public void rendererClockSetPlaybackSpeed_getPlaybackParametersShouldReturnSameValue()
throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
new MediaClockRenderer(
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true);
mediaClock.onRendererEnabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
}
@Test
public void rendererClockSetPlaybackSpeed_shouldNotTriggerCallback() throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
new MediaClockRenderer(
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true);
mediaClock.onRendererEnabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
verifyNoMoreInteractions(listener);
}
@Test
public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackSpeedShouldReturnSameValue()
public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackParametersShouldReturnSameValue()
throws ExoPlaybackException {
FakeMediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
new MediaClockRenderer(
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false);
mediaClock.onRendererEnabled(mediaClockRenderer);
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
}
@Test
@ -266,12 +271,13 @@ public class DefaultMediaClockTest {
public void getPositionWithPlaybackSpeedChange_shouldTriggerCallback()
throws ExoPlaybackException {
MediaClockRenderer mediaClockRenderer =
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
new MediaClockRenderer(
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true);
mediaClock.onRendererEnabled(mediaClockRenderer);
// Silently change playback speed of renderer clock.
mediaClockRenderer.playbackSpeed = TEST_PLAYBACK_SPEED;
mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS;
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED);
verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);
}
@Test
@ -356,7 +362,7 @@ public class DefaultMediaClockTest {
private void assertClockIsRunning(boolean isReadingAhead) {
long clockStartUs = mediaClock.syncAndGetPositionUs(isReadingAhead);
fakeClock.advanceTime(SLEEP_TIME_MS);
int scaledUsPerMs = Math.round(mediaClock.getPlaybackSpeed() * 1000f);
int scaledUsPerMs = Math.round(mediaClock.getPlaybackParameters().speed * 1000f);
assertThat(mediaClock.syncAndGetPositionUs(isReadingAhead))
.isEqualTo(clockStartUs + (SLEEP_TIME_MS * scaledUsPerMs));
}
@ -371,37 +377,53 @@ public class DefaultMediaClockTest {
@SuppressWarnings("HidingField")
private static class MediaClockRenderer extends FakeMediaClockRenderer {
private final boolean playbackSpeedIsMutable;
private final boolean playbackParametersAreMutable;
private final boolean isReady;
private final boolean isEnded;
public float playbackSpeed;
public PlaybackParameters playbackParameters;
public long positionUs;
public MediaClockRenderer() throws ExoPlaybackException {
this(Player.DEFAULT_PLAYBACK_SPEED, false, true, false, false);
this(
PlaybackParameters.DEFAULT,
/* playbackParametersAreMutable= */ false,
/* isReady= */ true,
/* isEnded= */ false,
/* hasReadStreamToEnd= */ false);
}
public MediaClockRenderer(float playbackSpeed, boolean playbackSpeedIsMutable)
public MediaClockRenderer(
PlaybackParameters playbackParameters, boolean playbackParametersAreMutable)
throws ExoPlaybackException {
this(playbackSpeed, playbackSpeedIsMutable, true, false, false);
this(
playbackParameters,
playbackParametersAreMutable,
/* isReady= */ true,
/* isEnded= */ false,
/* hasReadStreamToEnd= */ false);
}
public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd)
throws ExoPlaybackException {
this(Player.DEFAULT_PLAYBACK_SPEED, false, isReady, isEnded, hasReadStreamToEnd);
this(
PlaybackParameters.DEFAULT,
/* playbackParametersAreMutable= */ false,
isReady,
isEnded,
hasReadStreamToEnd);
}
private MediaClockRenderer(
float playbackSpeed,
boolean playbackSpeedIsMutable,
PlaybackParameters playbackParameters,
boolean playbackParametersAreMutable,
boolean isReady,
boolean isEnded,
boolean hasReadStreamToEnd)
throws ExoPlaybackException {
super(C.TRACK_TYPE_UNKNOWN);
this.playbackSpeed = playbackSpeed;
this.playbackSpeedIsMutable = playbackSpeedIsMutable;
this.playbackParameters = playbackParameters;
this.playbackParametersAreMutable = playbackParametersAreMutable;
this.isReady = isReady;
this.isEnded = isEnded;
this.positionUs = TEST_POSITION_US;
@ -416,15 +438,15 @@ public class DefaultMediaClockTest {
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
if (playbackSpeedIsMutable) {
this.playbackSpeed = playbackSpeed;
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
if (playbackParametersAreMutable) {
this.playbackParameters = playbackParameters;
}
}
@Override
public float getPlaybackSpeed() {
return playbackSpeed;
public PlaybackParameters getPlaybackParameters() {
return playbackParameters;
}
@Override

View File

@ -328,11 +328,11 @@ public final class ExoPlayerTest {
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {}
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
@Override
public float getPlaybackSpeed() {
return Player.DEFAULT_PLAYBACK_SPEED;
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@Override
@ -1010,7 +1010,7 @@ public final class ExoPlayerTest {
}
})
// Set playback speed (while the fake media period is not yet prepared).
.setPlaybackSpeed(2f)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f))
// Complete preparation of the fake media period.
.executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())
.build();
@ -3378,18 +3378,18 @@ public final class ExoPlayerTest {
SimpleExoPlayer player,
DefaultTrackSelector trackSelector,
@Nullable Surface surface) {
maskedPlaybackSpeeds.add(player.getPlaybackSpeed());
maskedPlaybackSpeeds.add(player.getPlaybackParameters().speed);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setPlaybackSpeed(1.1f)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f))
.apply(getPlaybackSpeedAction)
.setPlaybackSpeed(1.2f)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f))
.apply(getPlaybackSpeedAction)
.setPlaybackSpeed(1.3f)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f))
.apply(getPlaybackSpeedAction)
.play()
.build();
@ -3397,8 +3397,8 @@ public final class ExoPlayerTest {
EventListener listener =
new EventListener() {
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
reportedPlaybackSpeeds.add(playbackSpeed);
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
reportedPlaybackSpeeds.add(playbackParameters.speed);
}
};
new ExoPlayerTestRunner.Builder(context)
@ -3424,28 +3424,28 @@ public final class ExoPlayerTest {
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {}
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
@Override
public float getPlaybackSpeed() {
return Player.DEFAULT_PLAYBACK_SPEED;
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setPlaybackSpeed(1.1f)
.setPlaybackSpeed(1.2f)
.setPlaybackSpeed(1.3f)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f))
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f))
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f))
.play()
.build();
List<Float> reportedPlaybackParameters = new ArrayList<>();
List<PlaybackParameters> reportedPlaybackParameters = new ArrayList<>();
EventListener listener =
new EventListener() {
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
reportedPlaybackParameters.add(playbackSpeed);
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
reportedPlaybackParameters.add(playbackParameters);
}
};
new ExoPlayerTestRunner.Builder(context)
@ -3458,7 +3458,11 @@ public final class ExoPlayerTest {
.blockUntilEnded(TIMEOUT_MS);
assertThat(reportedPlaybackParameters)
.containsExactly(1.1f, 1.2f, 1.3f, Player.DEFAULT_PLAYBACK_SPEED)
.containsExactly(
new PlaybackParameters(/* speed= */ 1.1f),
new PlaybackParameters(/* speed= */ 1.2f),
new PlaybackParameters(/* speed= */ 1.3f),
PlaybackParameters.DEFAULT)
.inOrder();
}

View File

@ -435,7 +435,7 @@ public final class MediaPeriodQueueTest {
/* loadingMediaPeriodId= */ null,
/* playWhenReady= */ false,
Player.PLAYBACK_SUPPRESSION_REASON_NONE,
/* playbackSpeed= */ Player.DEFAULT_PLAYBACK_SPEED,
/* playbackParameters= */ PlaybackParameters.DEFAULT,
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,

View File

@ -26,6 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
@ -82,7 +83,7 @@ public final class AnalyticsCollectorTest {
private static final int EVENT_POSITION_DISCONTINUITY = 2;
private static final int EVENT_SEEK_STARTED = 3;
private static final int EVENT_SEEK_PROCESSED = 4;
private static final int EVENT_PLAYBACK_SPEED_CHANGED = 5;
private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5;
private static final int EVENT_REPEAT_MODE_CHANGED = 6;
private static final int EVENT_SHUFFLE_MODE_CHANGED = 7;
private static final int EVENT_LOADING_CHANGED = 8;
@ -106,21 +107,22 @@ public final class AnalyticsCollectorTest {
private static final int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 26;
private static final int EVENT_AUDIO_DISABLED = 27;
private static final int EVENT_AUDIO_SESSION_ID = 28;
private static final int EVENT_AUDIO_UNDERRUN = 29;
private static final int EVENT_VIDEO_ENABLED = 30;
private static final int EVENT_VIDEO_DECODER_INIT = 31;
private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 32;
private static final int EVENT_DROPPED_FRAMES = 33;
private static final int EVENT_VIDEO_DISABLED = 34;
private static final int EVENT_RENDERED_FIRST_FRAME = 35;
private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 36;
private static final int EVENT_VIDEO_SIZE_CHANGED = 37;
private static final int EVENT_DRM_KEYS_LOADED = 38;
private static final int EVENT_DRM_ERROR = 39;
private static final int EVENT_DRM_KEYS_RESTORED = 40;
private static final int EVENT_DRM_KEYS_REMOVED = 41;
private static final int EVENT_DRM_SESSION_ACQUIRED = 42;
private static final int EVENT_DRM_SESSION_RELEASED = 43;
private static final int EVENT_AUDIO_POSITION_ADVANCING = 29;
private static final int EVENT_AUDIO_UNDERRUN = 30;
private static final int EVENT_VIDEO_ENABLED = 31;
private static final int EVENT_VIDEO_DECODER_INIT = 32;
private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 33;
private static final int EVENT_DROPPED_FRAMES = 34;
private static final int EVENT_VIDEO_DISABLED = 35;
private static final int EVENT_RENDERED_FIRST_FRAME = 36;
private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 37;
private static final int EVENT_VIDEO_SIZE_CHANGED = 38;
private static final int EVENT_DRM_KEYS_LOADED = 39;
private static final int EVENT_DRM_ERROR = 40;
private static final int EVENT_DRM_KEYS_RESTORED = 41;
private static final int EVENT_DRM_KEYS_REMOVED = 42;
private static final int EVENT_DRM_SESSION_ACQUIRED = 43;
private static final int EVENT_DRM_SESSION_RELEASED = 44;
private static final UUID DRM_SCHEME_UUID =
UUID.nameUUIDFromBytes(TestUtil.createByteArray(7, 8, 9));
@ -226,6 +228,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
@ -305,6 +308,7 @@ public final class AnalyticsCollectorTest {
.containsExactly(period0, period1)
.inOrder();
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT))
.containsExactly(period0, period1)
@ -380,6 +384,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
@ -476,6 +481,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID))
.containsExactly(period0, period1)
.inOrder();
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING))
.containsExactly(period0, period1)
.inOrder();
assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
@ -576,6 +584,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID))
.containsExactly(period1Seq1, period1Seq2)
.inOrder();
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING))
.containsExactly(period1Seq1, period1Seq2)
.inOrder();
assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0, period0);
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT))
@ -1785,8 +1796,9 @@ public final class AnalyticsCollectorTest {
}
@Override
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_SPEED_CHANGED, eventTime));
public void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {
reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime));
}
@Override
@ -1922,6 +1934,11 @@ public final class AnalyticsCollectorTest {
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime));
}
@Override
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_POSITION_ADVANCING, eventTime));
}
@Override
public void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {

View File

@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource;
@ -75,8 +76,8 @@ public final class PlaybackStatsListenerTest {
playbackStatsListener.onPositionDiscontinuity(
EMPTY_TIMELINE_EVENT_TIME, Player.DISCONTINUITY_REASON_SEEK);
playbackStatsListener.onPlaybackSpeedChanged(
EMPTY_TIMELINE_EVENT_TIME, /* playbackSpeed= */ 2.0f);
playbackStatsListener.onPlaybackParametersChanged(
EMPTY_TIMELINE_EVENT_TIME, new PlaybackParameters(/* speed= */ 2.0f));
playbackStatsListener.onPlayWhenReadyChanged(
EMPTY_TIMELINE_EVENT_TIME,
/* playWhenReady= */ true,

View File

@ -25,6 +25,7 @@ import static org.robolectric.annotation.Config.TARGET_SDK;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@ -89,7 +90,7 @@ public final class DefaultAudioSinkTest {
@Test
public void handlesBufferAfterReset_withPlaybackSpeed() throws Exception {
defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f);
defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f));
configureDefaultAudioSink(CHANNEL_COUNT_STEREO);
defaultAudioSink.handleBuffer(
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
@ -99,7 +100,8 @@ public final class DefaultAudioSinkTest {
configureDefaultAudioSink(CHANNEL_COUNT_STEREO);
defaultAudioSink.handleBuffer(
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f);
assertThat(defaultAudioSink.getPlaybackParameters())
.isEqualTo(new PlaybackParameters(/* speed= */ 1.5f));
}
@Test
@ -117,7 +119,7 @@ public final class DefaultAudioSinkTest {
@Test
public void handlesBufferAfterReset_withFormatChangeAndPlaybackSpeed() throws Exception {
defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f);
defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f));
configureDefaultAudioSink(CHANNEL_COUNT_STEREO);
defaultAudioSink.handleBuffer(
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
@ -127,7 +129,8 @@ public final class DefaultAudioSinkTest {
configureDefaultAudioSink(CHANNEL_COUNT_MONO);
defaultAudioSink.handleBuffer(
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f);
assertThat(defaultAudioSink.getPlaybackParameters())
.isEqualTo(new PlaybackParameters(/* speed= */ 1.5f));
}
@Test

View File

@ -49,8 +49,10 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
/** Unit tests for {@link MediaCodecAudioRenderer} */
@Config(sdk = 29)
@RunWith(AndroidJUnit4.class)
public class MediaCodecAudioRendererTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();

View File

@ -0,0 +1,64 @@
/*
* 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.e2etest;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.PlaybackOutput;
import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.testutil.TestExoPlayer;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** End-to-end tests using MP4 samples. */
// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
@Config(sdk = 29)
@RunWith(AndroidJUnit4.class)
public class Mp4PlaybackTest {
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Test
public void h264VideoAacAudio() throws Exception {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
.setClock(new AutoAdvancingFakeClock())
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig);
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4"));
player.prepare();
player.play();
TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED);
DumpFileAsserts.assertOutput(
ApplicationProvider.getApplicationContext(),
playbackOutput,
"playbackdumps/mp4/sample.mp4.dump");
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.e2etest;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.PlaybackOutput;
import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.testutil.TestExoPlayer;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** End-to-end tests using TS samples. */
// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
@Config(sdk = 29)
@RunWith(AndroidJUnit4.class)
public class TsPlaybackTest {
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Test
public void mpegVideoMpegAudioScte35() throws Exception {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
.setClock(new AutoAdvancingFakeClock())
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig);
player.setMediaItem(MediaItem.fromUri("asset:///media/ts/sample_scte35.ts"));
player.prepare();
player.play();
TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED);
DumpFileAsserts.assertOutput(
ApplicationProvider.getApplicationContext(),
playbackOutput,
"playbackdumps/ts/sample_scte35.ts.dump");
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.source;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link MediaSourceDrmHelper}. */
@RunWith(AndroidJUnit4.class)
public class MediaSourceDrmHelperTest {
@Test
public void create_noDrmProperties_createsNoopManager() {
DrmSessionManager drmSessionManager =
new MediaSourceDrmHelper().create(MediaItem.fromUri(Uri.EMPTY));
assertThat(drmSessionManager).isEqualTo(DrmSessionManager.DUMMY);
}
@Test
public void create_createsManager() {
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setDrmLicenseUri(Uri.EMPTY)
.setDrmUuid(C.WIDEVINE_UUID)
.build();
DrmSessionManager drmSessionManager = new MediaSourceDrmHelper().create(mediaItem);
assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DUMMY);
}
}

View File

@ -38,6 +38,7 @@ import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.Allocator;
@ -54,7 +55,6 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
/** Test for {@link SampleQueue}. */
@ -69,6 +69,8 @@ public final class SampleQueueTest {
private static final Format FORMAT_SPLICED = buildFormat(/* id= */ "spliced");
private static final Format FORMAT_ENCRYPTED =
new Format.Builder().setId(/* id= */ "encrypted").setDrmInitData(new DrmInitData()).build();
private static final Format FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE =
FORMAT_ENCRYPTED.copyWithExoMediaCryptoType(MockExoMediaCrypto.class);
private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
/*
@ -128,7 +130,7 @@ public final class SampleQueueTest {
new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0);
private Allocator allocator;
private DrmSessionManager mockDrmSessionManager;
private MockDrmSessionManager mockDrmSessionManager;
private DrmSession mockDrmSession;
private DrmSessionEventListener.EventDispatcher eventDispatcher;
private SampleQueue sampleQueue;
@ -138,11 +140,8 @@ public final class SampleQueueTest {
@Before
public void setUp() {
allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
mockDrmSessionManager = Mockito.mock(DrmSessionManager.class);
mockDrmSession = Mockito.mock(DrmSession.class);
when(mockDrmSessionManager.acquireSession(
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(mockDrmSession);
mockDrmSessionManager = new MockDrmSessionManager(mockDrmSession);
eventDispatcher = new DrmSessionEventListener.EventDispatcher();
sampleQueue =
new SampleQueue(
@ -399,7 +398,7 @@ public final class SampleQueueTest {
@Test
public void isReadyReturnsTrueForValidDrmSession() {
writeTestDataWithEncryptedSections();
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isFalse();
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue();
@ -424,7 +423,7 @@ public final class SampleQueueTest {
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
writeTestDataWithEncryptedSections();
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
assertReadNothing(/* formatRequired= */ false);
assertThat(inputBuffer.waitingForKeys).isTrue();
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
@ -464,9 +463,7 @@ public final class SampleQueueTest {
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class);
when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
when(mockDrmSessionManager.acquirePlaceholderSession(
ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
.thenReturn(mockPlaceholderDrmSession);
mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession;
writeTestDataWithEncryptedSections();
int result =
@ -497,9 +494,7 @@ public final class SampleQueueTest {
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class);
when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
when(mockDrmSessionManager.acquirePlaceholderSession(
ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
.thenReturn(mockPlaceholderDrmSession);
mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession;
writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]);
byte[] sampleData = new byte[] {0, 1, 2};
@ -540,7 +535,7 @@ public final class SampleQueueTest {
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
writeTestDataWithEncryptedSections();
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
assertReadNothing(/* formatRequired= */ false);
sampleQueue.maybeThrowError();
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_ERROR);
@ -569,7 +564,7 @@ public final class SampleQueueTest {
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
writeTestDataWithEncryptedSections();
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
assertReadEncryptedSample(/* sampleIndex= */ 0);
}
@ -1497,4 +1492,33 @@ public final class SampleQueueTest {
private static Format copyWithLabel(Format format, String label) {
return format.buildUpon().setLabel(label).build();
}
private static final class MockExoMediaCrypto implements ExoMediaCrypto {}
private static final class MockDrmSessionManager implements DrmSessionManager {
private final DrmSession mockDrmSession;
@Nullable private DrmSession mockPlaceholderDrmSession;
private MockDrmSessionManager(DrmSession mockDrmSession) {
this.mockDrmSession = mockDrmSession;
}
@Nullable
@Override
public DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
return format.drmInitData != null ? mockDrmSession : mockPlaceholderDrmSession;
}
@Nullable
@Override
public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format) {
return mockPlaceholderDrmSession != null || format.drmInitData != null
? MockExoMediaCrypto.class
: null;
}
}
}

View File

@ -30,7 +30,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
@ -42,6 +41,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaSourceFactory;
@ -54,6 +54,7 @@ import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
import com.google.android.exoplayer2.upstream.Loader;
@ -91,9 +92,10 @@ public final class DashMediaSource extends BaseMediaSource {
public static final class Factory implements MediaSourceFactory {
private final DashChunkSource.Factory chunkSourceFactory;
private final MediaSourceDrmHelper mediaSourceDrmHelper;
@Nullable private final DataSource.Factory manifestDataSourceFactory;
private DrmSessionManager drmSessionManager;
@Nullable private DrmSessionManager drmSessionManager;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
@ -126,7 +128,7 @@ public final class DashMediaSource extends BaseMediaSource {
@Nullable DataSource.Factory manifestDataSourceFactory) {
this.chunkSourceFactory = checkNotNull(chunkSourceFactory);
this.manifestDataSourceFactory = manifestDataSourceFactory;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
mediaSourceDrmHelper = new MediaSourceDrmHelper();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
@ -155,19 +157,22 @@ public final class DashMediaSource extends BaseMediaSource {
return this;
}
/**
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
* default value is {@link DrmSessionManager#DUMMY}.
*
* @param drmSessionManager The {@link DrmSessionManager}.
* @return This factory, for convenience.
*/
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
this.drmSessionManager = drmSessionManager;
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
return this;
}
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
return this;
}
@ -312,7 +317,7 @@ public final class DashMediaSource extends BaseMediaSource {
/* manifestParser= */ null,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
drmSessionManager,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs,
livePresentationDelayOverridesManifest);
@ -403,7 +408,7 @@ public final class DashMediaSource extends BaseMediaSource {
manifestParser,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
drmSessionManager,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs,
livePresentationDelayOverridesManifest);

View File

@ -20,10 +20,34 @@ package com.google.android.exoplayer2.extractor;
*/
public interface ExtractorOutput {
/**
* Placeholder {@link ExtractorOutput} implementation throwing an {@link
* UnsupportedOperationException} in each method.
*/
ExtractorOutput PLACEHOLDER =
new ExtractorOutput() {
@Override
public TrackOutput track(int id, int type) {
throw new UnsupportedOperationException();
}
@Override
public void endTracks() {
throw new UnsupportedOperationException();
}
@Override
public void seekMap(SeekMap seekMap) {
throw new UnsupportedOperationException();
}
};
/**
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
* <p>
* The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
*
* <p>The same {@link TrackOutput} is returned if multiple calls are made with the same {@code
* id}.
*
* @param id A track identifier.
* @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}

View File

@ -48,6 +48,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.AvcConfig;
import com.google.android.exoplayer2.video.ColorInfo;
import com.google.android.exoplayer2.video.DolbyVisionConfig;
import com.google.android.exoplayer2.video.HevcConfig;
import java.io.IOException;
import java.lang.annotation.Documented;
@ -170,6 +171,9 @@ public class MatroskaExtractor implements Extractor {
private static final int ID_FLAG_FORCED = 0x55AA;
private static final int ID_DEFAULT_DURATION = 0x23E383;
private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE;
private static final int ID_BLOCK_ADDITION_MAPPING = 0x41E4;
private static final int ID_BLOCK_ADD_ID_TYPE = 0x41E7;
private static final int ID_BLOCK_ADD_ID_EXTRA_DATA = 0x41ED;
private static final int ID_NAME = 0x536E;
private static final int ID_CODEC_ID = 0x86;
private static final int ID_CODEC_PRIVATE = 0x63A2;
@ -234,6 +238,17 @@ public class MatroskaExtractor implements Extractor {
*/
private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4;
/**
* BlockAddIdType value for Dolby Vision configuration with profile <= 7. See also
* https://www.matroska.org/technical/codec_specs.html.
*/
private static final int BLOCK_ADD_ID_TYPE_DVCC = 0x64766343;
/**
* BlockAddIdType value for Dolby Vision configuration with profile > 7. See also
* https://www.matroska.org/technical/codec_specs.html.
*/
private static final int BLOCK_ADD_ID_TYPE_DVVC = 0x64767643;
private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
private static final int LACING_FIXED_SIZE = 2;
@ -501,6 +516,7 @@ public class MatroskaExtractor implements Extractor {
case ID_CLUSTER:
case ID_TRACKS:
case ID_TRACK_ENTRY:
case ID_BLOCK_ADDITION_MAPPING:
case ID_AUDIO:
case ID_VIDEO:
case ID_CONTENT_ENCODINGS:
@ -535,6 +551,7 @@ public class MatroskaExtractor implements Extractor {
case ID_FLAG_FORCED:
case ID_DEFAULT_DURATION:
case ID_MAX_BLOCK_ADDITION_ID:
case ID_BLOCK_ADD_ID_TYPE:
case ID_CODEC_DELAY:
case ID_SEEK_PRE_ROLL:
case ID_CHANNELS:
@ -562,6 +579,7 @@ public class MatroskaExtractor implements Extractor {
case ID_LANGUAGE:
return EbmlProcessor.ELEMENT_TYPE_STRING;
case ID_SEEK_ID:
case ID_BLOCK_ADD_ID_EXTRA_DATA:
case ID_CONTENT_COMPRESSION_SETTINGS:
case ID_CONTENT_ENCRYPTION_KEY_ID:
case ID_SIMPLE_BLOCK:
@ -814,6 +832,9 @@ public class MatroskaExtractor implements Extractor {
case ID_MAX_BLOCK_ADDITION_ID:
currentTrack.maxBlockAdditionId = (int) value;
break;
case ID_BLOCK_ADD_ID_TYPE:
currentTrack.blockAddIdType = (int) value;
break;
case ID_CODEC_DELAY:
currentTrack.codecDelayNs = value;
break;
@ -1076,6 +1097,9 @@ public class MatroskaExtractor implements Extractor {
seekEntryIdBytes.setPosition(0);
seekEntryId = (int) seekEntryIdBytes.readUnsignedInt();
break;
case ID_BLOCK_ADD_ID_EXTRA_DATA:
handleBlockAddIDExtraData(currentTrack, input, contentSize);
break;
case ID_CODEC_PRIVATE:
currentTrack.codecPrivate = new byte[contentSize];
input.readFully(currentTrack.codecPrivate, 0, contentSize);
@ -1244,6 +1268,18 @@ public class MatroskaExtractor implements Extractor {
}
}
protected void handleBlockAddIDExtraData(Track track, ExtractorInput input, int contentSize)
throws IOException {
if (track.blockAddIdType == BLOCK_ADD_ID_TYPE_DVVC
|| track.blockAddIdType == BLOCK_ADD_ID_TYPE_DVCC) {
track.dolbyVisionConfigBytes = new byte[contentSize];
input.readFully(track.dolbyVisionConfigBytes, 0, contentSize);
} else {
// Unhandled BlockAddIDExtraData.
input.skipFully(contentSize);
}
}
protected void handleBlockAdditionalData(
Track track, int blockAdditionalId, ExtractorInput input, int contentSize)
throws IOException {
@ -1883,6 +1919,7 @@ public class MatroskaExtractor implements Extractor {
public int type;
public int defaultSampleDurationNs;
public int maxBlockAdditionId;
private int blockAddIdType;
public boolean hasContentEncryption;
public byte[] sampleStrippedBytes;
public TrackOutput.CryptoData cryptoData;
@ -1921,6 +1958,7 @@ public class MatroskaExtractor implements Extractor {
public float whitePointChromaticityY = Format.NO_VALUE;
public float maxMasteringLuminance = Format.NO_VALUE;
public float minMasteringLuminance = Format.NO_VALUE;
@Nullable public byte[] dolbyVisionConfigBytes;
// Audio elements. Initially set to their default values.
public int channelCount = 1;
@ -2091,6 +2129,16 @@ public class MatroskaExtractor implements Extractor {
throw new ParserException("Unrecognized codec identifier.");
}
if (dolbyVisionConfigBytes != null) {
@Nullable
DolbyVisionConfig dolbyVisionConfig =
DolbyVisionConfig.parse(new ParsableByteArray(this.dolbyVisionConfigBytes));
if (dolbyVisionConfig != null) {
codecs = dolbyVisionConfig.codecs;
mimeType = MimeTypes.VIDEO_DOLBY_VISION;
}
}
@C.SelectionFlags int selectionFlags = 0;
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;

View File

@ -16,6 +16,10 @@
package com.google.android.exoplayer2.extractor.mp4;
import static com.google.android.exoplayer2.extractor.mp4.AtomParsers.parseTraks;
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;
import static com.google.android.exoplayer2.util.Util.nullSafeArrayCopy;
import static java.lang.Math.max;
import android.util.Pair;
@ -42,7 +46,6 @@ import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;
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.NalUnitUtil;
@ -59,7 +62,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Extracts data from the FMP4 container format. */
@SuppressWarnings("ConstantField")
@ -175,7 +177,7 @@ public class FragmentedMp4Extractor implements Extractor {
private boolean processSeiNalUnitPayload;
// Outputs.
private @MonotonicNonNull ExtractorOutput extractorOutput;
private ExtractorOutput extractorOutput;
private TrackOutput[] emsgTrackOutputs;
private TrackOutput[] ceaTrackOutputs;
@ -270,9 +272,9 @@ public class FragmentedMp4Extractor implements Extractor {
durationUs = C.TIME_UNSET;
pendingSeekTimeUs = C.TIME_UNSET;
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
extractorOutput = ExtractorOutput.PLACEHOLDER;
emsgTrackOutputs = new TrackOutput[0];
ceaTrackOutputs = new TrackOutput[0];
enterReadingAtomHeaderState();
}
@Override
@ -283,6 +285,7 @@ public class FragmentedMp4Extractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
enterReadingAtomHeaderState();
initExtraTracks();
if (sideloadedTrack != null) {
TrackBundle bundle =
@ -429,8 +432,9 @@ public class FragmentedMp4Extractor implements Extractor {
if (atomSize > Integer.MAX_VALUE) {
throw new ParserException("Leaf atom with length > 2147483647 (unsupported).");
}
atomData = new ParsableByteArray((int) atomSize);
ParsableByteArray atomData = new ParsableByteArray((int) atomSize);
System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, Atom.HEADER_SIZE);
this.atomData = atomData;
parserState = STATE_READING_ATOM_PAYLOAD;
} else {
if (atomSize > Integer.MAX_VALUE) {
@ -445,6 +449,7 @@ public class FragmentedMp4Extractor implements Extractor {
private void readAtomPayload(ExtractorInput input) throws IOException {
int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;
@Nullable ParsableByteArray atomData = this.atomData;
if (atomData != null) {
input.readFully(atomData.getData(), Atom.HEADER_SIZE, atomPayloadSize);
onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
@ -485,12 +490,12 @@ public class FragmentedMp4Extractor implements Extractor {
}
private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
Assertions.checkState(sideloadedTrack == null, "Unexpected moov box.");
checkState(sideloadedTrack == null, "Unexpected moov box.");
@Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);
// Read declaration of track fragments in the Moov box.
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
// Read declaration of track fragments in the moov box.
ContainerAtom mvex = checkNotNull(moov.getContainerAtomOfType(Atom.TYPE_mvex));
SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>();
long duration = C.TIME_UNSET;
int mvexChildrenSize = mvex.leafChildren.size();
@ -531,7 +536,7 @@ public class FragmentedMp4Extractor implements Extractor {
}
extractorOutput.endTracks();
} else {
Assertions.checkState(trackBundles.size() == trackCount);
checkState(trackBundles.size() == trackCount);
for (int i = 0; i < trackCount; i++) {
TrackSampleTable sampleTable = sampleTables.get(i);
Track track = sampleTable.track;
@ -554,7 +559,7 @@ public class FragmentedMp4Extractor implements Extractor {
// See https://github.com/google/ExoPlayer/issues/4477.
return defaultSampleValuesArray.valueAt(/* index= */ 0);
}
return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId));
return checkNotNull(defaultSampleValuesArray.get(trackId));
}
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
@ -589,7 +594,7 @@ public class FragmentedMp4Extractor implements Extractor {
emsgTrackOutputs[emsgTrackOutputCount++] =
extractorOutput.track(nextExtraTrackId++, C.TRACK_TYPE_METADATA);
}
emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount);
emsgTrackOutputs = nullSafeArrayCopy(emsgTrackOutputs, emsgTrackOutputCount);
for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) {
eventMessageTrackOutput.format(EMSG_FORMAT);
}
@ -604,7 +609,7 @@ public class FragmentedMp4Extractor implements Extractor {
/** Handles an emsg atom (defined in 23009-1). */
private void onEmsgLeafAtomRead(ParsableByteArray atom) {
if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) {
if (emsgTrackOutputs.length == 0) {
return;
}
atom.setPosition(Atom.HEADER_SIZE);
@ -619,8 +624,8 @@ public class FragmentedMp4Extractor implements Extractor {
long id;
switch (version) {
case 0:
schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString());
value = Assertions.checkNotNull(atom.readNullTerminatedString());
schemeIdUri = checkNotNull(atom.readNullTerminatedString());
value = checkNotNull(atom.readNullTerminatedString());
timescale = atom.readUnsignedInt();
presentationTimeDeltaUs =
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
@ -638,8 +643,8 @@ public class FragmentedMp4Extractor implements Extractor {
durationMs =
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale);
id = atom.readUnsignedInt();
schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString());
value = Assertions.checkNotNull(atom.readNullTerminatedString());
schemeIdUri = checkNotNull(atom.readNullTerminatedString());
value = checkNotNull(atom.readNullTerminatedString());
break;
default:
Log.w(TAG, "Skipping unsupported emsg version: " + version);
@ -717,7 +722,7 @@ public class FragmentedMp4Extractor implements Extractor {
*/
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
LeafAtom tfhd = checkNotNull(traf.getLeafAtomOfType(Atom.TYPE_tfhd));
@Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
if (trackBundle == null) {
return;
@ -730,7 +735,7 @@ public class FragmentedMp4Extractor implements Extractor {
trackBundle.currentlyInFragment = true;
@Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) {
fragment.nextFragmentDecodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
fragment.nextFragmentDecodeTime = parseTfdt(tfdtAtom.data);
fragment.nextFragmentDecodeTimeIncludesMoov = true;
} else {
fragment.nextFragmentDecodeTime = fragmentDecodeTime;
@ -742,11 +747,11 @@ public class FragmentedMp4Extractor implements Extractor {
@Nullable
TrackEncryptionBox encryptionBox =
trackBundle.moovSampleTable.track.getSampleDescriptionEncryptionBox(
fragment.header.sampleDescriptionIndex);
checkNotNull(fragment.header).sampleDescriptionIndex);
@Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
parseSaiz(encryptionBox, saiz.data, fragment);
parseSaiz(checkNotNull(encryptionBox), saiz.data, fragment);
}
@Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
@ -964,7 +969,7 @@ public class FragmentedMp4Extractor implements Extractor {
Track track = trackBundle.moovSampleTable.track;
TrackFragment fragment = trackBundle.fragment;
DefaultSampleValues defaultSampleValues = fragment.header;
DefaultSampleValues defaultSampleValues = castNonNull(fragment.header);
fragment.trunLength[index] = trun.readUnsignedIntToInt();
fragment.trunDataPosition[index] = fragment.dataPosition;
@ -994,7 +999,7 @@ public class FragmentedMp4Extractor implements Extractor {
&& track.editListDurations[0] == 0) {
edtsOffsetUs =
Util.scaleLargeTimestamp(
track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale);
castNonNull(track.editListMediaTimes)[0], C.MICROS_PER_SECOND, track.timescale);
}
int[] sampleSizeTable = fragment.sampleSizeTable;
@ -1161,7 +1166,7 @@ public class FragmentedMp4Extractor implements Extractor {
int perSampleIvSize = sgpd.readUnsignedByte();
byte[] keyId = new byte[16];
sgpd.readBytes(keyId, 0, keyId.length);
byte[] constantIv = null;
@Nullable byte[] constantIv = null;
if (perSampleIvSize == 0) {
int constantIvSize = sgpd.readUnsignedByte();
constantIv = new byte[constantIvSize];
@ -1238,7 +1243,7 @@ public class FragmentedMp4Extractor implements Extractor {
}
private void readEncryptionData(ExtractorInput input) throws IOException {
TrackBundle nextTrackBundle = null;
@Nullable TrackBundle nextTrackBundle = null;
long nextDataOffset = Long.MAX_VALUE;
int trackBundlesSize = trackBundles.size();
for (int i = 0; i < trackBundlesSize; i++) {
@ -1277,10 +1282,10 @@ public class FragmentedMp4Extractor implements Extractor {
* @throws IOException If an error occurs reading from the input.
*/
private boolean readSample(ExtractorInput input) throws IOException {
if (parserState == STATE_READING_SAMPLE_START) {
if (currentTrackBundle == null) {
@Nullable TrackBundle currentTrackBundle = getNextTrackBundle(trackBundles);
if (currentTrackBundle == null) {
@Nullable TrackBundle trackBundle = currentTrackBundle;
if (trackBundle == null) {
trackBundle = getNextTrackBundle(trackBundles);
if (trackBundle == null) {
// We've run out of samples in the current mdat. Discard any trailing data and prepare to
// read the header of the next atom.
int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());
@ -1292,7 +1297,7 @@ public class FragmentedMp4Extractor implements Extractor {
return false;
}
long nextDataPosition = currentTrackBundle.getCurrentSampleOffset();
long nextDataPosition = trackBundle.getCurrentSampleOffset();
// We skip bytes preceding the next sample to read.
int bytesToSkip = (int) (nextDataPosition - input.getPosition());
if (bytesToSkip < 0) {
@ -1301,47 +1306,46 @@ public class FragmentedMp4Extractor implements Extractor {
bytesToSkip = 0;
}
input.skipFully(bytesToSkip);
this.currentTrackBundle = currentTrackBundle;
currentTrackBundle = trackBundle;
}
if (parserState == STATE_READING_SAMPLE_START) {
sampleSize = trackBundle.getCurrentSampleSize();
sampleSize = currentTrackBundle.getCurrentSampleSize();
if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) {
if (trackBundle.currentSampleIndex < trackBundle.firstSampleToOutputIndex) {
input.skipFully(sampleSize);
currentTrackBundle.skipSampleEncryptionData();
if (!currentTrackBundle.next()) {
trackBundle.skipSampleEncryptionData();
if (!trackBundle.next()) {
currentTrackBundle = null;
}
parserState = STATE_READING_SAMPLE_START;
return true;
}
if (currentTrackBundle.moovSampleTable.track.sampleTransformation
if (trackBundle.moovSampleTable.track.sampleTransformation
== Track.TRANSFORMATION_CEA608_CDAT) {
sampleSize -= Atom.HEADER_SIZE;
input.skipFully(Atom.HEADER_SIZE);
}
if (MimeTypes.AUDIO_AC4.equals(
currentTrackBundle.moovSampleTable.track.format.sampleMimeType)) {
if (MimeTypes.AUDIO_AC4.equals(trackBundle.moovSampleTable.track.format.sampleMimeType)) {
// AC4 samples need to be prefixed with a clear sample header.
sampleBytesWritten =
currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE);
trackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE);
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
trackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
} else {
sampleBytesWritten =
currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0);
trackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0);
}
sampleSize += sampleBytesWritten;
parserState = STATE_READING_SAMPLE_CONTINUE;
sampleCurrentNalBytesRemaining = 0;
}
Track track = currentTrackBundle.moovSampleTable.track;
TrackOutput output = currentTrackBundle.output;
long sampleTimeUs = currentTrackBundle.getCurrentSamplePresentationTimeUs();
Track track = trackBundle.moovSampleTable.track;
TrackOutput output = trackBundle.output;
long sampleTimeUs = trackBundle.getCurrentSamplePresentationTimeUs();
if (timestampAdjuster != null) {
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
}
@ -1407,11 +1411,11 @@ public class FragmentedMp4Extractor implements Extractor {
}
}
@C.BufferFlags int sampleFlags = currentTrackBundle.getCurrentSampleFlags();
@C.BufferFlags int sampleFlags = trackBundle.getCurrentSampleFlags();
// Encryption data.
TrackOutput.CryptoData cryptoData = null;
TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted();
@Nullable TrackOutput.CryptoData cryptoData = null;
@Nullable TrackEncryptionBox encryptionBox = trackBundle.getEncryptionBoxIfEncrypted();
if (encryptionBox != null) {
cryptoData = encryptionBox.cryptoData;
}
@ -1420,7 +1424,7 @@ public class FragmentedMp4Extractor implements Extractor {
// After we have the sampleTimeUs, we can commit all the pending metadata samples
outputPendingMetadataSamples(sampleTimeUs);
if (!currentTrackBundle.next()) {
if (!trackBundle.next()) {
currentTrackBundle = null;
}
parserState = STATE_READING_SAMPLE_START;
@ -1452,7 +1456,7 @@ public class FragmentedMp4Extractor implements Extractor {
*/
@Nullable
private static TrackBundle getNextTrackBundle(SparseArray<TrackBundle> trackBundles) {
TrackBundle nextTrackBundle = null;
@Nullable TrackBundle nextTrackBundle = null;
long nextSampleOffset = Long.MAX_VALUE;
int trackBundlesSize = trackBundles.size();
@ -1579,6 +1583,8 @@ public class FragmentedMp4Extractor implements Extractor {
TrackSampleTable moovSampleTable,
DefaultSampleValues defaultSampleValues) {
this.output = output;
this.moovSampleTable = moovSampleTable;
this.defaultSampleValues = defaultSampleValues;
fragment = new TrackFragment();
scratch = new ParsableByteArray();
encryptionSignalByte = new ParsableByteArray(1);
@ -1587,9 +1593,8 @@ public class FragmentedMp4Extractor implements Extractor {
}
public void reset(TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues) {
Assertions.checkNotNull(moovSampleTable.track);
this.moovSampleTable = moovSampleTable;
this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues);
this.defaultSampleValues = defaultSampleValues;
output.format(moovSampleTable.track.format);
resetFragmentInfo();
}
@ -1598,7 +1603,7 @@ public class FragmentedMp4Extractor implements Extractor {
@Nullable
TrackEncryptionBox encryptionBox =
moovSampleTable.track.getSampleDescriptionEncryptionBox(
fragment.header.sampleDescriptionIndex);
castNonNull(fragment.header).sampleDescriptionIndex);
@Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;
DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType);
Format format =
@ -1706,7 +1711,7 @@ public class FragmentedMp4Extractor implements Extractor {
* @return The number of written bytes.
*/
public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) {
TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
@Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
if (encryptionBox == null) {
return 0;
}
@ -1718,7 +1723,7 @@ public class FragmentedMp4Extractor implements Extractor {
vectorSize = encryptionBox.perSampleIvSize;
} else {
// The default initialization vector should be used.
byte[] initVectorData = encryptionBox.defaultInitializationVector;
byte[] initVectorData = castNonNull(encryptionBox.defaultInitializationVector);
defaultInitializationVector.reset(initVectorData, initVectorData.length);
initializationVectorData = defaultInitializationVector;
vectorSize = initVectorData.length;
@ -1815,7 +1820,7 @@ public class FragmentedMp4Extractor implements Extractor {
// Encryption is not supported yet for samples specified in the sample table.
return null;
}
int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
int sampleDescriptionIndex = castNonNull(fragment.header).sampleDescriptionIndex;
@Nullable
TrackEncryptionBox encryptionBox =
fragment.trackEncryptionBox != null

View File

@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.Extractor;
@ -36,6 +35,7 @@ import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.SequenceableLoader;
@ -49,6 +49,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
@ -93,12 +94,13 @@ public final class HlsMediaSource extends BaseMediaSource
public static final class Factory implements MediaSourceFactory {
private final HlsDataSourceFactory hlsDataSourceFactory;
private final MediaSourceDrmHelper mediaSourceDrmHelper;
private HlsExtractorFactory extractorFactory;
private HlsPlaylistParserFactory playlistParserFactory;
private HlsPlaylistTracker.Factory playlistTrackerFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private DrmSessionManager drmSessionManager;
@Nullable private DrmSessionManager drmSessionManager;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation;
@MetadataType private int metadataType;
@ -125,10 +127,10 @@ public final class HlsMediaSource extends BaseMediaSource
*/
public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
this.hlsDataSourceFactory = checkNotNull(hlsDataSourceFactory);
mediaSourceDrmHelper = new MediaSourceDrmHelper();
playlistParserFactory = new DefaultHlsPlaylistParserFactory();
playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;
extractorFactory = HlsExtractorFactory.DEFAULT;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
metadataType = METADATA_TYPE_ID3;
@ -285,19 +287,22 @@ public final class HlsMediaSource extends BaseMediaSource
return this;
}
/**
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
* default value is {@link DrmSessionManager#DUMMY}.
*
* @param drmSessionManager The {@link DrmSessionManager}.
* @return This factory, for convenience.
*/
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
this.drmSessionManager = drmSessionManager;
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
return this;
}
@Override
public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
return this;
}
@ -374,7 +379,7 @@ public final class HlsMediaSource extends BaseMediaSource
hlsDataSourceFactory,
extractorFactory,
compositeSequenceableLoaderFactory,
drmSessionManager,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
playlistTrackerFactory.createTracker(
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),

View File

@ -27,7 +27,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaSourceFactory;
@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
import com.google.android.exoplayer2.upstream.Loader;
@ -77,10 +78,11 @@ public final class SsMediaSource extends BaseMediaSource
public static final class Factory implements MediaSourceFactory {
private final SsChunkSource.Factory chunkSourceFactory;
private final MediaSourceDrmHelper mediaSourceDrmHelper;
@Nullable private final DataSource.Factory manifestDataSourceFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private DrmSessionManager drmSessionManager;
@Nullable private DrmSessionManager drmSessionManager;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
@Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;
@ -111,7 +113,7 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable DataSource.Factory manifestDataSourceFactory) {
this.chunkSourceFactory = checkNotNull(chunkSourceFactory);
this.manifestDataSourceFactory = manifestDataSourceFactory;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
mediaSourceDrmHelper = new MediaSourceDrmHelper();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
@ -197,19 +199,22 @@ public final class SsMediaSource extends BaseMediaSource
return this;
}
/**
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
* default value is {@link DrmSessionManager#DUMMY}.
*
* @param drmSessionManager The {@link DrmSessionManager}.
* @return This factory, for convenience.
*/
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
this.drmSessionManager = drmSessionManager;
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
return this;
}
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
return this;
}
@ -280,7 +285,7 @@ public final class SsMediaSource extends BaseMediaSource
/* manifestParser= */ null,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
drmSessionManager,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs);
}
@ -357,7 +362,7 @@ public final class SsMediaSource extends BaseMediaSource
manifestParser,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
drmSessionManager,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs);
}

View File

@ -3,7 +3,7 @@
# Constructor method accessed via reflection in TrackSelectionDialogBuilder
-dontnote androidx.appcompat.app.AlertDialog.Builder
-keepclassmembers class androidx.appcompat.app.AlertDialog$Builder {
<init>(android.content.Context);
<init>(android.content.Context, int);
public android.content.Context getContext();
public androidx.appcompat.app.AlertDialog$Builder setTitle(java.lang.CharSequence);
public androidx.appcompat.app.AlertDialog$Builder setView(android.view.View);

View File

@ -1085,8 +1085,8 @@ public class PlayerControlView extends FrameLayout {
long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);
// Calculate the delay until the next update in real time, taking playbackSpeed into account.
float playbackSpeed = player.getPlaybackSpeed();
// Calculate the delay until the next update in real time, taking playback speed into account.
float playbackSpeed = player.getPlaybackParameters().speed;
long delayMs =
playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;

View File

@ -37,6 +37,7 @@ import androidx.media.app.NotificationCompat.MediaStyle;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@ -923,7 +924,7 @@ public class PlayerNotificationManager {
* <li>The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its
* duration} (like for example a live stream).
* <li>The media is not {@link Player#isPlayingAd() interrupted by an ad}.
* <li>The media is played at {@link Player#getPlaybackSpeed() regular speed}.
* <li>The media is played at {@link Player#getPlaybackParameters() regular speed}.
* <li>The device is running at least API 21 (Lollipop).
* </ul>
*
@ -1086,7 +1087,7 @@ public class PlayerNotificationManager {
&& player.isPlaying()
&& !player.isPlayingAd()
&& !player.isCurrentWindowDynamic()
&& player.getPlaybackSpeed() == 1f) {
&& player.getPlaybackParameters().speed == 1f) {
builder
.setWhen(System.currentTimeMillis() - player.getContentPosition())
.setShowWhen(true)
@ -1336,7 +1337,7 @@ public class PlayerNotificationManager {
}
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
postStartOrUpdateNotification();
}

View File

@ -42,6 +42,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RendererCapabilities;
@ -1408,8 +1409,8 @@ public class StyledPlayerControlView extends FrameLayout {
long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);
// Calculate the delay until the next update in real time, taking playbackSpeed into account.
float playbackSpeed = player.getPlaybackSpeed();
// Calculate the delay until the next update in real time, taking playback speed into account.
float playbackSpeed = player.getPlaybackParameters().speed;
long delayMs =
playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;
@ -1425,7 +1426,7 @@ public class StyledPlayerControlView extends FrameLayout {
if (player == null) {
return;
}
float speed = player.getPlaybackSpeed();
float speed = player.getPlaybackParameters().speed;
int currentSpeedMultBy100 = Math.round(speed * 100);
int indexForCurrentSpeed = playbackSpeedMultBy100List.indexOf(currentSpeedMultBy100);
if (indexForCurrentSpeed == UNDEFINED_POSITION) {
@ -1481,7 +1482,7 @@ public class StyledPlayerControlView extends FrameLayout {
if (player == null) {
return;
}
player.setPlaybackSpeed(speed);
player.setPlaybackParameters(new PlaybackParameters(speed));
}
/* package */ void requestPlayPauseFocus() {
@ -1771,7 +1772,7 @@ public class StyledPlayerControlView extends FrameLayout {
}
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
updateSettingsPlaybackSpeedLists();
}

View File

@ -25,6 +25,7 @@ import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import androidx.annotation.StyleRes;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
@ -51,6 +52,7 @@ public final class TrackSelectionDialogBuilder {
}
private final Context context;
@StyleRes private int themeResId;
private final CharSequence title;
private final MappedTrackInfo mappedTrackInfo;
private final int rendererIndex;
@ -124,6 +126,17 @@ public final class TrackSelectionDialogBuilder {
newOverrides.isEmpty() ? null : newOverrides.get(0)));
}
/**
* Sets the resource ID of the theme used to inflate this dialog.
*
* @param themeResId The resource ID of the theme.
* @return This builder, for convenience.
*/
public TrackSelectionDialogBuilder setTheme(@StyleRes int themeResId) {
this.themeResId = themeResId;
return this;
}
/**
* Sets whether the selection is initially shown as disabled.
*
@ -221,7 +234,7 @@ public final class TrackSelectionDialogBuilder {
}
private Dialog buildForPlatform() {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
AlertDialog.Builder builder = new AlertDialog.Builder(context, themeResId);
// Inflate with the builder's context to ensure the correct style is used.
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
@ -245,8 +258,8 @@ public final class TrackSelectionDialogBuilder {
// the APK size even with shrinking. See https://issuetracker.google.com/161514204.
// LINT.IfChange
Class<?> builderClazz = Class.forName("androidx.appcompat.app.AlertDialog$Builder");
Constructor<?> builderConstructor = builderClazz.getConstructor(Context.class);
Object builder = builderConstructor.newInstance(context);
Constructor<?> builderConstructor = builderClazz.getConstructor(Context.class, int.class);
Object builder = builderConstructor.newInstance(context, themeResId);
// Inflate with the builder's context to ensure the correct style is used.
Context builderContext = (Context) builderClazz.getMethod("getContext").invoke(builder);

View File

@ -134,6 +134,7 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/exo_custom_progress_thumb_size"
android:visibility="invisible">
</LinearLayout>

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.playbacktests.gts;
import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import com.google.android.exoplayer2.Player;
@ -64,7 +66,7 @@ public final class CommonEncryptionDrmTest {
@Test
public void cencSchemeTypeV18() {
if (Util.SDK_INT < 18) {
if (Util.SDK_INT < 18 || shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -74,23 +76,9 @@ public final class CommonEncryptionDrmTest {
.run();
}
@Test
public void cbc1SchemeTypeV25() {
if (Util.SDK_INT < 25) {
// cbc1 support was added in API 24, but it is stable from API 25 onwards.
// See [internal: b/65634809].
// Pass.
return;
}
testRunner
.setStreamName("test_widevine_h264_scheme_cbc1")
.setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBC1)
.run();
}
@Test
public void cbcsSchemeTypeV25() {
if (Util.SDK_INT < 25) {
if (Util.SDK_INT < 25 || shouldSkipWidevineTest(testRule.getActivity())) {
// cbcs support was added in API 24, but it is stable from API 25 onwards.
// See [internal: b/65634809].
// Pass.
@ -101,9 +89,4 @@ public final class CommonEncryptionDrmTest {
.setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBCS)
.run();
}
@Test
public void censSchemeTypeV25() {
// TODO: Implement once content is available. Track [internal: b/31219813].
}
}

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.playbacktests.gts;
import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -356,7 +357,7 @@ public final class DashStreamingTest {
@Test
public void widevineH264FixedV18() throws Exception {
if (Util.SDK_INT < 18) {
if (Util.SDK_INT < 18 || shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -373,7 +374,9 @@ public final class DashStreamingTest {
@Test
public void widevineH264AdaptiveV18() throws Exception {
if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {
if (Util.SDK_INT < 18
|| shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)
|| shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -390,7 +393,9 @@ public final class DashStreamingTest {
@Test
public void widevineH264AdaptiveWithSeekingV18() throws Exception {
if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {
if (Util.SDK_INT < 18
|| shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)
|| shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -408,7 +413,9 @@ public final class DashStreamingTest {
@Test
public void widevineH264AdaptiveWithRendererDisablingV18() throws Exception {
if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {
if (Util.SDK_INT < 18
|| shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)
|| GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -428,7 +435,7 @@ public final class DashStreamingTest {
@Test
public void widevineH265FixedV23() throws Exception {
if (Util.SDK_INT < 23) {
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -445,7 +452,7 @@ public final class DashStreamingTest {
@Test
public void widevineH265AdaptiveV24() throws Exception {
if (Util.SDK_INT < 24) {
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -462,7 +469,7 @@ public final class DashStreamingTest {
@Test
public void widevineH265AdaptiveWithSeekingV24() throws Exception {
if (Util.SDK_INT < 24) {
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -480,7 +487,7 @@ public final class DashStreamingTest {
@Test
public void widevineH265AdaptiveWithRendererDisablingV24() throws Exception {
if (Util.SDK_INT < 24) {
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -500,7 +507,7 @@ public final class DashStreamingTest {
@Test
public void widevineVp9Fixed360pV23() throws Exception {
if (Util.SDK_INT < 23) {
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -517,7 +524,7 @@ public final class DashStreamingTest {
@Test
public void widevineVp9AdaptiveV24() throws Exception {
if (Util.SDK_INT < 24) {
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -534,7 +541,7 @@ public final class DashStreamingTest {
@Test
public void widevineVp9AdaptiveWithSeekingV24() throws Exception {
if (Util.SDK_INT < 24) {
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -552,7 +559,7 @@ public final class DashStreamingTest {
@Test
public void widevineVp9AdaptiveWithRendererDisablingV24() throws Exception {
if (Util.SDK_INT < 24) {
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -573,7 +580,7 @@ public final class DashStreamingTest {
// 23.976 fps.
@Test
public void widevine23FpsH264FixedV23() throws Exception {
if (Util.SDK_INT < 23) {
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -591,7 +598,7 @@ public final class DashStreamingTest {
// 24 fps.
@Test
public void widevine24FpsH264FixedV23() throws Exception {
if (Util.SDK_INT < 23) {
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}
@ -609,7 +616,7 @@ public final class DashStreamingTest {
// 29.97 fps.
@Test
public void widevine29FpsH264FixedV23() throws Exception {
if (Util.SDK_INT < 23) {
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
// Pass.
return;
}

View File

@ -45,8 +45,6 @@ import com.google.android.exoplayer2.util.Util;
// Widevine encrypted content manifests using different common encryption schemes.
public static final String WIDEVINE_SCHEME_CENC = BASE_URL_COMMON_ENCRYPTION + "tears-cenc.mpd";
public static final String WIDEVINE_SCHEME_CBC1 =
BASE_URL_COMMON_ENCRYPTION + "tears-aes-cbc1.mpd";
public static final String WIDEVINE_SCHEME_CBCS =
BASE_URL_COMMON_ENCRYPTION + "tears-aes-cbcs.mpd";

View File

@ -100,7 +100,7 @@ public final class DashWidevineOfflineTest {
@Test
public void widevineOfflineLicenseV22() throws Exception {
if (Util.SDK_INT < 22) {
if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
return; // Pass.
}
downloadLicense();
@ -113,7 +113,9 @@ public final class DashWidevineOfflineTest {
@Test
public void widevineOfflineReleasedLicenseV22() throws Throwable {
if (Util.SDK_INT < 22 || Util.SDK_INT > 28) {
if (Util.SDK_INT < 22
|| Util.SDK_INT > 28
|| GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
return; // Pass.
}
downloadLicense();
@ -136,7 +138,7 @@ public final class DashWidevineOfflineTest {
@Test
public void widevineOfflineReleasedLicenseV29() throws Throwable {
if (Util.SDK_INT < 29) {
if (Util.SDK_INT < 29 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
return; // Pass.
}
downloadLicense();
@ -158,7 +160,7 @@ public final class DashWidevineOfflineTest {
@Test
public void widevineOfflineExpiredLicenseV22() throws Exception {
if (Util.SDK_INT < 22) {
if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
return; // Pass.
}
downloadLicense();
@ -188,7 +190,7 @@ public final class DashWidevineOfflineTest {
@Test
public void widevineOfflineLicenseExpiresOnPauseV22() throws Exception {
if (Util.SDK_INT < 22) {
if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
return; // Pass.
}
downloadLicense();
@ -198,7 +200,7 @@ public final class DashWidevineOfflineTest {
offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
long licenseDuration = licenseDurationRemainingSec.first;
assertWithMessage(
"License duration should be less than 30 sec. " + "Server settings might have changed.")
"License duration should be less than 30 sec. Server settings might have changed.")
.that(licenseDuration < 30)
.isTrue();
ActionSchedule schedule = new ActionSchedule.Builder(TAG)

View File

@ -24,6 +24,7 @@ import android.media.MediaFormat;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
@ -84,13 +85,14 @@ import java.util.ArrayList;
private final long[] timestampsList;
private final ArrayDeque<Long> inputFormatChangeTimesUs;
private final boolean enableMediaFormatChangeTimeCheck;
private boolean skipToPositionBeforeRenderingFirstFrame;
private boolean shouldMediaFormatChangeTimesBeChecked;
private int startIndex;
private int queueSize;
private int bufferCount;
private int minimumInsertIndex;
private boolean skipToPositionBeforeRenderingFirstFrame;
private boolean inputFormatChanged;
private boolean outputMediaFormatChanged;
@ -112,10 +114,6 @@ import java.util.ArrayList;
maxDroppedFrameCountToNotify);
timestampsList = new long[ARRAY_SIZE];
inputFormatChangeTimesUs = new ArrayDeque<>();
// As per [Internal ref: b/149818050, b/149751672], MediaFormat changes can occur early for
// SDK 29 and 30. Should be fixed for SDK 31 onwards.
enableMediaFormatChangeTimeCheck = Util.SDK_INT < 29 || Util.SDK_INT >= 31;
}
@Override
@ -137,6 +135,10 @@ import java.util.ArrayList;
// frames up to the current playback position [Internal: b/66494991].
skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED;
super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate);
// Output MediaFormat changes are known to occur too early until API 30 (see [internal:
// b/149818050, b/149751672]).
shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30;
}
@Override
@ -247,10 +249,12 @@ import java.util.ArrayList;
}
if (outputMediaFormatChanged) {
long inputFormatChangeTimeUs = inputFormatChangeTimesUs.remove();
long inputFormatChangeTimeUs =
inputFormatChangeTimesUs.isEmpty() ? C.TIME_UNSET : inputFormatChangeTimesUs.remove();
outputMediaFormatChanged = false;
if (enableMediaFormatChangeTimeCheck && presentationTimeUs != inputFormatChangeTimeUs) {
if (shouldMediaFormatChangeTimesBeChecked
&& presentationTimeUs != inputFormatChangeTimeUs) {
throw new IllegalStateException(
"Expected output MediaFormat change timestamp ("
+ presentationTimeUs

View File

@ -0,0 +1,50 @@
/*
* 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.playbacktests.gts;
import static com.google.android.exoplayer2.C.WIDEVINE_UUID;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.MediaDrm;
/** Utility methods for GTS tests. */
public final class GtsTestUtil {
private GtsTestUtil() {}
/** Returns true if the device doesn't support Widevine and this is permitted. */
public static boolean shouldSkipWidevineTest(Context context) {
if (isGmsInstalled(context)) {
// GMS devices are required to support Widevine.
return false;
}
// For non-GMS devices Widevine is optional.
return !MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID);
}
private static boolean isGmsInstalled(Context context) {
try {
context
.getPackageManager()
.getPackageInfo("com.google.android.gms", PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,81 @@
MediaCodec (audio/mp4a-latm):
buffers.length = 46
buffers[0] = length 23, hash 47DE9131
buffers[1] = length 6, hash 31EC5206
buffers[2] = length 148, hash 894A176B
buffers[3] = length 189, hash CEF235A1
buffers[4] = length 205, hash BBF5F7B0
buffers[5] = length 210, hash F278B193
buffers[6] = length 210, hash 82DA1589
buffers[7] = length 207, hash 5BE231DF
buffers[8] = length 225, hash 18819EE1
buffers[9] = length 215, hash CA7FA67B
buffers[10] = length 211, hash 581A1C18
buffers[11] = length 216, hash ADB88187
buffers[12] = length 229, hash 2E8BA4DC
buffers[13] = length 232, hash 22F0C510
buffers[14] = length 235, hash 867AD0DC
buffers[15] = length 231, hash 84E823A8
buffers[16] = length 226, hash 1BEF3A95
buffers[17] = length 216, hash EAA345AE
buffers[18] = length 229, hash 6957411F
buffers[19] = length 219, hash 41275022
buffers[20] = length 241, hash 6495DF96
buffers[21] = length 228, hash 63D95906
buffers[22] = length 238, hash 34F676F9
buffers[23] = length 234, hash E5CBC045
buffers[24] = length 231, hash 5FC43661
buffers[25] = length 217, hash 682708ED
buffers[26] = length 239, hash D43780FC
buffers[27] = length 243, hash C5E17980
buffers[28] = length 231, hash AC5837BA
buffers[29] = length 230, hash 169EE895
buffers[30] = length 238, hash C48FF3F1
buffers[31] = length 225, hash 531E4599
buffers[32] = length 232, hash CB3C6B8D
buffers[33] = length 243, hash F8C94C7
buffers[34] = length 232, hash A646A7D0
buffers[35] = length 237, hash E8B787A5
buffers[36] = length 228, hash 3FA7A29F
buffers[37] = length 235, hash B9B33B0A
buffers[38] = length 264, hash 71A4869E
buffers[39] = length 257, hash D049B54C
buffers[40] = length 227, hash 66757231
buffers[41] = length 227, hash BD374F1B
buffers[42] = length 235, hash 999477F6
buffers[43] = length 229, hash FFF98DF0
buffers[44] = length 6, hash 31B22286
buffers[45] = length 0, hash 1
MediaCodec (video/avc):
buffers.length = 31
buffers[0] = length 36692, hash D216076E
buffers[1] = length 5312, hash D45D3CA0
buffers[2] = length 599, hash 1BE7812D
buffers[3] = length 7735, hash 4490F110
buffers[4] = length 987, hash 560B5036
buffers[5] = length 673, hash ED7CD8C7
buffers[6] = length 523, hash 3020DF50
buffers[7] = length 6061, hash 736C72B2
buffers[8] = length 992, hash FE132F23
buffers[9] = length 623, hash 5B2C1816
buffers[10] = length 421, hash 742E69C1
buffers[11] = length 4899, hash F72F86A1
buffers[12] = length 568, hash 519A8E50
buffers[13] = length 620, hash 3990AA39
buffers[14] = length 5450, hash F06EC4AA
buffers[15] = length 1051, hash 92DFA63A
buffers[16] = length 874, hash 69587FB4
buffers[17] = length 781, hash 36BE495B
buffers[18] = length 4725, hash AC0C8CD3
buffers[19] = length 1022, hash 5D8BFF34
buffers[20] = length 790, hash 99413A99
buffers[21] = length 610, hash 5E129290
buffers[22] = length 2751, hash 769974CB
buffers[23] = length 745, hash B78A477A
buffers[24] = length 621, hash CF741E7A
buffers[25] = length 505, hash 1DB4894E
buffers[26] = length 1268, hash C15348DC
buffers[27] = length 880, hash C2DE85D0
buffers[28] = length 530, hash C98BC6A8
buffers[29] = length 568, hash 4FE5C8EA
buffers[30] = length 0, hash 1

View File

@ -0,0 +1,19 @@
MediaCodec (audio/mpeg-L2):
buffers.length = 5
buffers[0] = length 1253, hash 727FD1C6
buffers[1] = length 1254, hash 73FB07B8
buffers[2] = length 1254, hash 73FB07B8
buffers[3] = length 1254, hash 73FB07B8
buffers[4] = length 0, hash 1
MediaCodec (video/mpeg2):
buffers.length = 3
buffers[0] = length 20711, hash 34341E8
buffers[1] = length 18112, hash EC44B35B
buffers[2] = length 0, hash 1
MetadataOutput:
Metadata[0]:
entry[0] = SpliceInsertCommand
Metadata[1]:
entry[0] = SpliceInsertCommand
Metadata[2]:
entry[0] = SpliceInsertCommand

View File

@ -24,6 +24,7 @@ dependencies {
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-core')
implementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View File

@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.IllegalSeekPositionException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.PlayerMessage.Target;
@ -608,26 +609,26 @@ public abstract class Action {
}
}
/** Calls {@link Player#setPlaybackSpeed(float)}. */
public static final class SetPlaybackSpeed extends Action {
/** Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. */
public static final class SetPlaybackParameters extends Action {
private final float playbackSpeed;
private final PlaybackParameters playbackParameters;
/**
* Creates a set playback speed action instance.
* Creates a set playback parameters action instance.
*
* @param tag A tag to use for logging.
* @param playbackSpeed The playback speed.
* @param playbackParameters The playback parameters.
*/
public SetPlaybackSpeed(String tag, float playbackSpeed) {
super(tag, "SetPlaybackSpeed:" + playbackSpeed);
this.playbackSpeed = playbackSpeed;
public SetPlaybackParameters(String tag, PlaybackParameters playbackParameters) {
super(tag, "SetPlaybackParameters:" + playbackParameters);
this.playbackParameters = playbackParameters;
}
@Override
protected void doActionImpl(
SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) {
player.setPlaybackSpeed(playbackSpeed);
player.setPlaybackParameters(playbackParameters);
}
}

View File

@ -20,6 +20,7 @@ import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.PlayerMessage.Target;
@ -35,7 +36,7 @@ import com.google.android.exoplayer2.testutil.Action.Seek;
import com.google.android.exoplayer2.testutil.Action.SendMessages;
import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes;
import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady;
import com.google.android.exoplayer2.testutil.Action.SetPlaybackSpeed;
import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters;
import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled;
import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
@ -214,14 +215,14 @@ public final class ActionSchedule {
}
/**
* Schedules a playback speed setting action.
* Schedules a playback parameters setting action.
*
* @param playbackSpeed The playback speed to set.
* @param playbackParameters The playback parameters to set.
* @return The builder, for convenience.
* @see Player#setPlaybackSpeed(float)
* @see Player#setPlaybackParameters(PlaybackParameters)
*/
public Builder setPlaybackSpeed(float playbackSpeed) {
return apply(new SetPlaybackSpeed(tag, playbackSpeed));
public Builder setPlaybackParameters(PlaybackParameters playbackParameters) {
return apply(new SetPlaybackParameters(tag, playbackParameters));
}
/**

View File

@ -52,7 +52,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest {
DefaultAudioSink.failOnSpuriousAudioTimestamp = true;
}
public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000;
public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 5000;
public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2;
public static final long EXPECTED_PLAYING_TIME_UNSET = -1;

View File

@ -31,6 +31,7 @@ import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@ -48,7 +49,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod
@Nullable private final TransferListener transferListener;
private final long durationUs;
@MonotonicNonNull private Callback callback;
private @MonotonicNonNull Callback callback;
private ChunkSampleStream<FakeChunkSource>[] sampleStreams;
private SequenceableLoader sequenceableLoader;
@ -99,7 +100,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod
}
}
sampleStreams = newSampleStreamArray(validStreams.size());
validStreams.toArray(sampleStreams);
Util.nullSafeListToArray(validStreams, sampleStreams);
this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
return returnPositionUs;
}

View File

@ -30,6 +30,7 @@ public class FakeAudioRenderer extends FakeRenderer {
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
private final DecoderCounters decoderCounters;
private boolean notifiedAudioSessionId;
private boolean notifiedPositionAdvancing;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
super(C.TRACK_TYPE_AUDIO);
@ -43,6 +44,7 @@ public class FakeAudioRenderer extends FakeRenderer {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
notifiedAudioSessionId = false;
notifiedPositionAdvancing = false;
}
@Override
@ -67,6 +69,10 @@ public class FakeAudioRenderer extends FakeRenderer {
eventDispatcher.audioSessionId(/* audioSessionId= */ 1);
notifiedAudioSessionId = true;
}
if (shouldProcess && !notifiedPositionAdvancing) {
eventDispatcher.positionAdvancing(System.currentTimeMillis());
notifiedPositionAdvancing = true;
}
return shouldProcess;
}
}

View File

@ -217,8 +217,7 @@ public class FakeDataSource extends BaseDataSource {
* this method.
*/
public final DataSpec[] getAndClearOpenedDataSpecs() {
DataSpec[] dataSpecs = new DataSpec[openedDataSpecs.size()];
openedDataSpecs.toArray(dataSpecs);
DataSpec[] dataSpecs = openedDataSpecs.toArray(new DataSpec[0]);
openedDataSpecs.clear();
return dataSpecs;
}

View File

@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.Iterables;
import java.io.IOException;
@ -274,10 +273,7 @@ public class FakeSampleStream implements SampleStream {
@Nullable DrmSession previousSession = currentDrmSession;
Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper());
currentDrmSession =
newDrmInitData != null
? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat)
: drmSessionManager.acquirePlaceholderSession(
playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType));
drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat);
outputFormatHolder.drmSession = currentDrmSession;
if (previousSession != null) {

View File

@ -0,0 +1,93 @@
/*
* 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.testutil;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Class to capture output from a playback test.
*
* <p>Implements {@link Dumper.Dumpable} so the output can be easily dumped to a string for
* comparison against previous test runs.
*/
public final class PlaybackOutput implements Dumper.Dumpable {
private final ShadowMediaCodecConfig codecConfig;
// TODO: Add support for subtitles too
private final List<Metadata> metadatas;
private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) {
this.codecConfig = codecConfig;
metadatas = Collections.synchronizedList(new ArrayList<>());
// TODO: Consider passing playback position into MetadataOutput and TextOutput. Calling
// player.getCurrentPosition() inside onMetadata/Cues will likely be non-deterministic
// because renderer-thread != playback-thread.
player.addMetadataOutput(metadatas::add);
}
/**
* Create an instance that captures the metadata and text output from {@code player} and the audio
* and video output via the {@link TeeCodec TeeCodecs} exposed by {@code mediaCodecConfig}.
*
* <p>Must be called <b>before</b> playback to ensure metadata and text output is captured
* correctly.
*
* @param player The {@link SimpleExoPlayer} to capture metadata and text output from.
* @param mediaCodecConfig The {@link ShadowMediaCodecConfig} to capture audio and video output
* from.
* @return A new instance that can be used to dump the playback output.
*/
public static PlaybackOutput register(
SimpleExoPlayer player, ShadowMediaCodecConfig mediaCodecConfig) {
return new PlaybackOutput(player, mediaCodecConfig);
}
@Override
public void dump(Dumper dumper) {
ImmutableMap<String, TeeCodec> codecs = codecConfig.getCodecs();
ImmutableList<String> mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet());
for (String mimeType : mimeTypes) {
dumper.add(Assertions.checkNotNull(codecs.get(mimeType)));
}
dumpMetadata(dumper);
}
private void dumpMetadata(Dumper dumper) {
if (metadatas.isEmpty()) {
return;
}
dumper.startBlock("MetadataOutput");
for (int i = 0; i < metadatas.size(); i++) {
dumper.startBlock("Metadata[" + i + "]");
Metadata metadata = metadatas.get(i);
for (int j = 0; j < metadata.length(); j++) {
dumper.add("entry[" + j + "]", metadata.get(j).getClass().getSimpleName());
}
dumper.endBlock();
}
dumper.endBlock();
}
}

View File

@ -0,0 +1,133 @@
/*
* 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.testutil;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.rules.ExternalResource;
import org.robolectric.shadows.MediaCodecInfoBuilder;
import org.robolectric.shadows.ShadowMediaCodec;
import org.robolectric.shadows.ShadowMediaCodecList;
/**
* A JUnit @Rule to configure Roboelectric's {@link ShadowMediaCodec}.
*
* <p>Registers a {@link org.robolectric.shadows.ShadowMediaCodec.CodecConfig} for each audio/video
* MIME type known by ExoPlayer, and provides access to the bytes passed to these via {@link
* TeeCodec}.
*/
public final class ShadowMediaCodecConfig extends ExternalResource {
private final Map<String, TeeCodec> codecsByMimeType;
private ShadowMediaCodecConfig() {
this.codecsByMimeType = new HashMap<>();
}
public static ShadowMediaCodecConfig forAllSupportedMimeTypes() {
return new ShadowMediaCodecConfig();
}
public ImmutableMap<String, TeeCodec> getCodecs() {
return ImmutableMap.copyOf(codecsByMimeType);
}
@Override
protected void before() throws Throwable {
// Video codecs
MediaCodecInfo.CodecProfileLevel avcProfileLevel =
createProfileLevel(
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
MediaCodecInfo.CodecProfileLevel.AVCLevel62);
configureCodec(
/* codecName= */ "exotest.video.avc",
MimeTypes.VIDEO_H264,
ImmutableList.of(avcProfileLevel),
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
MediaCodecInfo.CodecProfileLevel mpeg2ProfileLevel =
createProfileLevel(
MediaCodecInfo.CodecProfileLevel.MPEG2ProfileMain,
MediaCodecInfo.CodecProfileLevel.MPEG2LevelML);
configureCodec(
/* codecName= */ "exotest.video.mpeg2",
MimeTypes.VIDEO_MPEG2,
ImmutableList.of(mpeg2ProfileLevel),
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
// Audio codecs
configureCodec("exotest.audio.aac", MimeTypes.AUDIO_AAC);
configureCodec("exotest.audio.mpegl2", MimeTypes.AUDIO_MPEG_L2);
}
@Override
protected void after() {
codecsByMimeType.clear();
ShadowMediaCodecList.reset();
ShadowMediaCodec.clearCodecs();
}
private void configureCodec(String codecName, String mimeType) {
configureCodec(
codecName,
mimeType,
/* profileLevels= */ ImmutableList.of(),
/* colorFormats= */ ImmutableList.of());
}
private void configureCodec(
String codecName,
String mimeType,
List<MediaCodecInfo.CodecProfileLevel> profileLevels,
List<Integer> colorFormats) {
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString(MediaFormat.KEY_MIME, mimeType);
MediaCodecInfoBuilder.CodecCapabilitiesBuilder capabilities =
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder().setMediaFormat(mediaFormat);
if (!profileLevels.isEmpty()) {
capabilities.setProfileLevels(profileLevels.toArray(new MediaCodecInfo.CodecProfileLevel[0]));
}
if (!colorFormats.isEmpty()) {
capabilities.setColorFormats(Ints.toArray(colorFormats));
}
ShadowMediaCodecList.addCodec(
MediaCodecInfoBuilder.newBuilder()
.setName(codecName)
.setCapabilities(capabilities.build())
.build());
// TODO: Update ShadowMediaCodec to consider the MediaFormat.KEY_MAX_INPUT_SIZE value passed
// to configure() so we don't have to specify large buffers here.
TeeCodec codec = new TeeCodec(mimeType);
ShadowMediaCodec.addDecoder(
codecName,
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 50_000, /* outputBufferSize= */ 50_000, codec));
codecsByMimeType.put(mimeType, codec);
}
private static MediaCodecInfo.CodecProfileLevel createProfileLevel(int profile, int level) {
MediaCodecInfo.CodecProfileLevel profileLevel = new MediaCodecInfo.CodecProfileLevel();
profileLevel.profile = profile;
profileLevel.level = level;
return profileLevel;
}
}

View File

@ -314,32 +314,16 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
throw new UnsupportedOperationException();
}
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
throw new UnsupportedOperationException();
}
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public PlaybackParameters getPlaybackParameters() {
throw new UnsupportedOperationException();
}
@Override
public void setPlaybackSpeed(float playbackSpeed) {
throw new UnsupportedOperationException();
}
@Override
public float getPlaybackSpeed() {
throw new UnsupportedOperationException();
}
@Override
public void setSeekParameters(@Nullable SeekParameters seekParameters) {
throw new UnsupportedOperationException();

View File

@ -0,0 +1,80 @@
/*
* 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.testutil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.robolectric.shadows.ShadowMediaCodec;
/**
* A {@link ShadowMediaCodec.CodecConfig.Codec} for Robolectric's {@link ShadowMediaCodec} that
* records the contents of buffers passed to it before copying the contents into the output buffer.
*
* <p>This also implements {@link Dumper.Dumpable} so the recorded buffers can be written out to a
* dump file.
*/
public final class TeeCodec implements ShadowMediaCodec.CodecConfig.Codec, Dumper.Dumpable {
private final String mimeType;
private final List<byte[]> receivedBuffers;
public TeeCodec(String mimeType) {
this.mimeType = mimeType;
this.receivedBuffers = Collections.synchronizedList(new ArrayList<>());
}
@Override
public void process(ByteBuffer in, ByteBuffer out) {
byte[] bytes = new byte[in.remaining()];
in.get(bytes);
receivedBuffers.add(bytes);
if (!MimeTypes.isAudio(mimeType)) {
// Don't output audio bytes, because ShadowAudioTrack doesn't advance the playback position so
// playback never completes.
// TODO: Update ShadowAudioTrack to advance the playback position in a realistic way.
out.put(bytes);
}
}
@Override
public void dump(Dumper dumper) {
if (receivedBuffers.isEmpty()) {
return;
}
dumper.startBlock("MediaCodec (" + mimeType + ")");
dumper.add("buffers.length", receivedBuffers.size());
for (int i = 0; i < receivedBuffers.size(); i++) {
dumper.add("buffers[" + i + "]", receivedBuffers.get(i));
}
dumper.endBlock();
}
/**
* Return the buffers received by this codec.
*
* <p>The list is sorted in the order the buffers were passed to {@link #process(ByteBuffer,
* ByteBuffer)}.
*/
public ImmutableList<byte[]> getReceivedBuffers() {
return ImmutableList.copyOf(receivedBuffers);
}
}