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