mirror of
https://github.com/samsonjs/media.git
synced 2026-04-02 10:45:51 +00:00
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:
parent
4ed01c42bd
commit
89972dbc38
3 changed files with 140 additions and 131 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue