mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add experimental opt-in to parse DASH subtitles during extraction
This currently only applies to subtitles muxed into mp4 segments, and not standalone text files linked directly from the manifest. Issue: androidx/media#288 #minor-release PiperOrigin-RevId: 572263764 (cherry picked from commit 66fa5919590789b384506a4e604fe02a5a5e0877)
This commit is contained in:
parent
7254f5aca5
commit
03a3f77340
@ -37,6 +37,13 @@ This release includes the following changes since the
|
|||||||
Android Auto.
|
Android Auto.
|
||||||
* DASH Extension:
|
* DASH Extension:
|
||||||
* Allow multiple of the same DASH identifier in segment template url.
|
* Allow multiple of the same DASH identifier in segment template url.
|
||||||
|
* Add experimental support for parsing subtitles during extraction. This
|
||||||
|
has better support for merging overlapping subtitles, including
|
||||||
|
resolving flickering when transitioning between subtitle segments. You
|
||||||
|
can enable this using
|
||||||
|
`DashMediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`
|
||||||
|
([#288](https://github.com/androidx/media/issues/288)).
|
||||||
|
* Smooth Streaming Extension:
|
||||||
* RTSP Extension:
|
* RTSP Extension:
|
||||||
* Use RTSP Setup Response timeout value in time interval of sending
|
* Use RTSP Setup Response timeout value in time interval of sending
|
||||||
keep-alive RTSP Options requests
|
keep-alive RTSP Options requests
|
||||||
|
@ -26,6 +26,7 @@ import androidx.media3.common.MimeTypes;
|
|||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import androidx.media3.common.util.ParsableByteArray;
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
import androidx.media3.extractor.ChunkIndex;
|
import androidx.media3.extractor.ChunkIndex;
|
||||||
import androidx.media3.extractor.DummyTrackOutput;
|
import androidx.media3.extractor.DummyTrackOutput;
|
||||||
import androidx.media3.extractor.Extractor;
|
import androidx.media3.extractor.Extractor;
|
||||||
@ -36,7 +37,10 @@ import androidx.media3.extractor.SeekMap;
|
|||||||
import androidx.media3.extractor.TrackOutput;
|
import androidx.media3.extractor.TrackOutput;
|
||||||
import androidx.media3.extractor.mkv.MatroskaExtractor;
|
import androidx.media3.extractor.mkv.MatroskaExtractor;
|
||||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
|
import androidx.media3.extractor.text.SubtitleTranscodingExtractor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,36 +50,68 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtractor {
|
public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtractor {
|
||||||
|
|
||||||
/** {@link ChunkExtractor.Factory} for instances of this class. */
|
/** {@link ChunkExtractor.Factory} for {@link BundledChunkExtractor}. */
|
||||||
public static final ChunkExtractor.Factory FACTORY =
|
public static final class Factory implements ChunkExtractor.Factory {
|
||||||
(primaryTrackType,
|
|
||||||
format,
|
/** Non-null if subtitles should be parsed during extraction, null otherwise. */
|
||||||
enableEventMessageTrack,
|
@Nullable private SubtitleParser.Factory subtitleParserFactory;
|
||||||
closedCaptionFormats,
|
|
||||||
playerEmsgTrackOutput,
|
/**
|
||||||
playerId) -> {
|
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction, or
|
||||||
@Nullable String containerMimeType = format.containerMimeType;
|
* null to parse subtitles during decoding. The default is null (subtitles parsed after
|
||||||
Extractor extractor;
|
* decoding).
|
||||||
if (MimeTypes.isText(containerMimeType)) {
|
*
|
||||||
// Text types do not need an extractor.
|
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
|
||||||
return null;
|
* in a future release.
|
||||||
} else if (MimeTypes.isMatroska(containerMimeType)) {
|
*
|
||||||
extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);
|
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
|
||||||
} else {
|
* extraction.
|
||||||
int flags = 0;
|
* @return This factory, for convenience.
|
||||||
if (enableEventMessageTrack) {
|
*/
|
||||||
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
|
public Factory experimentalSetSubtitleParserFactory(
|
||||||
}
|
@Nullable SubtitleParser.Factory subtitleParserFactory) {
|
||||||
extractor =
|
this.subtitleParserFactory = subtitleParserFactory;
|
||||||
new FragmentedMp4Extractor(
|
return this;
|
||||||
flags,
|
}
|
||||||
/* timestampAdjuster= */ null,
|
|
||||||
/* sideloadedTrack= */ null,
|
@Nullable
|
||||||
closedCaptionFormats,
|
@Override
|
||||||
playerEmsgTrackOutput);
|
public ChunkExtractor createProgressiveMediaExtractor(
|
||||||
|
@C.TrackType int primaryTrackType,
|
||||||
|
Format representationFormat,
|
||||||
|
boolean enableEventMessageTrack,
|
||||||
|
List<Format> closedCaptionFormats,
|
||||||
|
@Nullable TrackOutput playerEmsgTrackOutput,
|
||||||
|
PlayerId playerId) {
|
||||||
|
@Nullable String containerMimeType = representationFormat.containerMimeType;
|
||||||
|
Extractor extractor;
|
||||||
|
if (MimeTypes.isText(containerMimeType)) {
|
||||||
|
// Text types do not need an extractor.
|
||||||
|
return null;
|
||||||
|
} else if (MimeTypes.isMatroska(containerMimeType)) {
|
||||||
|
extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);
|
||||||
|
} else {
|
||||||
|
int flags = 0;
|
||||||
|
if (enableEventMessageTrack) {
|
||||||
|
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
|
||||||
}
|
}
|
||||||
return new BundledChunkExtractor(extractor, primaryTrackType, format);
|
extractor =
|
||||||
};
|
new FragmentedMp4Extractor(
|
||||||
|
flags,
|
||||||
|
/* timestampAdjuster= */ null,
|
||||||
|
/* sideloadedTrack= */ null,
|
||||||
|
closedCaptionFormats,
|
||||||
|
playerEmsgTrackOutput);
|
||||||
|
}
|
||||||
|
if (subtitleParserFactory != null) {
|
||||||
|
extractor = new SubtitleTranscodingExtractor(extractor, subtitleParserFactory);
|
||||||
|
}
|
||||||
|
return new BundledChunkExtractor(extractor, primaryTrackType, representationFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link Factory} for {@link BundledChunkExtractor}. */
|
||||||
|
public static final Factory FACTORY = new Factory();
|
||||||
|
|
||||||
private static final PositionHolder POSITION_HOLDER = new PositionHolder();
|
private static final PositionHolder POSITION_HOLDER = new PositionHolder();
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ import androidx.media3.exoplayer.upstream.Allocator;
|
|||||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -130,7 +131,8 @@ import java.util.regex.Pattern;
|
|||||||
Allocator allocator,
|
Allocator allocator,
|
||||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||||
PlayerEmsgCallback playerEmsgCallback,
|
PlayerEmsgCallback playerEmsgCallback,
|
||||||
PlayerId playerId) {
|
PlayerId playerId,
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.baseUrlExclusionList = baseUrlExclusionList;
|
this.baseUrlExclusionList = baseUrlExclusionList;
|
||||||
@ -156,7 +158,8 @@ import java.util.regex.Pattern;
|
|||||||
Period period = manifest.getPeriod(periodIndex);
|
Period period = manifest.getPeriod(periodIndex);
|
||||||
eventStreams = period.eventStreams;
|
eventStreams = period.eventStreams;
|
||||||
Pair<TrackGroupArray, TrackGroupInfo[]> result =
|
Pair<TrackGroupArray, TrackGroupInfo[]> result =
|
||||||
buildTrackGroups(drmSessionManager, period.adaptationSets, eventStreams);
|
buildTrackGroups(
|
||||||
|
drmSessionManager, subtitleParserFactory, period.adaptationSets, eventStreams);
|
||||||
trackGroups = result.first;
|
trackGroups = result.first;
|
||||||
trackGroupInfos = result.second;
|
trackGroupInfos = result.second;
|
||||||
}
|
}
|
||||||
@ -501,6 +504,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
|
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
|
||||||
DrmSessionManager drmSessionManager,
|
DrmSessionManager drmSessionManager,
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory,
|
||||||
List<AdaptationSet> adaptationSets,
|
List<AdaptationSet> adaptationSets,
|
||||||
List<EventStream> eventStreams) {
|
List<EventStream> eventStreams) {
|
||||||
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
|
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
|
||||||
@ -523,6 +527,7 @@ import java.util.regex.Pattern;
|
|||||||
int trackGroupCount =
|
int trackGroupCount =
|
||||||
buildPrimaryAndEmbeddedTrackGroupInfos(
|
buildPrimaryAndEmbeddedTrackGroupInfos(
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
|
subtitleParserFactory,
|
||||||
adaptationSets,
|
adaptationSets,
|
||||||
groupedAdaptationSetIndices,
|
groupedAdaptationSetIndices,
|
||||||
primaryGroupCount,
|
primaryGroupCount,
|
||||||
@ -662,6 +667,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
private static int buildPrimaryAndEmbeddedTrackGroupInfos(
|
private static int buildPrimaryAndEmbeddedTrackGroupInfos(
|
||||||
DrmSessionManager drmSessionManager,
|
DrmSessionManager drmSessionManager,
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory,
|
||||||
List<AdaptationSet> adaptationSets,
|
List<AdaptationSet> adaptationSets,
|
||||||
int[][] groupedAdaptationSetIndices,
|
int[][] groupedAdaptationSetIndices,
|
||||||
int primaryGroupCount,
|
int primaryGroupCount,
|
||||||
@ -678,8 +684,24 @@ import java.util.regex.Pattern;
|
|||||||
}
|
}
|
||||||
Format[] formats = new Format[representations.size()];
|
Format[] formats = new Format[representations.size()];
|
||||||
for (int j = 0; j < formats.length; j++) {
|
for (int j = 0; j < formats.length; j++) {
|
||||||
Format format = representations.get(j).format;
|
Format originalFormat = representations.get(j).format;
|
||||||
formats[j] = format.copyWithCryptoType(drmSessionManager.getCryptoType(format));
|
Format.Builder updatedFormat =
|
||||||
|
originalFormat
|
||||||
|
.buildUpon()
|
||||||
|
.setCryptoType(drmSessionManager.getCryptoType(originalFormat));
|
||||||
|
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(originalFormat)) {
|
||||||
|
updatedFormat
|
||||||
|
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
|
||||||
|
.setCueReplacementBehavior(
|
||||||
|
subtitleParserFactory.getCueReplacementBehavior(originalFormat))
|
||||||
|
.setCodecs(
|
||||||
|
originalFormat.sampleMimeType
|
||||||
|
+ (originalFormat.codecs != null ? " " + originalFormat.codecs : ""))
|
||||||
|
// Reset this value to the default. All non-default timestamp adjustments are done
|
||||||
|
// by SubtitleTranscodingExtractor and there are no 'subsamples' after transcoding.
|
||||||
|
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE);
|
||||||
|
}
|
||||||
|
formats[j] = updatedFormat.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
|
AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
|
||||||
|
@ -77,6 +77,8 @@ import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction;
|
|||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||||
import androidx.media3.exoplayer.util.SntpClient;
|
import androidx.media3.exoplayer.util.SntpClient;
|
||||||
|
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.math.LongMath;
|
import com.google.common.math.LongMath;
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
@ -112,6 +114,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
private DrmSessionManagerProvider drmSessionManagerProvider;
|
private DrmSessionManagerProvider drmSessionManagerProvider;
|
||||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
|
@Nullable private SubtitleParser.Factory subtitleParserFactory;
|
||||||
private long fallbackTargetLiveOffsetMs;
|
private long fallbackTargetLiveOffsetMs;
|
||||||
private long minLiveStartPositionUs;
|
private long minLiveStartPositionUs;
|
||||||
@Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
@Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
||||||
@ -196,6 +199,40 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether subtitles should be parsed as part of extraction (before the sample queue) or as
|
||||||
|
* part of rendering (after the sample queue). Defaults to false (i.e. subtitles will be parsed
|
||||||
|
* as part of rendering).
|
||||||
|
*
|
||||||
|
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
|
||||||
|
* in a future release.
|
||||||
|
*
|
||||||
|
* <p>This method may only be used with {@link DefaultDashChunkSource.Factory}.
|
||||||
|
*
|
||||||
|
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
|
||||||
|
* rendering.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
// TODO: b/289916598 - Flip the default of this to true (probably wired up to a single method on
|
||||||
|
// DefaultMediaSourceFactory via the MediaSource.Factory interface).
|
||||||
|
public Factory experimentalParseSubtitlesDuringExtraction(
|
||||||
|
boolean parseSubtitlesDuringExtraction) {
|
||||||
|
if (parseSubtitlesDuringExtraction) {
|
||||||
|
if (subtitleParserFactory == null) {
|
||||||
|
this.subtitleParserFactory = new DefaultSubtitleParserFactory();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.subtitleParserFactory = null;
|
||||||
|
}
|
||||||
|
if (chunkSourceFactory instanceof DefaultDashChunkSource.Factory) {
|
||||||
|
((DefaultDashChunkSource.Factory) chunkSourceFactory)
|
||||||
|
.setSubtitleParserFactory(subtitleParserFactory);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the target {@link Player#getCurrentLiveOffset() offset for live streams} that is used if
|
* Sets the target {@link Player#getCurrentLiveOffset() offset for live streams} that is used if
|
||||||
* no value is defined in the {@link MediaItem} or the manifest.
|
* no value is defined in the {@link MediaItem} or the manifest.
|
||||||
@ -315,6 +352,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
drmSessionManagerProvider.get(mediaItem),
|
drmSessionManagerProvider.get(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
|
subtitleParserFactory,
|
||||||
fallbackTargetLiveOffsetMs,
|
fallbackTargetLiveOffsetMs,
|
||||||
minLiveStartPositionUs);
|
minLiveStartPositionUs);
|
||||||
}
|
}
|
||||||
@ -353,6 +391,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
drmSessionManagerProvider.get(mediaItem),
|
drmSessionManagerProvider.get(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
|
subtitleParserFactory,
|
||||||
fallbackTargetLiveOffsetMs,
|
fallbackTargetLiveOffsetMs,
|
||||||
minLiveStartPositionUs);
|
minLiveStartPositionUs);
|
||||||
}
|
}
|
||||||
@ -411,6 +450,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
private final Runnable simulateManifestRefreshRunnable;
|
private final Runnable simulateManifestRefreshRunnable;
|
||||||
private final PlayerEmsgCallback playerEmsgCallback;
|
private final PlayerEmsgCallback playerEmsgCallback;
|
||||||
private final LoaderErrorThrower manifestLoadErrorThrower;
|
private final LoaderErrorThrower manifestLoadErrorThrower;
|
||||||
|
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
|
||||||
|
|
||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
private Loader loader;
|
private Loader loader;
|
||||||
@ -446,6 +486,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
@Nullable CmcdConfiguration cmcdConfiguration,
|
@Nullable CmcdConfiguration cmcdConfiguration,
|
||||||
DrmSessionManager drmSessionManager,
|
DrmSessionManager drmSessionManager,
|
||||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory,
|
||||||
long fallbackTargetLiveOffsetMs,
|
long fallbackTargetLiveOffsetMs,
|
||||||
long minLiveStartPositionUs) {
|
long minLiveStartPositionUs) {
|
||||||
this.mediaItem = mediaItem;
|
this.mediaItem = mediaItem;
|
||||||
@ -459,6 +500,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
this.cmcdConfiguration = cmcdConfiguration;
|
this.cmcdConfiguration = cmcdConfiguration;
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||||
|
this.subtitleParserFactory = subtitleParserFactory;
|
||||||
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
|
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
|
||||||
this.minLiveStartPositionUs = minLiveStartPositionUs;
|
this.minLiveStartPositionUs = minLiveStartPositionUs;
|
||||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||||
@ -564,7 +606,8 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
allocator,
|
allocator,
|
||||||
compositeSequenceableLoaderFactory,
|
compositeSequenceableLoaderFactory,
|
||||||
playerEmsgCallback,
|
playerEmsgCallback,
|
||||||
getPlayerId());
|
getPlayerId(),
|
||||||
|
subtitleParserFactory);
|
||||||
periodsById.put(mediaPeriod.id, mediaPeriod);
|
periodsById.put(mediaPeriod.id, mediaPeriod);
|
||||||
return mediaPeriod;
|
return mediaPeriod;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ import androidx.media3.exoplayer.upstream.CmcdData;
|
|||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
import androidx.media3.extractor.ChunkIndex;
|
import androidx.media3.extractor.ChunkIndex;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -71,6 +72,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class DefaultDashChunkSource implements DashChunkSource {
|
public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
|
|
||||||
|
/** {@link DashChunkSource.Factory} for {@link DefaultDashChunkSource} instances. */
|
||||||
public static final class Factory implements DashChunkSource.Factory {
|
public static final class Factory implements DashChunkSource.Factory {
|
||||||
|
|
||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
@ -110,6 +112,21 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
|
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SubtitleParser.Factory} to be used for parsing subtitles during extraction,
|
||||||
|
* or null to parse subtitles during decoding.
|
||||||
|
*
|
||||||
|
* <p>This may only be used with {@link BundledChunkExtractor.Factory}.
|
||||||
|
*/
|
||||||
|
/* package */ Factory setSubtitleParserFactory(
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory) {
|
||||||
|
if (chunkExtractorFactory instanceof BundledChunkExtractor.Factory) {
|
||||||
|
((BundledChunkExtractor.Factory) chunkExtractorFactory)
|
||||||
|
.experimentalSetSubtitleParserFactory(subtitleParserFactory);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DashChunkSource createDashChunkSource(
|
public DashChunkSource createDashChunkSource(
|
||||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
|
@ -224,7 +224,8 @@ public final class DashMediaPeriodTest {
|
|||||||
mock(Allocator.class),
|
mock(Allocator.class),
|
||||||
mock(CompositeSequenceableLoaderFactory.class),
|
mock(CompositeSequenceableLoaderFactory.class),
|
||||||
mock(PlayerEmsgCallback.class),
|
mock(PlayerEmsgCallback.class),
|
||||||
PlayerId.UNSET);
|
PlayerId.UNSET,
|
||||||
|
/* subtitleParserFactory= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DashManifest parseManifest(String fileName) throws IOException {
|
private static DashManifest parseManifest(String fileName) throws IOException {
|
||||||
|
@ -22,9 +22,11 @@ import android.graphics.SurfaceTexture;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.datasource.DefaultDataSource;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.exoplayer.Renderer;
|
import androidx.media3.exoplayer.Renderer;
|
||||||
import androidx.media3.exoplayer.RenderersFactory;
|
import androidx.media3.exoplayer.RenderersFactory;
|
||||||
|
import androidx.media3.exoplayer.dash.DashMediaSource;
|
||||||
import androidx.media3.exoplayer.metadata.MetadataDecoderFactory;
|
import androidx.media3.exoplayer.metadata.MetadataDecoderFactory;
|
||||||
import androidx.media3.exoplayer.metadata.MetadataRenderer;
|
import androidx.media3.exoplayer.metadata.MetadataRenderer;
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||||
@ -79,14 +81,15 @@ public final class DashPlaybackTest {
|
|||||||
|
|
||||||
// https://github.com/google/ExoPlayer/issues/7985
|
// https://github.com/google/ExoPlayer/issues/7985
|
||||||
@Test
|
@Test
|
||||||
@Ignore(
|
|
||||||
"Disabled until subtitles are reliably asserted in robolectric tests [internal b/174661563].")
|
|
||||||
public void webvttInMp4() throws Exception {
|
public void webvttInMp4() throws Exception {
|
||||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
CapturingRenderersFactory capturingRenderersFactory =
|
CapturingRenderersFactory capturingRenderersFactory =
|
||||||
new CapturingRenderersFactory(applicationContext);
|
new CapturingRenderersFactory(applicationContext);
|
||||||
ExoPlayer player =
|
ExoPlayer player =
|
||||||
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
|
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
|
||||||
|
.setMediaSourceFactory(
|
||||||
|
new DashMediaSource.Factory(new DefaultDataSource.Factory(applicationContext))
|
||||||
|
.experimentalParseSubtitlesDuringExtraction(true))
|
||||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
.build();
|
.build();
|
||||||
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||||
|
@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.DataReader;
|
import androidx.media3.common.DataReader;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.Format.CueReplacementBehavior;
|
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.ParsableByteArray;
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
@ -86,8 +85,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
if (currentSubtitleParser == null) {
|
if (currentSubtitleParser == null) {
|
||||||
delegate.format(format);
|
delegate.format(format);
|
||||||
} else {
|
} else {
|
||||||
@CueReplacementBehavior
|
|
||||||
int nextCuesBehavior = currentSubtitleParser.getCueReplacementBehavior();
|
|
||||||
delegate.format(
|
delegate.format(
|
||||||
format
|
format
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
@ -96,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
// Reset this value to the default. All non-default timestamp adjustments are done
|
// Reset this value to the default. All non-default timestamp adjustments are done
|
||||||
// below in sampleMetadata() and there are no 'subsamples' after transcoding.
|
// below in sampleMetadata() and there are no 'subsamples' after transcoding.
|
||||||
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
|
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
|
||||||
.setCueReplacementBehavior(nextCuesBehavior)
|
.setCueReplacementBehavior(subtitleParserFactory.getCueReplacementBehavior(format))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,3 +244,6 @@ TextOutput:
|
|||||||
position = 0.5
|
position = 0.5
|
||||||
positionAnchor = 1
|
positionAnchor = 1
|
||||||
size = 1.0
|
size = 1.0
|
||||||
|
Subtitle[4]:
|
||||||
|
presentationTimeUs = 456000
|
||||||
|
Cues = []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user