| /* |
| * Copyright (C) 2016 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.playlist; |
| |
| import android.net.Uri; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.Format; |
| import com.google.android.exoplayer2.drm.DrmInitData; |
| import com.google.android.exoplayer2.offline.StreamKey; |
| import com.google.android.exoplayer2.util.MimeTypes; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** Represents an HLS master playlist. */ |
| public final class HlsMasterPlaylist extends HlsPlaylist { |
| |
| /** Represents an empty master playlist, from which no attributes can be inherited. */ |
| public static final HlsMasterPlaylist EMPTY = |
| new HlsMasterPlaylist( |
| /* baseUri= */ "", |
| /* tags= */ Collections.emptyList(), |
| /* variants= */ Collections.emptyList(), |
| /* videos= */ Collections.emptyList(), |
| /* audios= */ Collections.emptyList(), |
| /* subtitles= */ Collections.emptyList(), |
| /* closedCaptions= */ Collections.emptyList(), |
| /* muxedAudioFormat= */ null, |
| /* muxedCaptionFormats= */ Collections.emptyList(), |
| /* hasIndependentSegments= */ false, |
| /* variableDefinitions= */ Collections.emptyMap(), |
| /* sessionKeyDrmInitData= */ Collections.emptyList()); |
| |
| // These constants must not be changed because they are persisted in offline stream keys. |
| public static final int GROUP_INDEX_VARIANT = 0; |
| public static final int GROUP_INDEX_AUDIO = 1; |
| public static final int GROUP_INDEX_SUBTITLE = 2; |
| |
| /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */ |
| public static final class Variant { |
| |
| /** The variant's url. */ |
| public final Uri url; |
| |
| /** Format information associated with this variant. */ |
| public final Format format; |
| |
| /** The video rendition group referenced by this variant, or {@code null}. */ |
| @Nullable public final String videoGroupId; |
| |
| /** The audio rendition group referenced by this variant, or {@code null}. */ |
| @Nullable public final String audioGroupId; |
| |
| /** The subtitle rendition group referenced by this variant, or {@code null}. */ |
| @Nullable public final String subtitleGroupId; |
| |
| /** The caption rendition group referenced by this variant, or {@code null}. */ |
| @Nullable public final String captionGroupId; |
| |
| /** |
| * @param url See {@link #url}. |
| * @param format See {@link #format}. |
| * @param videoGroupId See {@link #videoGroupId}. |
| * @param audioGroupId See {@link #audioGroupId}. |
| * @param subtitleGroupId See {@link #subtitleGroupId}. |
| * @param captionGroupId See {@link #captionGroupId}. |
| */ |
| public Variant( |
| Uri url, |
| Format format, |
| @Nullable String videoGroupId, |
| @Nullable String audioGroupId, |
| @Nullable String subtitleGroupId, |
| @Nullable String captionGroupId) { |
| this.url = url; |
| this.format = format; |
| this.videoGroupId = videoGroupId; |
| this.audioGroupId = audioGroupId; |
| this.subtitleGroupId = subtitleGroupId; |
| this.captionGroupId = captionGroupId; |
| } |
| |
| /** |
| * Creates a variant for a given media playlist url. |
| * |
| * @param url The media playlist url. |
| * @return The variant instance. |
| */ |
| public static Variant createMediaPlaylistVariantUrl(Uri url) { |
| Format format = |
| new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build(); |
| return new Variant( |
| url, |
| format, |
| /* videoGroupId= */ null, |
| /* audioGroupId= */ null, |
| /* subtitleGroupId= */ null, |
| /* captionGroupId= */ null); |
| } |
| |
| /** Returns a copy of this instance with the given {@link Format}. */ |
| public Variant copyWithFormat(Format format) { |
| return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId); |
| } |
| } |
| |
| /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */ |
| public static final class Rendition { |
| |
| /** The rendition's url, or null if the tag does not have a URI attribute. */ |
| @Nullable public final Uri url; |
| |
| /** Format information associated with this rendition. */ |
| public final Format format; |
| |
| /** The group to which this rendition belongs. */ |
| public final String groupId; |
| |
| /** The name of the rendition. */ |
| public final String name; |
| |
| /** |
| * @param url See {@link #url}. |
| * @param format See {@link #format}. |
| * @param groupId See {@link #groupId}. |
| * @param name See {@link #name}. |
| */ |
| public Rendition(@Nullable Uri url, Format format, String groupId, String name) { |
| this.url = url; |
| this.format = format; |
| this.groupId = groupId; |
| this.name = name; |
| } |
| |
| } |
| |
| /** All of the media playlist URLs referenced by the playlist. */ |
| public final List<Uri> mediaPlaylistUrls; |
| /** The variants declared by the playlist. */ |
| public final List<Variant> variants; |
| /** The video renditions declared by the playlist. */ |
| public final List<Rendition> videos; |
| /** The audio renditions declared by the playlist. */ |
| public final List<Rendition> audios; |
| /** The subtitle renditions declared by the playlist. */ |
| public final List<Rendition> subtitles; |
| /** The closed caption renditions declared by the playlist. */ |
| public final List<Rendition> closedCaptions; |
| |
| /** |
| * The format of the audio muxed in the variants. May be null if the playlist does not declare any |
| * muxed audio. |
| */ |
| @Nullable public final Format muxedAudioFormat; |
| /** |
| * The format of the closed captions declared by the playlist. May be empty if the playlist |
| * explicitly declares no captions are available, or null if the playlist does not declare any |
| * captions information. |
| */ |
| @Nullable public final List<Format> muxedCaptionFormats; |
| /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ |
| public final Map<String, String> variableDefinitions; |
| /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ |
| public final List<DrmInitData> sessionKeyDrmInitData; |
| |
| /** |
| * @param baseUri See {@link #baseUri}. |
| * @param tags See {@link #tags}. |
| * @param variants See {@link #variants}. |
| * @param videos See {@link #videos}. |
| * @param audios See {@link #audios}. |
| * @param subtitles See {@link #subtitles}. |
| * @param closedCaptions See {@link #closedCaptions}. |
| * @param muxedAudioFormat See {@link #muxedAudioFormat}. |
| * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. |
| * @param hasIndependentSegments See {@link #hasIndependentSegments}. |
| * @param variableDefinitions See {@link #variableDefinitions}. |
| * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. |
| */ |
| public HlsMasterPlaylist( |
| String baseUri, |
| List<String> tags, |
| List<Variant> variants, |
| List<Rendition> videos, |
| List<Rendition> audios, |
| List<Rendition> subtitles, |
| List<Rendition> closedCaptions, |
| @Nullable Format muxedAudioFormat, |
| @Nullable List<Format> muxedCaptionFormats, |
| boolean hasIndependentSegments, |
| Map<String, String> variableDefinitions, |
| List<DrmInitData> sessionKeyDrmInitData) { |
| super(baseUri, tags, hasIndependentSegments); |
| this.mediaPlaylistUrls = |
| Collections.unmodifiableList( |
| getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions)); |
| this.variants = Collections.unmodifiableList(variants); |
| this.videos = Collections.unmodifiableList(videos); |
| this.audios = Collections.unmodifiableList(audios); |
| this.subtitles = Collections.unmodifiableList(subtitles); |
| this.closedCaptions = Collections.unmodifiableList(closedCaptions); |
| this.muxedAudioFormat = muxedAudioFormat; |
| this.muxedCaptionFormats = muxedCaptionFormats != null |
| ? Collections.unmodifiableList(muxedCaptionFormats) : null; |
| this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); |
| this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); |
| } |
| |
| @Override |
| public HlsMasterPlaylist copy(List<StreamKey> streamKeys) { |
| return new HlsMasterPlaylist( |
| baseUri, |
| tags, |
| copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), |
| // TODO: Allow stream keys to specify video renditions to be retained. |
| /* videos= */ Collections.emptyList(), |
| copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), |
| copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), |
| // TODO: Update to retain all closed captions. |
| /* closedCaptions= */ Collections.emptyList(), |
| muxedAudioFormat, |
| muxedCaptionFormats, |
| hasIndependentSegments, |
| variableDefinitions, |
| sessionKeyDrmInitData); |
| } |
| |
| /** |
| * Creates a playlist with a single variant. |
| * |
| * @param variantUrl The url of the single variant. |
| * @return A master playlist with a single variant for the provided url. |
| */ |
| public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { |
| List<Variant> variant = |
| Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl))); |
| return new HlsMasterPlaylist( |
| /* baseUri= */ "", |
| /* tags= */ Collections.emptyList(), |
| variant, |
| /* videos= */ Collections.emptyList(), |
| /* audios= */ Collections.emptyList(), |
| /* subtitles= */ Collections.emptyList(), |
| /* closedCaptions= */ Collections.emptyList(), |
| /* muxedAudioFormat= */ null, |
| /* muxedCaptionFormats= */ null, |
| /* hasIndependentSegments= */ false, |
| /* variableDefinitions= */ Collections.emptyMap(), |
| /* sessionKeyDrmInitData= */ Collections.emptyList()); |
| } |
| |
| private static List<Uri> getMediaPlaylistUrls( |
| List<Variant> variants, |
| List<Rendition> videos, |
| List<Rendition> audios, |
| List<Rendition> subtitles, |
| List<Rendition> closedCaptions) { |
| ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>(); |
| for (int i = 0; i < variants.size(); i++) { |
| Uri uri = variants.get(i).url; |
| if (!mediaPlaylistUrls.contains(uri)) { |
| mediaPlaylistUrls.add(uri); |
| } |
| } |
| addMediaPlaylistUrls(videos, mediaPlaylistUrls); |
| addMediaPlaylistUrls(audios, mediaPlaylistUrls); |
| addMediaPlaylistUrls(subtitles, mediaPlaylistUrls); |
| addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls); |
| return mediaPlaylistUrls; |
| } |
| |
| private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) { |
| for (int i = 0; i < renditions.size(); i++) { |
| Uri uri = renditions.get(i).url; |
| if (uri != null && !out.contains(uri)) { |
| out.add(uri); |
| } |
| } |
| } |
| |
| private static <T> List<T> copyStreams( |
| List<T> streams, int groupIndex, List<StreamKey> streamKeys) { |
| List<T> copiedStreams = new ArrayList<>(streamKeys.size()); |
| // TODO: |
| // 1. When variants with the same URL are not de-duplicated, duplicates must not increment |
| // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All |
| // duplicates should be copied if the first variant is copied, or discarded otherwise. |
| // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to |
| // avoid breaking stream keys that have been persisted for offline. All renitions with null |
| // URLs should be copied. They may become unreachable if all variants that reference them are |
| // removed, but this is OK. |
| // 3. Renditions with URLs matching copied variants should always themselves be copied, even if |
| // the corresponding stream key is omitted. Else we're throwing away information for no gain. |
| for (int i = 0; i < streams.size(); i++) { |
| T stream = streams.get(i); |
| for (int j = 0; j < streamKeys.size(); j++) { |
| StreamKey streamKey = streamKeys.get(j); |
| if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) { |
| copiedStreams.add(stream); |
| break; |
| } |
| } |
| } |
| return copiedStreams; |
| } |
| |
| } |