mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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;
|
package androidx.media3.exoplayer.text;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
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.CuesWithTiming;
|
||||||
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
||||||
import androidx.media3.extractor.text.Subtitle;
|
import androidx.media3.extractor.text.Subtitle;
|
||||||
import androidx.media3.extractor.text.SubtitleParser;
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Ordering;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,7 +50,7 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
/* package */ final class DelegatingSubtitleDecoder extends SimpleSubtitleDecoder {
|
/* 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;
|
private final SubtitleParser subtitleParser;
|
||||||
|
|
||||||
/* package */ DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) {
|
/* package */ DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) {
|
||||||
|
|
@ -69,77 +65,8 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
@Nullable List<CuesWithTiming> cuesWithTiming = subtitleParser.parse(data);
|
@Nullable List<CuesWithTiming> cuesWithTiming = subtitleParser.parse(data);
|
||||||
if (cuesWithTiming == null) {
|
if (cuesWithTiming == null) {
|
||||||
return EMPTY;
|
return EMPTY_SUBTITLE;
|
||||||
}
|
|
||||||
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 new CuesWithTimingSubtitle(cuesWithTiming);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,8 @@ import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.text.Cue;
|
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.extractor.text.CueDecoder;
|
import androidx.media3.extractor.text.CueDecoder;
|
||||||
import androidx.media3.extractor.text.CuesWithTiming;
|
|
||||||
import androidx.media3.extractor.text.Subtitle;
|
import androidx.media3.extractor.text.Subtitle;
|
||||||
import androidx.media3.extractor.text.SubtitleDecoder;
|
import androidx.media3.extractor.text.SubtitleDecoder;
|
||||||
import androidx.media3.extractor.text.SubtitleDecoderException;
|
import androidx.media3.extractor.text.SubtitleDecoderException;
|
||||||
|
|
@ -40,7 +38,6 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SubtitleDecoder} that decodes subtitle samples of type {@link
|
* A {@link SubtitleDecoder} that decodes subtitle samples of type {@link
|
||||||
|
|
@ -118,9 +115,10 @@ public final class ExoplayerCuesDecoder implements SubtitleDecoder {
|
||||||
if (inputBuffer.isEndOfStream()) {
|
if (inputBuffer.isEndOfStream()) {
|
||||||
outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
||||||
} else {
|
} else {
|
||||||
SubtitleImpl subtitle =
|
Subtitle subtitle =
|
||||||
new SubtitleImpl(
|
new CuesWithTimingSubtitle(
|
||||||
cueDecoder.decode(inputBuffer.timeUs, checkNotNull(inputBuffer.data).array()));
|
ImmutableList.of(
|
||||||
|
cueDecoder.decode(inputBuffer.timeUs, checkNotNull(inputBuffer.data).array())));
|
||||||
outputBuffer.setContent(inputBuffer.timeUs, subtitle, /* subsampleOffsetUs= */ 0);
|
outputBuffer.setContent(inputBuffer.timeUs, subtitle, /* subsampleOffsetUs= */ 0);
|
||||||
}
|
}
|
||||||
inputBuffer.clear();
|
inputBuffer.clear();
|
||||||
|
|
@ -151,53 +149,4 @@ public final class ExoplayerCuesDecoder implements SubtitleDecoder {
|
||||||
outputBuffer.clear();
|
outputBuffer.clear();
|
||||||
availableOutputBuffers.addFirst(outputBuffer);
|
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