CuesWithTimingSubtitle.java
/*
* 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<ImmutableList<Cue>> eventCues;
private final long[] eventTimesUs;
/** Ordering of two CuesWithTiming objects based on their startTimeUs values. */
private static final Ordering<CuesWithTiming> CUES_BY_START_TIME_ASCENDING =
Ordering.natural().onResultOf(c -> normalizeUnsetStartTimeToZero(c.startTimeUs));
public CuesWithTimingSubtitle(List<CuesWithTiming> 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<ImmutableList<Cue>> eventCues = new ArrayList<>();
ImmutableList<CuesWithTiming> 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<Cue> 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;
}
}