Share PTS timestamp adjustment across format changes.

When switching format in HLS, we instantiate a new extractor, which
adjusts TS presentation timestamps so that they align properly with
the start of the first segment in the new format. Some HLS streams
appear to have slightly misalignment that causes a glitch when using
this approach.

It's better to re-use the same timestamp adjustment across formats,
and only reset it when seeking or when there's an actual discontinuity.
This is because the HLS spec guarantees PTS timestamp alignment across
different formats.

We'll also need something like PtsTimestampAdjuster to share between
separated audio and WebVTT tracks, which also contain PTS timestamps
that are aligned, and will need to share a common adjustment.

Issue: #692
This commit is contained in:
Oliver Woodman 2015-09-10 18:23:20 +01:00
parent 7d38d2ef3c
commit e48851d8cc
3 changed files with 104 additions and 46 deletions

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2014 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.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
/**
* Scales and adjusts MPEG-2 TS presentation timestamps, taking into account an initial offset and
* timestamp rollover.
*/
public final class PtsTimestampAdjuster {
/**
* The value one greater than the largest representable (33 bit) presentation timestamp.
*/
private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
private final long firstSampleTimestampUs;
private long timestampOffsetUs;
private long lastPts;
/**
* @param firstSampleTimestampUs The desired result of the first call to
* {@link #adjustTimestamp(long)}.
*/
public PtsTimestampAdjuster(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
lastPts = Long.MIN_VALUE;
}
/**
* Resets the instance to its initial state.
*/
public void reset() {
lastPts = Long.MIN_VALUE;
}
/**
* Scales and adjusts an MPEG-2 TS presentation timestamp.
*
* @param pts The unscaled MPEG-2 TS presentation timestamp.
* @return The adjusted timestamp in microseconds.
*/
public long adjustTimestamp(long pts) {
if (lastPts != Long.MIN_VALUE) {
// The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
// and we need to snap to the one closest to lastPts.
long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
? ptsWrapBelow : ptsWrapAbove;
}
// Calculate the corresponding timestamp.
long timeUs = (pts * C.MICROS_PER_SECOND) / 90000;
// If we haven't done the initial timestamp adjustment, do it now.
if (lastPts == Long.MIN_VALUE) {
timestampOffsetUs = firstSampleTimestampUs - timeUs;
}
// Record the adjusted PTS to adjust for wraparound next time.
lastPts = pts;
return timeUs + timestampOffsetUs;
}
}

View File

@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
@ -51,38 +50,33 @@ public final class TsExtractor implements Extractor {
private static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1
private static final long MAX_PTS = 0x1FFFFFFFFL;
private final PtsTimestampAdjuster ptsTimestampAdjuster;
private final ParsableByteArray tsPacketBuffer;
private final ParsableBitArray tsScratch;
private final boolean idrKeyframesOnly;
private final long firstSampleTimestampUs;
/* package */ final SparseBooleanArray streamTypes;
/* package */ final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
// Accessed only by the loading thread.
private ExtractorOutput output;
private long timestampOffsetUs;
private long lastPts;
/* package */ Id3Reader id3Reader;
public TsExtractor() {
this(0);
this(new PtsTimestampAdjuster(0));
}
public TsExtractor(long firstSampleTimestampUs) {
this(firstSampleTimestampUs, true);
public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster) {
this(ptsTimestampAdjuster, true);
}
public TsExtractor(long firstSampleTimestampUs, boolean idrKeyframesOnly) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster, boolean idrKeyframesOnly) {
this.idrKeyframesOnly = idrKeyframesOnly;
tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
streamTypes = new SparseBooleanArray();
tsPayloadReaders = new SparseArray<>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE;
this.ptsTimestampAdjuster = ptsTimestampAdjuster;
}
// Extractor implementation.
@ -108,8 +102,7 @@ public final class TsExtractor implements Extractor {
@Override
public void seek() {
timestampOffsetUs = 0;
lastPts = Long.MIN_VALUE;
ptsTimestampAdjuster.reset();
for (int i = 0; i < tsPayloadReaders.size(); i++) {
tsPayloadReaders.valueAt(i).seek();
}
@ -160,33 +153,6 @@ public final class TsExtractor implements Extractor {
// Internals.
/**
* Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound.
*
* @param pts The raw PTS value.
* @return The corresponding time in microseconds.
*/
/* package */ long ptsToTimeUs(long pts) {
if (lastPts != Long.MIN_VALUE) {
// The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
// and we need to snap to the one closest to lastPts.
long closestWrapCount = (lastPts + (MAX_PTS / 2)) / MAX_PTS;
long ptsWrapBelow = pts + (MAX_PTS * (closestWrapCount - 1));
long ptsWrapAbove = pts + (MAX_PTS * closestWrapCount);
pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
? ptsWrapBelow : ptsWrapAbove;
}
// Calculate the corresponding timestamp.
long timeUs = (pts * C.MICROS_PER_SECOND) / 90000;
// If we haven't done the initial timestamp adjustment, do it now.
if (lastPts == Long.MIN_VALUE) {
timestampOffsetUs = firstSampleTimestampUs - timeUs;
}
// Record the adjusted PTS to adjust for wraparound next time.
lastPts = pts;
return timeUs + timestampOffsetUs;
}
/**
* Parses TS packet payload data.
*/
@ -543,7 +509,7 @@ public final class TsExtractor implements Extractor {
pesScratch.skipBits(1); // marker_bit
pts |= pesScratch.readBits(15);
pesScratch.skipBits(1); // marker_bit
timeUs = ptsToTimeUs(pts);
timeUs = ptsTimestampAdjuster.adjustTimestamp(pts);
}
}

View File

@ -24,6 +24,7 @@ import com.google.android.exoplayer.chunk.DataChunk;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster;
import com.google.android.exoplayer.extractor.ts.TsExtractor;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
@ -140,6 +141,7 @@ public class HlsChunkSource {
private boolean live;
private long durationUs;
private IOException fatalError;
private PtsTimestampAdjuster ptsTimestampAdjuster;
private Uri encryptionKeyUri;
private byte[] encryptionKey;
@ -352,10 +354,21 @@ public class HlsChunkSource {
// Configure the extractor that will read the chunk.
HlsExtractorWrapper extractorWrapper;
if (previousTsChunk == null || segment.discontinuity || !format.equals(previousTsChunk.format)
|| liveDiscontinuity) {
Extractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(startTimeUs) : new TsExtractor(startTimeUs);
if (previousTsChunk == null || segment.discontinuity || liveDiscontinuity
|| !format.equals(previousTsChunk.format)) {
Extractor extractor;
if (chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)) {
extractor = new AdtsExtractor(startTimeUs);
} else {
if (previousTsChunk == null || segment.discontinuity || liveDiscontinuity
|| ptsTimestampAdjuster == null) {
// TODO: Use this for AAC as well, along with the ID3 PRIV priv tag values with owner
// identifier com.apple.streaming.transportStreamTimestamp.
ptsTimestampAdjuster = new PtsTimestampAdjuster(startTimeUs);
}
extractor = new TsExtractor(ptsTimestampAdjuster);
}
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced, adaptiveMaxWidth, adaptiveMaxHeight);
} else {