Skip to content

Commit

Permalink
Update TextRenderer to handle CuesWithTiming instances directly
Browse files Browse the repository at this point in the history
The existing `Subtitle` handling code is left intact to support the
legacy post-`SampleQueue` decoding path for now.

This also includes full support for merging overlapping `CuesWithTiming`
instances, which explains the test dump file changes, and which should
resolve the following issues (if used with the
decoder-before-`SampleQueue` subtitle logic added in
5d453fc):

* Issue: google/ExoPlayer#10295
* Issue: google/ExoPlayer#4794

It should also help resolve Issue: #288, but that will also require
some changes in the DASH module to enable pre-`SampleQueue` subtitle
parsing (which should happen soon).

#minor-release

PiperOrigin-RevId: 571021417
  • Loading branch information
icbaker authored and copybara-github committed Oct 5, 2023
1 parent 49b1e0b commit 002ee05
Show file tree
Hide file tree
Showing 15 changed files with 1,051 additions and 447 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
`AudioSink.Listener`.
* Video:
* Text:
* Remove `ExoplayerCuesDecoder`. Text tracks with `sampleMimeType =
application/x-media3-cues` are now directly handled by `TextRenderer`
without needing a `SubtitleDecoder` instance.
* Metadata:
* `MetadataDecoder.decode` will no longer be called for "decode-only"
samples as the implementation must return null anyway.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.media3.common.C;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.extractor.text.CuesWithTiming;
import com.google.common.collect.ImmutableList;

