From 89972dbc386144ff716900f1092a6abdf68e1bac Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 17 Jul 2023 09:29:21 +0100 Subject: [PATCH] Update `DelegatingSubtitleDecoder` to use `CuesWithTiming.durationUs` Also re-use the `CuesWithTimingSubtitle` implementation (previously a private class inside `DelegatingSubtitleDecoder`) in `ExoPlayerCuesDecoder`. PiperOrigin-RevId: 548612040 --- .../text/CuesWithTimingSubtitle.java | 133 ++++++++++++++++++ .../text/DelegatingSubtitleDecoder.java | 79 +---------- .../exoplayer/text/ExoplayerCuesDecoder.java | 59 +------- 3 files changed, 140 insertions(+), 131 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/CuesWithTimingSubtitle.java diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/CuesWithTimingSubtitle.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/CuesWithTimingSubtitle.java new file mode 100644 index 0000000000..b5e1ced914 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/CuesWithTimingSubtitle.java @@ -0,0 +1,133 @@ +/* + * Copyright 2023 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 + * + * https://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.exoplayer.text; + +import static androidx.media3.common.util.Assertions.checkArgument; + +import androidx.media3.common.C; +import androidx.media3.common.text.Cue; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import androidx.media3.extractor.text.CuesWithTiming; +import androidx.media3.extractor.text.Subtitle; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** A {@link Subtitle} backed by a list of {@link CuesWithTiming} instances. */ +/* package */ final class CuesWithTimingSubtitle implements Subtitle { + + private static final String TAG = "CuesWithTimingSubtitle"; + + // eventCues and eventTimesUs are parallel collections. eventTimesUs is sorted in ascending + // order, and eventCues.get(i) contains the the cues for the event at time eventTimesUs[i]. + // eventTimesUs may be longer than eventCues (with padding elements at the end). + // eventCues.size() is the authoritative source for the number of events in this Subtitle. + private final ImmutableList> eventCues; + private final long[] eventTimesUs; + + /** Ordering of two CuesWithTiming objects based on their startTimeUs values. */ + private static final Ordering CUES_BY_START_TIME_ASCENDING = + Ordering.natural().onResultOf(c -> normalizeUnsetStartTimeToZero(c.startTimeUs)); + + public CuesWithTimingSubtitle(List cuesWithTimingList) { + if (cuesWithTimingList.size() == 1) { + CuesWithTiming cuesWithTiming = Iterables.getOnlyElement(cuesWithTimingList); + long startTimeUs = normalizeUnsetStartTimeToZero(cuesWithTiming.startTimeUs); + if (cuesWithTiming.durationUs == C.TIME_UNSET) { + eventCues = ImmutableList.of(cuesWithTiming.cues); + eventTimesUs = new long[] {startTimeUs}; + } else { + eventCues = ImmutableList.of(cuesWithTiming.cues, ImmutableList.of()); + eventTimesUs = new long[] {startTimeUs, startTimeUs + cuesWithTiming.durationUs}; + } + return; + } + + eventTimesUs = new long[cuesWithTimingList.size() * 2]; + // Ensure that any unused slots at the end of eventTimesUs remain 'sorted' so don't mess + // with the binary search. + Arrays.fill(eventTimesUs, Long.MAX_VALUE); + ArrayList> eventCues = new ArrayList<>(); + ImmutableList sortedCuesWithTimingList = + ImmutableList.sortedCopyOf(CUES_BY_START_TIME_ASCENDING, cuesWithTimingList); + int eventIndex = 0; + for (int i = 0; i < sortedCuesWithTimingList.size(); i++) { + CuesWithTiming cuesWithTiming = sortedCuesWithTimingList.get(i); + + long startTimeUs = normalizeUnsetStartTimeToZero(cuesWithTiming.startTimeUs); + long endTimeUs = startTimeUs + cuesWithTiming.durationUs; + if (eventIndex == 0 || eventTimesUs[eventIndex - 1] < startTimeUs) { + eventTimesUs[eventIndex++] = startTimeUs; + eventCues.add(cuesWithTiming.cues); + } else if (eventTimesUs[eventIndex - 1] == startTimeUs + && eventCues.get(eventIndex - 1).isEmpty()) { + // The previous CuesWithTiming ends at the same time this one starts, so overwrite the + // empty cue list with the cues from this one. + eventCues.set(eventIndex - 1, cuesWithTiming.cues); + } else { + Log.w(TAG, "Truncating unsupported overlapping cues."); + // The previous CuesWithTiming ends after this one starts, so overwrite the empty cue list + // with the cues from this one. + eventTimesUs[eventIndex - 1] = startTimeUs; + eventCues.set(eventIndex - 1, cuesWithTiming.cues); + } + if (cuesWithTiming.durationUs != C.TIME_UNSET) { + eventTimesUs[eventIndex++] = endTimeUs; + eventCues.add(ImmutableList.of()); + } + } + this.eventCues = ImmutableList.copyOf(eventCues); + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = + Util.binarySearchCeil( + eventTimesUs, /* value= */ timeUs, /* inclusive= */ false, /* stayInBounds= */ false); + return index < eventCues.size() ? index : C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return eventCues.size(); + } + + @Override + public long getEventTime(int index) { + checkArgument(index < eventCues.size()); + return eventTimesUs[index]; + } + + @Override + public ImmutableList getCues(long timeUs) { + int index = + Util.binarySearchFloor( + eventTimesUs, /* value= */ timeUs, /* inclusive= */ true, /* stayInBounds= */ false); + return index == -1 ? ImmutableList.of() : eventCues.get(index); + } + + // SubtitleParser can return CuesWithTiming with startTimeUs == TIME_UNSET, indicating the + // start time should be derived from the surrounding sample timestamp. In the context of the + // Subtitle interface, this means starting at zero, so we can just always interpret TIME_UNSET + // as zero here. + private static long normalizeUnsetStartTimeToZero(long startTime) { + return startTime == C.TIME_UNSET ? 0 : startTime; + } +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoder.java index b70cb60d95..ebbb156af7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoder.java @@ -16,15 +16,11 @@ package androidx.media3.exoplayer.text; import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.text.Cue; -import androidx.media3.common.util.Util; import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.SimpleSubtitleDecoder; import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.SubtitleParser; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Ordering; import java.util.List; /** @@ -54,7 +50,7 @@ import java.util.List; */ /* package */ final class DelegatingSubtitleDecoder extends SimpleSubtitleDecoder { - private static final Subtitle EMPTY = new SubtitleFromCuesWithTiming(ImmutableList.of()); + private static final Subtitle EMPTY_SUBTITLE = new CuesWithTimingSubtitle(ImmutableList.of()); private final SubtitleParser subtitleParser; /* package */ DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) { @@ -69,77 +65,8 @@ import java.util.List; } @Nullable List cuesWithTiming = subtitleParser.parse(data); if (cuesWithTiming == null) { - return EMPTY; - } - return new SubtitleFromCuesWithTiming(cuesWithTiming); - } - - private static final class SubtitleFromCuesWithTiming implements Subtitle { - - private final ImmutableList> cuesListForUniqueStartTimes; - private final long[] cuesStartTimesUs; - - /** Ordering of two CuesWithTiming objects based on their startTimeUs values. */ - private static final Ordering CUES_BY_START_TIME_ASCENDING = - Ordering.natural().onResultOf(c -> normalizeUnsetStartTimeToZero(c.startTimeUs)); - - SubtitleFromCuesWithTiming(List cuesWithTimingList) { - this.cuesStartTimesUs = new long[cuesWithTimingList.size()]; - ImmutableList.Builder> cuesListForUniqueStartTimes = - ImmutableList.builder(); - ImmutableList sortedCuesWithTimingList = - ImmutableList.sortedCopyOf(CUES_BY_START_TIME_ASCENDING, cuesWithTimingList); - for (int i = 0; i < sortedCuesWithTimingList.size(); i++) { - cuesListForUniqueStartTimes.add(sortedCuesWithTimingList.get(i).cues); - cuesStartTimesUs[i] = - normalizeUnsetStartTimeToZero(sortedCuesWithTimingList.get(i).startTimeUs); - } - this.cuesListForUniqueStartTimes = cuesListForUniqueStartTimes.build(); - } - - @Override - public int getNextEventTimeIndex(long timeUs) { - int index = - Util.binarySearchCeil( - cuesStartTimesUs, - /* value= */ timeUs, - /* inclusive= */ false, - /* stayInBounds= */ false); - return index < cuesStartTimesUs.length ? index : C.INDEX_UNSET; - } - - @Override - public int getEventTimeCount() { - return cuesStartTimesUs.length; - } - - @Override - public long getEventTime(int index) { - return cuesStartTimesUs[index]; - } - - @Override - public ImmutableList getCues(long timeUs) { - int index = - Util.binarySearchFloor( - cuesStartTimesUs, - /* value= */ timeUs, - /* inclusive= */ true, - /* stayInBounds= */ false); - if (index == -1) { - // timeUs is earlier than the start of the first List in cuesListForUniqueStartTimes. - return ImmutableList.of(); - } else { - return cuesListForUniqueStartTimes.get(index); - } - } - - // SubtitleParser can return CuesWithTiming with startTimeUs == TIME_UNSET, indicating the - // start time should be derived from the surrounding sample timestamp. In the context of the - // Subtitle interface, this means starting at zero, so we can just always interpret TIME_UNSET - // as zero here. - private static long normalizeUnsetStartTimeToZero(long startTime) { - return startTime == C.TIME_UNSET ? 0 : startTime; + return EMPTY_SUBTITLE; } + return new CuesWithTimingSubtitle(cuesWithTiming); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/ExoplayerCuesDecoder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/ExoplayerCuesDecoder.java index b32dd99c1a..6d1f47d979 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/ExoplayerCuesDecoder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/ExoplayerCuesDecoder.java @@ -24,10 +24,8 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MimeTypes; -import androidx.media3.common.text.Cue; import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.text.CueDecoder; -import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.SubtitleDecoder; import androidx.media3.extractor.text.SubtitleDecoderException; @@ -40,7 +38,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayDeque; import java.util.Deque; -import java.util.List; /** * A {@link SubtitleDecoder} that decodes subtitle samples of type {@link @@ -118,9 +115,10 @@ public final class ExoplayerCuesDecoder implements SubtitleDecoder { if (inputBuffer.isEndOfStream()) { outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); } else { - SubtitleImpl subtitle = - new SubtitleImpl( - cueDecoder.decode(inputBuffer.timeUs, checkNotNull(inputBuffer.data).array())); + Subtitle subtitle = + new CuesWithTimingSubtitle( + ImmutableList.of( + cueDecoder.decode(inputBuffer.timeUs, checkNotNull(inputBuffer.data).array()))); outputBuffer.setContent(inputBuffer.timeUs, subtitle, /* subsampleOffsetUs= */ 0); } inputBuffer.clear(); @@ -151,53 +149,4 @@ public final class ExoplayerCuesDecoder implements SubtitleDecoder { outputBuffer.clear(); availableOutputBuffers.addFirst(outputBuffer); } - - private static final class SubtitleImpl implements Subtitle { - private final CuesWithTiming cuesWithTiming; - - public SubtitleImpl(CuesWithTiming cuesWithTiming) { - this.cuesWithTiming = cuesWithTiming; - } - - @Override - public int getNextEventTimeIndex(long timeUs) { - if (timeUs < cuesWithTiming.startTimeUs) { - return 0; - } else if (cuesWithTiming.durationUs != C.TIME_UNSET - && timeUs < cuesWithTiming.startTimeUs + cuesWithTiming.durationUs) { - return 1; - } else { - return C.INDEX_UNSET; - } - } - - @Override - public int getEventTimeCount() { - return cuesWithTiming.durationUs == C.TIME_UNSET ? 1 : 2; - } - - @Override - public long getEventTime(int index) { - if (index == 0) { - return cuesWithTiming.startTimeUs; - } else if (cuesWithTiming.durationUs != C.TIME_UNSET && index == 1) { - return cuesWithTiming.startTimeUs + cuesWithTiming.durationUs; - } else { - throw new IndexOutOfBoundsException("Invalid index: " + index); - } - } - - @Override - public List getCues(long timeUs) { - if (timeUs < cuesWithTiming.startTimeUs) { - return ImmutableList.of(); - } else if (cuesWithTiming.durationUs == C.TIME_UNSET) { - return cuesWithTiming.cues; - } else if (timeUs < cuesWithTiming.startTimeUs + cuesWithTiming.durationUs) { - return cuesWithTiming.cues; - } else { - return ImmutableList.of(); - } - } - } }