Enable ID3-in-EMSG for HLS streams
This supports both chunkless & traditional preparation PiperOrigin-RevId: 273938344
This commit is contained in:
parent
a96a82588e
commit
60566721d4
@ -105,6 +105,8 @@
|
|||||||
* Fail more explicitly when local-file Uris contain invalid parts (e.g.
|
* Fail more explicitly when local-file Uris contain invalid parts (e.g.
|
||||||
fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)).
|
fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)).
|
||||||
* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state.
|
* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state.
|
||||||
|
* Add support for ID3-in-EMSG in HLS streams
|
||||||
|
([spec](https://aomediacodec.github.io/av1-id3/)).
|
||||||
|
|
||||||
### 2.10.5 (2019-09-20) ###
|
### 2.10.5 (2019-09-20) ###
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
|
|||||||
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
|
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
@ -158,7 +159,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||||||
|
|
||||||
if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
|
if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
|
||||||
FragmentedMp4Extractor fragmentedMp4Extractor =
|
FragmentedMp4Extractor fragmentedMp4Extractor =
|
||||||
createFragmentedMp4Extractor(timestampAdjuster, drmInitData, muxedCaptionFormats);
|
createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats);
|
||||||
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
|
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
|
||||||
return buildResult(fragmentedMp4Extractor);
|
return buildResult(fragmentedMp4Extractor);
|
||||||
}
|
}
|
||||||
@ -208,7 +209,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||||||
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
|
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
|
||||||
|| lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)
|
|| lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)
|
||||||
|| lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
|
|| lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
|
||||||
return createFragmentedMp4Extractor(timestampAdjuster, drmInitData, muxedCaptionFormats);
|
return createFragmentedMp4Extractor(
|
||||||
|
timestampAdjuster, format, drmInitData, muxedCaptionFormats);
|
||||||
} else {
|
} else {
|
||||||
// For any other file extension, we assume TS format.
|
// For any other file extension, we assume TS format.
|
||||||
return createTsExtractor(
|
return createTsExtractor(
|
||||||
@ -267,10 +269,21 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||||||
|
|
||||||
private static FragmentedMp4Extractor createFragmentedMp4Extractor(
|
private static FragmentedMp4Extractor createFragmentedMp4Extractor(
|
||||||
TimestampAdjuster timestampAdjuster,
|
TimestampAdjuster timestampAdjuster,
|
||||||
|
Format format,
|
||||||
DrmInitData drmInitData,
|
DrmInitData drmInitData,
|
||||||
@Nullable List<Format> muxedCaptionFormats) {
|
@Nullable List<Format> muxedCaptionFormats) {
|
||||||
|
boolean isVariant = false;
|
||||||
|
for (int i = 0; i < format.metadata.length(); i++) {
|
||||||
|
Metadata.Entry entry = format.metadata.get(i);
|
||||||
|
if (entry instanceof HlsTrackMetadataEntry) {
|
||||||
|
isVariant = !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid
|
||||||
|
// creating a separate EMSG track for every audio track in a video stream.
|
||||||
return new FragmentedMp4Extractor(
|
return new FragmentedMp4Extractor(
|
||||||
/* flags= */ 0,
|
/* flags= */ isVariant ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,
|
||||||
timestampAdjuster,
|
timestampAdjuster,
|
||||||
/* sideloadedTrack= */ null,
|
/* sideloadedTrack= */ null,
|
||||||
drmInitData,
|
drmInitData,
|
||||||
|
@ -75,6 +75,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
private final TimestampAdjusterProvider timestampAdjusterProvider;
|
private final TimestampAdjusterProvider timestampAdjusterProvider;
|
||||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
private final boolean allowChunklessPreparation;
|
private final boolean allowChunklessPreparation;
|
||||||
|
private final @HlsMetadataType int metadataType;
|
||||||
private final boolean useSessionKeys;
|
private final boolean useSessionKeys;
|
||||||
|
|
||||||
@Nullable private Callback callback;
|
@Nullable private Callback callback;
|
||||||
@ -117,6 +118,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
Allocator allocator,
|
Allocator allocator,
|
||||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||||
boolean allowChunklessPreparation,
|
boolean allowChunklessPreparation,
|
||||||
|
@HlsMetadataType int metadataType,
|
||||||
boolean useSessionKeys) {
|
boolean useSessionKeys) {
|
||||||
this.extractorFactory = extractorFactory;
|
this.extractorFactory = extractorFactory;
|
||||||
this.playlistTracker = playlistTracker;
|
this.playlistTracker = playlistTracker;
|
||||||
@ -128,6 +130,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||||
|
this.metadataType = metadataType;
|
||||||
this.useSessionKeys = useSessionKeys;
|
this.useSessionKeys = useSessionKeys;
|
||||||
compositeSequenceableLoader =
|
compositeSequenceableLoader =
|
||||||
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
|
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
|
||||||
@ -755,7 +758,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
muxedAudioFormat,
|
muxedAudioFormat,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
eventDispatcher);
|
eventDispatcher,
|
||||||
|
metadataType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, DrmInitData> deriveOverridingDrmInitData(
|
private static Map<String, DrmInitData> deriveOverridingDrmInitData(
|
||||||
|
@ -70,6 +70,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
private DrmSessionManager<?> drmSessionManager;
|
private DrmSessionManager<?> drmSessionManager;
|
||||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
private boolean allowChunklessPreparation;
|
private boolean allowChunklessPreparation;
|
||||||
|
@HlsMetadataType private int metadataType;
|
||||||
private boolean useSessionKeys;
|
private boolean useSessionKeys;
|
||||||
private boolean isCreateCalled;
|
private boolean isCreateCalled;
|
||||||
@Nullable private Object tag;
|
@Nullable private Object tag;
|
||||||
@ -99,6 +100,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||||
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
||||||
|
metadataType = HlsMetadataType.ID3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,6 +244,31 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the type of metadata to extract from the HLS source (defaults to {@link
|
||||||
|
* HlsMetadataType#ID3}).
|
||||||
|
*
|
||||||
|
* <p>HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is
|
||||||
|
* wrapped in an EMSG box [<a href="https://aomediacodec.github.io/av1-id3/">spec</a>].
|
||||||
|
*
|
||||||
|
* <p>If this is set to {@link HlsMetadataType#ID3} then raw ID3 metadata of will be extracted
|
||||||
|
* from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant
|
||||||
|
* stream only) will be unwrapped to expose the inner data. All other in-band metadata will be
|
||||||
|
* dropped.
|
||||||
|
*
|
||||||
|
* <p>If this is set to {@link HlsMetadataType#EMSG} then all EMSG data from the fMP4 variant
|
||||||
|
* stream will be extracted. No metadata will be extracted from TS streams, since they don't
|
||||||
|
* support EMSG.
|
||||||
|
*
|
||||||
|
* @param metadataType The type of metadata to extract.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
public Factory setMetadataType(@HlsMetadataType int metadataType) {
|
||||||
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
this.metadataType = metadataType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's
|
* Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's
|
||||||
* assumed that any single session key declared in the master playlist can be used to obtain all
|
* assumed that any single session key declared in the master playlist can be used to obtain all
|
||||||
@ -294,6 +321,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
playlistTrackerFactory.createTracker(
|
playlistTrackerFactory.createTracker(
|
||||||
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
|
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
|
||||||
allowChunklessPreparation,
|
allowChunklessPreparation,
|
||||||
|
metadataType,
|
||||||
useSessionKeys,
|
useSessionKeys,
|
||||||
tag);
|
tag);
|
||||||
}
|
}
|
||||||
@ -319,6 +347,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
private final DrmSessionManager<?> drmSessionManager;
|
private final DrmSessionManager<?> drmSessionManager;
|
||||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
private final boolean allowChunklessPreparation;
|
private final boolean allowChunklessPreparation;
|
||||||
|
private final @HlsMetadataType int metadataType;
|
||||||
private final boolean useSessionKeys;
|
private final boolean useSessionKeys;
|
||||||
private final HlsPlaylistTracker playlistTracker;
|
private final HlsPlaylistTracker playlistTracker;
|
||||||
@Nullable private final Object tag;
|
@Nullable private final Object tag;
|
||||||
@ -334,6 +363,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||||
HlsPlaylistTracker playlistTracker,
|
HlsPlaylistTracker playlistTracker,
|
||||||
boolean allowChunklessPreparation,
|
boolean allowChunklessPreparation,
|
||||||
|
@HlsMetadataType int metadataType,
|
||||||
boolean useSessionKeys,
|
boolean useSessionKeys,
|
||||||
@Nullable Object tag) {
|
@Nullable Object tag) {
|
||||||
this.manifestUri = manifestUri;
|
this.manifestUri = manifestUri;
|
||||||
@ -344,6 +374,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||||
this.playlistTracker = playlistTracker;
|
this.playlistTracker = playlistTracker;
|
||||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||||
|
this.metadataType = metadataType;
|
||||||
this.useSessionKeys = useSessionKeys;
|
this.useSessionKeys = useSessionKeys;
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
@ -381,6 +412,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
allocator,
|
allocator,
|
||||||
compositeSequenceableLoaderFactory,
|
compositeSequenceableLoaderFactory,
|
||||||
allowChunklessPreparation,
|
allowChunklessPreparation,
|
||||||
|
metadataType,
|
||||||
useSessionKeys);
|
useSessionKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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 com.google.android.exoplayer2.source.hls;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The types of metadata that can be extracted from HLS streams.
|
||||||
|
*
|
||||||
|
* <p>See {@link HlsMediaSource.Factory#setMetadataType(int)}.
|
||||||
|
*/
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG})
|
||||||
|
public @interface HlsMetadataType {
|
||||||
|
int ID3 = 1;
|
||||||
|
int EMSG = 3;
|
||||||
|
}
|
@ -28,10 +28,13 @@ import com.google.android.exoplayer2.drm.DrmInitData;
|
|||||||
import com.google.android.exoplayer2.drm.DrmSession;
|
import com.google.android.exoplayer2.drm.DrmSession;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||||
|
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
|
||||||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
||||||
import com.google.android.exoplayer2.source.DecryptableSampleQueueReader;
|
import com.google.android.exoplayer2.source.DecryptableSampleQueueReader;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||||
@ -51,7 +54,9 @@ import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
|
|||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -60,6 +65,7 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
|
* Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
|
||||||
@ -96,7 +102,8 @@ import java.util.Set;
|
|||||||
|
|
||||||
private static final Set<Integer> MAPPABLE_TYPES =
|
private static final Set<Integer> MAPPABLE_TYPES =
|
||||||
Collections.unmodifiableSet(
|
Collections.unmodifiableSet(
|
||||||
new HashSet<>(Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO)));
|
new HashSet<>(
|
||||||
|
Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_METADATA)));
|
||||||
|
|
||||||
private final int trackType;
|
private final int trackType;
|
||||||
private final Callback callback;
|
private final Callback callback;
|
||||||
@ -107,6 +114,7 @@ import java.util.Set;
|
|||||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
private final Loader loader;
|
private final Loader loader;
|
||||||
private final EventDispatcher eventDispatcher;
|
private final EventDispatcher eventDispatcher;
|
||||||
|
private final @HlsMetadataType int metadataType;
|
||||||
private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
|
private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
|
||||||
private final ArrayList<HlsMediaChunk> mediaChunks;
|
private final ArrayList<HlsMediaChunk> mediaChunks;
|
||||||
private final List<HlsMediaChunk> readOnlyMediaChunks;
|
private final List<HlsMediaChunk> readOnlyMediaChunks;
|
||||||
@ -121,6 +129,7 @@ import java.util.Set;
|
|||||||
private int[] sampleQueueTrackIds;
|
private int[] sampleQueueTrackIds;
|
||||||
private Set<Integer> sampleQueueMappingDoneByType;
|
private Set<Integer> sampleQueueMappingDoneByType;
|
||||||
private SparseIntArray sampleQueueIndicesByType;
|
private SparseIntArray sampleQueueIndicesByType;
|
||||||
|
private TrackOutput emsgUnwrappingTrackOutput;
|
||||||
private int primarySampleQueueType;
|
private int primarySampleQueueType;
|
||||||
private int primarySampleQueueIndex;
|
private int primarySampleQueueIndex;
|
||||||
private boolean sampleQueuesBuilt;
|
private boolean sampleQueuesBuilt;
|
||||||
@ -130,7 +139,7 @@ import java.util.Set;
|
|||||||
private Format downstreamTrackFormat;
|
private Format downstreamTrackFormat;
|
||||||
private boolean released;
|
private boolean released;
|
||||||
|
|
||||||
// Tracks are complicated in HLS. See documentation of buildTracks for details.
|
// Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details.
|
||||||
// Indexed by track (as exposed by this source).
|
// Indexed by track (as exposed by this source).
|
||||||
private TrackGroupArray trackGroups;
|
private TrackGroupArray trackGroups;
|
||||||
private Set<TrackGroup> optionalTrackGroups;
|
private Set<TrackGroup> optionalTrackGroups;
|
||||||
@ -178,7 +187,8 @@ import java.util.Set;
|
|||||||
Format muxedAudioFormat,
|
Format muxedAudioFormat,
|
||||||
DrmSessionManager<?> drmSessionManager,
|
DrmSessionManager<?> drmSessionManager,
|
||||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||||
EventDispatcher eventDispatcher) {
|
EventDispatcher eventDispatcher,
|
||||||
|
@HlsMetadataType int metadataType) {
|
||||||
this.trackType = trackType;
|
this.trackType = trackType;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.chunkSource = chunkSource;
|
this.chunkSource = chunkSource;
|
||||||
@ -188,6 +198,7 @@ import java.util.Set;
|
|||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||||
this.eventDispatcher = eventDispatcher;
|
this.eventDispatcher = eventDispatcher;
|
||||||
|
this.metadataType = metadataType;
|
||||||
loader = new Loader("Loader:HlsSampleStreamWrapper");
|
loader = new Loader("Loader:HlsSampleStreamWrapper");
|
||||||
nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
|
nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
|
||||||
sampleQueueTrackIds = new int[0];
|
sampleQueueTrackIds = new int[0];
|
||||||
@ -819,46 +830,34 @@ import java.util.Set;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TrackOutput track(int id, int type) {
|
public TrackOutput track(int id, int type) {
|
||||||
|
@Nullable TrackOutput trackOutput = null;
|
||||||
if (MAPPABLE_TYPES.contains(type)) {
|
if (MAPPABLE_TYPES.contains(type)) {
|
||||||
// Track types in MAPPABLE_TYPES are handled manually to ignore IDs.
|
// Track types in MAPPABLE_TYPES are handled manually to ignore IDs.
|
||||||
@Nullable TrackOutput mappedTrackOutput = getMappedTrackOutput(id, type);
|
trackOutput = getMappedTrackOutput(id, type);
|
||||||
if (mappedTrackOutput != null) {
|
} else /* non-mappable type track */ {
|
||||||
return mappedTrackOutput;
|
|
||||||
}
|
|
||||||
} else /* sparse track */ {
|
|
||||||
for (int i = 0; i < sampleQueues.length; i++) {
|
for (int i = 0; i < sampleQueues.length; i++) {
|
||||||
if (sampleQueueTrackIds[i] == id) {
|
if (sampleQueueTrackIds[i] == id) {
|
||||||
return sampleQueues[i];
|
trackOutput = sampleQueues[i];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tracksEnded) {
|
|
||||||
return createDummyTrackOutput(id, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
int trackCount = sampleQueues.length;
|
if (trackOutput == null) {
|
||||||
SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData);
|
if (tracksEnded) {
|
||||||
trackOutput.setSampleOffsetUs(sampleOffsetUs);
|
return createDummyTrackOutput(id, type);
|
||||||
trackOutput.sourceId(chunkUid);
|
} else {
|
||||||
trackOutput.setUpstreamFormatChangeListener(this);
|
// The relevant SampleQueue hasn't been constructed yet - so construct it.
|
||||||
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
|
trackOutput = createSampleQueue(id, type);
|
||||||
sampleQueueTrackIds[trackCount] = id;
|
}
|
||||||
sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1);
|
}
|
||||||
sampleQueues[trackCount] = trackOutput;
|
|
||||||
sampleQueueReaders = Arrays.copyOf(sampleQueueReaders, trackCount + 1);
|
if (type == C.TRACK_TYPE_METADATA) {
|
||||||
sampleQueueReaders[trackCount] =
|
if (emsgUnwrappingTrackOutput == null) {
|
||||||
new DecryptableSampleQueueReader(sampleQueues[trackCount], drmSessionManager);
|
emsgUnwrappingTrackOutput = new EmsgUnwrappingTrackOutput(trackOutput, metadataType);
|
||||||
sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
|
}
|
||||||
sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO
|
return emsgUnwrappingTrackOutput;
|
||||||
|| type == C.TRACK_TYPE_VIDEO;
|
|
||||||
haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];
|
|
||||||
sampleQueueMappingDoneByType.add(type);
|
|
||||||
sampleQueueIndicesByType.append(type, trackCount);
|
|
||||||
if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) {
|
|
||||||
primarySampleQueueIndex = trackCount;
|
|
||||||
primarySampleQueueType = type;
|
|
||||||
}
|
}
|
||||||
sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1);
|
|
||||||
return trackOutput;
|
return trackOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -893,6 +892,34 @@ import java.util.Set;
|
|||||||
: createDummyTrackOutput(id, type);
|
: createDummyTrackOutput(id, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SampleQueue createSampleQueue(int id, int type) {
|
||||||
|
int trackCount = sampleQueues.length;
|
||||||
|
|
||||||
|
SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData);
|
||||||
|
trackOutput.setSampleOffsetUs(sampleOffsetUs);
|
||||||
|
trackOutput.sourceId(chunkUid);
|
||||||
|
trackOutput.setUpstreamFormatChangeListener(this);
|
||||||
|
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
|
||||||
|
sampleQueueTrackIds[trackCount] = id;
|
||||||
|
sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1);
|
||||||
|
sampleQueues[trackCount] = trackOutput;
|
||||||
|
sampleQueueReaders = Arrays.copyOf(sampleQueueReaders, trackCount + 1);
|
||||||
|
sampleQueueReaders[trackCount] =
|
||||||
|
new DecryptableSampleQueueReader(sampleQueues[trackCount], drmSessionManager);
|
||||||
|
sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
|
||||||
|
sampleQueueIsAudioVideoFlags[trackCount] =
|
||||||
|
type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
|
||||||
|
haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];
|
||||||
|
sampleQueueMappingDoneByType.add(type);
|
||||||
|
sampleQueueIndicesByType.append(type, trackCount);
|
||||||
|
if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) {
|
||||||
|
primarySampleQueueIndex = trackCount;
|
||||||
|
primarySampleQueueType = type;
|
||||||
|
}
|
||||||
|
sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1);
|
||||||
|
return trackOutput;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void endTracks() {
|
public void endTracks() {
|
||||||
tracksEnded = true;
|
tracksEnded = true;
|
||||||
@ -1285,4 +1312,141 @@ import java.util.Set;
|
|||||||
return new Metadata(newMetadataEntries);
|
return new Metadata(newMetadataEntries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class EmsgUnwrappingTrackOutput implements TrackOutput {
|
||||||
|
|
||||||
|
private static final String TAG = "EmsgUnwrappingTrackOutput";
|
||||||
|
|
||||||
|
// TODO(ibaker): Create a Formats util class with common constants like this.
|
||||||
|
private static final Format ID3_FORMAT =
|
||||||
|
Format.createSampleFormat(
|
||||||
|
/* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE);
|
||||||
|
private static final Format EMSG_FORMAT =
|
||||||
|
Format.createSampleFormat(
|
||||||
|
/* id= */ null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);
|
||||||
|
|
||||||
|
private final EventMessageDecoder emsgDecoder;
|
||||||
|
private final TrackOutput delegate;
|
||||||
|
private final Format delegateFormat;
|
||||||
|
@MonotonicNonNull private Format format;
|
||||||
|
|
||||||
|
private byte[] buffer;
|
||||||
|
private int bufferPosition;
|
||||||
|
|
||||||
|
public EmsgUnwrappingTrackOutput(TrackOutput delegate, @HlsMetadataType int metadataType) {
|
||||||
|
this.emsgDecoder = new EventMessageDecoder();
|
||||||
|
this.delegate = delegate;
|
||||||
|
switch (metadataType) {
|
||||||
|
case HlsMetadataType.ID3:
|
||||||
|
delegateFormat = ID3_FORMAT;
|
||||||
|
break;
|
||||||
|
case HlsMetadataType.EMSG:
|
||||||
|
delegateFormat = EMSG_FORMAT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown metadataType: " + metadataType);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffer = new byte[0];
|
||||||
|
this.bufferPosition = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(Format format) {
|
||||||
|
this.format = format;
|
||||||
|
delegate.format(delegateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
ensureBufferCapacity(bufferPosition + length);
|
||||||
|
int numBytesRead = input.read(buffer, bufferPosition, length);
|
||||||
|
if (numBytesRead == C.RESULT_END_OF_INPUT) {
|
||||||
|
if (allowEndOfInput) {
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
} else {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufferPosition += numBytesRead;
|
||||||
|
return numBytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sampleData(ParsableByteArray buffer, int length) {
|
||||||
|
ensureBufferCapacity(bufferPosition + length);
|
||||||
|
buffer.readBytes(this.buffer, bufferPosition, length);
|
||||||
|
bufferPosition += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sampleMetadata(
|
||||||
|
long timeUs,
|
||||||
|
@C.BufferFlags int flags,
|
||||||
|
int size,
|
||||||
|
int offset,
|
||||||
|
@Nullable CryptoData cryptoData) {
|
||||||
|
Assertions.checkState(format != null);
|
||||||
|
ParsableByteArray sample = getSampleAndTrimBuffer(size, offset);
|
||||||
|
ParsableByteArray sampleForDelegate;
|
||||||
|
if (Util.areEqual(format.sampleMimeType, delegateFormat.sampleMimeType)) {
|
||||||
|
// Incoming format matches delegate track's format, so pass straight through.
|
||||||
|
sampleForDelegate = sample;
|
||||||
|
} else if (MimeTypes.APPLICATION_EMSG.equals(format.sampleMimeType)) {
|
||||||
|
// Incoming sample is EMSG, and delegate track is not expecting EMSG, so try unwrapping.
|
||||||
|
EventMessage emsg = emsgDecoder.decode(sample);
|
||||||
|
if (!emsgContainsExpectedWrappedFormat(emsg)) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"Ignoring EMSG. Expected it to contain wrapped %s but actual wrapped format: %s",
|
||||||
|
delegateFormat.sampleMimeType, emsg.getWrappedMetadataFormat()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sampleForDelegate =
|
||||||
|
new ParsableByteArray(Assertions.checkNotNull(emsg.getWrappedMetadataBytes()));
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Ignoring sample for unsupported format: " + format.sampleMimeType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sampleSize = sampleForDelegate.bytesLeft();
|
||||||
|
|
||||||
|
delegate.sampleData(sampleForDelegate, sampleSize);
|
||||||
|
delegate.sampleMetadata(timeUs, flags, sampleSize, offset, cryptoData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean emsgContainsExpectedWrappedFormat(EventMessage emsg) {
|
||||||
|
@Nullable Format wrappedMetadataFormat = emsg.getWrappedMetadataFormat();
|
||||||
|
return wrappedMetadataFormat != null
|
||||||
|
&& Util.areEqual(delegateFormat.sampleMimeType, wrappedMetadataFormat.sampleMimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureBufferCapacity(int requiredLength) {
|
||||||
|
if (buffer.length < requiredLength) {
|
||||||
|
buffer = Arrays.copyOf(buffer, requiredLength + requiredLength / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a complete sample from the {@link #buffer} field & reshuffles the tail data skipped
|
||||||
|
* by {@code offset} to the head of the array.
|
||||||
|
*
|
||||||
|
* @param size see {@code size} param of {@link #sampleMetadata}.
|
||||||
|
* @param offset see {@code offset} param of {@link #sampleMetadata}.
|
||||||
|
* @return A {@link ParsableByteArray} containing the sample removed from {@link #buffer}.
|
||||||
|
*/
|
||||||
|
private ParsableByteArray getSampleAndTrimBuffer(int size, int offset) {
|
||||||
|
int sampleEnd = bufferPosition - offset;
|
||||||
|
int sampleStart = sampleEnd - size;
|
||||||
|
|
||||||
|
byte[] sampleBytes = Arrays.copyOfRange(buffer, sampleStart, sampleEnd);
|
||||||
|
ParsableByteArray sample = new ParsableByteArray(sampleBytes);
|
||||||
|
|
||||||
|
System.arraycopy(buffer, sampleEnd, buffer, 0, offset);
|
||||||
|
bufferPosition = offset;
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,7 @@ public final class HlsMediaPeriodTest {
|
|||||||
mock(Allocator.class),
|
mock(Allocator.class),
|
||||||
mock(CompositeSequenceableLoaderFactory.class),
|
mock(CompositeSequenceableLoaderFactory.class),
|
||||||
/* allowChunklessPreparation =*/ true,
|
/* allowChunklessPreparation =*/ true,
|
||||||
|
HlsMetadataType.ID3,
|
||||||
/* useSessionKeys= */ false);
|
/* useSessionKeys= */ false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user