mirror of
https://github.com/androidx/media.git
synced 2025-05-12 10:09:55 +08:00
commit
4b1e0fa9fc
@ -1,5 +1,33 @@
|
||||
# Release notes
|
||||
|
||||
### 2.13.1 (2021-02-12)
|
||||
|
||||
* Live streaming:
|
||||
* Fix playback issue for HLS live streams without program date time
|
||||
information ([#8560](https://github.com/google/ExoPlayer/issues/8560)).
|
||||
* Fix playback issue for multi-period DASH live streams
|
||||
([#8537](https://github.com/google/ExoPlayer/issues/8537)).
|
||||
* Fix playback failures when playing live streams with video tunneling
|
||||
enabled ([#8570](https://github.com/google/ExoPlayer/issues/8570)).
|
||||
* IMA extension:
|
||||
* Fix handling of repeated ad loads, to avoid ads being discarded if the
|
||||
user seeks away and then back to a preloaded postroll (for example).
|
||||
* Fix a bug where an assertion would fail if the player started to buffer
|
||||
an ad media period before the ad URI was known then an ad state update
|
||||
arrived that didn't set the ad URI.
|
||||
* Add `ImaAdsLoader.focusSkipButton` to allow apps to request that the
|
||||
skip button should receive UI focus, if shown
|
||||
([#8565](https://github.com/google/ExoPlayer/issues/8565)).
|
||||
* DRM:
|
||||
* Re-use the previous `DrmSessionManager` instance when playing a playlist
|
||||
(if possible)
|
||||
([#8523](https://github.com/google/ExoPlayer/issues/8523)).
|
||||
* Propagate DRM configuration when creating media sources for ad content
|
||||
([#8568](https://github.com/google/ExoPlayer/issues/8568)).
|
||||
* Only release 'keepalive' references to `DrmSession` in
|
||||
`DefaultDrmSessionManager#release()` if keepalive is enabled
|
||||
([#8576](https://github.com/google/ExoPlayer/issues/8576)).
|
||||
|
||||
### 2.13.0 (2021-02-04)
|
||||
|
||||
* Core library:
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.13.0'
|
||||
releaseVersionCode = 2013000
|
||||
releaseVersion = '2.13.1'
|
||||
releaseVersionCode = 2013001
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||
|
@ -281,6 +281,16 @@ import java.util.Map;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves UI focus to the skip button (or other interactive elements), if currently shown. See
|
||||
* {@link AdsManager#focus()}.
|
||||
*/
|
||||
public void focusSkipButton() {
|
||||
if (adsManager != null) {
|
||||
adsManager.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts passing events from this instance (including any pending ad playback state) and
|
||||
* registers obstructions.
|
||||
@ -879,7 +889,8 @@ import java.util.Map;
|
||||
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
|
||||
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
|
||||
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
||||
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
|
||||
// The ad URI may already be known, so force put to update it if needed.
|
||||
adInfoByAdMediaInfo.forcePut(adMediaInfo, adInfo);
|
||||
if (configuration.debugModeEnabled) {
|
||||
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo));
|
||||
}
|
||||
|
@ -473,6 +473,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves UI focus to the skip button (or other interactive elements), if currently shown. See
|
||||
* {@link AdsManager#focus()}.
|
||||
*/
|
||||
public void focusSkipButton() {
|
||||
if (currentAdTagLoader != null) {
|
||||
currentAdTagLoader.focusSkipButton();
|
||||
}
|
||||
}
|
||||
|
||||
// AdsLoader implementation.
|
||||
|
||||
@Override
|
||||
|
@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.13.0";
|
||||
public static final String VERSION = "2.13.1";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.0";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.1";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2013000;
|
||||
public static final int VERSION_INT = 2013001;
|
||||
|
||||
/**
|
||||
* The default user agent for requests made by the library.
|
||||
|
@ -182,9 +182,10 @@ public final class AdPlaybackState {
|
||||
/** Returns a new instance with the specified ad durations, in microseconds. */
|
||||
@CheckResult
|
||||
public AdGroup withAdDurationsUs(long[] durationsUs) {
|
||||
Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);
|
||||
if (durationsUs.length < this.uris.length) {
|
||||
if (durationsUs.length < uris.length) {
|
||||
durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length);
|
||||
} else if (count != C.LENGTH_UNSET && durationsUs.length > uris.length) {
|
||||
durationsUs = Arrays.copyOf(durationsUs, uris.length);
|
||||
}
|
||||
return new AdGroup(count, states, uris, durationsUs);
|
||||
}
|
||||
|
@ -880,7 +880,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
// Adjust live playback speed to new position.
|
||||
if (playbackInfo.playWhenReady
|
||||
&& playbackInfo.playbackState == Player.STATE_READY
|
||||
&& isCurrentPeriodInMovingLiveWindow()
|
||||
&& shouldUseLivePlaybackSpeedControl(playbackInfo.timeline, playbackInfo.periodId)
|
||||
&& playbackInfo.playbackParameters.speed == 1f) {
|
||||
float adjustedSpeed =
|
||||
livePlaybackSpeedControl.getAdjustedPlaybackSpeed(
|
||||
@ -1051,17 +1051,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
- (periodPositionUs + period.getPositionInWindowUs());
|
||||
}
|
||||
|
||||
private boolean isCurrentPeriodInMovingLiveWindow() {
|
||||
return isInMovingLiveWindow(playbackInfo.timeline, playbackInfo.periodId);
|
||||
}
|
||||
|
||||
private boolean isInMovingLiveWindow(Timeline timeline, MediaPeriodId mediaPeriodId) {
|
||||
private boolean shouldUseLivePlaybackSpeedControl(
|
||||
Timeline timeline, MediaPeriodId mediaPeriodId) {
|
||||
if (mediaPeriodId.isAd() || timeline.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
|
||||
timeline.getWindow(windowIndex, window);
|
||||
return window.isLive() && window.isDynamic;
|
||||
return window.isLive() && window.isDynamic && window.windowStartTimeMs != C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
|
||||
@ -1725,7 +1722,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
|
||||
long targetLiveOffsetUs =
|
||||
isInMovingLiveWindow(playbackInfo.timeline, queue.getPlayingPeriod().info.id)
|
||||
shouldUseLivePlaybackSpeedControl(playbackInfo.timeline, queue.getPlayingPeriod().info.id)
|
||||
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
|
||||
: C.TIME_UNSET;
|
||||
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
|
||||
@ -1831,7 +1828,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
Timeline oldTimeline,
|
||||
MediaPeriodId oldPeriodId,
|
||||
long positionForTargetOffsetOverrideUs) {
|
||||
if (newTimeline.isEmpty() || !isInMovingLiveWindow(newTimeline, newPeriodId)) {
|
||||
if (newTimeline.isEmpty() || !shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) {
|
||||
// Live playback speed control is unused.
|
||||
return;
|
||||
}
|
||||
|
@ -488,7 +488,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
throws ConfigurationException {
|
||||
int inputPcmFrameSize;
|
||||
@Nullable AudioProcessor[] availableAudioProcessors;
|
||||
boolean canApplyPlaybackParameters;
|
||||
|
||||
@OutputMode int outputMode;
|
||||
@C.Encoding int outputEncoding;
|
||||
@ -500,11 +499,10 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
Assertions.checkArgument(Util.isEncodingLinearPcm(inputFormat.pcmEncoding));
|
||||
|
||||
inputPcmFrameSize = Util.getPcmFrameSize(inputFormat.pcmEncoding, inputFormat.channelCount);
|
||||
boolean useFloatOutput =
|
||||
enableFloatOutput && Util.isEncodingHighResolutionPcm(inputFormat.pcmEncoding);
|
||||
availableAudioProcessors =
|
||||
useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors;
|
||||
canApplyPlaybackParameters = !useFloatOutput;
|
||||
shouldUseFloatOutput(inputFormat.pcmEncoding)
|
||||
? toFloatPcmAvailableAudioProcessors
|
||||
: toIntPcmAvailableAudioProcessors;
|
||||
|
||||
trimmingAudioProcessor.setTrimFrameCount(
|
||||
inputFormat.encoderDelay, inputFormat.encoderPadding);
|
||||
@ -541,7 +539,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
} else {
|
||||
inputPcmFrameSize = C.LENGTH_UNSET;
|
||||
availableAudioProcessors = new AudioProcessor[0];
|
||||
canApplyPlaybackParameters = false;
|
||||
outputSampleRate = inputFormat.sampleRate;
|
||||
outputPcmFrameSize = C.LENGTH_UNSET;
|
||||
if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) {
|
||||
@ -586,7 +583,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
outputEncoding,
|
||||
specifiedBufferSize,
|
||||
enableAudioTrackPlaybackParams,
|
||||
canApplyPlaybackParameters,
|
||||
availableAudioProcessors);
|
||||
if (isAudioTrackInitialized()) {
|
||||
this.pendingConfiguration = pendingConfiguration;
|
||||
@ -1336,11 +1332,11 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
|
||||
private void applyAudioProcessorPlaybackParametersAndSkipSilence(long presentationTimeUs) {
|
||||
PlaybackParameters playbackParameters =
|
||||
configuration.canApplyPlaybackParameters
|
||||
shouldApplyAudioProcessorPlaybackParameters()
|
||||
? audioProcessorChain.applyPlaybackParameters(getAudioProcessorPlaybackParameters())
|
||||
: PlaybackParameters.DEFAULT;
|
||||
boolean skipSilenceEnabled =
|
||||
configuration.canApplyPlaybackParameters
|
||||
shouldApplyAudioProcessorPlaybackParameters()
|
||||
? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled())
|
||||
: DEFAULT_SKIP_SILENCE;
|
||||
mediaPositionParametersCheckpoints.add(
|
||||
@ -1355,6 +1351,31 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether audio processor playback parameters should be applied in the current
|
||||
* configuration.
|
||||
*/
|
||||
private boolean shouldApplyAudioProcessorPlaybackParameters() {
|
||||
// We don't apply speed/pitch adjustment using an audio processor in the following cases:
|
||||
// - in tunneling mode, because audio processing can change the duration of audio yet the video
|
||||
// frame presentation times are currently not modified (see also
|
||||
// https://github.com/google/ExoPlayer/issues/4803);
|
||||
// - when playing encoded audio via passthrough/offload, because modifying the audio stream
|
||||
// would require decoding/re-encoding; and
|
||||
// - when outputting float PCM audio, because SonicAudioProcessor outputs 16-bit integer PCM.
|
||||
return !tunneling
|
||||
&& MimeTypes.AUDIO_RAW.equals(configuration.inputFormat.sampleMimeType)
|
||||
&& !shouldUseFloatOutput(configuration.inputFormat.pcmEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether audio in the specified PCM encoding should be written to the audio track as
|
||||
* float PCM.
|
||||
*/
|
||||
private boolean shouldUseFloatOutput(@C.PcmEncoding int pcmEncoding) {
|
||||
return enableFloatOutput && Util.isEncodingHighResolutionPcm(pcmEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies and updates media position parameters.
|
||||
*
|
||||
@ -1897,7 +1918,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
public final int outputChannelConfig;
|
||||
@C.Encoding public final int outputEncoding;
|
||||
public final int bufferSize;
|
||||
public final boolean canApplyPlaybackParameters;
|
||||
public final AudioProcessor[] availableAudioProcessors;
|
||||
|
||||
public Configuration(
|
||||
@ -1910,7 +1930,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
int outputEncoding,
|
||||
int specifiedBufferSize,
|
||||
boolean enableAudioTrackPlaybackParams,
|
||||
boolean canApplyPlaybackParameters,
|
||||
AudioProcessor[] availableAudioProcessors) {
|
||||
this.inputFormat = inputFormat;
|
||||
this.inputPcmFrameSize = inputPcmFrameSize;
|
||||
@ -1919,7 +1938,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
this.outputSampleRate = outputSampleRate;
|
||||
this.outputChannelConfig = outputChannelConfig;
|
||||
this.outputEncoding = outputEncoding;
|
||||
this.canApplyPlaybackParameters = canApplyPlaybackParameters;
|
||||
this.availableAudioProcessors = availableAudioProcessors;
|
||||
|
||||
// Call computeBufferSize() last as it depends on the other configuration values.
|
||||
|
@ -457,12 +457,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
if (--prepareCallsCount != 0) {
|
||||
return;
|
||||
}
|
||||
// Make a local copy, because sessions are removed from this.sessions during release (via
|
||||
// callback).
|
||||
List<DefaultDrmSession> sessions = new ArrayList<>(this.sessions);
|
||||
for (int i = 0; i < sessions.size(); i++) {
|
||||
// Release all the keepalive acquisitions.
|
||||
sessions.get(i).release(/* eventDispatcher= */ null);
|
||||
// Release all keepalive acquisitions if keepalive is enabled.
|
||||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||
// Make a local copy, because sessions are removed from this.sessions during release (via
|
||||
// callback).
|
||||
List<DefaultDrmSession> sessions = new ArrayList<>(this.sessions);
|
||||
for (int i = 0; i < sessions.size(); i++) {
|
||||
sessions.get(i).release(/* eventDispatcher= */ null);
|
||||
}
|
||||
}
|
||||
Assertions.checkNotNull(exoMediaDrm).release();
|
||||
exoMediaDrm = null;
|
||||
|
@ -16,23 +16,38 @@
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
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;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Default implementation of {@link DrmSessionManagerProvider}. */
|
||||
public final class DefaultDrmSessionManagerProvider implements DrmSessionManagerProvider {
|
||||
|
||||
private final Object lock;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private MediaItem.@MonotonicNonNull DrmConfiguration drmConfiguration;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private @MonotonicNonNull DrmSessionManager manager;
|
||||
|
||||
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
|
||||
@Nullable private String userAgent;
|
||||
|
||||
public DefaultDrmSessionManagerProvider() {
|
||||
lock = new Object();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
|
||||
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
|
||||
@ -60,12 +75,24 @@ public final class DefaultDrmSessionManagerProvider implements DrmSessionManager
|
||||
|
||||
@Override
|
||||
public DrmSessionManager get(MediaItem mediaItem) {
|
||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
||||
checkNotNull(mediaItem.playbackProperties);
|
||||
@Nullable
|
||||
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
||||
if (drmConfiguration == null || Util.SDK_INT < 18) {
|
||||
return DrmSessionManager.DRM_UNSUPPORTED;
|
||||
}
|
||||
|
||||
synchronized (lock) {
|
||||
if (!Util.areEqual(drmConfiguration, this.drmConfiguration)) {
|
||||
this.drmConfiguration = drmConfiguration;
|
||||
this.manager = createManager(drmConfiguration);
|
||||
}
|
||||
return checkNotNull(this.manager);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(18)
|
||||
private DrmSessionManager createManager(MediaItem.DrmConfiguration drmConfiguration) {
|
||||
HttpDataSource.Factory dataSourceFactory =
|
||||
drmHttpDataSourceFactory != null
|
||||
? drmHttpDataSourceFactory
|
||||
|
@ -318,8 +318,28 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
&& adIndexInAdGroup < adPlaybackState.adGroups[adGroupIndex].uris.length) {
|
||||
@Nullable Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup];
|
||||
if (adUri != null) {
|
||||
MediaSource adMediaSource =
|
||||
adMediaSourceFactory.createMediaSource(MediaItem.fromUri(adUri));
|
||||
MediaItem.Builder adMediaItem = new MediaItem.Builder().setUri(adUri);
|
||||
// Propagate the content's DRM config into the ad media source.
|
||||
@Nullable
|
||||
MediaItem.PlaybackProperties contentPlaybackProperties =
|
||||
contentMediaSource.getMediaItem().playbackProperties;
|
||||
if (contentPlaybackProperties != null
|
||||
&& contentPlaybackProperties.drmConfiguration != null) {
|
||||
MediaItem.DrmConfiguration drmConfiguration =
|
||||
contentPlaybackProperties.drmConfiguration;
|
||||
// TODO(internal b/179984779): Use MediaItem.Builder#setDrmConfiguration() when it's
|
||||
// available.
|
||||
adMediaItem.setDrmUuid(drmConfiguration.uuid);
|
||||
adMediaItem.setDrmKeySetId(drmConfiguration.getKeySetId());
|
||||
adMediaItem.setDrmLicenseUri(drmConfiguration.licenseUri);
|
||||
adMediaItem.setDrmForceDefaultLicenseUri(drmConfiguration.forceDefaultLicenseUri);
|
||||
adMediaItem.setDrmLicenseRequestHeaders(drmConfiguration.requestHeaders);
|
||||
adMediaItem.setDrmMultiSession(drmConfiguration.multiSession);
|
||||
adMediaItem.setDrmPlayClearContentWithoutKey(
|
||||
drmConfiguration.playClearContentWithoutKey);
|
||||
adMediaItem.setDrmSessionForClearTypes(drmConfiguration.sessionForClearTypes);
|
||||
}
|
||||
MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adMediaItem.build());
|
||||
adMediaSourceHolder.initializeWithMediaSource(adMediaSource, adUri);
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.SilenceMediaSource;
|
||||
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
@ -83,6 +84,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.FakeAdaptiveDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeChunkSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeClock;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
|
||||
@ -8833,6 +8835,42 @@ public final class ExoPlayerTest {
|
||||
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void targetLiveOffsetInMedia_unknownWindowStartTime_doesNotAdjustLiveOffset()
|
||||
throws Exception {
|
||||
FakeClock fakeClock = new AutoAdvancingFakeClock(/* initialTimeMs= */ 987_654_321L);
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).setClock(fakeClock).build();
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(4_000).build();
|
||||
Timeline liveTimeline =
|
||||
new SinglePeriodTimeline(
|
||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
||||
/* periodDurationUs= */ 1000 * C.MICROS_PER_SECOND,
|
||||
/* windowDurationUs= */ 1000 * C.MICROS_PER_SECOND,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowDefaultStartPositionUs= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* manifest= */ null,
|
||||
mediaItem,
|
||||
mediaItem.liveConfiguration);
|
||||
player.pause();
|
||||
player.setMediaSource(new FakeMediaSource(liveTimeline));
|
||||
player.prepare();
|
||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
|
||||
|
||||
long playbackStartTimeMs = fakeClock.elapsedRealtime();
|
||||
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
|
||||
long playbackEndTimeMs = fakeClock.elapsedRealtime();
|
||||
player.release();
|
||||
|
||||
// Assert that the time it took to play 999 seconds of media is 999 seconds (asserting that no
|
||||
// playback speed adjustment was used).
|
||||
assertThat(playbackEndTimeMs - playbackStartTimeMs).isEqualTo(999_000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noTargetLiveOffsetInMedia_doesNotAdjustLiveOffset() throws Exception {
|
||||
long windowStartUnixTimeMs = 987_654_321_000L;
|
||||
|
@ -320,6 +320,20 @@ public final class DefaultAudioSinkTest {
|
||||
assertThat(thrown.format).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPlaybackParameters_doesNothingWhenTunnelingIsEnabled() throws Exception {
|
||||
defaultAudioSink.setAudioSessionId(1);
|
||||
defaultAudioSink.enableTunnelingV21();
|
||||
defaultAudioSink.setPlaybackParameters(new PlaybackParameters(2));
|
||||
configureDefaultAudioSink(/* channelCount= */ 2);
|
||||
defaultAudioSink.handleBuffer(
|
||||
createDefaultSilenceBuffer(),
|
||||
/* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND,
|
||||
/* encodedAccessUnitCount= */ 1);
|
||||
|
||||
assertThat(defaultAudioSink.getPlaybackParameters().speed).isEqualTo(1);
|
||||
}
|
||||
|
||||
private void configureDefaultAudioSink(int channelCount) throws AudioSink.ConfigurationException {
|
||||
configureDefaultAudioSink(channelCount, /* trimStartFrames= */ 0, /* trimEndFrames= */ 0);
|
||||
}
|
||||
|
@ -147,6 +147,32 @@ public class DefaultDrmSessionManagerTest {
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void managerRelease_keepaliveDisabled_doesntReleaseAnySessions() throws Exception {
|
||||
FakeExoMediaDrm.LicenseServer licenseServer =
|
||||
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
|
||||
DrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||
.build(/* mediaDrmCallback= */ licenseServer);
|
||||
|
||||
drmSessionManager.prepare();
|
||||
DrmSession drmSession =
|
||||
checkNotNull(
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
FORMAT_WITH_DRM_INIT_DATA));
|
||||
waitForOpenedWithKeys(drmSession);
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
|
||||
// Release the manager, the session should still be open (though it's unusable because
|
||||
// the underlying ExoMediaDrm is released).
|
||||
drmSessionManager.release();
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception {
|
||||
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
|
||||
|
@ -51,4 +51,39 @@ public class DefaultDrmSessionManagerProviderTest {
|
||||
|
||||
assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DRM_UNSUPPORTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_reusesCachedInstanceWherePossible() {
|
||||
MediaItem mediaItem1 =
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://example.test/content-1")
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.build();
|
||||
// Same DRM info as item1, but different URL to check it doesn't prevent re-using a manager.
|
||||
MediaItem mediaItem2 =
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://example.test/content-2")
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.build();
|
||||
// Different DRM info to 1 and 2, needs a different manager instance.
|
||||
MediaItem mediaItem3 =
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://example.test/content-3")
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.setDrmLicenseUri("https://example.test/license")
|
||||
.build();
|
||||
|
||||
DefaultDrmSessionManagerProvider provider = new DefaultDrmSessionManagerProvider();
|
||||
DrmSessionManager drmSessionManager1 = provider.get(mediaItem1);
|
||||
DrmSessionManager drmSessionManager2 = provider.get(mediaItem2);
|
||||
DrmSessionManager drmSessionManager3 = provider.get(mediaItem3);
|
||||
|
||||
// Get a manager for the first item again - expect it to be a different instance to last time
|
||||
// since we only cache one.
|
||||
DrmSessionManager drmSessionManager4 = provider.get(mediaItem1);
|
||||
|
||||
assertThat(drmSessionManager1).isSameInstanceAs(drmSessionManager2);
|
||||
assertThat(drmSessionManager1).isNotSameInstanceAs(drmSessionManager3);
|
||||
assertThat(drmSessionManager1).isNotSameInstanceAs(drmSessionManager4);
|
||||
}
|
||||
}
|
||||
|
@ -901,77 +901,54 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
}
|
||||
}
|
||||
// Update the window.
|
||||
boolean windowChangingImplicitly = false;
|
||||
Period firstPeriod = manifest.getPeriod(0);
|
||||
int lastPeriodIndex = manifest.getPeriodCount() - 1;
|
||||
Period lastPeriod = manifest.getPeriod(lastPeriodIndex);
|
||||
long lastPeriodDurationUs = manifest.getPeriodDurationUs(lastPeriodIndex);
|
||||
long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs));
|
||||
// Get the period-relative start/end times.
|
||||
long currentStartTimeUs =
|
||||
getAvailableStartTimeUs(
|
||||
manifest.getPeriod(0), manifest.getPeriodDurationUs(0), nowUnixTimeUs);
|
||||
long currentEndTimeUs = getAvailableEndTimeUs(lastPeriod, lastPeriodDurationUs, nowUnixTimeUs);
|
||||
if (manifest.dynamic && !isIndexExplicit(lastPeriod)) {
|
||||
// The manifest describes an incomplete live stream. Update the start/end times to reflect the
|
||||
// live stream duration and the manifest's time shift buffer depth.
|
||||
long liveStreamEndPositionInLastPeriodUs = currentEndTimeUs - C.msToUs(lastPeriod.startMs);
|
||||
currentEndTimeUs = min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs);
|
||||
if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {
|
||||
long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);
|
||||
long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs;
|
||||
int periodIndex = lastPeriodIndex;
|
||||
while (offsetInPeriodUs < 0 && periodIndex > 0) {
|
||||
offsetInPeriodUs += manifest.getPeriodDurationUs(--periodIndex);
|
||||
}
|
||||
if (periodIndex == 0) {
|
||||
currentStartTimeUs = max(currentStartTimeUs, offsetInPeriodUs);
|
||||
} else {
|
||||
// The time shift buffer starts after the earliest period.
|
||||
// TODO: Does this ever happen?
|
||||
currentStartTimeUs = manifest.getPeriodDurationUs(0);
|
||||
}
|
||||
}
|
||||
windowChangingImplicitly = true;
|
||||
long windowStartTimeInManifestUs =
|
||||
getAvailableStartTimeInManifestUs(
|
||||
firstPeriod, manifest.getPeriodDurationUs(0), nowUnixTimeUs);
|
||||
long windowEndTimeInManifestUs =
|
||||
getAvailableEndTimeInManifestUs(lastPeriod, lastPeriodDurationUs, nowUnixTimeUs);
|
||||
boolean windowChangingImplicitly = manifest.dynamic && !isIndexExplicit(lastPeriod);
|
||||
if (windowChangingImplicitly && manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {
|
||||
// Update the available start time to reflect the manifest's time shift buffer depth.
|
||||
long timeShiftBufferStartTimeInManifestUs =
|
||||
windowEndTimeInManifestUs - C.msToUs(manifest.timeShiftBufferDepthMs);
|
||||
windowStartTimeInManifestUs =
|
||||
max(windowStartTimeInManifestUs, timeShiftBufferStartTimeInManifestUs);
|
||||
}
|
||||
long windowDurationUs = currentEndTimeUs - currentStartTimeUs;
|
||||
for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {
|
||||
windowDurationUs += manifest.getPeriodDurationUs(i);
|
||||
}
|
||||
|
||||
long windowStartTimeMs = C.TIME_UNSET;
|
||||
if (manifest.availabilityStartTimeMs != C.TIME_UNSET) {
|
||||
windowStartTimeMs =
|
||||
manifest.availabilityStartTimeMs
|
||||
+ manifest.getPeriod(0).startMs
|
||||
+ C.usToMs(currentStartTimeUs);
|
||||
}
|
||||
|
||||
long windowDefaultStartPositionUs = 0;
|
||||
long windowDurationUs = windowEndTimeInManifestUs - windowStartTimeInManifestUs;
|
||||
long windowStartUnixTimeMs = C.TIME_UNSET;
|
||||
long windowDefaultPositionUs = 0;
|
||||
if (manifest.dynamic) {
|
||||
updateMediaItemLiveConfiguration(
|
||||
/* nowPeriodTimeUs= */ currentStartTimeUs + nowUnixTimeUs - C.msToUs(windowStartTimeMs),
|
||||
/* windowStartPeriodTimeUs= */ currentStartTimeUs,
|
||||
/* windowEndPeriodTimeUs= */ currentEndTimeUs);
|
||||
windowDefaultStartPositionUs =
|
||||
nowUnixTimeUs - C.msToUs(windowStartTimeMs + liveConfiguration.targetOffsetMs);
|
||||
long minimumDefaultStartPositionUs =
|
||||
checkState(manifest.availabilityStartTimeMs != C.TIME_UNSET);
|
||||
long nowInWindowUs =
|
||||
nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs) - windowStartTimeInManifestUs;
|
||||
updateMediaItemLiveConfiguration(nowInWindowUs, windowDurationUs);
|
||||
windowStartUnixTimeMs =
|
||||
manifest.availabilityStartTimeMs + C.usToMs(windowStartTimeInManifestUs);
|
||||
windowDefaultPositionUs = nowInWindowUs - C.msToUs(liveConfiguration.targetOffsetMs);
|
||||
long minimumWindowDefaultPositionUs =
|
||||
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
||||
if (windowDefaultStartPositionUs < minimumDefaultStartPositionUs) {
|
||||
// The default start position is too close to the start of the live window. Set it to the
|
||||
// minimum default start position provided the window is at least twice as big. Else set
|
||||
// it to the middle of the window.
|
||||
windowDefaultStartPositionUs = minimumDefaultStartPositionUs;
|
||||
if (windowDefaultPositionUs < minimumWindowDefaultPositionUs) {
|
||||
// The default position is too close to the start of the live window. Set it to the minimum
|
||||
// default position provided the window is at least twice as big. Else set it to the middle
|
||||
// of the window.
|
||||
windowDefaultPositionUs = minimumWindowDefaultPositionUs;
|
||||
}
|
||||
}
|
||||
long offsetInFirstPeriodUs = windowStartTimeInManifestUs - C.msToUs(firstPeriod.startMs);
|
||||
DashTimeline timeline =
|
||||
new DashTimeline(
|
||||
manifest.availabilityStartTimeMs,
|
||||
windowStartTimeMs,
|
||||
windowStartUnixTimeMs,
|
||||
elapsedRealtimeOffsetMs,
|
||||
firstPeriodId,
|
||||
/* offsetInFirstPeriodUs= */ currentStartTimeUs,
|
||||
offsetInFirstPeriodUs,
|
||||
windowDurationUs,
|
||||
windowDefaultStartPositionUs,
|
||||
windowDefaultPositionUs,
|
||||
manifest,
|
||||
mediaItem,
|
||||
manifest.dynamic ? liveConfiguration : null);
|
||||
@ -1008,8 +985,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMediaItemLiveConfiguration(
|
||||
long nowPeriodTimeUs, long windowStartPeriodTimeUs, long windowEndPeriodTimeUs) {
|
||||
private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) {
|
||||
long maxLiveOffsetMs;
|
||||
if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) {
|
||||
maxLiveOffsetMs = mediaItem.liveConfiguration.maxOffsetMs;
|
||||
@ -1017,7 +993,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
&& manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) {
|
||||
maxLiveOffsetMs = manifest.serviceDescription.maxOffsetMs;
|
||||
} else {
|
||||
maxLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowStartPeriodTimeUs);
|
||||
maxLiveOffsetMs = C.usToMs(nowInWindowUs);
|
||||
}
|
||||
long minLiveOffsetMs;
|
||||
if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) {
|
||||
@ -1026,7 +1002,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
&& manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) {
|
||||
minLiveOffsetMs = manifest.serviceDescription.minOffsetMs;
|
||||
} else {
|
||||
minLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowEndPeriodTimeUs);
|
||||
minLiveOffsetMs = C.usToMs(nowInWindowUs - windowDurationUs);
|
||||
if (minLiveOffsetMs < 0 && maxLiveOffsetMs > 0) {
|
||||
// The current time is in the window, so assume all clocks are synchronized and set the
|
||||
// minimum to a live offset of zero.
|
||||
@ -1052,12 +1028,10 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
targetOffsetMs = minLiveOffsetMs;
|
||||
}
|
||||
if (targetOffsetMs > maxLiveOffsetMs) {
|
||||
long windowDurationUs = windowEndPeriodTimeUs - windowStartPeriodTimeUs;
|
||||
long liveOffsetAtWindowStartUs = nowPeriodTimeUs - windowStartPeriodTimeUs;
|
||||
long safeDistanceFromWindowStartUs =
|
||||
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
||||
long maxTargetOffsetForSafeDistanceToWindowStartMs =
|
||||
C.usToMs(liveOffsetAtWindowStartUs - safeDistanceFromWindowStartUs);
|
||||
C.usToMs(nowInWindowUs - safeDistanceFromWindowStartUs);
|
||||
targetOffsetMs =
|
||||
Util.constrainValue(
|
||||
maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs);
|
||||
@ -1147,9 +1121,10 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING);
|
||||
}
|
||||
|
||||
private static long getAvailableStartTimeUs(
|
||||
private static long getAvailableStartTimeInManifestUs(
|
||||
Period period, long periodDurationUs, long nowUnixTimeUs) {
|
||||
long availableStartTimeUs = 0;
|
||||
long periodStartTimeInManifestUs = C.msToUs(period.startMs);
|
||||
long availableStartTimeInManifestUs = periodStartTimeInManifestUs;
|
||||
boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period);
|
||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||
@ -1162,23 +1137,26 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
}
|
||||
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
|
||||
if (index == null) {
|
||||
return 0;
|
||||
return periodStartTimeInManifestUs;
|
||||
}
|
||||
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
|
||||
if (availableSegmentCount == 0) {
|
||||
return 0;
|
||||
return periodStartTimeInManifestUs;
|
||||
}
|
||||
long firstAvailableSegmentNum =
|
||||
index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
|
||||
long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstAvailableSegmentNum);
|
||||
availableStartTimeUs = max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);
|
||||
long adaptationSetAvailableStartTimeInManifestUs =
|
||||
periodStartTimeInManifestUs + index.getTimeUs(firstAvailableSegmentNum);
|
||||
availableStartTimeInManifestUs =
|
||||
max(availableStartTimeInManifestUs, adaptationSetAvailableStartTimeInManifestUs);
|
||||
}
|
||||
return availableStartTimeUs;
|
||||
return availableStartTimeInManifestUs;
|
||||
}
|
||||
|
||||
private static long getAvailableEndTimeUs(
|
||||
private static long getAvailableEndTimeInManifestUs(
|
||||
Period period, long periodDurationUs, long nowUnixTimeUs) {
|
||||
long availableEndTimeUs = Long.MAX_VALUE;
|
||||
long periodStartTimeInManifestUs = C.msToUs(period.startMs);
|
||||
long availableEndTimeInManifestUs = Long.MAX_VALUE;
|
||||
boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period);
|
||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||
@ -1191,21 +1169,23 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
}
|
||||
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
|
||||
if (index == null) {
|
||||
return periodDurationUs;
|
||||
return periodStartTimeInManifestUs + periodDurationUs;
|
||||
}
|
||||
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
|
||||
if (availableSegmentCount == 0) {
|
||||
return 0;
|
||||
return periodStartTimeInManifestUs;
|
||||
}
|
||||
long firstAvailableSegmentNum =
|
||||
index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
|
||||
long lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1;
|
||||
long adaptationSetAvailableEndTimeUs =
|
||||
index.getTimeUs(lastAvailableSegmentNum)
|
||||
long adaptationSetAvailableEndTimeInManifestUs =
|
||||
periodStartTimeInManifestUs
|
||||
+ index.getTimeUs(lastAvailableSegmentNum)
|
||||
+ index.getDurationUs(lastAvailableSegmentNum, periodDurationUs);
|
||||
availableEndTimeUs = min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);
|
||||
availableEndTimeInManifestUs =
|
||||
min(availableEndTimeInManifestUs, adaptationSetAvailableEndTimeInManifestUs);
|
||||
}
|
||||
return availableEndTimeUs;
|
||||
return availableEndTimeInManifestUs;
|
||||
}
|
||||
|
||||
private static boolean isIndexExplicit(Period period) {
|
||||
|
@ -30,9 +30,7 @@ public class Period {
|
||||
*/
|
||||
@Nullable public final String id;
|
||||
|
||||
/**
|
||||
* The start time of the period in milliseconds.
|
||||
*/
|
||||
/** The start time of the period in milliseconds, relative to the start of the manifest. */
|
||||
public final long startMs;
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,7 @@
|
||||
package com.google.android.exoplayer2.transformer;
|
||||
|
||||
/** A custom interface that determines the speed for media at specific timestamps. */
|
||||
public interface SpeedProvider {
|
||||
/* package */ interface SpeedProvider {
|
||||
|
||||
/**
|
||||
* Provides the speed that the media should be played at, based on the timeUs.
|
||||
|
Loading…
x
Reference in New Issue
Block a user