1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist;
17 
18 import android.net.Uri;
19 import androidx.annotation.Nullable;
20 import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
21 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
22 import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey;
23 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.Map;
28 
29 /** Represents an HLS master playlist. */
30 public final class HlsMasterPlaylist extends HlsPlaylist {
31 
32   /** Represents an empty master playlist, from which no attributes can be inherited. */
33   public static final HlsMasterPlaylist EMPTY =
34       new HlsMasterPlaylist(
35           /* baseUri= */ "",
36           /* tags= */ Collections.emptyList(),
37           /* variants= */ Collections.emptyList(),
38           /* videos= */ Collections.emptyList(),
39           /* audios= */ Collections.emptyList(),
40           /* subtitles= */ Collections.emptyList(),
41           /* closedCaptions= */ Collections.emptyList(),
42           /* muxedAudioFormat= */ null,
43           /* muxedCaptionFormats= */ Collections.emptyList(),
44           /* hasIndependentSegments= */ false,
45           /* variableDefinitions= */ Collections.emptyMap(),
46           /* sessionKeyDrmInitData= */ Collections.emptyList());
47 
48   // These constants must not be changed because they are persisted in offline stream keys.
49   public static final int GROUP_INDEX_VARIANT = 0;
50   public static final int GROUP_INDEX_AUDIO = 1;
51   public static final int GROUP_INDEX_SUBTITLE = 2;
52 
53   /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */
54   public static final class Variant {
55 
56     /** The variant's url. */
57     public final Uri url;
58 
59     /** Format information associated with this variant. */
60     public final Format format;
61 
62     /** The video rendition group referenced by this variant, or {@code null}. */
63     @Nullable public final String videoGroupId;
64 
65     /** The audio rendition group referenced by this variant, or {@code null}. */
66     @Nullable public final String audioGroupId;
67 
68     /** The subtitle rendition group referenced by this variant, or {@code null}. */
69     @Nullable public final String subtitleGroupId;
70 
71     /** The caption rendition group referenced by this variant, or {@code null}. */
72     @Nullable public final String captionGroupId;
73 
74     /**
75      * @param url See {@link #url}.
76      * @param format See {@link #format}.
77      * @param videoGroupId See {@link #videoGroupId}.
78      * @param audioGroupId See {@link #audioGroupId}.
79      * @param subtitleGroupId See {@link #subtitleGroupId}.
80      * @param captionGroupId See {@link #captionGroupId}.
81      */
Variant( Uri url, Format format, @Nullable String videoGroupId, @Nullable String audioGroupId, @Nullable String subtitleGroupId, @Nullable String captionGroupId)82     public Variant(
83         Uri url,
84         Format format,
85         @Nullable String videoGroupId,
86         @Nullable String audioGroupId,
87         @Nullable String subtitleGroupId,
88         @Nullable String captionGroupId) {
89       this.url = url;
90       this.format = format;
91       this.videoGroupId = videoGroupId;
92       this.audioGroupId = audioGroupId;
93       this.subtitleGroupId = subtitleGroupId;
94       this.captionGroupId = captionGroupId;
95     }
96 
97     /**
98      * Creates a variant for a given media playlist url.
99      *
100      * @param url The media playlist url.
101      * @return The variant instance.
102      */
createMediaPlaylistVariantUrl(Uri url)103     public static Variant createMediaPlaylistVariantUrl(Uri url) {
104       Format format =
105           Format.createContainerFormat(
106               "0",
107               /* label= */ null,
108               MimeTypes.APPLICATION_M3U8,
109               /* sampleMimeType= */ null,
110               /* codecs= */ null,
111               /* bitrate= */ Format.NO_VALUE,
112               /* selectionFlags= */ 0,
113               /* roleFlags= */ 0,
114               /* language= */ null);
115       return new Variant(
116           url,
117           format,
118           /* videoGroupId= */ null,
119           /* audioGroupId= */ null,
120           /* subtitleGroupId= */ null,
121           /* captionGroupId= */ null);
122     }
123 
124     /** Returns a copy of this instance with the given {@link Format}. */
copyWithFormat(Format format)125     public Variant copyWithFormat(Format format) {
126       return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId);
127     }
128   }
129 
130   /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */
131   public static final class Rendition {
132 
133     /** The rendition's url, or null if the tag does not have a URI attribute. */
134     @Nullable public final Uri url;
135 
136     /** Format information associated with this rendition. */
137     public final Format format;
138 
139     /** The group to which this rendition belongs. */
140     public final String groupId;
141 
142     /** The name of the rendition. */
143     public final String name;
144 
145     /**
146      * @param url See {@link #url}.
147      * @param format See {@link #format}.
148      * @param groupId See {@link #groupId}.
149      * @param name See {@link #name}.
150      */
Rendition(@ullable Uri url, Format format, String groupId, String name)151     public Rendition(@Nullable Uri url, Format format, String groupId, String name) {
152       this.url = url;
153       this.format = format;
154       this.groupId = groupId;
155       this.name = name;
156     }
157 
158   }
159 
160   /** All of the media playlist URLs referenced by the playlist. */
161   public final List<Uri> mediaPlaylistUrls;
162   /** The variants declared by the playlist. */
163   public final List<Variant> variants;
164   /** The video renditions declared by the playlist. */
165   public final List<Rendition> videos;
166   /** The audio renditions declared by the playlist. */
167   public final List<Rendition> audios;
168   /** The subtitle renditions declared by the playlist. */
169   public final List<Rendition> subtitles;
170   /** The closed caption renditions declared by the playlist. */
171   public final List<Rendition> closedCaptions;
172 
173   /**
174    * The format of the audio muxed in the variants. May be null if the playlist does not declare any
175    * muxed audio.
176    */
177   @Nullable public final Format muxedAudioFormat;
178   /**
179    * The format of the closed captions declared by the playlist. May be empty if the playlist
180    * explicitly declares no captions are available, or null if the playlist does not declare any
181    * captions information.
182    */
183   @Nullable public final List<Format> muxedCaptionFormats;
184   /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
185   public final Map<String, String> variableDefinitions;
186   /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
187   public final List<DrmInitData> sessionKeyDrmInitData;
188 
189   /**
190    * @param baseUri See {@link #baseUri}.
191    * @param tags See {@link #tags}.
192    * @param variants See {@link #variants}.
193    * @param videos See {@link #videos}.
194    * @param audios See {@link #audios}.
195    * @param subtitles See {@link #subtitles}.
196    * @param closedCaptions See {@link #closedCaptions}.
197    * @param muxedAudioFormat See {@link #muxedAudioFormat}.
198    * @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
199    * @param hasIndependentSegments See {@link #hasIndependentSegments}.
200    * @param variableDefinitions See {@link #variableDefinitions}.
201    * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.
202    */
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)203   public HlsMasterPlaylist(
204       String baseUri,
205       List<String> tags,
206       List<Variant> variants,
207       List<Rendition> videos,
208       List<Rendition> audios,
209       List<Rendition> subtitles,
210       List<Rendition> closedCaptions,
211       @Nullable Format muxedAudioFormat,
212       @Nullable List<Format> muxedCaptionFormats,
213       boolean hasIndependentSegments,
214       Map<String, String> variableDefinitions,
215       List<DrmInitData> sessionKeyDrmInitData) {
216     super(baseUri, tags, hasIndependentSegments);
217     this.mediaPlaylistUrls =
218         Collections.unmodifiableList(
219             getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions));
220     this.variants = Collections.unmodifiableList(variants);
221     this.videos = Collections.unmodifiableList(videos);
222     this.audios = Collections.unmodifiableList(audios);
223     this.subtitles = Collections.unmodifiableList(subtitles);
224     this.closedCaptions = Collections.unmodifiableList(closedCaptions);
225     this.muxedAudioFormat = muxedAudioFormat;
226     this.muxedCaptionFormats = muxedCaptionFormats != null
227         ? Collections.unmodifiableList(muxedCaptionFormats) : null;
228     this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
229     this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
230   }
231 
232   @Override
copy(List<StreamKey> streamKeys)233   public HlsMasterPlaylist copy(List<StreamKey> streamKeys) {
234     return new HlsMasterPlaylist(
235         baseUri,
236         tags,
237         copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys),
238         // TODO: Allow stream keys to specify video renditions to be retained.
239         /* videos= */ Collections.emptyList(),
240         copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys),
241         copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys),
242         // TODO: Update to retain all closed captions.
243         /* closedCaptions= */ Collections.emptyList(),
244         muxedAudioFormat,
245         muxedCaptionFormats,
246         hasIndependentSegments,
247         variableDefinitions,
248         sessionKeyDrmInitData);
249   }
250 
251   /**
252    * Creates a playlist with a single variant.
253    *
254    * @param variantUrl The url of the single variant.
255    * @return A master playlist with a single variant for the provided url.
256    */
createSingleVariantMasterPlaylist(String variantUrl)257   public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) {
258     List<Variant> variant =
259         Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl)));
260     return new HlsMasterPlaylist(
261         /* baseUri= */ "",
262         /* tags= */ Collections.emptyList(),
263         variant,
264         /* videos= */ Collections.emptyList(),
265         /* audios= */ Collections.emptyList(),
266         /* subtitles= */ Collections.emptyList(),
267         /* closedCaptions= */ Collections.emptyList(),
268         /* muxedAudioFormat= */ null,
269         /* muxedCaptionFormats= */ null,
270         /* hasIndependentSegments= */ false,
271         /* variableDefinitions= */ Collections.emptyMap(),
272         /* sessionKeyDrmInitData= */ Collections.emptyList());
273   }
274 
getMediaPlaylistUrls( List<Variant> variants, List<Rendition> videos, List<Rendition> audios, List<Rendition> subtitles, List<Rendition> closedCaptions)275   private static List<Uri> getMediaPlaylistUrls(
276       List<Variant> variants,
277       List<Rendition> videos,
278       List<Rendition> audios,
279       List<Rendition> subtitles,
280       List<Rendition> closedCaptions) {
281     ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>();
282     for (int i = 0; i < variants.size(); i++) {
283       Uri uri = variants.get(i).url;
284       if (!mediaPlaylistUrls.contains(uri)) {
285         mediaPlaylistUrls.add(uri);
286       }
287     }
288     addMediaPlaylistUrls(videos, mediaPlaylistUrls);
289     addMediaPlaylistUrls(audios, mediaPlaylistUrls);
290     addMediaPlaylistUrls(subtitles, mediaPlaylistUrls);
291     addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls);
292     return mediaPlaylistUrls;
293   }
294 
addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out)295   private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) {
296     for (int i = 0; i < renditions.size(); i++) {
297       Uri uri = renditions.get(i).url;
298       if (uri != null && !out.contains(uri)) {
299         out.add(uri);
300       }
301     }
302   }
303 
copyStreams( List<T> streams, int groupIndex, List<StreamKey> streamKeys)304   private static <T> List<T> copyStreams(
305       List<T> streams, int groupIndex, List<StreamKey> streamKeys) {
306     List<T> copiedStreams = new ArrayList<>(streamKeys.size());
307     // TODO:
308     // 1. When variants with the same URL are not de-duplicated, duplicates must not increment
309     //    trackIndex so as to avoid breaking stream keys that have been persisted for offline. All
310     //    duplicates should be copied if the first variant is copied, or discarded otherwise.
311     // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to
312     //    avoid breaking stream keys that have been persisted for offline. All renitions with null
313     //    URLs should be copied. They may become unreachable if all variants that reference them are
314     //    removed, but this is OK.
315     // 3. Renditions with URLs matching copied variants should always themselves be copied, even if
316     //    the corresponding stream key is omitted. Else we're throwing away information for no gain.
317     for (int i = 0; i < streams.size(); i++) {
318       T stream = streams.get(i);
319       for (int j = 0; j < streamKeys.size(); j++) {
320         StreamKey streamKey = streamKeys.get(j);
321         if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) {
322           copiedStreams.add(stream);
323           break;
324         }
325       }
326     }
327     return copiedStreams;
328   }
329 
330 }
331