Skip to content

Commit

Permalink
Add an Extractor to parse subtitles before SampleQueue
Browse files Browse the repository at this point in the history
The end-to-end test output for the overlapping SRT and SSA subtitles
is currently incorrect. They will be fixed in a future change that
updates `TextRenderer` to support this overlap.

The 'extra' samples visible in the extractor test output files are
'empty cue list' samples produced by `SsaParser`. They will go away
when this implementation is updated to remove this behaviour and rely
on `CuesWithTiming.durationUs` instead (the 'empty list' behaviour is
not required by the `SubtitleParser` interface).

PiperOrigin-RevId: 549264593
  • Loading branch information
icbaker committed Jul 20, 2023
1 parent 804dfe2 commit 304dadc
Show file tree
Hide file tree
Showing 34 changed files with 6,578 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.FakeClock;
Expand Down Expand Up @@ -66,21 +68,27 @@ public void test() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext);
// TODO: b/289916598 - Remove this when transcoding is the default.
DefaultExtractorsFactory extractorsFactory =
new DefaultExtractorsFactory().setTextTrackTranscodingEnabled(true);
DefaultMediaSourceFactory mediaSourceFactory =
new DefaultMediaSourceFactory(applicationContext, extractorsFactory);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory, mediaSourceFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
// TODO(internal b/174661563): Remove the for-loop below to enable the text renderer when
// subtitle output is not flaky.
for (int textRendererIndex = 0;
textRendererIndex < player.getRendererCount();
textRendererIndex++) {
if (player.getRendererType(textRendererIndex) == C.TRACK_TYPE_TEXT) {
player.setTrackSelectionParameters(
new DefaultTrackSelector.ParametersBuilder(applicationContext)
.setRendererDisabled(textRendererIndex, /* disabled= */ true)
.build());
break;
// TODO: b/181312195 - Remove this when WebVTT is supported by DefaultSubtitleParserFactory.
if (inputFile.contains("_vtt_")) {
for (int textRendererIndex = 0;
textRendererIndex < player.getRendererCount();
textRendererIndex++) {
if (player.getRendererType(textRendererIndex) == C.TRACK_TYPE_TEXT) {
player.setTrackSelectionParameters(
new DefaultTrackSelector.ParametersBuilder(applicationContext)
.setRendererDisabled(textRendererIndex, /* disabled= */ true)
.build());
break;
}
}
}
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader;
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
import com.google.android.exoplayer2.text.DefaultSubtitleParserFactory;
import com.google.android.exoplayer2.text.SubtitleParser;
import com.google.android.exoplayer2.text.SubtitleTranscodingExtractor;
import com.google.android.exoplayer2.util.FileTypes;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
Expand Down Expand Up @@ -137,10 +141,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
// TODO (b/261183220): Initialize tsSubtitleFormats in constructor once shrinking bug is fixed.
@Nullable private ImmutableList<Format> tsSubtitleFormats;
private int tsTimestampSearchBytes;
private boolean textTrackTranscodingEnabled;
private SubtitleParser.Factory subtitleParserFactory;

public DefaultExtractorsFactory() {
tsMode = TsExtractor.MODE_SINGLE_PMT;
tsTimestampSearchBytes = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES;
subtitleParserFactory = new DefaultSubtitleParserFactory();
}

/**
Expand Down Expand Up @@ -340,6 +347,40 @@ public synchronized DefaultExtractorsFactory setTsExtractorTimestampSearchBytes(
return this;
}

/**
* Enables transcoding of text track samples to {@link MimeTypes#TEXT_EXOPLAYER_CUES} before the
* data is emitted to {@link TrackOutput}.
*
* <p>Transcoding is disabled by default.
*
* @param textTrackTranscodingEnabled Whether to enable transcoding.
* @return The factory, for convenience.
*/
// TODO: b/289916598 - Flip this to default to enabled and deprecate it.
@CanIgnoreReturnValue
public synchronized DefaultExtractorsFactory setTextTrackTranscodingEnabled(
boolean textTrackTranscodingEnabled) {
this.textTrackTranscodingEnabled = textTrackTranscodingEnabled;
return this;
}

/**
* Sets a {@link SubtitleParser.Factory} to use when transcoding text tracks.
*
* <p>This is only used if {@link #setTextTrackTranscodingEnabled(boolean)} is enabled.
*
* <p>The default value is {@link DefaultSubtitleParserFactory}.
*
* @param subtitleParserFactory The factory for {@link SubtitleParser} instances.
* @return The factory, for convenience.
*/
@CanIgnoreReturnValue
public synchronized DefaultExtractorsFactory setSubtitleParserFactory(
SubtitleParser.Factory subtitleParserFactory) {
this.subtitleParserFactory = subtitleParserFactory;
return this;
}

@Override
public synchronized Extractor[] createExtractors() {
return createExtractors(Uri.EMPTY, new HashMap<>());
Expand Down Expand Up @@ -368,8 +409,14 @@ public synchronized Extractor[] createExtractors(
addExtractorsForFileType(fileType, extractors);
}
}

return extractors.toArray(new Extractor[extractors.size()]);
Extractor[] result = new Extractor[extractors.size()];
for (int i = 0; i < extractors.size(); i++) {
result[i] =
textTrackTranscodingEnabled
? new SubtitleTranscodingExtractor(extractors.get(i), subtitleParserFactory)
: extractors.get(i);
}
return result;
}

private void addExtractorsForFileType(@FileTypes.Type int fileType, List<Extractor> extractors) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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 com.google.android.exoplayer2.text;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/**
* A wrapping {@link Extractor} that transcodes {@linkplain C#TRACK_TYPE_TEXT text samples} from
* supported subtitle formats to {@link MimeTypes#TEXT_EXOPLAYER_CUES}.
*
* <p>Samples emitted by the delegate {@link Extractor} to {@linkplain C#TRACK_TYPE_TEXT text
* tracks} with a supported subtitle format are transcoded and the resulting {@link
* MimeTypes#TEXT_EXOPLAYER_CUES} samples are emitted to the underlying {@link TrackOutput}.
*
* <p>Samples emitted by the delegate {@link Extractor} to non-text tracks (or text tracks with an
* unsupported format) are passed through to the underlying {@link TrackOutput} without
* modification.
*
* <p>Support for subtitle formats is determined by {@link
* SubtitleParser.Factory#supportsFormat(Format)} on the {@link SubtitleParser.Factory} passed to
* the constructor of this class.
*
* @deprecated com.google.android.exoplayer2 is deprecated. Please migrate to androidx.media3 (which
* contains the same ExoPlayer code). See <a
* href="https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide">the
* migration guide</a> for more details, including a script to help with the migration.
*/
@Deprecated
public class SubtitleTranscodingExtractor implements Extractor {

private final Extractor delegate;
private final SubtitleParser.Factory subtitleParserFactory;

private @MonotonicNonNull SubtitleTranscodingExtractorOutput transcodingExtractorOutput;

public SubtitleTranscodingExtractor(
Extractor delegate, SubtitleParser.Factory subtitleParserFactory) {
this.delegate = delegate;
this.subtitleParserFactory = subtitleParserFactory;
}

@Override
public boolean sniff(ExtractorInput input) throws IOException {
return delegate.sniff(input);
}

@Override
public void init(ExtractorOutput output) {
transcodingExtractorOutput =
new SubtitleTranscodingExtractorOutput(output, subtitleParserFactory);
delegate.init(transcodingExtractorOutput);
}

@Override
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
return delegate.read(input, seekPosition);
}

@Override
public void seek(long position, long timeUs) {
if (transcodingExtractorOutput != null) {
transcodingExtractorOutput.resetSubtitleParsers();
}
delegate.seek(position, timeUs);
}

@Override
public void release() {
delegate.release();
}

@Override
public Extractor getUnderlyingImplementation() {
return delegate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 com.google.android.exoplayer2.text;

import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;

/**
* A wrapping {@link ExtractorOutput} for use by {@link SubtitleTranscodingExtractor}.
*
* @deprecated com.google.android.exoplayer2 is deprecated. Please migrate to androidx.media3 (which
* contains the same ExoPlayer code). See <a
* href="https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide">the
* migration guide</a> for more details, including a script to help with the migration.
*/
@Deprecated
/* package */ class SubtitleTranscodingExtractorOutput implements ExtractorOutput {

private final ExtractorOutput delegate;
private final SubtitleParser.Factory subtitleParserFactory;
private final SparseArray<SubtitleTranscodingTrackOutput> textTrackOutputs;

public SubtitleTranscodingExtractorOutput(
ExtractorOutput delegate, SubtitleParser.Factory subtitleParserFactory) {
this.delegate = delegate;
this.subtitleParserFactory = subtitleParserFactory;
textTrackOutputs = new SparseArray<>();
}

public void resetSubtitleParsers() {
for (int i = 0; i < textTrackOutputs.size(); i++) {
textTrackOutputs.valueAt(i).resetSubtitleParser();
}
}

// ExtractorOutput implementation

@Override
public TrackOutput track(int id, @C.TrackType int type) {
if (type != C.TRACK_TYPE_TEXT) {
return delegate.track(id, type);
}
SubtitleTranscodingTrackOutput existingTrackOutput = textTrackOutputs.get(id);
if (existingTrackOutput != null) {
return existingTrackOutput;
}
SubtitleTranscodingTrackOutput trackOutput =
new SubtitleTranscodingTrackOutput(delegate.track(id, type), subtitleParserFactory);
textTrackOutputs.put(id, trackOutput);
return trackOutput;
}

@Override
public void endTracks() {
delegate.endTracks();
}

@Override
public void seekMap(SeekMap seekMap) {
delegate.seekMap(seekMap);
}
}
Loading

0 comments on commit 304dadc

Please sign in to comment.