diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9dce6ce067e..4102887c784 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,11 @@ Custom `AssetLoader` implementations must implement these new methods. * Track Selection: * Extractors: + * Fix corrupted AC-3, DTS and LPCM audio when playing DVD-style MPEG-PS + content, by stripping the `private_stream_1` sub-stream header from each + PES packet in `PsExtractor` before the payload is passed to the audio + reader + ([#3327](https://github.com/androidx/media/issues/3327)). * Inspector: * Inspector Frame: * Audio: diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsExtractor.java index f4f816ee08e..41edaa8ce17 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsExtractor.java @@ -260,7 +260,11 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce if (elementaryStreamReader != null) { TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); elementaryStreamReader.createTracks(output, idGenerator); - payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster); + payloadReader = + new PesReader( + elementaryStreamReader, + timestampAdjuster, + /* isPrivateStream1= */ streamId == PRIVATE_STREAM_1); psPayloadReaders.put(streamId, payloadReader); } } @@ -328,6 +332,10 @@ private static final class PesReader { private final ElementaryStreamReader pesPayloadReader; private final TimestampAdjuster timestampAdjuster; private final ParsableBitArray pesScratch; + // Whether this reader consumes the MPEG-PS private_stream_1 (0xBD), which in DVD-style content + // carries a sub-stream header (sub_stream_id, number_of_frame_headers and + // first_access_unit_pointer) before the audio elementary stream. + private final boolean isPrivateStream1; private boolean ptsFlag; private boolean dtsFlag; @@ -335,9 +343,13 @@ private static final class PesReader { private int extendedHeaderLength; private long timeUs; - public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) { + public PesReader( + ElementaryStreamReader pesPayloadReader, + TimestampAdjuster timestampAdjuster, + boolean isPrivateStream1) { this.pesPayloadReader = pesPayloadReader; this.timestampAdjuster = timestampAdjuster; + this.isPrivateStream1 = isPrivateStream1; pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); } @@ -366,12 +378,39 @@ public void consume(ParsableByteArray data) throws ParserException { data.readBytes(pesScratch.data, 0, extendedHeaderLength); pesScratch.setPosition(0); parseHeaderExtension(); + // For DVD-style private_stream_1 (AC-3/DTS/LPCM), a sub-stream header follows the PES header + // and precedes the audio elementary stream. It must be stripped so it isn't fed into the + // audio reader as elementary data, which would corrupt the audio frame spanning this PES + // boundary (see ISO/IEC 13818-1 / DVD-Video PS spec). + maybeSkipPrivateStream1SubStreamHeader(data); pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR); pesPayloadReader.consume(data); // We always have complete PES packets with program stream. pesPayloadReader.packetFinished(); } + /** + * Skips the DVD-style private_stream_1 sub-stream header, if present, so that {@code data} is + * positioned at the start of the audio elementary stream. + * + *

In DVD-Video and similar MPEG-PS content, each private_stream_1 (0xBD) PES packet begins + * with a 4-byte header: {@code sub_stream_id} (1 byte, 0x80-0x87 for AC-3, 0x88-0x8F for DTS, + * 0xA0-0xA7 for LPCM), {@code number_of_frame_headers} (1 byte) and {@code + * first_access_unit_pointer} (2 bytes). These bytes are not part of the audio elementary stream + * and must not be forwarded to the audio reader. + */ + private void maybeSkipPrivateStream1SubStreamHeader(ParsableByteArray data) { + if (!isPrivateStream1 || data.bytesLeft() < 4) { + return; + } + int subStreamId = data.peekUnsignedByte(); + // AC-3 (0x80-0x87), DTS (0x88-0x8F) and LPCM (0xA0-0xA7) sub-streams carry the 4-byte header. + if ((subStreamId >= 0x80 && subStreamId <= 0x8F) + || (subStreamId >= 0xA0 && subStreamId <= 0xA7)) { + data.skipBytes(4); + } + } + private void consumeEndOfInput() { pesPayloadReader.endOfInputReached(); } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/ts/PsExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/ts/PsExtractorTest.java index 4f82fd4f945..35b7a06c355 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/ts/PsExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/ts/PsExtractorTest.java @@ -44,4 +44,12 @@ public void sampleWithH262AndMpegAudio() throws Exception { public void sampleWithAc3() throws Exception { ExtractorAsserts.assertBehavior(PsExtractor::new, "media/ts/sample_ac3.ps", simulationConfig); } + + @Test + public void sampleWithAc3StraddlingPesBoundaries() throws Exception { + // Regression test for a private_stream_1 (0xBD) sample whose AC-3 frames straddle PES packet + // boundaries, so the DVD sub-stream header preceding each packet's payload must be stripped. + ExtractorAsserts.assertBehavior( + PsExtractor::new, "media/ts/sample_ac3_straddle.ps", simulationConfig); + } } diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.0.dump new file mode 100644 index 00000000000..ffab422028d --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.0.dump @@ -0,0 +1,173 @@ +seekMap: + isSeekable = true + duration = 1201344 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(600672) = [[timeUs=600672, position=35805]] + getPosition(1201344) = [[timeUs=1201344, position=71799]] +numberOfTracks = 2 +track 189: + total output bytes = 19200 + sample count = 25 + format 0: + averageBitrate = 192000 + peakBitrate = 192000 + id = 189 + sampleMimeType = audio/ac3 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 61333 + flags = 1 + data = length 768, hash 4513D6ED + sample 1: + time = 93333 + flags = 1 + data = length 768, hash F7DD71AD + sample 2: + time = 157333 + flags = 1 + data = length 768, hash B46BDFC2 + sample 3: + time = 189333 + flags = 1 + data = length 768, hash 6E3DC97F + sample 4: + time = 221333 + flags = 1 + data = length 768, hash BCF0EE73 + sample 5: + time = 253333 + flags = 1 + data = length 768, hash 4FD0241F + sample 6: + time = 285333 + flags = 1 + data = length 768, hash DF256D70 + sample 7: + time = 317333 + flags = 1 + data = length 768, hash BD3A8741 + sample 8: + time = 349333 + flags = 1 + data = length 768, hash 6CDA87FA + sample 9: + time = 381333 + flags = 1 + data = length 768, hash D7542AD6 + sample 10: + time = 413333 + flags = 1 + data = length 768, hash 650B3B21 + sample 11: + time = 445333 + flags = 1 + data = length 768, hash 7AA9EA5 + sample 12: + time = 477333 + flags = 1 + data = length 768, hash 36E854AB + sample 13: + time = 509333 + flags = 1 + data = length 768, hash 1958F2B0 + sample 14: + time = 541333 + flags = 1 + data = length 768, hash E12C6DFC + sample 15: + time = 573333 + flags = 1 + data = length 768, hash 64E149B3 + sample 16: + time = 605333 + flags = 1 + data = length 768, hash A053FE3F + sample 17: + time = 637333 + flags = 1 + data = length 768, hash 326041DB + sample 18: + time = 669333 + flags = 1 + data = length 768, hash 865250F + sample 19: + time = 701333 + flags = 1 + data = length 768, hash B45ADB43 + sample 20: + time = 733333 + flags = 1 + data = length 768, hash 99626351 + sample 21: + time = 765333 + flags = 1 + data = length 768, hash BCED46AE + sample 22: + time = 797333 + flags = 1 + data = length 768, hash 63B6188F + sample 23: + time = 829333 + flags = 1 + data = length 768, hash 86C6C28B + sample 24: + time = 861333 + flags = 1 + data = length 768, hash A1D014D1 +track 224: + total output bytes = 52248 + sample count = 11 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 160 + height = 120 + initializationData: + data = length 22, hash 3B91971B + sample 0: + time = 66666 + flags = 1 + data = length 5986, hash 6A578416 + sample 1: + time = 133333 + flags = 0 + data = length 5571, hash A276297C + sample 2: + time = 200000 + flags = 0 + data = length 4475, hash 50693038 + sample 3: + time = 266666 + flags = 0 + data = length 5187, hash 8AD660F2 + sample 4: + time = 333333 + flags = 0 + data = length 4552, hash 773C4FC3 + sample 5: + time = 400000 + flags = 0 + data = length 5611, hash A70A9C09 + sample 6: + time = 466666 + flags = 0 + data = length 4740, hash F8C7264F + sample 7: + time = 533333 + flags = 0 + data = length 3679, hash BEF11657 + sample 8: + time = 600000 + flags = 0 + data = length 2910, hash 5F82B9AA + sample 9: + time = 666666 + flags = 0 + data = length 3357, hash 91082EE0 + sample 10: + time = 733333 + flags = 0 + data = length 2471, hash B011430D +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.1.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.1.dump new file mode 100644 index 00000000000..be6574068c7 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.1.dump @@ -0,0 +1,141 @@ +seekMap: + isSeekable = true + duration = 1201344 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(600672) = [[timeUs=600672, position=35805]] + getPosition(1201344) = [[timeUs=1201344, position=71799]] +numberOfTracks = 2 +track 189: + total output bytes = 16896 + sample count = 22 + format 0: + averageBitrate = 192000 + peakBitrate = 192000 + id = 189 + sampleMimeType = audio/ac3 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 157333 + flags = 1 + data = length 768, hash 6E3DC97F + sample 1: + time = 189333 + flags = 1 + data = length 768, hash BCF0EE73 + sample 2: + time = 253333 + flags = 1 + data = length 768, hash 4FD0241F + sample 3: + time = 285333 + flags = 1 + data = length 768, hash DF256D70 + sample 4: + time = 317333 + flags = 1 + data = length 768, hash BD3A8741 + sample 5: + time = 349333 + flags = 1 + data = length 768, hash 6CDA87FA + sample 6: + time = 381333 + flags = 1 + data = length 768, hash D7542AD6 + sample 7: + time = 413333 + flags = 1 + data = length 768, hash 650B3B21 + sample 8: + time = 445333 + flags = 1 + data = length 768, hash 7AA9EA5 + sample 9: + time = 477333 + flags = 1 + data = length 768, hash 36E854AB + sample 10: + time = 509333 + flags = 1 + data = length 768, hash 1958F2B0 + sample 11: + time = 541333 + flags = 1 + data = length 768, hash E12C6DFC + sample 12: + time = 573333 + flags = 1 + data = length 768, hash 64E149B3 + sample 13: + time = 605333 + flags = 1 + data = length 768, hash A053FE3F + sample 14: + time = 637333 + flags = 1 + data = length 768, hash 326041DB + sample 15: + time = 669333 + flags = 1 + data = length 768, hash 865250F + sample 16: + time = 701333 + flags = 1 + data = length 768, hash B45ADB43 + sample 17: + time = 733333 + flags = 1 + data = length 768, hash 99626351 + sample 18: + time = 765333 + flags = 1 + data = length 768, hash BCED46AE + sample 19: + time = 797333 + flags = 1 + data = length 768, hash 63B6188F + sample 20: + time = 829333 + flags = 1 + data = length 768, hash 86C6C28B + sample 21: + time = 861333 + flags = 1 + data = length 768, hash A1D014D1 +track 224: + total output bytes = 30037 + sample count = 6 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 160 + height = 120 + initializationData: + data = length 22, hash 3B91971B + sample 0: + time = 400000 + flags = 0 + data = length 5611, hash A70A9C09 + sample 1: + time = 466666 + flags = 0 + data = length 4740, hash F8C7264F + sample 2: + time = 533333 + flags = 0 + data = length 3679, hash BEF11657 + sample 3: + time = 600000 + flags = 0 + data = length 2910, hash 5F82B9AA + sample 4: + time = 666666 + flags = 0 + data = length 3357, hash 91082EE0 + sample 5: + time = 733333 + flags = 0 + data = length 2471, hash B011430D +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.2.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.2.dump new file mode 100644 index 00000000000..d51c3346f4b --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.2.dump @@ -0,0 +1,97 @@ +seekMap: + isSeekable = true + duration = 1201344 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(600672) = [[timeUs=600672, position=35805]] + getPosition(1201344) = [[timeUs=1201344, position=71799]] +numberOfTracks = 2 +track 189: + total output bytes = 13056 + sample count = 17 + format 0: + averageBitrate = 192000 + peakBitrate = 192000 + id = 189 + sampleMimeType = audio/ac3 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 317333 + flags = 1 + data = length 768, hash 6CDA87FA + sample 1: + time = 349333 + flags = 1 + data = length 768, hash D7542AD6 + sample 2: + time = 413333 + flags = 1 + data = length 768, hash 650B3B21 + sample 3: + time = 445333 + flags = 1 + data = length 768, hash 7AA9EA5 + sample 4: + time = 477333 + flags = 1 + data = length 768, hash 36E854AB + sample 5: + time = 509333 + flags = 1 + data = length 768, hash 1958F2B0 + sample 6: + time = 541333 + flags = 1 + data = length 768, hash E12C6DFC + sample 7: + time = 573333 + flags = 1 + data = length 768, hash 64E149B3 + sample 8: + time = 605333 + flags = 1 + data = length 768, hash A053FE3F + sample 9: + time = 637333 + flags = 1 + data = length 768, hash 326041DB + sample 10: + time = 669333 + flags = 1 + data = length 768, hash 865250F + sample 11: + time = 701333 + flags = 1 + data = length 768, hash B45ADB43 + sample 12: + time = 733333 + flags = 1 + data = length 768, hash 99626351 + sample 13: + time = 765333 + flags = 1 + data = length 768, hash BCED46AE + sample 14: + time = 797333 + flags = 1 + data = length 768, hash 63B6188F + sample 15: + time = 829333 + flags = 1 + data = length 768, hash 86C6C28B + sample 16: + time = 861333 + flags = 1 + data = length 768, hash A1D014D1 +track 224: + total output bytes = 0 + sample count = 0 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 160 + height = 120 + initializationData: + data = length 22, hash 3B91971B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.3.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.3.dump new file mode 100644 index 00000000000..3f381a5b719 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.3.dump @@ -0,0 +1,33 @@ +seekMap: + isSeekable = true + duration = 1201344 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(600672) = [[timeUs=600672, position=35805]] + getPosition(1201344) = [[timeUs=1201344, position=71799]] +numberOfTracks = 2 +track 189: + total output bytes = 768 + sample count = 1 + format 0: + averageBitrate = 192000 + peakBitrate = 192000 + id = 189 + sampleMimeType = audio/ac3 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 829333 + flags = 1 + data = length 768, hash A1D014D1 +track 224: + total output bytes = 0 + sample count = 0 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 160 + height = 120 + initializationData: + data = length 22, hash 3B91971B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.unknown_length.dump new file mode 100644 index 00000000000..44c5e4edf96 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3_straddle.ps.unknown_length.dump @@ -0,0 +1,170 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 189: + total output bytes = 19200 + sample count = 25 + format 0: + averageBitrate = 192000 + peakBitrate = 192000 + id = 189 + sampleMimeType = audio/ac3 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 61333 + flags = 1 + data = length 768, hash 4513D6ED + sample 1: + time = 93333 + flags = 1 + data = length 768, hash F7DD71AD + sample 2: + time = 157333 + flags = 1 + data = length 768, hash B46BDFC2 + sample 3: + time = 189333 + flags = 1 + data = length 768, hash 6E3DC97F + sample 4: + time = 221333 + flags = 1 + data = length 768, hash BCF0EE73 + sample 5: + time = 253333 + flags = 1 + data = length 768, hash 4FD0241F + sample 6: + time = 285333 + flags = 1 + data = length 768, hash DF256D70 + sample 7: + time = 317333 + flags = 1 + data = length 768, hash BD3A8741 + sample 8: + time = 349333 + flags = 1 + data = length 768, hash 6CDA87FA + sample 9: + time = 381333 + flags = 1 + data = length 768, hash D7542AD6 + sample 10: + time = 413333 + flags = 1 + data = length 768, hash 650B3B21 + sample 11: + time = 445333 + flags = 1 + data = length 768, hash 7AA9EA5 + sample 12: + time = 477333 + flags = 1 + data = length 768, hash 36E854AB + sample 13: + time = 509333 + flags = 1 + data = length 768, hash 1958F2B0 + sample 14: + time = 541333 + flags = 1 + data = length 768, hash E12C6DFC + sample 15: + time = 573333 + flags = 1 + data = length 768, hash 64E149B3 + sample 16: + time = 605333 + flags = 1 + data = length 768, hash A053FE3F + sample 17: + time = 637333 + flags = 1 + data = length 768, hash 326041DB + sample 18: + time = 669333 + flags = 1 + data = length 768, hash 865250F + sample 19: + time = 701333 + flags = 1 + data = length 768, hash B45ADB43 + sample 20: + time = 733333 + flags = 1 + data = length 768, hash 99626351 + sample 21: + time = 765333 + flags = 1 + data = length 768, hash BCED46AE + sample 22: + time = 797333 + flags = 1 + data = length 768, hash 63B6188F + sample 23: + time = 829333 + flags = 1 + data = length 768, hash 86C6C28B + sample 24: + time = 861333 + flags = 1 + data = length 768, hash A1D014D1 +track 224: + total output bytes = 52248 + sample count = 11 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 160 + height = 120 + initializationData: + data = length 22, hash 3B91971B + sample 0: + time = 66666 + flags = 1 + data = length 5986, hash 6A578416 + sample 1: + time = 133333 + flags = 0 + data = length 5571, hash A276297C + sample 2: + time = 200000 + flags = 0 + data = length 4475, hash 50693038 + sample 3: + time = 266666 + flags = 0 + data = length 5187, hash 8AD660F2 + sample 4: + time = 333333 + flags = 0 + data = length 4552, hash 773C4FC3 + sample 5: + time = 400000 + flags = 0 + data = length 5611, hash A70A9C09 + sample 6: + time = 466666 + flags = 0 + data = length 4740, hash F8C7264F + sample 7: + time = 533333 + flags = 0 + data = length 3679, hash BEF11657 + sample 8: + time = 600000 + flags = 0 + data = length 2910, hash 5F82B9AA + sample 9: + time = 666666 + flags = 0 + data = length 3357, hash 91082EE0 + sample 10: + time = 733333 + flags = 0 + data = length 2471, hash B011430D +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/ts/sample_ac3_straddle.ps b/libraries/test_data/src/test/assets/media/ts/sample_ac3_straddle.ps new file mode 100644 index 00000000000..18e498a2020 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/ts/sample_ac3_straddle.ps differ