Merge pull request #7034 from TiVo:p-exception-unreported-discontinuity

PiperOrigin-RevId: 305006564
This commit is contained in:
Oliver Woodman 2020-04-06 13:28:40 +01:00
commit 8991586c3b
3 changed files with 150 additions and 25 deletions

View file

@ -488,7 +488,7 @@ public class SampleQueue implements TrackOutput {
}
@Override
public final void sampleMetadata(
public void sampleMetadata(
long timeUs,
@C.BufferFlags int flags,
int size,

View file

@ -129,7 +129,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final ArrayList<HlsSampleStream> hlsSampleStreams;
private final Map<String, DrmInitData> overridingDrmInitData;
private FormatAdjustingSampleQueue[] sampleQueues;
private HlsSampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds;
private Set<Integer> sampleQueueMappingDoneByType;
private SparseIntArray sampleQueueIndicesByType;
@ -164,7 +164,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean tracksEnded;
private long sampleOffsetUs;
@Nullable private DrmInitData drmInitData;
private int sourceId;
@Nullable private HlsMediaChunk sourceChunk;
/**
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
@ -209,7 +209,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueTrackIds = new int[0];
sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size());
sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size());
sampleQueues = new FormatAdjustingSampleQueue[0];
sampleQueues = new HlsSampleQueue[0];
sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>();
@ -817,19 +817,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Performs initialization for a media chunk that's about to start loading.
*
* @param mediaChunk The media chunk that's about to start loading.
* @param chunk The media chunk that's about to start loading.
*/
private void initMediaChunkLoad(HlsMediaChunk mediaChunk) {
sourceId = mediaChunk.uid;
upstreamTrackFormat = mediaChunk.trackFormat;
private void initMediaChunkLoad(HlsMediaChunk chunk) {
sourceChunk = chunk;
upstreamTrackFormat = chunk.trackFormat;
pendingResetPositionUs = C.TIME_UNSET;
mediaChunks.add(mediaChunk);
mediaChunks.add(chunk);
mediaChunk.init(this);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.sourceId(sourceId);
chunk.init(this);
for (HlsSampleQueue sampleQueue : sampleQueues) {
sampleQueue.setSourceChunk(chunk);
}
if (mediaChunk.shouldSpliceIn) {
if (chunk.shouldSpliceIn) {
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.splice();
}
@ -906,18 +906,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
int trackCount = sampleQueues.length;
boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
FormatAdjustingSampleQueue trackOutput =
new FormatAdjustingSampleQueue(
allocator, drmSessionManager, eventDispatcher, overridingDrmInitData);
HlsSampleQueue sampleQueue =
new HlsSampleQueue(allocator, drmSessionManager, eventDispatcher, overridingDrmInitData);
if (isAudioVideo) {
trackOutput.setDrmInitData(drmInitData);
sampleQueue.setDrmInitData(drmInitData);
}
trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.sourceId(sourceId);
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
if (sourceChunk != null) {
sampleQueue.setSourceChunk(sourceChunk);
}
sampleQueue.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
sampleQueueTrackIds[trackCount] = id;
sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);
sampleQueues = Util.nullSafeArrayAppend(sampleQueues, sampleQueue);
sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo;
haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];
@ -928,7 +929,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
primarySampleQueueType = type;
}
sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1);
return trackOutput;
return sampleQueue;
}
@Override
@ -1341,12 +1342,40 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return new DummyTrackOutput();
}
private static final class FormatAdjustingSampleQueue extends SampleQueue {
/**
* A {@link SampleQueue} that adds HLS specific functionality:
*
* <ul>
* <li>Detection of spurious discontinuities, by checking sample timestamps against the range
* expected for the currently loading chunk.
* <li>Stripping private timestamp metadata from {@link Format Formats} to avoid an excessive
* number of format switches in the queue.
* <li>Overriding of {@link Format#drmInitData}.
* </ul>
*/
private static final class HlsSampleQueue extends SampleQueue {
/**
* The fraction of the chunk duration from which timestamps of samples loaded from within a
* chunk are allowed to deviate from the expected range.
*/
private static final double MAX_TIMESTAMP_DEVIATION_FRACTION = 0.5;
/**
* A minimum tolerance for sample timestamps in microseconds. Timestamps of samples loaded from
* within a chunk are always allowed to deviate up to this amount from the expected range.
*/
private static final long MIN_TIMESTAMP_DEVIATION_TOLERANCE_US = 4_000_000;
@Nullable private HlsMediaChunk sourceChunk;
private long sourceChunkLastSampleTimeUs;
private long minAllowedSampleTimeUs;
private long maxAllowedSampleTimeUs;
private final Map<String, DrmInitData> overridingDrmInitData;
@Nullable private DrmInitData drmInitData;
public FormatAdjustingSampleQueue(
private HlsSampleQueue(
Allocator allocator,
DrmSessionManager drmSessionManager,
MediaSourceEventDispatcher eventDispatcher,
@ -1355,6 +1384,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.overridingDrmInitData = overridingDrmInitData;
}
public void setSourceChunk(HlsMediaChunk chunk) {
sourceChunk = chunk;
sourceChunkLastSampleTimeUs = C.TIME_UNSET;
sourceId(chunk.uid);
long allowedDeviationUs =
Math.max(
(long) ((chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_FRACTION),
MIN_TIMESTAMP_DEVIATION_TOLERANCE_US);
minAllowedSampleTimeUs = chunk.startTimeUs - allowedDeviationUs;
maxAllowedSampleTimeUs = chunk.endTimeUs + allowedDeviationUs;
}
public void setDrmInitData(@Nullable DrmInitData drmInitData) {
this.drmInitData = drmInitData;
invalidateUpstreamFormatAdjustment();
@ -1415,13 +1457,31 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
return new Metadata(newMetadataEntries);
}
@Override
public void sampleMetadata(
long timeUs,
@C.BufferFlags int flags,
int size,
int offset,
@Nullable CryptoData cryptoData) {
// TODO: Uncomment this to reject samples with unexpected timestamps. See
// https://github.com/google/ExoPlayer/issues/7030.
// if (timeUs < minAllowedSampleTimeUs || timeUs > maxAllowedSampleTimeUs) {
// Util.sneakyThrow(
// new UnexpectedSampleTimestampException(
// sourceChunk, sourceChunkLastSampleTimeUs, timeUs));
// }
sourceChunkLastSampleTimeUs = timeUs;
super.sampleMetadata(timeUs, flags, size, offset, cryptoData);
}
}
private static class EmsgUnwrappingTrackOutput implements TrackOutput {
private static final String TAG = "EmsgUnwrappingTrackOutput";
// TODO(ibaker): Create a Formats util class with common constants like this.
// TODO: Create a Formats util class with common constants like this.
private static final Format ID3_FORMAT =
new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_ID3).build();
private static final Format EMSG_FORMAT =

View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import java.io.IOException;
/**
* Thrown when an attempt is made to write a sample to a {@link SampleQueue} whose timestamp is
* inconsistent with the chunk from which it originates.
*/
/* package */ final class UnexpectedSampleTimestampException extends IOException {
/** The {@link MediaChunk} that contained the rejected sample. */
public final MediaChunk mediaChunk;
/**
* The timestamp of the last sample that was loaded from {@link #mediaChunk} and successfully
* written to the {@link SampleQueue}, in microseconds. {@link C#TIME_UNSET} if the first sample
* in the chunk was rejected.
*/
public final long lastAcceptedSampleTimeUs;
/** The timestamp of the rejected sample, in microseconds. */
public final long rejectedSampleTimeUs;
/**
* Constructs an instance.
*
* @param mediaChunk The {@link MediaChunk} with the unexpected sample timestamp.
* @param lastAcceptedSampleTimeUs The timestamp of the last sample that was loaded from the chunk
* and successfully written to the {@link SampleQueue}, in microseconds. {@link C#TIME_UNSET}
* if the first sample in the chunk was rejected.
* @param rejectedSampleTimeUs The timestamp of the rejected sample, in microseconds.
*/
public UnexpectedSampleTimestampException(
MediaChunk mediaChunk, long lastAcceptedSampleTimeUs, long rejectedSampleTimeUs) {
super(
"Unexpected sample timestamp: "
+ C.usToMs(rejectedSampleTimeUs)
+ " in chunk ["
+ mediaChunk.startTimeUs
+ ", "
+ mediaChunk.endTimeUs
+ "]");
this.mediaChunk = mediaChunk;
this.lastAcceptedSampleTimeUs = lastAcceptedSampleTimeUs;
this.rejectedSampleTimeUs = rejectedSampleTimeUs;
}
}