From f1a5825d73308ac4e6a506f3a95aae3d15b01fd2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 1 Dec 2021 17:05:25 +0000 Subject: [PATCH] Add optional id to TrackGroup. This allows to give TrackGroups an identifier. The underlying goal is to provide a way to make otherwise identical TrackGroups distinguishable. Also set this id in all internal sources that may produce identical TrackGroups in certain edge cases. Issue: google/ExoPlayer#9718 PiperOrigin-RevId: 413430719 --- .../java/androidx/media3/cast/CastPlayer.java | 3 +- .../androidx/media3/common/TrackGroup.java | 43 ++++- .../media3/common/TrackGroupTest.java | 3 +- .../exoplayer/source/MergingMediaPeriod.java | 167 +++++++++++++++++- .../source/ProgressiveMediaPeriod.java | 2 +- .../media3/exoplayer/util/EventLogger.java | 2 +- .../exoplayer/dash/DashMediaPeriod.java | 17 +- .../exoplayer/dash/DashMediaPeriodTest.java | 10 +- .../media3/exoplayer/hls/HlsMediaPeriod.java | 22 ++- .../exoplayer/hls/HlsSampleStreamWrapper.java | 11 +- .../exoplayer/rtsp/RtspMediaPeriod.java | 4 +- .../smoothstreaming/SsMediaPeriod.java | 2 +- 12 files changed, 254 insertions(+), 32 deletions(-) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index b3f981986e..4ffe3ecbd6 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -1060,7 +1060,8 @@ public final class CastPlayer extends BasePlayer { new TracksInfo.TrackGroupInfo[castMediaTracks.size()]; for (int i = 0; i < castMediaTracks.size(); i++) { MediaTrack mediaTrack = castMediaTracks.get(i); - trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); + trackGroups[i] = + new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack)); long id = mediaTrack.getId(); @C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java index a43ad1b61d..b393e43d3d 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java @@ -18,6 +18,7 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkArgument; import android.os.Bundle; +import androidx.annotation.CheckResult; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.BundleableUtil; @@ -38,6 +39,8 @@ public final class TrackGroup implements Bundleable { /** The number of tracks in the group. */ public final int length; + /** An identifier for the track group. */ + public final String id; private final Format[] formats; @@ -45,18 +48,42 @@ public final class TrackGroup implements Bundleable { private int hashCode; /** - * Constructs an instance {@code TrackGroup} containing the provided {@code formats}. + * Constructs a track group containing the provided {@code formats}. * - * @param formats Non empty array of format. + * @param formats The list of {@link Format Formats}. Must not be empty. */ @UnstableApi public TrackGroup(Format... formats) { + this(/* id= */ "", formats); + } + + /** + * Constructs a track group with the provided {@code id} and {@code formats}. + * + * @param id The identifier of the track group. May be an empty string. + * @param formats The list of {@link Format Formats}. Must not be empty. + */ + @UnstableApi + public TrackGroup(String id, Format... formats) { checkArgument(formats.length > 0); + this.id = id; this.formats = formats; this.length = formats.length; verifyCorrectness(); } + /** + * Returns a copy of this track group with the specified {@code id}. + * + * @param id The identifier for the copy of the track group. + * @return The copied track group. + */ + @UnstableApi + @CheckResult + public TrackGroup copyWithId(String id) { + return new TrackGroup(id, formats); + } + /** * Returns the format of the track at a given index. * @@ -89,6 +116,7 @@ public final class TrackGroup implements Bundleable { public int hashCode() { if (hashCode == 0) { int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + Arrays.hashCode(formats); hashCode = result; } @@ -104,19 +132,18 @@ public final class TrackGroup implements Bundleable { return false; } TrackGroup other = (TrackGroup) obj; - return length == other.length && Arrays.equals(formats, other.formats); + return length == other.length && id.equals(other.id) && Arrays.equals(formats, other.formats); } // Bundleable implementation. @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({ - FIELD_FORMATS, - }) + @IntDef({FIELD_FORMATS, FIELD_ID}) private @interface FieldNumber {} private static final int FIELD_FORMATS = 0; + private static final int FIELD_ID = 1; @UnstableApi @Override @@ -124,6 +151,7 @@ public final class TrackGroup implements Bundleable { Bundle bundle = new Bundle(); bundle.putParcelableArrayList( keyForField(FIELD_FORMATS), BundleableUtil.toBundleArrayList(Lists.newArrayList(formats))); + bundle.putString(keyForField(FIELD_ID), id); return bundle; } @@ -136,7 +164,8 @@ public final class TrackGroup implements Bundleable { Format.CREATOR, bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)), ImmutableList.of()); - return new TrackGroup(formats.toArray(new Format[0])); + String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ ""); + return new TrackGroup(id, formats.toArray(new Format[0])); }; private static String keyForField(@FieldNumber int field) { diff --git a/libraries/common/src/test/java/androidx/media3/common/TrackGroupTest.java b/libraries/common/src/test/java/androidx/media3/common/TrackGroupTest.java index d5ce315374..bd4075a60a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TrackGroupTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TrackGroupTest.java @@ -30,8 +30,9 @@ public final class TrackGroupTest { Format.Builder formatBuilder = new Format.Builder(); Format format1 = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build(); Format format2 = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_AAC).build(); + String id = "abc"; - TrackGroup trackGroupToBundle = new TrackGroup(format1, format2); + TrackGroup trackGroupToBundle = new TrackGroup(id, format1, format2); TrackGroup trackGroupFromBundle = TrackGroup.CREATOR.fromBundle(trackGroupToBundle.toBundle()); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java index 41bc684b6b..584c40a378 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java @@ -15,10 +15,12 @@ */ package androidx.media3.exoplayer.source; +import static androidx.media3.common.util.Assertions.checkNotNull; import static java.lang.Math.max; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackGroupArray; @@ -26,10 +28,14 @@ import androidx.media3.common.util.Assertions; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.FormatHolder; import androidx.media3.exoplayer.SeekParameters; +import androidx.media3.exoplayer.source.chunk.Chunk; +import androidx.media3.exoplayer.source.chunk.MediaChunk; +import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -42,6 +48,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final IdentityHashMap streamPeriodIndices; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final ArrayList childrenPendingPreparation; + private final HashMap childTrackGroupByMergedTrackGroup; @Nullable private Callback callback; @Nullable private TrackGroupArray trackGroups; @@ -55,6 +62,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; childrenPendingPreparation = new ArrayList<>(); + childTrackGroupByMergedTrackGroup = new HashMap<>(); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); @@ -113,9 +121,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex; selectionChildIndices[i] = C.INDEX_UNSET; if (selections[i] != null) { - TrackGroup trackGroup = selections[i].getTrackGroup(); + TrackGroup mergedTrackGroup = selections[i].getTrackGroup(); + TrackGroup childTrackGroup = + checkNotNull(childTrackGroupByMergedTrackGroup.get(mergedTrackGroup)); for (int j = 0; j < periods.length; j++) { - if (periods[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) { + if (periods[j].getTrackGroups().indexOf(childTrackGroup) != C.INDEX_UNSET) { selectionChildIndices[i] = j; break; } @@ -131,7 +141,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; for (int i = 0; i < periods.length; i++) { for (int j = 0; j < selections.length; j++) { childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; - childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; + if (selectionChildIndices[j] == i) { + ExoTrackSelection mergedTrackSelection = checkNotNull(selections[j]); + TrackGroup mergedTrackGroup = mergedTrackSelection.getTrackGroup(); + TrackGroup childTrackGroup = + checkNotNull(childTrackGroupByMergedTrackGroup.get(mergedTrackGroup)); + childSelections[j] = new ForwardingTrackSelection(mergedTrackSelection, childTrackGroup); + } else { + childSelections[j] = null; + } } long selectPositionUs = periods[i].selectTracks( @@ -270,11 +288,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; int trackGroupIndex = 0; - for (MediaPeriod period : periods) { - TrackGroupArray periodTrackGroups = period.getTrackGroups(); + for (int i = 0; i < periods.length; i++) { + TrackGroupArray periodTrackGroups = periods[i].getTrackGroups(); int periodTrackGroupCount = periodTrackGroups.length; for (int j = 0; j < periodTrackGroupCount; j++) { - trackGroupArray[trackGroupIndex++] = periodTrackGroups.get(j); + TrackGroup childTrackGroup = periodTrackGroups.get(j); + TrackGroup mergedTrackGroup = childTrackGroup.copyWithId(i + ":" + childTrackGroup.id); + childTrackGroupByMergedTrackGroup.put(mergedTrackGroup, childTrackGroup); + trackGroupArray[trackGroupIndex++] = mergedTrackGroup; } } trackGroups = new TrackGroupArray(trackGroupArray); @@ -455,4 +476,138 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return sampleStream.skipData(positionUs - timeOffsetUs); } } + + private static final class ForwardingTrackSelection implements ExoTrackSelection { + + private final ExoTrackSelection trackSelection; + private final TrackGroup trackGroup; + + public ForwardingTrackSelection(ExoTrackSelection trackSelection, TrackGroup trackGroup) { + this.trackSelection = trackSelection; + this.trackGroup = trackGroup; + } + + @Override + public @Type int getType() { + return trackSelection.getType(); + } + + @Override + public TrackGroup getTrackGroup() { + return trackGroup; + } + + @Override + public int length() { + return trackSelection.length(); + } + + @Override + public Format getFormat(int index) { + return trackSelection.getFormat(index); + } + + @Override + public int getIndexInTrackGroup(int index) { + return trackSelection.getIndexInTrackGroup(index); + } + + @Override + public int indexOf(Format format) { + return trackSelection.indexOf(format); + } + + @Override + public int indexOf(int indexInTrackGroup) { + return trackSelection.indexOf(indexInTrackGroup); + } + + @Override + public void enable() { + trackSelection.enable(); + } + + @Override + public void disable() { + trackSelection.disable(); + } + + @Override + public Format getSelectedFormat() { + return trackSelection.getSelectedFormat(); + } + + @Override + public int getSelectedIndexInTrackGroup() { + return trackSelection.getSelectedIndexInTrackGroup(); + } + + @Override + public int getSelectedIndex() { + return trackSelection.getSelectedIndex(); + } + + @Override + public int getSelectionReason() { + return trackSelection.getSelectionReason(); + } + + @Nullable + @Override + public Object getSelectionData() { + return trackSelection.getSelectionData(); + } + + @Override + public void onPlaybackSpeed(float playbackSpeed) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + + @Override + public void onDiscontinuity() { + trackSelection.onDiscontinuity(); + } + + @Override + public void onRebuffer() { + trackSelection.onRebuffer(); + } + + @Override + public void onPlayWhenReadyChanged(boolean playWhenReady) { + trackSelection.onPlayWhenReadyChanged(playWhenReady); + } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + trackSelection.updateSelectedTrack( + playbackPositionUs, bufferedDurationUs, availableDurationUs, queue, mediaChunkIterators); + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + return trackSelection.evaluateQueueSize(playbackPositionUs, queue); + } + + @Override + public boolean shouldCancelChunkLoad( + long playbackPositionUs, Chunk loadingChunk, List queue) { + return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue); + } + + @Override + public boolean blacklist(int index, long exclusionDurationMs) { + return trackSelection.blacklist(index, exclusionDurationMs); + } + + @Override + public boolean isBlacklisted(int index, long nowMs) { + return trackSelection.isBlacklisted(index, nowMs); + } + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index d98fcec67d..472c56be9a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -783,7 +783,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } trackFormat = trackFormat.copyWithCryptoType(drmSessionManager.getCryptoType(trackFormat)); - trackArray[i] = new TrackGroup(trackFormat); + trackArray[i] = new TrackGroup(/* id= */ Integer.toString(i), trackFormat); } trackState = new TrackState(new TrackGroupArray(trackArray), trackIsAudioVideoFlags); prepared = true; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java index 4ba0729519..455b37157c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java @@ -277,7 +277,7 @@ public class EventLogger implements AnalyticsListener { trackGroup.length, mappedTrackInfo.getAdaptiveSupport( rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)); - logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); + logd(" Group:" + trackGroup.id + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java index 67aeb8011e..326ac356b9 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java @@ -675,13 +675,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); + String trackGroupId = + firstAdaptationSet.id != AdaptationSet.ID_UNSET + ? Integer.toString(firstAdaptationSet.id) + : ("unset:" + i); int primaryTrackGroupIndex = trackGroupCount++; int eventMessageTrackGroupIndex = primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET; int closedCaptionTrackGroupIndex = primaryGroupClosedCaptionTrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET; - trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats); + trackGroups[primaryTrackGroupIndex] = new TrackGroup(trackGroupId, formats); trackGroupInfos[primaryTrackGroupIndex] = TrackGroupInfo.primaryTrack( firstAdaptationSet.type, @@ -690,18 +694,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; eventMessageTrackGroupIndex, closedCaptionTrackGroupIndex); if (eventMessageTrackGroupIndex != C.INDEX_UNSET) { + String eventMessageTrackGroupId = trackGroupId + ":emsg"; Format format = new Format.Builder() - .setId(firstAdaptationSet.id + ":emsg") + .setId(eventMessageTrackGroupId) .setSampleMimeType(MimeTypes.APPLICATION_EMSG) .build(); - trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format); + trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(eventMessageTrackGroupId, format); trackGroupInfos[eventMessageTrackGroupIndex] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex); } if (closedCaptionTrackGroupIndex != C.INDEX_UNSET) { + String closedCaptionTrackGroupId = trackGroupId + ":cc"; trackGroups[closedCaptionTrackGroupIndex] = - new TrackGroup(primaryGroupClosedCaptionTrackFormats[i]); + new TrackGroup(closedCaptionTrackGroupId, primaryGroupClosedCaptionTrackFormats[i]); trackGroupInfos[closedCaptionTrackGroupIndex] = TrackGroupInfo.embeddedClosedCaptionTrack(adaptationSetIndices, primaryTrackGroupIndex); } @@ -721,7 +727,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .setId(eventStream.id()) .setSampleMimeType(MimeTypes.APPLICATION_EMSG) .build(); - trackGroups[existingTrackGroupCount] = new TrackGroup(format); + String uniqueTrackGroupId = eventStream.id() + ":" + i; + trackGroups[existingTrackGroupCount] = new TrackGroup(uniqueTrackGroupId, format); trackGroupInfos[existingTrackGroupCount++] = TrackGroupInfo.mpdEventTrack(i); } } diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java index 6fc8279dbd..1a518729ad 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java @@ -80,12 +80,13 @@ public final class DashMediaPeriodTest { TrackGroupArray expectedTrackGroups = new TrackGroupArray( new TrackGroup( + /* id= */ "0", adaptationSets.get(0).representations.get(0).format, adaptationSets.get(0).representations.get(1).format, adaptationSets.get(2).representations.get(0).format, adaptationSets.get(2).representations.get(1).format, adaptationSets.get(3).representations.get(0).format), - new TrackGroup(adaptationSets.get(1).representations.get(0).format)); + new TrackGroup(/* id= */ "3", adaptationSets.get(1).representations.get(0).format)); MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); } @@ -101,10 +102,12 @@ public final class DashMediaPeriodTest { TrackGroupArray expectedTrackGroups = new TrackGroupArray( new TrackGroup( + /* id= */ "0", adaptationSets.get(0).representations.get(0).format, adaptationSets.get(0).representations.get(1).format, adaptationSets.get(1).representations.get(0).format), new TrackGroup( + /* id= */ "2", adaptationSets.get(2).representations.get(0).format, adaptationSets.get(2).representations.get(1).format, adaptationSets.get(3).representations.get(0).format)); @@ -124,6 +127,7 @@ public final class DashMediaPeriodTest { TrackGroupArray expectedTrackGroups = new TrackGroupArray( new TrackGroup( + /* id= */ "0", adaptationSets.get(0).representations.get(0).format, adaptationSets.get(0).representations.get(1).format, adaptationSets.get(1).representations.get(0).format, @@ -147,9 +151,11 @@ public final class DashMediaPeriodTest { TrackGroupArray expectedTrackGroups = new TrackGroupArray( new TrackGroup( + /* id= */ "123", adaptationSets.get(0).representations.get(0).format, adaptationSets.get(0).representations.get(1).format), new TrackGroup( + /* id= */ "123:cc", cea608FormatBuilder .setId("123:cea608:1") .setLanguage("eng") @@ -177,9 +183,11 @@ public final class DashMediaPeriodTest { TrackGroupArray expectedTrackGroups = new TrackGroupArray( new TrackGroup( + /* id= */ "123", adaptationSets.get(0).representations.get(0).format, adaptationSets.get(0).representations.get(1).format), new TrackGroup( + /* id= */ "123:cc", cea608FormatBuilder .setId("123:cea708:1") .setLanguage("eng") diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index 280cf02d60..96c94d3aae 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -528,8 +528,10 @@ public final class HlsMediaPeriod // Subtitle stream wrappers. We can always use master playlist information to prepare these. for (int i = 0; i < subtitleRenditions.size(); i++) { Rendition subtitleRendition = subtitleRenditions.get(i); + String sampleStreamWrapperUid = "subtitle:" + i + ":" + subtitleRendition.name; HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( + sampleStreamWrapperUid, C.TRACK_TYPE_TEXT, new Uri[] {subtitleRendition.url}, new Format[] {subtitleRendition.format}, @@ -540,7 +542,7 @@ public final class HlsMediaPeriod manifestUrlIndicesPerWrapper.add(new int[] {i}); sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroup[] {new TrackGroup(subtitleRendition.format)}, + new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, subtitleRendition.format)}, /* primaryTrackGroupIndex= */ 0); } @@ -646,8 +648,10 @@ public final class HlsMediaPeriod !useVideoVariantsOnly && numberOfAudioCodecs > 0 ? C.TRACK_TYPE_AUDIO : C.TRACK_TYPE_DEFAULT; + String sampleStreamWrapperUid = "main"; HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( + sampleStreamWrapperUid, trackType, selectedPlaylistUrls, selectedPlaylistFormats, @@ -664,12 +668,13 @@ public final class HlsMediaPeriod for (int i = 0; i < videoFormats.length; i++) { videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]); } - muxedTrackGroups.add(new TrackGroup(videoFormats)); + muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, videoFormats)); if (numberOfAudioCodecs > 0 && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { muxedTrackGroups.add( new TrackGroup( + /* id= */ sampleStreamWrapperUid + ":audio", deriveAudioFormat( selectedPlaylistFormats[0], masterPlaylist.muxedAudioFormat, @@ -678,7 +683,8 @@ public final class HlsMediaPeriod List ccFormats = masterPlaylist.muxedCaptionFormats; if (ccFormats != null) { for (int i = 0; i < ccFormats.size(); i++) { - muxedTrackGroups.add(new TrackGroup(ccFormats.get(i))); + String ccId = sampleStreamWrapperUid + ":cc:" + i; + muxedTrackGroups.add(new TrackGroup(ccId, ccFormats.get(i))); } } } else /* numberOfAudioCodecs > 0 */ { @@ -691,11 +697,12 @@ public final class HlsMediaPeriod masterPlaylist.muxedAudioFormat, /* isPrimaryTrackInVariant= */ true); } - muxedTrackGroups.add(new TrackGroup(audioFormats)); + muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, audioFormats)); } TrackGroup id3TrackGroup = new TrackGroup( + /* id= */ sampleStreamWrapperUid + ":id3", new Format.Builder() .setId("ID3") .setSampleMimeType(MimeTypes.APPLICATION_ID3) @@ -747,8 +754,10 @@ public final class HlsMediaPeriod } } + String sampleStreamWrapperUid = "audio:" + name; HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( + sampleStreamWrapperUid, C.TRACK_TYPE_AUDIO, scratchPlaylistUrls.toArray(Util.castNonNullTypeArray(new Uri[0])), scratchPlaylistFormats.toArray(new Format[0]), @@ -762,12 +771,14 @@ public final class HlsMediaPeriod if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) { Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0); + new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, renditionFormats)}, + /* primaryTrackGroupIndex= */ 0); } } } private HlsSampleStreamWrapper buildSampleStreamWrapper( + String uid, @C.TrackType int trackType, Uri[] playlistUrls, Format[] playlistFormats, @@ -787,6 +798,7 @@ public final class HlsMediaPeriod muxedCaptionFormats, playerId); return new HlsSampleStreamWrapper( + uid, trackType, /* callback= */ this, defaultChunkSource, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index 8e409bf5c6..a4212076f2 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -125,6 +125,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; new HashSet<>( Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_METADATA))); + private final String uid; private final @C.TrackType int trackType; private final Callback callback; private final HlsChunkSource chunkSource; @@ -185,6 +186,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Nullable private HlsMediaChunk sourceChunk; /** + * @param uid A identifier for this sample stream wrapper. Identifiers must be unique within the + * period. * @param trackType The {@link C.TrackType track type}. * @param callback A callback for the wrapper. * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. @@ -203,6 +206,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * events. */ public HlsSampleStreamWrapper( + String uid, @C.TrackType int trackType, Callback callback, HlsChunkSource chunkSource, @@ -215,6 +219,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; LoadErrorHandlingPolicy loadErrorHandlingPolicy, MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, @HlsMediaSource.MetadataType int metadataType) { + this.uid = uid; this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; @@ -1416,7 +1421,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ? sampleFormat.withManifestFormatInfo(playlistFormat) : deriveFormat(playlistFormat, sampleFormat, /* propagateBitrates= */ true); } - trackGroups[i] = new TrackGroup(formats); + trackGroups[i] = new TrackGroup(uid, formats); primaryTrackGroupIndex = i; } else { @Nullable @@ -1425,8 +1430,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; + String muxedTrackGroupId = uid + ":muxed:" + (i < primaryExtractorTrackIndex ? i : i - 1); trackGroups[i] = new TrackGroup( + muxedTrackGroupId, deriveFormat(playlistFormat, sampleFormat, /* propagateBitrates= */ false)); } } @@ -1443,7 +1450,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Format format = trackGroup.getFormat(j); exposedFormats[j] = format.copyWithCryptoType(drmSessionManager.getCryptoType(format)); } - trackGroups[i] = new TrackGroup(exposedFormats); + trackGroups[i] = new TrackGroup(trackGroup.id, exposedFormats); } return new TrackGroupArray(trackGroups); } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java index a4902d61fa..681ed5f27e 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java @@ -402,7 +402,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; SampleQueue sampleQueue; for (int i = 0; i < rtspLoaderWrappers.size(); i++) { sampleQueue = rtspLoaderWrappers.get(i).sampleQueue; - listBuilder.add(new TrackGroup(checkNotNull(sampleQueue.getUpstreamFormat()))); + listBuilder.add( + new TrackGroup( + /* id= */ Integer.toString(i), checkNotNull(sampleQueue.getUpstreamFormat()))); } return listBuilder.build(); } diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java index 61de70a224..a634fbd54f 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java @@ -263,7 +263,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; exposedFormats[j] = manifestFormat.copyWithCryptoType(drmSessionManager.getCryptoType(manifestFormat)); } - trackGroups[i] = new TrackGroup(exposedFormats); + trackGroups[i] = new TrackGroup(/* id= */ Integer.toString(i), exposedFormats); } return new TrackGroupArray(trackGroups); }