diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index 6be37323bd..7c0b46e77d 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -33,7 +33,9 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.ErrorMessageProvider; +import androidx.media3.common.Format; import androidx.media3.common.MediaItem; +import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionParameters; @@ -55,10 +57,15 @@ import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ads.AdsLoader; import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.exoplayer.util.EventLogger; +import androidx.media3.extractor.DefaultExtractorsFactory; +import androidx.media3.extractor.ForwardingTrackOutput; +import androidx.media3.extractor.OutputModifyingExtractor; +import androidx.media3.extractor.TrackOutput; import androidx.media3.ui.PlayerView; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** An activity that plays media using {@link ExoPlayer}. */ @@ -315,7 +322,10 @@ public class PlayerActivity extends AppCompatActivity serverSideAdsLoader, new DefaultMediaSourceFactory(/* context= */ this) .setDataSourceFactory(dataSourceFactory)); - return new DefaultMediaSourceFactory(/* context= */ this) + return new DefaultMediaSourceFactory( + /* context= */ this, + new OutputModifyingExtractor.Factory( + new DefaultExtractorsFactory(), Mp4VttKeyFrameTrackOutput::new)) .setDataSourceFactory(dataSourceFactory) .setDrmSessionManagerProvider(drmSessionManagerProvider) .setLocalAdInsertionComponents( @@ -323,6 +333,35 @@ public class PlayerActivity extends AppCompatActivity .setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory); } + @OptIn(markerClass = UnstableApi.class) + private static final class Mp4VttKeyFrameTrackOutput extends ForwardingTrackOutput { + + @Nullable private Format format; + + public Mp4VttKeyFrameTrackOutput(TrackOutput trackOutput) { + super(trackOutput); + } + + @Override + public void format(Format format) { + super.format(format); + this.format = format; + } + + @Override + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { + if (format != null && Objects.equals(format.sampleMimeType, MimeTypes.APPLICATION_MP4VTT)) { + flags |= C.BUFFER_FLAG_KEY_FRAME; + } + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } + } + @OptIn(markerClass = UnstableApi.class) private void setRenderersFactory( ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) { diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/OutputModifyingExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/OutputModifyingExtractor.java new file mode 100644 index 0000000000..9429a5410a --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/OutputModifyingExtractor.java @@ -0,0 +1,149 @@ +/* + * Copyright 2024 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 + * + * http://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.extractor; + +import android.net.Uri; +import android.util.SparseArray; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.extractor.text.SubtitleParser; +import com.google.common.base.Function; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** DO NOT SUBMIT document this and rename it */ +public final class OutputModifyingExtractor implements Extractor { + + /** + * A wrapping {@link androidx.media3.extractor.ExtractorsFactory} implementation that wraps each + * returned {@link Extractor} instance with an {@link OutputModifyingExtractor}. + */ + public static final class Factory implements ExtractorsFactory { + + private final ExtractorsFactory delegate; + private final Function wrappingTrackOutputFactory; + + public Factory( + ExtractorsFactory delegate, Function wrappingTrackOutputFactory) { + this.delegate = delegate; + this.wrappingTrackOutputFactory = wrappingTrackOutputFactory; + } + + @Override + public ExtractorsFactory setSubtitleParserFactory( + SubtitleParser.Factory subtitleParserFactory) { + return delegate.setSubtitleParserFactory(subtitleParserFactory); + } + + @Override + public Extractor[] createExtractors() { + return wrapExtractors(delegate.createExtractors()); + } + + @Override + public Extractor[] createExtractors(Uri uri, Map> responseHeaders) { + return wrapExtractors(delegate.createExtractors(uri, responseHeaders)); + } + + private Extractor[] wrapExtractors(Extractor[] extractors) { + for (int i = 0; i < extractors.length; i++) { + extractors[i] = new OutputModifyingExtractor(extractors[i], wrappingTrackOutputFactory); + } + return extractors; + } + } + + private final Extractor delegate; + private final Function wrappingTrackOutputFactory; + + public OutputModifyingExtractor( + Extractor delegate, Function wrappingTrackOutputFactory) { + this.delegate = delegate; + this.wrappingTrackOutputFactory = wrappingTrackOutputFactory; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + return delegate.sniff(input); + } + + @Override + public void init(ExtractorOutput output) { + delegate.init(new WrappingExtractorOutput(output, wrappingTrackOutputFactory)); + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException { + return delegate.read(input, seekPosition); + } + + @Override + public void seek(long position, long timeUs) { + delegate.seek(position, timeUs); + } + + @Override + public List getSniffFailureDetails() { + return delegate.getSniffFailureDetails(); + } + + @Override + public Extractor getUnderlyingImplementation() { + return delegate.getUnderlyingImplementation(); + } + + @Override + public void release() { + delegate.release(); + } + + private static final class WrappingExtractorOutput implements ExtractorOutput { + + private final ExtractorOutput delegateExtractorOutput; + private final Function wrappingTrackOutputFactory; + private final SparseArray trackOutputs; + + private WrappingExtractorOutput( + ExtractorOutput delegateExtractorOutput, + Function wrappingTrackOutputFactory) { + this.delegateExtractorOutput = delegateExtractorOutput; + this.wrappingTrackOutputFactory = wrappingTrackOutputFactory; + trackOutputs = new SparseArray<>(); + } + + @Override + public TrackOutput track(int id, @C.TrackType int type) { + @Nullable TrackOutput trackOutput = trackOutputs.get(id); + if (trackOutput == null) { + trackOutput = wrappingTrackOutputFactory.apply(delegateExtractorOutput.track(id, type)); + trackOutputs.put(id, trackOutput); + } + return trackOutput; + } + + @Override + public void endTracks() { + delegateExtractorOutput.endTracks(); + } + + @Override + public void seekMap(SeekMap seekMap) { + delegateExtractorOutput.seekMap(seekMap); + } + } +}