Merge pull request #8582 from google/dev-v2-r2.13.1

r2.13.1
This commit is contained in:
Oliver Woodman 2021-02-13 00:56:14 +00:00 committed by GitHub
commit 4b1e0fa9fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 326 additions and 121 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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