Update DelegatingSubtitleDecoder to use CuesWithTiming.durationUs

Also re-use the `CuesWithTimingSubtitle` implementation (previously a
private class inside `DelegatingSubtitleDecoder`) in `ExoPlayerCuesDecoder`.

PiperOrigin-RevId: 548612040
This commit is contained in:
ibaker 2023-07-17 09:29:21 +01:00 committed by Ian Baker
parent 4ed01c42bd
commit 89972dbc38
3 changed files with 140 additions and 131 deletions

View file

@ -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<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;
}
}

View file

@ -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> cuesWithTiming = subtitleParser.parse(data);
if (cuesWithTiming == null) {
return EMPTY;
}
return new SubtitleFromCuesWithTiming(cuesWithTiming);
}
private static final class SubtitleFromCuesWithTiming implements Subtitle {
private final ImmutableList<ImmutableList<Cue>> cuesListForUniqueStartTimes;
private final long[] cuesStartTimesUs;
/** 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));
SubtitleFromCuesWithTiming(List<CuesWithTiming> cuesWithTimingList) {
this.cuesStartTimesUs = new long[cuesWithTimingList.size()];
ImmutableList.Builder<ImmutableList<Cue>> cuesListForUniqueStartTimes =
ImmutableList.builder();
ImmutableList<CuesWithTiming> 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<Cue> 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<Cue> 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);
}
}

View file

@ -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<Cue> 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();
}
}
}
}