diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac4Reader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac4Reader.java index 0d97e8a6e72..518104a6631 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac4Reader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac4Reader.java @@ -19,6 +19,7 @@ import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; +import android.media.AudioPresentation; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -37,6 +38,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -59,6 +61,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Nullable private final String language; private final @C.RoleFlags int roleFlags; private final String containerMimeType; + @Nullable private final List audioPresentations; private @MonotonicNonNull String formatId; private @MonotonicNonNull TrackOutput output; @@ -84,7 +87,7 @@ public final class Ac4Reader implements ElementaryStreamReader { * @param containerMimeType The MIME type of the container holding the stream. */ public Ac4Reader(String containerMimeType) { - this(null, /* roleFlags= */ 0, containerMimeType); + this(null, /* roleFlags= */ 0, containerMimeType, null); } /** @@ -93,9 +96,11 @@ public Ac4Reader(String containerMimeType) { * @param language Track language. * @param roleFlags Track role flags. * @param containerMimeType The MIME type of the container holding the stream. + * @param audioPresentations Track audio presentations. */ public Ac4Reader( - @Nullable String language, @C.RoleFlags int roleFlags, String containerMimeType) { + @Nullable String language, @C.RoleFlags int roleFlags, String containerMimeType, + @Nullable List audioPresentations) { headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -106,6 +111,7 @@ public Ac4Reader( this.language = language; this.roleFlags = roleFlags; this.containerMimeType = containerMimeType; + this.audioPresentations = audioPresentations; } @Override @@ -230,6 +236,7 @@ private void parseHeader() { .setSampleRate(frameInfo.sampleRate) .setLanguage(language) .setRoleFlags(roleFlags) + .setAudioPresentations(audioPresentations) .build(); output.format(format); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/DefaultTsPayloadReaderFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/DefaultTsPayloadReaderFactory.java index d2aae227d69..3cdd0c0e56b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -170,7 +170,8 @@ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { new Ac3Reader(esInfo.language, esInfo.getRoleFlags(), MimeTypes.VIDEO_MP2T)); case TsExtractor.TS_STREAM_TYPE_AC4: return new PesReader( - new Ac4Reader(esInfo.language, esInfo.getRoleFlags(), MimeTypes.VIDEO_MP2T)); + new Ac4Reader(esInfo.language, esInfo.getRoleFlags(), MimeTypes.VIDEO_MP2T, + esInfo.audioPresentations)); case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: if (!isSet(FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)) { return null; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java index 73b75e1ac1d..06d50869a2b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java @@ -19,11 +19,16 @@ import static androidx.media3.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR; import static java.lang.annotation.ElementType.TYPE_USE; +import android.icu.util.ULocale; +import android.media.AudioPresentation; +import android.os.Build; +import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media3.common.C; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; @@ -54,13 +59,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.ListIterator; +import java.util.Locale; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Extracts data from the MPEG-2 TS container format. */ @UnstableApi public final class TsExtractor implements Extractor { + private static final String TAG = "TsExtractor"; /** * Creates a factory for {@link TsExtractor} instances with the provided {@link * SubtitleParser.Factory}. @@ -149,6 +158,10 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. private static final int TS_PAT_PID = 0; + private static final int TS_TABLE_ID_SERVICE_DESCRIPTION_SECTION = 0x42; + private static final int TS_SERVICE_DESCRIPTOR_TAG = 0x48; + // Service Description Table, Stuffing Table and Bouquet Association Table all have the same PID. + private static final int TS_SDT_BAT_ST_PID = 0x11; private static final int MAX_PID_PLUS_ONE = 0x2000; private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33; @@ -170,10 +183,14 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser private final SparseArray tsPayloadReaders; // Indexed by pid private final SparseBooleanArray trackIds; private final SparseBooleanArray trackPids; + + private final SparseIntArray presentationMessageIds; private final TsDurationReader durationReader; // Accessed only by the loading thread. private @MonotonicNonNull TsBinarySearchSeeker tsBinarySearchSeeker; + + List audioPresentations; private ExtractorOutput output; private int remainingPmts; private boolean tracksEnded; @@ -340,6 +357,8 @@ public TsExtractor( tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0); trackIds = new SparseBooleanArray(); trackPids = new SparseBooleanArray(); + presentationMessageIds = new SparseIntArray(); + audioPresentations = new ArrayList<>(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); durationReader = new TsDurationReader(timestampSearchBytes); @@ -620,7 +639,41 @@ private void resetPayloadReaders() { tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i)); } tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader())); + tsPayloadReaders.put(TS_SDT_BAT_ST_PID, new SectionReader(new SdtSectionReader())); id3Reader = null; + audioPresentations.clear(); + presentationMessageIds.clear(); + } + + @RequiresApi(api = Build.VERSION_CODES.R) + private boolean updatePresentationLabels(List aps, + SparseArray> presentationLabels) { + for (ListIterator itr = aps.listIterator(); itr.hasNext(); ) { + AudioPresentation ap = itr.next(); + final int valueIfKeyNotFound = -1; + final int messageId = presentationMessageIds.get(ap.getPresentationId(), valueIfKeyNotFound); + if (messageId == valueIfKeyNotFound) { + Log.v(TAG, "No message id detected"); + return false; + } + final HashMap labels = presentationLabels.get(messageId); + if (labels.isEmpty()) { + continue; + } + // Reconstruct the AudioPresentation object with valid labels in it. + AudioPresentation newAp = (new AudioPresentation.Builder(ap.getPresentationId()) + .setProgramId(ap.getProgramId()) + .setLocale(ULocale.forLocale(ap.getLocale())) + .setLabels(labels) + .setMasteringIndication(ap.getMasteringIndication()) + .setHasAudioDescription(ap.hasAudioDescription()) + .setHasSpokenSubtitles(ap.hasSpokenSubtitles()) + .setHasDialogueEnhancement(ap.hasDialogueEnhancement())).build(); + itr.set(newAp); + presentationMessageIds.delete(ap.getPresentationId()); + } + + return true; } /** Parses Program Association Table data. */ @@ -678,6 +731,106 @@ public void consume(ParsableByteArray sectionData) { } } + /** + * Parses Service Description Table data. + */ + private class SdtSectionReader implements SectionPayloadReader { + public SdtSectionReader() { + } + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + @RequiresApi(api = Build.VERSION_CODES.R) + @Override + public void consume(ParsableByteArray sectionData) { + if (sectionData.bytesLeft() < 1) { + return; + } + final int tableId = sectionData.readUnsignedByte(); + if (tableId != TS_TABLE_ID_SERVICE_DESCRIPTION_SECTION) { + // See DVB BlueBook A038, section 5.1.3 for more information on table id assignment. + return; + } + if (sectionData.bytesLeft() < 15) { + return; + } + // section_syntax_indicator(1), reserved_future_use(1), reserved(2), + // section_length(12), transport_stream_id(16), reserved(2), version_number(5), + // current_next_indicator(1), section_number(8), last_section_number(8), + // original_network_id(16), reserved_future_use(8) + sectionData.skipBytes(10); + sectionData.skipBytes(2); // serviceId + // reserved_future_use(6), EIT_schedule_flag(1), EIT_present_following_flag(1) + sectionData.skipBytes(1); + // Remove running_status(3), free_CA_mode(1) from the descriptors_loop_length with the mask. + final int descriptorsLoopLength = sectionData.readUnsignedShort() & 0xFFF; + if (descriptorsLoopLength != sectionData.bytesLeft()) { + Log.e(TAG, "Invalid section data length"); + return; + } + // Indexed by message id and value contains labels in each language + SparseArray> presentationLabels = new SparseArray<>(); + while (sectionData.bytesLeft() > 0) { + final int descriptorTag = sectionData.readUnsignedByte(); + if (sectionData.bytesLeft() < 1) { + break; + } + int descriptorLength = sectionData.readUnsignedByte(); + if (sectionData.bytesLeft() < descriptorLength) { + break; + } + if (descriptorTag == PmtReader.TS_PMT_DESC_DVB_EXT) { + final int descriptorTagExt = sectionData.readUnsignedByte(); + int descriptorExtLength = descriptorLength - 1; + // Message_descriptor in DVB A038 + if (descriptorTagExt == PmtReader.TS_PMT_DESC_DVB_EXT_MESSAGE) { + int message_id = sectionData.readUnsignedByte(); + descriptorExtLength--; + if (sectionData.bytesLeft() < 3) { + break; + } + String language = new String(sectionData.getData(), + sectionData.getPosition(), 3).trim(); + sectionData.setPosition(sectionData.getPosition() + 3); + descriptorExtLength -= 3; + if (descriptorExtLength < 1) { + break; + } + CharSequence label = new String( + sectionData.getData(), sectionData.getPosition(), descriptorExtLength).trim(); + sectionData.setPosition(sectionData.getPosition() + descriptorExtLength); + HashMap labels = new HashMap() + {{ put(ULocale.forLocale(new Locale(language)), label); }}; + presentationLabels.append(message_id, labels); + } else { + // Ignore other extended descriptors. + sectionData.skipBytes(descriptorExtLength - 1); + } + } else if (descriptorTag == TS_SERVICE_DESCRIPTOR_TAG) { + final int serviceType = sectionData.readUnsignedByte(); + Log.d(TAG,"serviceType : " + serviceType); + final int serviceProviderNameLength = sectionData.readUnsignedByte(); + String serviceProvider = sectionData.readString(serviceProviderNameLength); + Log.d(TAG,"serviceProvider : " + serviceProvider); + final int serviceNameLength = sectionData.readUnsignedByte(); + String serviceName = sectionData.readString(serviceNameLength); + Log.d(TAG,"serviceName: " + serviceName); + } else { + sectionData.skipBytes(descriptorLength); + } + } + boolean gotLabels = updatePresentationLabels(audioPresentations, presentationLabels); + if (gotLabels) { + Log.d(TAG,"Audio presentations with labels: " + audioPresentations); + } + if (mode != MODE_HLS) { + tsPayloadReaders.remove(TS_SDT_BAT_ST_PID); + } + } + } + /** Parses Program Map Table. */ private class PmtReader implements SectionPayloadReader { @@ -688,10 +841,12 @@ private class PmtReader implements SectionPayloadReader { private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; private static final int TS_PMT_DESC_DVB_EXT = 0x7F; + private static final int TS_PMT_DESC_DVB_EXT_MESSAGE = 0x08; private static final int TS_PMT_DESC_DVBSUBS = 0x59; private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15; private static final int TS_PMT_DESC_DVB_EXT_DTS_HD = 0x0E; + private static final int TS_PMT_DESC_DVB_EXT_AUDIO_PRESELECTION = 0x19; private static final int TS_PMT_DESC_DVB_EXT_DTS_UHD = 0x21; private final ParsableBitArray pmtScratch; @@ -763,7 +918,8 @@ public void consume(ParsableByteArray sectionData) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. EsInfo id3EsInfo = - new EsInfo(TS_STREAM_TYPE_ID3, null, AUDIO_TYPE_UNDEFINED, null, Util.EMPTY_BYTE_ARRAY); + new EsInfo(TS_STREAM_TYPE_ID3, null, AUDIO_TYPE_UNDEFINED, null, + Util.EMPTY_BYTE_ARRAY, null); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, id3EsInfo); if (id3Reader != null) { id3Reader.init( @@ -881,10 +1037,95 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { } else if (descriptorTag == TS_PMT_DESC_DVB_EXT) { // Extension descriptor in DVB (ETSI EN 300 468). int descriptorTagExt = data.readUnsignedByte(); + final int descriptorExtLength = descriptorLength - 1; if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AC4) { // AC-4_descriptor in DVB (ETSI EN 300 468). streamType = TS_STREAM_TYPE_AC4; - } else if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_DTS_HD) { + } else if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AUDIO_PRESELECTION) { + // Audio_preselection_descriptor in DVB A038 + ParsableBitArray extData = new ParsableBitArray(new byte[descriptorExtLength]); + data.readBytes(extData, descriptorExtLength); + if (extData.bitsLeft() < 8) { + break; + } + int numPreselections = extData.readBits(5); + extData.skipBits(3); // reserved_zero_future_use + boolean lookingForLabels = false; + for (int i = 0; i < numPreselections; i++) { + if (extData.bitsLeft() < ((numPreselections - i) * 16)) { + break; + } + int presentationId = extData.readBits(5); + int masteringIndication = extData.readBits(3); // audio_rendering_indication + boolean audioDescriptionAvailable = extData.readBit(); + boolean spokenSubtitlesAvailable = extData.readBit(); + boolean dialogueEnhancementAvailable = extData.readBit(); + extData.skipBits(1); // interactivityEnabled + boolean languageCodePresent = extData.readBit(); + boolean textLabelPresent = extData.readBit(); + boolean multiStreamInfoPresent = extData.readBit(); + boolean futureExtension = extData.readBit(); + String languageTag = null; + if (languageCodePresent) { + if (extData.bitsLeft() < 24) { + break; + } + languageTag = new String(extData.data, extData.getPosition()/8, 3).trim(); + extData.setPosition(extData.getPosition() + 24); + } + if (textLabelPresent) { + if (extData.bitsLeft() < 8) { + break; + } + int messageId = extData.readBits(8); + presentationMessageIds.put(presentationId, messageId); + lookingForLabels = true; + } + if (multiStreamInfoPresent) { + if (extData.bitsLeft() < 8) { + break; + } + int numAuxComponents = extData.readBits(3); + extData.skipBits(5); // reserved_zero_future_use + if (extData.bitsLeft() < (numAuxComponents * 8)) { + break; + } + extData.skipBits(numAuxComponents * 8); // component_tag + } + if (futureExtension) { + if (extData.bitsLeft() < 8) { + break; + } + extData.skipBits(3); // reserved + int futureExtensionLength = extData.readBits(5); + if (extData.bitsLeft() < (futureExtensionLength * 8)) { + break; + } + extData.skipBits(futureExtensionLength * 8); // futureExtensionByte + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AudioPresentation.Builder presentation = + (new AudioPresentation.Builder(presentationId) + .setMasteringIndication(masteringIndication) + .setHasAudioDescription(audioDescriptionAvailable) + .setHasSpokenSubtitles(spokenSubtitlesAvailable) + .setHasDialogueEnhancement(dialogueEnhancementAvailable)); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + presentation.setProgramId(AudioPresentation.PROGRAM_ID_UNKNOWN); + } else { + presentation.setProgramId(-1); + } + if (languageTag != null) { + presentation.setLocale(ULocale.forLocale(new Locale(languageTag))); + } + audioPresentations.add(presentation.build()); + } + } + if (!lookingForLabels) { + Log.d(TAG,"Audio presentations without labels: " + audioPresentations); + } + } + else if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_DTS_HD) { // DTS-HD descriptor in DVB (ETSI EN 300 468). streamType = TS_STREAM_TYPE_DTS_HD; } else if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_DTS_UHD) { @@ -919,7 +1160,8 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { language, audioType, dvbSubtitleInfos, - Arrays.copyOfRange(data.getData(), descriptorsStartPosition, descriptorsEndPosition)); + Arrays.copyOfRange(data.getData(), descriptorsStartPosition, descriptorsEndPosition), + audioPresentations); } } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsPayloadReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsPayloadReader.java index 2b16a0e3006..d1c05f82eb7 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsPayloadReader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsPayloadReader.java @@ -18,6 +18,7 @@ import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.media.AudioPresentation; import android.util.SparseArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -111,6 +112,7 @@ final class EsInfo { public final @AudioType int audioType; public final List dvbSubtitleInfos; public final byte[] descriptorBytes; + @Nullable public final List audioPresentations; /** * @param streamType The type of the stream as defined by the {@link TsExtractor}{@code @@ -119,13 +121,15 @@ final class EsInfo { * @param audioType The audio type of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. + * @param audioPresentations The audio presentations associated with the stream. */ public EsInfo( int streamType, @Nullable String language, @AudioType int audioType, @Nullable List dvbSubtitleInfos, - byte[] descriptorBytes) { + byte[] descriptorBytes, + @Nullable List audioPresentations) { this.streamType = streamType; this.language = language; this.audioType = audioType; @@ -134,6 +138,7 @@ public EsInfo( ? Collections.emptyList() : Collections.unmodifiableList(dvbSubtitleInfos); this.descriptorBytes = descriptorBytes; + this.audioPresentations = audioPresentations; } } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsAc4AudioPresentationsExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsAc4AudioPresentationsExtractorTest.java new file mode 100644 index 00000000000..349c5d53185 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsAc4AudioPresentationsExtractorTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 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 androidx.media3.extractor.ts; + +import static com.google.common.truth.Truth.assertThat; + +import android.icu.util.ULocale; +import android.media.AudioPresentation; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.datasource.DataSourceUtil; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.PositionHolder; +import androidx.media3.extractor.text.DefaultSubtitleParserFactory; +import androidx.media3.test.utils.FakeExtractorOutput; +import androidx.media3.test.utils.TestUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TsAc4AudioPresentationsExtractorTest}. */ +@RunWith(AndroidJUnit4.class) +public final class TsAc4AudioPresentationsExtractorTest { + + @Test + public void verifyAc4MultipleAudioPresentations() throws Exception { + TsExtractor extractor = new TsExtractor(new DefaultSubtitleParserFactory()); + Uri fileUri = TestUtil.buildAssetUri("media/ts/sample_ac4_multiple_presentations.ts"); + List refPresentations = new ArrayList<>(); + Map ulocaleLabels = new HashMap<>(); + ulocaleLabels.put(ULocale.forLocale(Locale.ENGLISH), "Standard"); + refPresentations.add(new AudioPresentation.Builder(10) + .setProgramId(AudioPresentation.PROGRAM_ID_UNKNOWN) + .setLocale(ULocale.ENGLISH) + .setLabels(ulocaleLabels) + .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) + .setHasSpokenSubtitles(false) + .setHasDialogueEnhancement(true) + .build()); + ulocaleLabels.put(ULocale.forLocale(Locale.ENGLISH), "Kids' choice"); + refPresentations.add(new AudioPresentation.Builder(11) + .setProgramId(AudioPresentation.PROGRAM_ID_UNKNOWN) + .setLocale(ULocale.ENGLISH) + .setLabels(ulocaleLabels) + .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) + .setHasSpokenSubtitles(false) + .setHasAudioDescription(true) + .setHasDialogueEnhancement(true) + .build()); + ulocaleLabels.put(ULocale.forLocale(Locale.ENGLISH), "Artists' commentary"); + refPresentations.add(new AudioPresentation.Builder(12) + .setProgramId(AudioPresentation.PROGRAM_ID_UNKNOWN) + .setLocale(ULocale.FRENCH) + .setLabels(ulocaleLabels) + .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) + .setHasSpokenSubtitles(false) + .setHasDialogueEnhancement(true) + .build()); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + extractor.init(extractorOutput); + int readResult = Extractor.RESULT_CONTINUE; + PositionHolder positionHolder = new PositionHolder(); + DefaultDataSource dataSource = + new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()) + .createDataSource(); + ExtractorInput input = TestUtil.getExtractorInputFromPosition(dataSource, 0, fileUri); + while (readResult != Extractor.RESULT_END_OF_INPUT) { + try { + while (readResult == Extractor.RESULT_CONTINUE) { + readResult = extractor.read(input, positionHolder); + } + } finally { + DataSourceUtil.closeQuietly(dataSource); + } + if (readResult == Extractor.RESULT_SEEK) { + input = + TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, fileUri); + readResult = Extractor.RESULT_CONTINUE; + } + } + Format formatFromBundle = null; + List audioPresentations = null; + for (int i = 0; i < extractorOutput.numberOfTracks; ++i) { + int trackId = extractorOutput.trackOutputs.keyAt(i); + Format format = extractorOutput.trackOutputs.get(trackId).lastFormat; + if (format != null && format.sampleMimeType.equals(MimeTypes.AUDIO_AC4)) { + assertThat(format).isNotNull(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + audioPresentations = format.audioPresentations; + } else { + Bundle formatAsBundle = format.toBundle(); + assertThat(formatAsBundle).isNotNull(); + formatFromBundle = Format.fromBundle(format.toBundle()); + audioPresentations = formatFromBundle.audioPresentations; + } + } + } + assertThat(audioPresentations).isNotNull(); + assertThat(refPresentations.size()).isEqualTo(audioPresentations.size()); + for (int i = 0; i < refPresentations.size(); i++) { + assertThat(refPresentations.get(i)).isEqualTo((audioPresentations.get(i))); + } + } +} diff --git a/libraries/test_data/src/test/assets/media/ts/sample_ac4_multiple_presentations.ts b/libraries/test_data/src/test/assets/media/ts/sample_ac4_multiple_presentations.ts new file mode 100644 index 00000000000..5aa9989ce7c Binary files /dev/null and b/libraries/test_data/src/test/assets/media/ts/sample_ac4_multiple_presentations.ts differ