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 new file mode 100644 index 0000000000..d6578f980e --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoder.java @@ -0,0 +1,138 @@ +/* + * 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 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; + +/** + * Wrapper around a {@link SubtitleParser} that can be used instead of any current {@link + * SimpleSubtitleDecoder} subclass. The main {@link #decode(byte[], int, boolean)} method will be + * delegating the parsing of the data to the underlying {@link SubtitleParser} instance and its + * {@link SubtitleParser#parse(byte[], int, int)} implementation. + * + *

Functionally, once each XXXDecoder class is refactored to be a XXXParser that implements + * {@link SubtitleParser}, the following should be equivalent: + * + *

+ * + *

Or in the case with initialization data: + * + *

+ * + *

TODO(b/289983417): this will only be used in the old decoding flow (Decoder after SampleQueue) + * while we maintain dual architecture. Once we fully migrate to the pre-SampleQueue flow, it can be + * deprecated and later deleted. + */ +/* package */ final class DelegatingSubtitleDecoder extends SimpleSubtitleDecoder { + + private static final Subtitle EMPTY = new SubtitleFromCuesWithTiming(ImmutableList.of()); + private final SubtitleParser subtitleParser; + + /* package */ DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) { + super(name); + this.subtitleParser = subtitleParser; + } + + @Override + protected Subtitle decode(byte[] data, int length, boolean reset) { + if (reset) { + subtitleParser.reset(); + } + @Nullable List cuesWithTiming = subtitleParser.parse(data); + if (cuesWithTiming == null) { + return EMPTY; + } + return new SubtitleFromCuesWithTiming(cuesWithTiming); + } + + // TODO: ImmutableLongArray is no longer Beta, remove suppression when we upgrade Guava dep + @SuppressWarnings("UnstableApiUsage") + 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 -> 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] = 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); + } + } + } +}