/** * Called by the bundles when a snapshot changes. * * @param url The url of the playlist. * @param newSnapshot The new snapshot. * @return True if a refresh should be scheduled. */ private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { if (url == primaryHlsUrl) { if (primaryUrlSnapshot == null) { // This is the first primary url snapshot. isLive = !newSnapshot.hasEndTag; } primaryUrlSnapshot = newSnapshot; primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); } int listenersSize = listeners.size(); for (int i = 0; i < listenersSize; i++) { listeners.get(i).onPlaylistChanged(); } // If the primary playlist is not the final one, we should schedule a refresh. return url == primaryHlsUrl && !newSnapshot.hasEndTag; }
/** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the * chunks. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the master playlist. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, List<Format> muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { variantFormats[i] = variants[i].format; initialTrackSelection[i] = i; } mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); }
/** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the * chunks. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. * @param muxedCaptionFormats List of muxed caption {@link Format}s. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, List<Format> muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { variantFormats[i] = variants[i].format; initialTrackSelection[i] = i; } mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); }
/** * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for * media chunks. * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the * chunks. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the master playlist. */ public HlsChunkSource(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, List<Format> muxedCaptionFormats) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; liveEdgeTimeUs = C.TIME_UNSET; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { variantFormats[i] = variants[i].format; initialTrackSelection[i] = i; } mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); }
/** * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the master playlist. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. * @param endTimeUs The end time of the chunk in microseconds. * @param chunkIndex The media sequence number of the chunk. * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List<Format> muxedCaptionFormats, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; lastPathSegment = dataSpec.uri.getLastPathSegment(); isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) || lastPathSegment.endsWith(AC3_FILE_EXTENSION) || lastPathSegment.endsWith(EC3_FILE_EXTENSION) || lastPathSegment.endsWith(MP3_FILE_EXTENSION); if (previousChunk != null) { id3Decoder = previousChunk.id3Decoder; id3Data = previousChunk.id3Data; previousExtractor = previousChunk.extractor; shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber || shouldSpliceIn; } else { id3Decoder = isPackedAudio ? new Id3Decoder() : null; id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null; previousExtractor = null; shouldSpliceIn = false; needNewExtractor = true; } initDataSource = dataSource; uid = UID_SOURCE.getAndIncrement(); }
@Override public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) { HlsPlaylist result = loadable.getResult(); HlsMasterPlaylist masterPlaylist; boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; if (isMediaPlaylist) { masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); } else /* result instanceof HlsMasterPlaylist */ { masterPlaylist = (HlsMasterPlaylist) result; } this.masterPlaylist = masterPlaylist; primaryHlsUrl = masterPlaylist.variants.get(0); ArrayList<HlsUrl> urls = new ArrayList<>(); urls.addAll(masterPlaylist.variants); urls.addAll(masterPlaylist.audios); urls.addAll(masterPlaylist.subtitles); createBundles(urls); MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); if (isMediaPlaylist) { // We don't need to load the playlist again. We can use the same result. primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); } else { primaryBundle.loadPlaylist(); } eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); }
private boolean maybeSelectNewPrimaryUrl() { List<HlsUrl> variants = masterPlaylist.variants; int variantsSize = variants.size(); long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < variantsSize; i++) { MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); if (currentTimeMs > bundle.blacklistUntilMs) { primaryHlsUrl = bundle.playlistUrl; bundle.loadPlaylist(); return true; } } return false; }
private void maybeSetPrimaryUrl(HlsUrl url) { if (!masterPlaylist.variants.contains(url) || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { // Only allow variant urls to be chosen as primary. Also prevent changing the primary url if // the last primary snapshot contains an end tag. return; } MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl); long primarySnapshotAccessAgeMs = currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime(); if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) { primaryHlsUrl = url; playlistBundles.get(primaryHlsUrl).loadPlaylist(); } }
private void createBundles(List<HlsUrl> urls) { int listSize = urls.size(); long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < listSize; i++) { HlsUrl url = urls.get(i); MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs); playlistBundles.put(url, bundle); } }
public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) { this.playlistUrl = playlistUrl; lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs; mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); mediaPlaylistLoadable = new ParsingLoadable<>( dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, playlistParser); }
@Override public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { streamWrapper.onPlaylistBlacklisted(url, blacklistMs); } continuePreparingOrLoading(); }
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, Format muxedAudioFormat, List<Format> muxedCaptionFormats) { HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); }
private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { String codecs = variant.format.codecs; if (TextUtils.isEmpty(codecs)) { return false; } String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); for (String codec : codecArray) { if (codec.startsWith(prefix)) { return true; } } return false; }
/** * Called when a playlist is blacklisted. * * @param url The url that references the blacklisted playlist. * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted. */ public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { int trackGroupIndex = trackGroup.indexOf(url.format); if (trackGroupIndex != C.INDEX_UNSET) { int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); if (trackSelectionIndex != C.INDEX_UNSET) { trackSelection.blacklist(trackSelectionIndex, blacklistMs); } } }
@Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { return false; } chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist; nextChunkHolder.clear(); if (endOfStream) { loadingFinished = true; return true; } if (loadable == null) { if (playlistToLoad != null) { callback.onPlaylistRefreshRequired(playlistToLoad); } return false; } if (isMediaChunk(loadable)) { pendingResetPositionUs = C.TIME_UNSET; HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; mediaChunk.init(this); mediaChunks.add(mediaChunk); } long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); return true; }
/** * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. * @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. * @param endTimeUs The end time of the chunk in microseconds. * @param chunkIndex The media sequence number of the chunk. * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List<Format> muxedCaptionFormats, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; lastPathSegment = dataSpec.uri.getLastPathSegment(); isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) || lastPathSegment.endsWith(AC3_FILE_EXTENSION) || lastPathSegment.endsWith(EC3_FILE_EXTENSION) || lastPathSegment.endsWith(MP3_FILE_EXTENSION); if (previousChunk != null) { id3Decoder = previousChunk.id3Decoder; id3Data = previousChunk.id3Data; previousExtractor = previousChunk.extractor; shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber || shouldSpliceIn; } else { id3Decoder = isPackedAudio ? new Id3Decoder() : null; id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null; previousExtractor = null; shouldSpliceIn = false; needNewExtractor = true; } initDataSource = dataSource; uid = UID_SOURCE.getAndIncrement(); }
private void maybeSetPrimaryUrl(HlsUrl url) { if (url == primaryHlsUrl || !masterPlaylist.variants.contains(url) || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last // primary snapshot contains an end tag. return; } primaryHlsUrl = url; playlistBundles.get(primaryHlsUrl).loadPlaylist(); }
private void createBundles(List<HlsUrl> urls) { int listSize = urls.size(); for (int i = 0; i < listSize; i++) { HlsUrl url = urls.get(i); MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); playlistBundles.put(url, bundle); } }
/** * Called by the bundles when a snapshot changes. * * @param url The url of the playlist. * @param newSnapshot The new snapshot. */ private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { if (url == primaryHlsUrl) { if (primaryUrlSnapshot == null) { // This is the first primary url snapshot. isLive = !newSnapshot.hasEndTag; } primaryUrlSnapshot = newSnapshot; primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); } int listenersSize = listeners.size(); for (int i = 0; i < listenersSize; i++) { listeners.get(i).onPlaylistChanged(); } }
public MediaPlaylistBundle(HlsUrl playlistUrl) { this.playlistUrl = playlistUrl; mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); mediaPlaylistLoadable = new ParsingLoadable<>( dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, playlistParser); }
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, Format muxedAudioFormat, List<Format> muxedCaptionFormats, long positionUs) { HlsChunkSource defaultChunkSource = new HlsChunkSource(extractorFactory, playlistTracker, variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); }
private void notifyPlaylistBlacklisting(HlsUrl url, long blacklistMs) { int listenersSize = listeners.size(); for (int i = 0; i < listenersSize; i++) { listeners.get(i).onPlaylistBlacklisted(url, blacklistMs); } }
@Override public void onPlaylistRefreshRequired(HlsUrl url) { playlistTracker.refreshPlaylist(url); }
public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { chunkSource.onPlaylistBlacklisted(url, blacklistMs); }