mirror of
https://github.com/androidx/media.git
synced 2025-05-15 03:29:53 +08:00
commit
4b1e0fa9fc
@ -1,5 +1,33 @@
|
|||||||
# Release notes
|
# 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)
|
### 2.13.0 (2021-02-04)
|
||||||
|
|
||||||
* Core library:
|
* Core library:
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.13.0'
|
releaseVersion = '2.13.1'
|
||||||
releaseVersionCode = 2013000
|
releaseVersionCode = 2013001
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
appTargetSdkVersion = 29
|
appTargetSdkVersion = 29
|
||||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
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
|
* Starts passing events from this instance (including any pending ad playback state) and
|
||||||
* registers obstructions.
|
* registers obstructions.
|
||||||
@ -879,7 +889,8 @@ import java.util.Map;
|
|||||||
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
|
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
|
||||||
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
|
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
|
||||||
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
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) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo));
|
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.
|
// AdsLoader implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
|
|
||||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
/** 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.
|
// 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}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// 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.
|
* 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).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// 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.
|
* 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. */
|
/** Returns a new instance with the specified ad durations, in microseconds. */
|
||||||
@CheckResult
|
@CheckResult
|
||||||
public AdGroup withAdDurationsUs(long[] durationsUs) {
|
public AdGroup withAdDurationsUs(long[] durationsUs) {
|
||||||
Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);
|
if (durationsUs.length < uris.length) {
|
||||||
if (durationsUs.length < this.uris.length) {
|
|
||||||
durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, 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);
|
return new AdGroup(count, states, uris, durationsUs);
|
||||||
}
|
}
|
||||||
|
@ -880,7 +880,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
// Adjust live playback speed to new position.
|
// Adjust live playback speed to new position.
|
||||||
if (playbackInfo.playWhenReady
|
if (playbackInfo.playWhenReady
|
||||||
&& playbackInfo.playbackState == Player.STATE_READY
|
&& playbackInfo.playbackState == Player.STATE_READY
|
||||||
&& isCurrentPeriodInMovingLiveWindow()
|
&& shouldUseLivePlaybackSpeedControl(playbackInfo.timeline, playbackInfo.periodId)
|
||||||
&& playbackInfo.playbackParameters.speed == 1f) {
|
&& playbackInfo.playbackParameters.speed == 1f) {
|
||||||
float adjustedSpeed =
|
float adjustedSpeed =
|
||||||
livePlaybackSpeedControl.getAdjustedPlaybackSpeed(
|
livePlaybackSpeedControl.getAdjustedPlaybackSpeed(
|
||||||
@ -1051,17 +1051,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
- (periodPositionUs + period.getPositionInWindowUs());
|
- (periodPositionUs + period.getPositionInWindowUs());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCurrentPeriodInMovingLiveWindow() {
|
private boolean shouldUseLivePlaybackSpeedControl(
|
||||||
return isInMovingLiveWindow(playbackInfo.timeline, playbackInfo.periodId);
|
Timeline timeline, MediaPeriodId mediaPeriodId) {
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInMovingLiveWindow(Timeline timeline, MediaPeriodId mediaPeriodId) {
|
|
||||||
if (mediaPeriodId.isAd() || timeline.isEmpty()) {
|
if (mediaPeriodId.isAd() || timeline.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
|
int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
|
||||||
timeline.getWindow(windowIndex, window);
|
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) {
|
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.
|
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
|
||||||
long targetLiveOffsetUs =
|
long targetLiveOffsetUs =
|
||||||
isInMovingLiveWindow(playbackInfo.timeline, queue.getPlayingPeriod().info.id)
|
shouldUseLivePlaybackSpeedControl(playbackInfo.timeline, queue.getPlayingPeriod().info.id)
|
||||||
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
|
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
|
||||||
: C.TIME_UNSET;
|
: C.TIME_UNSET;
|
||||||
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
|
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
|
||||||
@ -1831,7 +1828,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
Timeline oldTimeline,
|
Timeline oldTimeline,
|
||||||
MediaPeriodId oldPeriodId,
|
MediaPeriodId oldPeriodId,
|
||||||
long positionForTargetOffsetOverrideUs) {
|
long positionForTargetOffsetOverrideUs) {
|
||||||
if (newTimeline.isEmpty() || !isInMovingLiveWindow(newTimeline, newPeriodId)) {
|
if (newTimeline.isEmpty() || !shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) {
|
||||||
// Live playback speed control is unused.
|
// Live playback speed control is unused.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -488,7 +488,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
throws ConfigurationException {
|
throws ConfigurationException {
|
||||||
int inputPcmFrameSize;
|
int inputPcmFrameSize;
|
||||||
@Nullable AudioProcessor[] availableAudioProcessors;
|
@Nullable AudioProcessor[] availableAudioProcessors;
|
||||||
boolean canApplyPlaybackParameters;
|
|
||||||
|
|
||||||
@OutputMode int outputMode;
|
@OutputMode int outputMode;
|
||||||
@C.Encoding int outputEncoding;
|
@C.Encoding int outputEncoding;
|
||||||
@ -500,11 +499,10 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
Assertions.checkArgument(Util.isEncodingLinearPcm(inputFormat.pcmEncoding));
|
Assertions.checkArgument(Util.isEncodingLinearPcm(inputFormat.pcmEncoding));
|
||||||
|
|
||||||
inputPcmFrameSize = Util.getPcmFrameSize(inputFormat.pcmEncoding, inputFormat.channelCount);
|
inputPcmFrameSize = Util.getPcmFrameSize(inputFormat.pcmEncoding, inputFormat.channelCount);
|
||||||
boolean useFloatOutput =
|
|
||||||
enableFloatOutput && Util.isEncodingHighResolutionPcm(inputFormat.pcmEncoding);
|
|
||||||
availableAudioProcessors =
|
availableAudioProcessors =
|
||||||
useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors;
|
shouldUseFloatOutput(inputFormat.pcmEncoding)
|
||||||
canApplyPlaybackParameters = !useFloatOutput;
|
? toFloatPcmAvailableAudioProcessors
|
||||||
|
: toIntPcmAvailableAudioProcessors;
|
||||||
|
|
||||||
trimmingAudioProcessor.setTrimFrameCount(
|
trimmingAudioProcessor.setTrimFrameCount(
|
||||||
inputFormat.encoderDelay, inputFormat.encoderPadding);
|
inputFormat.encoderDelay, inputFormat.encoderPadding);
|
||||||
@ -541,7 +539,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
} else {
|
} else {
|
||||||
inputPcmFrameSize = C.LENGTH_UNSET;
|
inputPcmFrameSize = C.LENGTH_UNSET;
|
||||||
availableAudioProcessors = new AudioProcessor[0];
|
availableAudioProcessors = new AudioProcessor[0];
|
||||||
canApplyPlaybackParameters = false;
|
|
||||||
outputSampleRate = inputFormat.sampleRate;
|
outputSampleRate = inputFormat.sampleRate;
|
||||||
outputPcmFrameSize = C.LENGTH_UNSET;
|
outputPcmFrameSize = C.LENGTH_UNSET;
|
||||||
if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) {
|
if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) {
|
||||||
@ -586,7 +583,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
outputEncoding,
|
outputEncoding,
|
||||||
specifiedBufferSize,
|
specifiedBufferSize,
|
||||||
enableAudioTrackPlaybackParams,
|
enableAudioTrackPlaybackParams,
|
||||||
canApplyPlaybackParameters,
|
|
||||||
availableAudioProcessors);
|
availableAudioProcessors);
|
||||||
if (isAudioTrackInitialized()) {
|
if (isAudioTrackInitialized()) {
|
||||||
this.pendingConfiguration = pendingConfiguration;
|
this.pendingConfiguration = pendingConfiguration;
|
||||||
@ -1336,11 +1332,11 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
|
|
||||||
private void applyAudioProcessorPlaybackParametersAndSkipSilence(long presentationTimeUs) {
|
private void applyAudioProcessorPlaybackParametersAndSkipSilence(long presentationTimeUs) {
|
||||||
PlaybackParameters playbackParameters =
|
PlaybackParameters playbackParameters =
|
||||||
configuration.canApplyPlaybackParameters
|
shouldApplyAudioProcessorPlaybackParameters()
|
||||||
? audioProcessorChain.applyPlaybackParameters(getAudioProcessorPlaybackParameters())
|
? audioProcessorChain.applyPlaybackParameters(getAudioProcessorPlaybackParameters())
|
||||||
: PlaybackParameters.DEFAULT;
|
: PlaybackParameters.DEFAULT;
|
||||||
boolean skipSilenceEnabled =
|
boolean skipSilenceEnabled =
|
||||||
configuration.canApplyPlaybackParameters
|
shouldApplyAudioProcessorPlaybackParameters()
|
||||||
? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled())
|
? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled())
|
||||||
: DEFAULT_SKIP_SILENCE;
|
: DEFAULT_SKIP_SILENCE;
|
||||||
mediaPositionParametersCheckpoints.add(
|
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.
|
* Applies and updates media position parameters.
|
||||||
*
|
*
|
||||||
@ -1897,7 +1918,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
public final int outputChannelConfig;
|
public final int outputChannelConfig;
|
||||||
@C.Encoding public final int outputEncoding;
|
@C.Encoding public final int outputEncoding;
|
||||||
public final int bufferSize;
|
public final int bufferSize;
|
||||||
public final boolean canApplyPlaybackParameters;
|
|
||||||
public final AudioProcessor[] availableAudioProcessors;
|
public final AudioProcessor[] availableAudioProcessors;
|
||||||
|
|
||||||
public Configuration(
|
public Configuration(
|
||||||
@ -1910,7 +1930,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
int outputEncoding,
|
int outputEncoding,
|
||||||
int specifiedBufferSize,
|
int specifiedBufferSize,
|
||||||
boolean enableAudioTrackPlaybackParams,
|
boolean enableAudioTrackPlaybackParams,
|
||||||
boolean canApplyPlaybackParameters,
|
|
||||||
AudioProcessor[] availableAudioProcessors) {
|
AudioProcessor[] availableAudioProcessors) {
|
||||||
this.inputFormat = inputFormat;
|
this.inputFormat = inputFormat;
|
||||||
this.inputPcmFrameSize = inputPcmFrameSize;
|
this.inputPcmFrameSize = inputPcmFrameSize;
|
||||||
@ -1919,7 +1938,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
this.outputSampleRate = outputSampleRate;
|
this.outputSampleRate = outputSampleRate;
|
||||||
this.outputChannelConfig = outputChannelConfig;
|
this.outputChannelConfig = outputChannelConfig;
|
||||||
this.outputEncoding = outputEncoding;
|
this.outputEncoding = outputEncoding;
|
||||||
this.canApplyPlaybackParameters = canApplyPlaybackParameters;
|
|
||||||
this.availableAudioProcessors = availableAudioProcessors;
|
this.availableAudioProcessors = availableAudioProcessors;
|
||||||
|
|
||||||
// Call computeBufferSize() last as it depends on the other configuration values.
|
// Call computeBufferSize() last as it depends on the other configuration values.
|
||||||
|
@ -457,12 +457,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
if (--prepareCallsCount != 0) {
|
if (--prepareCallsCount != 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Make a local copy, because sessions are removed from this.sessions during release (via
|
// Release all keepalive acquisitions if keepalive is enabled.
|
||||||
// callback).
|
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||||
List<DefaultDrmSession> sessions = new ArrayList<>(this.sessions);
|
// Make a local copy, because sessions are removed from this.sessions during release (via
|
||||||
for (int i = 0; i < sessions.size(); i++) {
|
// callback).
|
||||||
// Release all the keepalive acquisitions.
|
List<DefaultDrmSession> sessions = new ArrayList<>(this.sessions);
|
||||||
sessions.get(i).release(/* eventDispatcher= */ null);
|
for (int i = 0; i < sessions.size(); i++) {
|
||||||
|
sessions.get(i).release(/* eventDispatcher= */ null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Assertions.checkNotNull(exoMediaDrm).release();
|
Assertions.checkNotNull(exoMediaDrm).release();
|
||||||
exoMediaDrm = null;
|
exoMediaDrm = null;
|
||||||
|
@ -16,23 +16,38 @@
|
|||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
|
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.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/** Default implementation of {@link DrmSessionManagerProvider}. */
|
/** Default implementation of {@link DrmSessionManagerProvider}. */
|
||||||
public final class DefaultDrmSessionManagerProvider implements 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 HttpDataSource.Factory drmHttpDataSourceFactory;
|
||||||
@Nullable private String userAgent;
|
@Nullable private String userAgent;
|
||||||
|
|
||||||
|
public DefaultDrmSessionManagerProvider() {
|
||||||
|
lock = new Object();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
|
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
|
||||||
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
|
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
|
||||||
@ -60,12 +75,24 @@ public final class DefaultDrmSessionManagerProvider implements DrmSessionManager
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DrmSessionManager get(MediaItem mediaItem) {
|
public DrmSessionManager get(MediaItem mediaItem) {
|
||||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
checkNotNull(mediaItem.playbackProperties);
|
||||||
@Nullable
|
@Nullable
|
||||||
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
||||||
if (drmConfiguration == null || Util.SDK_INT < 18) {
|
if (drmConfiguration == null || Util.SDK_INT < 18) {
|
||||||
return DrmSessionManager.DRM_UNSUPPORTED;
|
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 =
|
HttpDataSource.Factory dataSourceFactory =
|
||||||
drmHttpDataSourceFactory != null
|
drmHttpDataSourceFactory != null
|
||||||
? drmHttpDataSourceFactory
|
? drmHttpDataSourceFactory
|
||||||
|
@ -318,8 +318,28 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
&& adIndexInAdGroup < adPlaybackState.adGroups[adGroupIndex].uris.length) {
|
&& adIndexInAdGroup < adPlaybackState.adGroups[adGroupIndex].uris.length) {
|
||||||
@Nullable Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup];
|
@Nullable Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup];
|
||||||
if (adUri != null) {
|
if (adUri != null) {
|
||||||
MediaSource adMediaSource =
|
MediaItem.Builder adMediaItem = new MediaItem.Builder().setUri(adUri);
|
||||||
adMediaSourceFactory.createMediaSource(MediaItem.fromUri(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);
|
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.MediaSource.MediaPeriodId;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||||
import com.google.android.exoplayer2.source.SilenceMediaSource;
|
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.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
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.FakeAdaptiveDataSet;
|
||||||
import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource;
|
import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource;
|
||||||
import com.google.android.exoplayer2.testutil.FakeChunkSource;
|
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.FakeDataSource;
|
||||||
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||||
import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
|
import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
|
||||||
@ -8833,6 +8835,42 @@ public final class ExoPlayerTest {
|
|||||||
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
|
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
|
@Test
|
||||||
public void noTargetLiveOffsetInMedia_doesNotAdjustLiveOffset() throws Exception {
|
public void noTargetLiveOffsetInMedia_doesNotAdjustLiveOffset() throws Exception {
|
||||||
long windowStartUnixTimeMs = 987_654_321_000L;
|
long windowStartUnixTimeMs = 987_654_321_000L;
|
||||||
|
@ -320,6 +320,20 @@ public final class DefaultAudioSinkTest {
|
|||||||
assertThat(thrown.format).isEqualTo(format);
|
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 {
|
private void configureDefaultAudioSink(int channelCount) throws AudioSink.ConfigurationException {
|
||||||
configureDefaultAudioSink(channelCount, /* trimStartFrames= */ 0, /* trimEndFrames= */ 0);
|
configureDefaultAudioSink(channelCount, /* trimStartFrames= */ 0, /* trimEndFrames= */ 0);
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,32 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
|
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)
|
@Test(timeout = 10_000)
|
||||||
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception {
|
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception {
|
||||||
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
|
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
|
||||||
|
@ -51,4 +51,39 @@ public class DefaultDrmSessionManagerProviderTest {
|
|||||||
|
|
||||||
assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DRM_UNSUPPORTED);
|
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.
|
// Update the window.
|
||||||
boolean windowChangingImplicitly = false;
|
Period firstPeriod = manifest.getPeriod(0);
|
||||||
int lastPeriodIndex = manifest.getPeriodCount() - 1;
|
int lastPeriodIndex = manifest.getPeriodCount() - 1;
|
||||||
Period lastPeriod = manifest.getPeriod(lastPeriodIndex);
|
Period lastPeriod = manifest.getPeriod(lastPeriodIndex);
|
||||||
long lastPeriodDurationUs = manifest.getPeriodDurationUs(lastPeriodIndex);
|
long lastPeriodDurationUs = manifest.getPeriodDurationUs(lastPeriodIndex);
|
||||||
long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs));
|
long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs));
|
||||||
// Get the period-relative start/end times.
|
long windowStartTimeInManifestUs =
|
||||||
long currentStartTimeUs =
|
getAvailableStartTimeInManifestUs(
|
||||||
getAvailableStartTimeUs(
|
firstPeriod, manifest.getPeriodDurationUs(0), nowUnixTimeUs);
|
||||||
manifest.getPeriod(0), manifest.getPeriodDurationUs(0), nowUnixTimeUs);
|
long windowEndTimeInManifestUs =
|
||||||
long currentEndTimeUs = getAvailableEndTimeUs(lastPeriod, lastPeriodDurationUs, nowUnixTimeUs);
|
getAvailableEndTimeInManifestUs(lastPeriod, lastPeriodDurationUs, nowUnixTimeUs);
|
||||||
if (manifest.dynamic && !isIndexExplicit(lastPeriod)) {
|
boolean windowChangingImplicitly = manifest.dynamic && !isIndexExplicit(lastPeriod);
|
||||||
// The manifest describes an incomplete live stream. Update the start/end times to reflect the
|
if (windowChangingImplicitly && manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {
|
||||||
// live stream duration and the manifest's time shift buffer depth.
|
// Update the available start time to reflect the manifest's time shift buffer depth.
|
||||||
long liveStreamEndPositionInLastPeriodUs = currentEndTimeUs - C.msToUs(lastPeriod.startMs);
|
long timeShiftBufferStartTimeInManifestUs =
|
||||||
currentEndTimeUs = min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs);
|
windowEndTimeInManifestUs - C.msToUs(manifest.timeShiftBufferDepthMs);
|
||||||
if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {
|
windowStartTimeInManifestUs =
|
||||||
long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);
|
max(windowStartTimeInManifestUs, timeShiftBufferStartTimeInManifestUs);
|
||||||
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;
|
long windowDurationUs = windowEndTimeInManifestUs - windowStartTimeInManifestUs;
|
||||||
for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {
|
long windowStartUnixTimeMs = C.TIME_UNSET;
|
||||||
windowDurationUs += manifest.getPeriodDurationUs(i);
|
long windowDefaultPositionUs = 0;
|
||||||
}
|
|
||||||
|
|
||||||
long windowStartTimeMs = C.TIME_UNSET;
|
|
||||||
if (manifest.availabilityStartTimeMs != C.TIME_UNSET) {
|
|
||||||
windowStartTimeMs =
|
|
||||||
manifest.availabilityStartTimeMs
|
|
||||||
+ manifest.getPeriod(0).startMs
|
|
||||||
+ C.usToMs(currentStartTimeUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
long windowDefaultStartPositionUs = 0;
|
|
||||||
if (manifest.dynamic) {
|
if (manifest.dynamic) {
|
||||||
updateMediaItemLiveConfiguration(
|
checkState(manifest.availabilityStartTimeMs != C.TIME_UNSET);
|
||||||
/* nowPeriodTimeUs= */ currentStartTimeUs + nowUnixTimeUs - C.msToUs(windowStartTimeMs),
|
long nowInWindowUs =
|
||||||
/* windowStartPeriodTimeUs= */ currentStartTimeUs,
|
nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs) - windowStartTimeInManifestUs;
|
||||||
/* windowEndPeriodTimeUs= */ currentEndTimeUs);
|
updateMediaItemLiveConfiguration(nowInWindowUs, windowDurationUs);
|
||||||
windowDefaultStartPositionUs =
|
windowStartUnixTimeMs =
|
||||||
nowUnixTimeUs - C.msToUs(windowStartTimeMs + liveConfiguration.targetOffsetMs);
|
manifest.availabilityStartTimeMs + C.usToMs(windowStartTimeInManifestUs);
|
||||||
long minimumDefaultStartPositionUs =
|
windowDefaultPositionUs = nowInWindowUs - C.msToUs(liveConfiguration.targetOffsetMs);
|
||||||
|
long minimumWindowDefaultPositionUs =
|
||||||
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
||||||
if (windowDefaultStartPositionUs < minimumDefaultStartPositionUs) {
|
if (windowDefaultPositionUs < minimumWindowDefaultPositionUs) {
|
||||||
// The default start position is too close to the start of the live window. Set it to the
|
// The default position is too close to the start of the live window. Set it to the minimum
|
||||||
// minimum default start position provided the window is at least twice as big. Else set
|
// default position provided the window is at least twice as big. Else set it to the middle
|
||||||
// it to the middle of the window.
|
// of the window.
|
||||||
windowDefaultStartPositionUs = minimumDefaultStartPositionUs;
|
windowDefaultPositionUs = minimumWindowDefaultPositionUs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
long offsetInFirstPeriodUs = windowStartTimeInManifestUs - C.msToUs(firstPeriod.startMs);
|
||||||
DashTimeline timeline =
|
DashTimeline timeline =
|
||||||
new DashTimeline(
|
new DashTimeline(
|
||||||
manifest.availabilityStartTimeMs,
|
manifest.availabilityStartTimeMs,
|
||||||
windowStartTimeMs,
|
windowStartUnixTimeMs,
|
||||||
elapsedRealtimeOffsetMs,
|
elapsedRealtimeOffsetMs,
|
||||||
firstPeriodId,
|
firstPeriodId,
|
||||||
/* offsetInFirstPeriodUs= */ currentStartTimeUs,
|
offsetInFirstPeriodUs,
|
||||||
windowDurationUs,
|
windowDurationUs,
|
||||||
windowDefaultStartPositionUs,
|
windowDefaultPositionUs,
|
||||||
manifest,
|
manifest,
|
||||||
mediaItem,
|
mediaItem,
|
||||||
manifest.dynamic ? liveConfiguration : null);
|
manifest.dynamic ? liveConfiguration : null);
|
||||||
@ -1008,8 +985,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMediaItemLiveConfiguration(
|
private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) {
|
||||||
long nowPeriodTimeUs, long windowStartPeriodTimeUs, long windowEndPeriodTimeUs) {
|
|
||||||
long maxLiveOffsetMs;
|
long maxLiveOffsetMs;
|
||||||
if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) {
|
if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) {
|
||||||
maxLiveOffsetMs = mediaItem.liveConfiguration.maxOffsetMs;
|
maxLiveOffsetMs = mediaItem.liveConfiguration.maxOffsetMs;
|
||||||
@ -1017,7 +993,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
&& manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) {
|
&& manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) {
|
||||||
maxLiveOffsetMs = manifest.serviceDescription.maxOffsetMs;
|
maxLiveOffsetMs = manifest.serviceDescription.maxOffsetMs;
|
||||||
} else {
|
} else {
|
||||||
maxLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowStartPeriodTimeUs);
|
maxLiveOffsetMs = C.usToMs(nowInWindowUs);
|
||||||
}
|
}
|
||||||
long minLiveOffsetMs;
|
long minLiveOffsetMs;
|
||||||
if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) {
|
if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) {
|
||||||
@ -1026,7 +1002,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
&& manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) {
|
&& manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) {
|
||||||
minLiveOffsetMs = manifest.serviceDescription.minOffsetMs;
|
minLiveOffsetMs = manifest.serviceDescription.minOffsetMs;
|
||||||
} else {
|
} else {
|
||||||
minLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowEndPeriodTimeUs);
|
minLiveOffsetMs = C.usToMs(nowInWindowUs - windowDurationUs);
|
||||||
if (minLiveOffsetMs < 0 && maxLiveOffsetMs > 0) {
|
if (minLiveOffsetMs < 0 && maxLiveOffsetMs > 0) {
|
||||||
// The current time is in the window, so assume all clocks are synchronized and set the
|
// The current time is in the window, so assume all clocks are synchronized and set the
|
||||||
// minimum to a live offset of zero.
|
// minimum to a live offset of zero.
|
||||||
@ -1052,12 +1028,10 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
targetOffsetMs = minLiveOffsetMs;
|
targetOffsetMs = minLiveOffsetMs;
|
||||||
}
|
}
|
||||||
if (targetOffsetMs > maxLiveOffsetMs) {
|
if (targetOffsetMs > maxLiveOffsetMs) {
|
||||||
long windowDurationUs = windowEndPeriodTimeUs - windowStartPeriodTimeUs;
|
|
||||||
long liveOffsetAtWindowStartUs = nowPeriodTimeUs - windowStartPeriodTimeUs;
|
|
||||||
long safeDistanceFromWindowStartUs =
|
long safeDistanceFromWindowStartUs =
|
||||||
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
||||||
long maxTargetOffsetForSafeDistanceToWindowStartMs =
|
long maxTargetOffsetForSafeDistanceToWindowStartMs =
|
||||||
C.usToMs(liveOffsetAtWindowStartUs - safeDistanceFromWindowStartUs);
|
C.usToMs(nowInWindowUs - safeDistanceFromWindowStartUs);
|
||||||
targetOffsetMs =
|
targetOffsetMs =
|
||||||
Util.constrainValue(
|
Util.constrainValue(
|
||||||
maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs);
|
maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs);
|
||||||
@ -1147,9 +1121,10 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING);
|
return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long getAvailableStartTimeUs(
|
private static long getAvailableStartTimeInManifestUs(
|
||||||
Period period, long periodDurationUs, long nowUnixTimeUs) {
|
Period period, long periodDurationUs, long nowUnixTimeUs) {
|
||||||
long availableStartTimeUs = 0;
|
long periodStartTimeInManifestUs = C.msToUs(period.startMs);
|
||||||
|
long availableStartTimeInManifestUs = periodStartTimeInManifestUs;
|
||||||
boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period);
|
boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period);
|
||||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||||
@ -1162,23 +1137,26 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
|
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
return 0;
|
return periodStartTimeInManifestUs;
|
||||||
}
|
}
|
||||||
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
|
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
|
||||||
if (availableSegmentCount == 0) {
|
if (availableSegmentCount == 0) {
|
||||||
return 0;
|
return periodStartTimeInManifestUs;
|
||||||
}
|
}
|
||||||
long firstAvailableSegmentNum =
|
long firstAvailableSegmentNum =
|
||||||
index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
|
index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
|
||||||
long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstAvailableSegmentNum);
|
long adaptationSetAvailableStartTimeInManifestUs =
|
||||||
availableStartTimeUs = max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);
|
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) {
|
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);
|
boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period);
|
||||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||||
@ -1191,21 +1169,23 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
|
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
return periodDurationUs;
|
return periodStartTimeInManifestUs + periodDurationUs;
|
||||||
}
|
}
|
||||||
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
|
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
|
||||||
if (availableSegmentCount == 0) {
|
if (availableSegmentCount == 0) {
|
||||||
return 0;
|
return periodStartTimeInManifestUs;
|
||||||
}
|
}
|
||||||
long firstAvailableSegmentNum =
|
long firstAvailableSegmentNum =
|
||||||
index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
|
index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
|
||||||
long lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1;
|
long lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1;
|
||||||
long adaptationSetAvailableEndTimeUs =
|
long adaptationSetAvailableEndTimeInManifestUs =
|
||||||
index.getTimeUs(lastAvailableSegmentNum)
|
periodStartTimeInManifestUs
|
||||||
|
+ index.getTimeUs(lastAvailableSegmentNum)
|
||||||
+ index.getDurationUs(lastAvailableSegmentNum, periodDurationUs);
|
+ index.getDurationUs(lastAvailableSegmentNum, periodDurationUs);
|
||||||
availableEndTimeUs = min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);
|
availableEndTimeInManifestUs =
|
||||||
|
min(availableEndTimeInManifestUs, adaptationSetAvailableEndTimeInManifestUs);
|
||||||
}
|
}
|
||||||
return availableEndTimeUs;
|
return availableEndTimeInManifestUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isIndexExplicit(Period period) {
|
private static boolean isIndexExplicit(Period period) {
|
||||||
|
@ -30,9 +30,7 @@ public class Period {
|
|||||||
*/
|
*/
|
||||||
@Nullable public final String id;
|
@Nullable public final String id;
|
||||||
|
|
||||||
/**
|
/** The start time of the period in milliseconds, relative to the start of the manifest. */
|
||||||
* The start time of the period in milliseconds.
|
|
||||||
*/
|
|
||||||
public final long startMs;
|
public final long startMs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.transformer;
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
/** A custom interface that determines the speed for media at specific timestamps. */
|
/** 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.
|
* Provides the speed that the media should be played at, based on the timeUs.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user