Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -328,16 +332,24 @@ 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;
private boolean seenFirstDts;
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]);
}

Expand Down Expand Up @@ -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.
*
* <p>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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Loading