Add HlsCheckedSampleQueue to check timstamp range

Add a SampleQueue subclass that checks the timestamp range of media samples queued to it and reports an exception on load if the timestamp is outside of spec bounds.
(Smashed to a single commit prior to rebase)
This commit is contained in:
Steve Mayhew 2020-03-09 13:30:06 -07:00
parent 7b82a3c889
commit f78cbd2c9e
5 changed files with 105 additions and 6 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

@ -0,0 +1,26 @@
package com.google.android.exoplayer2.source;
import android.net.Uri;
import com.google.android.exoplayer2.C;
/**
* Thrown from the loader thread when an attempt is made to commit a sample that is far
* deviant from the expected sequence of timestamps in the SampleStream. This is likely
* caused by a discontinuity in a segment that was not split and reported by metadata in
* an HLS (EXT-X-DISCONTINUITY) or DASH stream.
*/
public class UnreportedDiscontinuityException extends RuntimeException {
public final long timesUs;
/**
* Consturct the exception
*
* @param timesUs last timestamp before attempted commit of the deviant sample
* @param uri uri of the segment with the unreported discontinuity
*/
public UnreportedDiscontinuityException(long timesUs, Uri uri) {
super("Unreported discontinuity timeMs: " + C.usToMs(timesUs) + " in URI: " + uri);
this.timesUs = timesUs;
}
}

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 androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.UnreportedDiscontinuityException;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Log;
public class HlsCheckedSampleQueue extends SampleQueue {
private static final String TAG = "HlsCheckedSampleQueue";
private long lowestTimeUs = C.TIME_UNSET;
private long highestTimeUs = C.TIME_UNSET;
private HlsMediaChunk chunk;
private boolean loggedFirst = false;
HlsCheckedSampleQueue(Allocator allocator, DrmSessionManager drmSessionManager) {
super(allocator, drmSessionManager);
}
void setCurrentLoadingChunk(HlsMediaChunk chunk) {
double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * 0.1;
this.lowestTimeUs = chunk.startTimeUs;
this.highestTimeUs = (long) (chunk.endTimeUs + tolerance);
this.chunk = chunk;
loggedFirst = false;
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) {
if (lowestTimeUs != C.TIME_UNSET && timeUs < lowestTimeUs && ! loggedFirst) {
Log.d(TAG, "sampleMetadata() - committed timeUs: " + timeUs + " is " + C.usToMs(lowestTimeUs - timeUs) + "ms less then segment start time. chunk: " + chunk.dataSpec.uri);
loggedFirst = true;
}
if (lowestTimeUs != C.TIME_UNSET && timeUs < (lowestTimeUs - C.msToUs(50_000))) {
Log.d(TAG, "sampleMetadata() - committed timeUs: " + timeUs + " is " + C.usToMs(lowestTimeUs - timeUs) + "ms less (MUCH!) then segment start time. chunk: " + chunk.dataSpec.uri);
throw new UnreportedDiscontinuityException(timeUs, chunk.dataSpec.uri);
}
if (highestTimeUs != C.TIME_UNSET && timeUs > highestTimeUs) {
Log.d(TAG, "sampleMetadata() - committed timeUs: " + timeUs + " is " + C.usToMs(lowestTimeUs - timeUs) + "ms greater then segment end time. chunk: " + chunk.dataSpec.uri);
throw new UnreportedDiscontinuityException(timeUs, chunk.dataSpec.uri);
}
super.sampleMetadata(timeUs, flags, size, offset, cryptoData);
}
}

View File

@ -27,11 +27,13 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.source.UnreportedDiscontinuityException;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.UriUtil;
@ -50,6 +52,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* An HLS {@link MediaChunk}.
*/
/* package */ final class HlsMediaChunk extends MediaChunk {
private static final String TAG = "HlsMediaChunk";
/**
* Creates a new instance.
@ -282,7 +285,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
*/
public void init(HlsSampleStreamWrapper output) {
this.output = output;
output.init(uid, shouldSpliceIn);
output.init(uid, this, shouldSpliceIn);
}
@Override
@ -376,6 +379,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, DUMMY_POSITION_HOLDER);
}
} catch (UnreportedDiscontinuityException e) {
Log.d(TAG, "Unreported discontinuity at timeUs: "+ e.timesUs + " uri: " + dataSpec.uri);
throw new IOException("Timestamp error", e);
} finally {
nextLoadPosition = (int) (input.getPosition() - dataSpec.position);
}

View File

@ -820,15 +820,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Initializes the wrapper for loading a chunk.
*
* @param chunkUid The chunk's uid.
* @param loadingChunk the Chunk that is about to start loading.
* @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any
* samples already queued to the wrapper.
*/
public void init(int chunkUid, boolean shouldSpliceIn) {
public void init(int chunkUid, HlsMediaChunk loadingChunk, boolean shouldSpliceIn) {
this.chunkUid = chunkUid;
for (SampleQueue sampleQueue : sampleQueues) {
for (HlsCheckedSampleQueue sampleQueue : sampleQueues) {
sampleQueue.sourceId(chunkUid);
sampleQueue.setCurrentLoadingChunk(loadingChunk);
}
if (shouldSpliceIn) {
for (SampleQueue sampleQueue : sampleQueues) {
@ -1342,7 +1343,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return new DummyTrackOutput();
}
private static final class FormatAdjustingSampleQueue extends SampleQueue {
private static final class FormatAdjustingSampleQueue extends HlsCheckedSampleQueue {
private final Map<String, DrmInitData> overridingDrmInitData;
@Nullable private DrmInitData drmInitData;