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

0 comments on commit 304dadc

Please sign in to comment.