From e48851d8ccc16e03c443ff875a32a7511b76a43d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 10 Sep 2015 18:23:20 +0100 Subject: [PATCH] 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 --- .../extractor/ts/PtsTimestampAdjuster.java | 79 +++++++++++++++++++ .../exoplayer/extractor/ts/TsExtractor.java | 50 ++---------- .../android/exoplayer/hls/HlsChunkSource.java | 21 ++++- 3 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ts/PtsTimestampAdjuster.java diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/PtsTimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/PtsTimestampAdjuster.java new file mode 100644 index 0000000000..59d96eb003 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/PtsTimestampAdjuster.java @@ -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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index b2dc044b8b..360435ba81 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -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 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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 6d78470caa..876d706281 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -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 {