diff --git a/RELEASENOTES.md b/RELEASENOTES.md index de76bce9a79..2a30fb03537 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* MPEG-PS: Support reading duration from MPEG-PS Streams + ([#4476](https://github.com/google/ExoPlayer/issues/4476)). * MediaSession extension: * Allow apps to set custom errors. * Audio: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java new file mode 100644 index 00000000000..fa4be4b2871 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2018 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.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG program stream (PS). + * + *

This reader extracts the duration by reading system clock reference (SCR) values from the + * header of a pack at the start and at the end of the stream, calculating the difference, and + * converting that into stream duration. This reader also handles the case when a single SCR + * wraparound takes place within the stream, which can make SCR values at the beginning of the + * stream larger than SCR values at the end. This class can only be used once to read duration from + * a given stream, and the usage of the class is not thread-safe, so all calls should be made from + * the same thread. + * + *

Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header. + */ +/* package */ final class PsDurationReader { + + private static final int DURATION_READ_BYTES = 20000; + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstScrValueRead; + private boolean isLastScrValueRead; + + private long firstScrValue; + private long lastScrValue; + private long durationUs; + + /* package */ PsDurationReader() { + scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstScrValue = C.TIME_UNSET; + lastScrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(DURATION_READ_BYTES); + } + + /** Returns true if a PS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + /** + * Reads a PS duration from the input. + * + *

This reader reads the duration by reading SCR values from the header of a pack at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + if (!isLastScrValueRead) { + return readLastScrValue(input, seekPositionHolder); + } + if (lastScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstScrValueRead) { + return readFirstScrValue(input, seekPositionHolder); + } + if (firstScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue); + long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue); + durationUs = maxScrPositionUs - minScrPositionUs; + return finishReadDuration(input); + } + + /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */ + public long getDurationUs() { + return durationUs; + } + + private int finishReadDuration(ExtractorInput input) { + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + if (input.getPosition() != 0) { + seekPositionHolder.position = 0; + return Extractor.RESULT_SEEK; + } + + int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesToRead); + + firstScrValue = readFirstScrValueFromBuffer(packetBuffer); + isFirstScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition - 3; + searchPosition++) { + int nextStartCode = peakIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + long scrValue = readScrValueFromPack(packetBuffer, searchPosition + 4); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); + long bufferStartStreamPosition = input.getLength() - bytesToRead; + if (input.getPosition() != bufferStartStreamPosition) { + seekPositionHolder.position = bufferStartStreamPosition; + return Extractor.RESULT_SEEK; + } + + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesToRead); + + lastScrValue = readLastScrValueFromBuffer(packetBuffer); + isLastScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 4; + searchPosition >= searchStartPosition; + searchPosition--) { + int nextStartCode = peakIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + long scrValue = readScrValueFromPack(packetBuffer, searchPosition + 4); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int peakIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } + + private long readScrValueFromPack(ParsableByteArray packetBuffer, int packHeaderStartPosition) { + packetBuffer.setPosition(packHeaderStartPosition); + if (packetBuffer.bytesLeft() < 9) { + // We require at 9 bytes for pack header to read scr value + return C.TIME_UNSET; + } + byte[] scrBytes = new byte[9]; + packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length); + if (!checkMarkerBits(scrBytes)) { + return C.TIME_UNSET; + } + return readScrValueFromPackHeader(scrBytes); + } + + private boolean checkMarkerBits(byte[] scrBytes) { + // Verify the 01xxx1xx marker on the 0th byte + if ((scrBytes[0] & 0xC4) != 0x44) { + return false; + } + // 1st byte belongs to scr field. + // Verify the xxxxx1xx marker on the 2nd byte + if ((scrBytes[2] & 0x04) != 0x04) { + return false; + } + // 3rd byte belongs to scr field. + // Verify the xxxxx1xx marker on the 4rd byte + if ((scrBytes[4] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxxxx1 marker on the 5th byte + if ((scrBytes[5] & 0x01) != 0x01) { + return false; + } + // 6th and 7th bytes belongs to program_max_rate field. + // Verify the xxxxxx11 marker on the 8th byte + return (scrBytes[8] & 0x03) == 0x03; + } + + /** + * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring + * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in + * pack_header. + * + *

We ignore SCR Ext, because it's too small to have any significance. + */ + private static long readScrValueFromPackHeader(byte[] scrBytes) { + return ((scrBytes[0] & 0b00111000L) >> 3) << 30 + | (scrBytes[0] & 0b00000011L) << 28 + | (scrBytes[1] & 0xFFL) << 20 + | ((scrBytes[2] & 0b11111000L) >> 3) << 15 + | (scrBytes[2] & 0b00000011L) << 13 + | (scrBytes[3] & 0xFFL) << 5 + | (scrBytes[4] & 0b11111000L) >> 3; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index 8acb36b41ed..f8e9ba15e6c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -47,7 +47,7 @@ public Extractor[] createExtractors() { }; - private static final int PACK_START_CODE = 0x000001BA; + /* package */ static final int PACK_START_CODE = 0x000001BA; private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; private static final int PACKET_START_CODE_PREFIX = 0x000001; private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; @@ -68,6 +68,8 @@ public Extractor[] createExtractors() { private final TimestampAdjuster timestampAdjuster; private final SparseArray psPayloadReaders; // Indexed by pid private final ParsableByteArray psPacketBuffer; + private final PsDurationReader durationReader; + private boolean foundAllTracks; private boolean foundAudioTrack; private boolean foundVideoTrack; @@ -75,6 +77,7 @@ public Extractor[] createExtractors() { // Accessed only by the loading thread. private ExtractorOutput output; + private boolean hasOutputSeekMap; public PsExtractor() { this(new TimestampAdjuster(0)); @@ -84,6 +87,7 @@ public PsExtractor(TimestampAdjuster timestampAdjuster) { this.timestampAdjuster = timestampAdjuster; psPacketBuffer = new ParsableByteArray(4096); psPayloadReaders = new SparseArray<>(); + durationReader = new PsDurationReader(); } // Extractor implementation. @@ -130,7 +134,6 @@ public boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept @Override public void init(ExtractorOutput output) { this.output = output; - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override @@ -149,6 +152,13 @@ public void release() { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + + boolean canReadDuration = input.getLength() != C.LENGTH_UNSET; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition); + } + maybeOutputSeekMap(); + // First peek and check what type of start code is next. if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) { return RESULT_END_OF_INPUT; @@ -250,6 +260,13 @@ public int read(ExtractorInput input, PositionHolder seekPosition) // Internals. + private void maybeOutputSeekMap() { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + /** * Parses PES packet data and extracts samples. */ diff --git a/library/core/src/test/assets/ts/sample.ps.0.dump b/library/core/src/test/assets/ts/sample.ps.0.dump index dda6de8ab4b..bfb1fcf5eed 100644 --- a/library/core/src/test/assets/ts/sample.ps.0.dump +++ b/library/core/src/test/assets/ts/sample.ps.0.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = false - duration = UNSET TIME + duration = 766 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 192: diff --git a/library/core/src/test/assets/ts/sample.ps.unklen.dump b/library/core/src/test/assets/ts/sample.ps.unklen.dump new file mode 100644 index 00000000000..dda6de8ab4b --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ps.unklen.dump @@ -0,0 +1,79 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 192: + format: + bitrate = -1 + id = 192 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 1671 + sample count = 4 + sample 0: + time = 29088 + flags = 1 + data = length 417, hash 5C710F78 + sample 1: + time = 55210 + flags = 1 + data = length 418, hash 79CF71F8 + sample 2: + time = 81332 + flags = 1 + data = length 418, hash 79CF71F8 + sample 3: + time = 107454 + flags = 1 + data = length 418, hash 79CF71F8 +track 224: + format: + bitrate = -1 + id = 224 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash 743CC6F8 + total output bytes = 44056 + sample count = 2 + sample 0: + time = 40000 + flags = 1 + data = length 20646, hash 576390B + sample 1: + time = 80000 + flags = 0 + data = length 17831, hash 5C5A57F5 +tracksEnded = true