/**
* A {@code CuesResolver} maps from time to the subtitle cues that should be shown.
*
* <p>It also exposes methods for querying when the next and previous change in subtitles is.
*
* <p>Different implementations may provide different resolution algorithms.
*/
/* package */ interface CuesResolver {

/** Adds cues to this instance. */
void addCues(CuesWithTiming cues);

/**
* Returns the {@linkplain Cue cues} that should be shown at time {@code timeUs}.
*
* @param timeUs The time to query, in microseconds.
* @return The cues that should be shown, ordered by ascending priority for compatibility with
* {@link CueGroup#cues}.
*/
ImmutableList<Cue> getCuesAtTimeUs(long timeUs);

/**
* Discards all cues that won't be shown at or after {@code timeUs}.
*
* @param timeUs The time to discard cues before, in microseconds.
*/
void discardCuesBeforeTimeUs(long timeUs);

/**
* Returns the time, in microseconds, of the change in {@linkplain #getCuesAtTimeUs(long) cue
* output} at or before {@code timeUs}.
*
* <p>If there's no change before {@code timeUs}, returns {@link C#TIME_UNSET}.
*/
long getPreviousCueChangeTimeUs(long timeUs);

/**
* Returns the time, in microseconds, of the next change in {@linkplain #getCuesAtTimeUs(long) cue
* output} after {@code timeUs} (exclusive).
*
* <p>If there's no change after {@code timeUs}, returns {@link C#TIME_END_OF_SOURCE}.
*/
long getNextCueChangeTimeUs(long timeUs);

/** Clears all cues that have been added to this instance. */
void clear();
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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 static java.lang.Math.max;
import static java.lang.Math.min;

import androidx.media3.common.C;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.extractor.text.CuesWithTiming;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import java.util.ArrayList;
import java.util.List;

/**
* A {@link CuesResolver} which merges possibly-overlapping {@link CuesWithTiming} instances.
*
* <p>This implementation only accepts with {@link CuesWithTiming} with a set {@link
* CuesWithTiming#durationUs}.
*/
// TODO: b/181312195 - Add memoization
/* package */ final class MergingCuesResolver implements CuesResolver {

/**
* An {@link Ordering} which sorts cues in ascending display priority, for compatibility with the
* ordering defined for {@link CueGroup#cues}.
*
* <p>Sorts first by start time ascending (later cues should be shown on top of older ones), then
* by duration descending (shorter duration cues that start at the same time should be shown on
* top, as the one underneath will be visible after they disappear).
*/
private static final Ordering<CuesWithTiming> CUES_DISPLAY_PRIORITY_COMPARATOR =
Ordering.<Long>natural()
.onResultOf((CuesWithTiming c) -> c.startTimeUs)
.compound(
Ordering.<Long>natural().reverse().onResultOf((CuesWithTiming c) -> c.durationUs));

/** Sorted by {@link CuesWithTiming#startTimeUs} ascending. */
private final List<CuesWithTiming> cuesWithTimingList;

public MergingCuesResolver() {
cuesWithTimingList = new ArrayList<>();
}

@Override
public void addCues(CuesWithTiming cues) {
checkArgument(cues.startTimeUs != C.TIME_UNSET);
checkArgument(cues.durationUs != C.TIME_UNSET);
for (int i = cuesWithTimingList.size() - 1; i >= 0; i--) {
if (cues.startTimeUs >= cuesWithTimingList.get(i).startTimeUs) {
cuesWithTimingList.add(i + 1, cues);
return;
}
}
cuesWithTimingList.add(0, cues);
}

@Override
public ImmutableList<Cue> getCuesAtTimeUs(long timeUs) {
if (cuesWithTimingList.isEmpty() || timeUs < cuesWithTimingList.get(0).startTimeUs) {
return ImmutableList.of();
}

List<CuesWithTiming> visibleCues = new ArrayList<>();
for (int i = 0; i < cuesWithTimingList.size(); i++) {
CuesWithTiming cues = cuesWithTimingList.get(i);
if (timeUs >= cues.startTimeUs && timeUs < cues.endTimeUs) {
visibleCues.add(cues);
}
if (timeUs < cues.startTimeUs) {
break;
}
}
ImmutableList<CuesWithTiming> sortedResult =
ImmutableList.sortedCopyOf(CUES_DISPLAY_PRIORITY_COMPARATOR, visibleCues);
ImmutableList.Builder<Cue> result = ImmutableList.builder();
for (int i = 0; i < sortedResult.size(); i++) {
result.addAll(sortedResult.get(i).cues);
}
return result.build();
}

@Override
public void discardCuesBeforeTimeUs(long timeUs) {
for (int i = 0; i < cuesWithTimingList.size(); i++) {
long startTimeUs = cuesWithTimingList.get(i).startTimeUs;
if (timeUs > startTimeUs && timeUs > cuesWithTimingList.get(i).endTimeUs) {
// In most cases only a single item will be removed in each invocation of this method, so
// the inefficiency of removing items one-by-one inside a loop is mitigated.
cuesWithTimingList.remove(i);
i--;
} else if (timeUs < startTimeUs) {
break;
}
}
}

@Override
public long getPreviousCueChangeTimeUs(long timeUs) {
if (cuesWithTimingList.isEmpty() || timeUs < cuesWithTimingList.get(0).startTimeUs) {
return C.TIME_UNSET;
}
long result = cuesWithTimingList.get(0).startTimeUs;
for (int i = 0; i < cuesWithTimingList.size(); i++) {
long startTimeUs = cuesWithTimingList.get(i).startTimeUs;
long endTimeUs = cuesWithTimingList.get(i).endTimeUs;
if (endTimeUs <= timeUs) {
result = max(result, endTimeUs);
} else if (startTimeUs <= timeUs) {
result = max(result, startTimeUs);
} else {
break;
}
}
return result;
}

@Override
public long getNextCueChangeTimeUs(long timeUs) {
long result = C.TIME_UNSET;
for (int i = 0; i < cuesWithTimingList.size(); i++) {
long startTimeUs = cuesWithTimingList.get(i).startTimeUs;
long endTimeUs = cuesWithTimingList.get(i).endTimeUs;
if (timeUs < startTimeUs) {
result = result == C.TIME_UNSET ? startTimeUs : min(result, startTimeUs);
break;
} else if (timeUs < endTimeUs) {
result = result == C.TIME_UNSET ? endTimeUs : min(result, endTimeUs);
}
}
return result != C.TIME_UNSET ? result : C.TIME_END_OF_SOURCE;
}

@Override
public void clear() {
cuesWithTimingList.clear();
}
}
Loading

0 comments on commit 002ee05

Please sign in to comment.