diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f5353a24e1c842f38b5ebe21af58bee3f62ffb76..b2ac9f0197db6e7bf8e510ff5c43e49482939a4d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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: diff --git a/constants.gradle b/constants.gradle index bb775e70509b81852fe5e45e593a5400aa10664d..7b73235144d50d493acc63c7855036c6ec8c849a 100644 --- a/constants.gradle +++ b/constants.gradle @@ -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. diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java index 4ce0610fb67c836b903e77aaa876ab4a4dadfb7d..9908e4940cc9cbf804f8bcf8c8b8ba191e8e2b9a 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java @@ -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)); } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index e2adbaf2d020f976afc41cd9df09cc446d8e53aa..336a560042b32b04842622dce5d09e1451180176 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -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 diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 21f352590cc7224c00f2b903cd77276843fecc84..0315cfc9cda7dea843c52a43633babd9212a6081 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -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. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index a50fcd7d1dcef931be920faf995e5581945016bd..b70dd10c380b22cf3a9416dcd4a9167ab0d25aa9 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 755d7511c4e631726868e58d249c64e62314d1f3..f791d05b1326e86cd3a505959604080f36234ed8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 8f16df115a45348b78add475083ca3bdc59e36e4..826839b3d2e1aaab2dba84a2969fe4e181b1a6d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 67cb095b8d92bfff8060aac75fc36243f7c70358..d24d5ce847fc6e51dfef03167ec52b8a8ab15948 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -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 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 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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java index 10bd2953d5c625569bef07967ae2c11e62785e65..d8a16fc077d730627cdf865c630c41993e62dc04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java @@ -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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 4f2617e868792c59d96cee41aa799cb3b4766aa2..be1ab81cd394c739c5204b8b5f321c75066b6002 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -318,8 +318,28 @@ public final class AdsMediaSource extends CompositeMediaSource { && 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); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 008d8c6b537474bb54d2c70a64e1d92be5c544d9..e743858875a2a1f994b911e353745a00f015a2c7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -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; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java index 7b9e639cd642b0c3279a48cd367ae1c3e75b65f7..13c62c96b9d6f7fa445f7742a2eee5122c208d81 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java @@ -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); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java index 5ac26a76b3116197b633349837d9e33c9a411393..c0b83e7a656c7e0335015538c8e9aa048d4d86c8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java @@ -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 secondSchemeDatas = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java index 4e597b6371710f5496d75a0fbbd12bd09706c7da..0c830ca5abfbacdb8be36740d1813e0602bbbb7e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java @@ -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); + } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 258ebf327045eccb07a91e56be03ce983e3eac33..d63295adde20ddec467176c7bfcf630d02137137 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -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 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 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 = 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) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index b472aed50cf9e7467eb98a1e3b261d48706eec6c..b5b852ed7e819dc9fd2c77bfcf6b8d1b75879e68 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -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; /** diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java index f8109e031c7db4e147e27534a8dea523e91896da..ad2b3077504f6234a34b6decf28958086ef4eed3 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java @@ -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.