UNPKG

10.5 kBJavaScriptView Raw
1import videojs from 'video.js';
2import window from 'global/window';
3import { Parser as M3u8Parser } from 'm3u8-parser';
4import { resolveUrl } from './resolve-url';
5import { getLastParts, isAudioOnly } from './playlist.js';
6
7const { log } = videojs;
8
9export const createPlaylistID = (index, uri) => {
10 return `${index}-${uri}`;
11};
12
13/**
14 * Parses a given m3u8 playlist
15 *
16 * @param {Function} [onwarn]
17 * a function to call when the parser triggers a warning event.
18 * @param {Function} [oninfo]
19 * a function to call when the parser triggers an info event.
20 * @param {string} manifestString
21 * The downloaded manifest string
22 * @param {Object[]} [customTagParsers]
23 * An array of custom tag parsers for the m3u8-parser instance
24 * @param {Object[]} [customTagMappers]
25 * An array of custom tag mappers for the m3u8-parser instance
26 * @param {boolean} [experimentalLLHLS=false]
27 * Whether to keep ll-hls features in the manifest after parsing.
28 * @return {Object}
29 * The manifest object
30 */
31export const parseManifest = ({
32 onwarn,
33 oninfo,
34 manifestString,
35 customTagParsers = [],
36 customTagMappers = [],
37 experimentalLLHLS
38}) => {
39 const parser = new M3u8Parser();
40
41 if (onwarn) {
42 parser.on('warn', onwarn);
43 }
44 if (oninfo) {
45 parser.on('info', oninfo);
46 }
47
48 customTagParsers.forEach(customParser => parser.addParser(customParser));
49 customTagMappers.forEach(mapper => parser.addTagMapper(mapper));
50
51 parser.push(manifestString);
52 parser.end();
53
54 const manifest = parser.manifest;
55
56 // remove llhls features from the parsed manifest
57 // if we don't want llhls support.
58 if (!experimentalLLHLS) {
59 [
60 'preloadSegment',
61 'skip',
62 'serverControl',
63 'renditionReports',
64 'partInf',
65 'partTargetDuration'
66 ].forEach(function(k) {
67 if (manifest.hasOwnProperty(k)) {
68 delete manifest[k];
69 }
70 });
71
72 if (manifest.segments) {
73 manifest.segments.forEach(function(segment) {
74 ['parts', 'preloadHints'].forEach(function(k) {
75 if (segment.hasOwnProperty(k)) {
76 delete segment[k];
77 }
78 });
79 });
80 }
81 }
82 if (!manifest.targetDuration) {
83 let targetDuration = 10;
84
85 if (manifest.segments && manifest.segments.length) {
86 targetDuration = manifest
87 .segments.reduce((acc, s) => Math.max(acc, s.duration), 0);
88 }
89
90 if (onwarn) {
91 onwarn(`manifest has no targetDuration defaulting to ${targetDuration}`);
92 }
93 manifest.targetDuration = targetDuration;
94 }
95
96 const parts = getLastParts(manifest);
97
98 if (parts.length && !manifest.partTargetDuration) {
99 const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0);
100
101 if (onwarn) {
102 onwarn(`manifest has no partTargetDuration defaulting to ${partTargetDuration}`);
103 log.error('LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.');
104 }
105 manifest.partTargetDuration = partTargetDuration;
106 }
107
108 return manifest;
109};
110
111/**
112 * Loops through all supported media groups in master and calls the provided
113 * callback for each group
114 *
115 * @param {Object} master
116 * The parsed master manifest object
117 * @param {Function} callback
118 * Callback to call for each media group
119 */
120export const forEachMediaGroup = (master, callback) => {
121 if (!master.mediaGroups) {
122 return;
123 }
124 ['AUDIO', 'SUBTITLES'].forEach((mediaType) => {
125 if (!master.mediaGroups[mediaType]) {
126 return;
127 }
128 for (const groupKey in master.mediaGroups[mediaType]) {
129 for (const labelKey in master.mediaGroups[mediaType][groupKey]) {
130 const mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
131
132 callback(mediaProperties, mediaType, groupKey, labelKey);
133 }
134 }
135 });
136};
137
138/**
139 * Adds properties and attributes to the playlist to keep consistent functionality for
140 * playlists throughout VHS.
141 *
142 * @param {Object} config
143 * Arguments object
144 * @param {Object} config.playlist
145 * The media playlist
146 * @param {string} [config.uri]
147 * The uri to the media playlist (if media playlist is not from within a master
148 * playlist)
149 * @param {string} id
150 * ID to use for the playlist
151 */
152export const setupMediaPlaylist = ({ playlist, uri, id }) => {
153 playlist.id = id;
154 playlist.playlistErrors_ = 0;
155
156 if (uri) {
157 // For media playlists, m3u8-parser does not have access to a URI, as HLS media
158 // playlists do not contain their own source URI, but one is needed for consistency in
159 // VHS.
160 playlist.uri = uri;
161 }
162
163 // For HLS master playlists, even though certain attributes MUST be defined, the
164 // stream may still be played without them.
165 // For HLS media playlists, m3u8-parser does not attach an attributes object to the
166 // manifest.
167 //
168 // To avoid undefined reference errors through the project, and make the code easier
169 // to write/read, add an empty attributes object for these cases.
170 playlist.attributes = playlist.attributes || {};
171};
172
173/**
174 * Adds ID, resolvedUri, and attributes properties to each playlist of the master, where
175 * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to
176 * playlist references to the playlists array.
177 *
178 * @param {Object} master
179 * The master playlist
180 */
181export const setupMediaPlaylists = (master) => {
182 let i = master.playlists.length;
183
184 while (i--) {
185 const playlist = master.playlists[i];
186
187 setupMediaPlaylist({
188 playlist,
189 id: createPlaylistID(i, playlist.uri)
190 });
191 playlist.resolvedUri = resolveUrl(master.uri, playlist.uri);
192 master.playlists[playlist.id] = playlist;
193 // URI reference added for backwards compatibility
194 master.playlists[playlist.uri] = playlist;
195
196 // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute,
197 // the stream can be played without it. Although an attributes property may have been
198 // added to the playlist to prevent undefined references, issue a warning to fix the
199 // manifest.
200 if (!playlist.attributes.BANDWIDTH) {
201 log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
202 }
203 }
204};
205
206/**
207 * Adds resolvedUri properties to each media group.
208 *
209 * @param {Object} master
210 * The master playlist
211 */
212export const resolveMediaGroupUris = (master) => {
213 forEachMediaGroup(master, (properties) => {
214 if (properties.uri) {
215 properties.resolvedUri = resolveUrl(master.uri, properties.uri);
216 }
217 });
218};
219
220/**
221 * Creates a master playlist wrapper to insert a sole media playlist into.
222 *
223 * @param {Object} media
224 * Media playlist
225 * @param {string} uri
226 * The media URI
227 *
228 * @return {Object}
229 * Master playlist
230 */
231export const masterForMedia = (media, uri) => {
232 const id = createPlaylistID(0, uri);
233 const master = {
234 mediaGroups: {
235 'AUDIO': {},
236 'VIDEO': {},
237 'CLOSED-CAPTIONS': {},
238 'SUBTITLES': {}
239 },
240 uri: window.location.href,
241 resolvedUri: window.location.href,
242 playlists: [{
243 uri,
244 id,
245 resolvedUri: uri,
246 // m3u8-parser does not attach an attributes property to media playlists so make
247 // sure that the property is attached to avoid undefined reference errors
248 attributes: {}
249 }]
250 };
251
252 // set up ID reference
253 master.playlists[id] = master.playlists[0];
254 // URI reference added for backwards compatibility
255 master.playlists[uri] = master.playlists[0];
256
257 return master;
258};
259
260/**
261 * Does an in-place update of the master manifest to add updated playlist URI references
262 * as well as other properties needed by VHS that aren't included by the parser.
263 *
264 * @param {Object} master
265 * Master manifest object
266 * @param {string} uri
267 * The source URI
268 */
269export const addPropertiesToMaster = (master, uri) => {
270 master.uri = uri;
271
272 for (let i = 0; i < master.playlists.length; i++) {
273 if (!master.playlists[i].uri) {
274 // Set up phony URIs for the playlists since playlists are referenced by their URIs
275 // throughout VHS, but some formats (e.g., DASH) don't have external URIs
276 // TODO: consider adding dummy URIs in mpd-parser
277 const phonyUri = `placeholder-uri-${i}`;
278
279 master.playlists[i].uri = phonyUri;
280 }
281 }
282 const audioOnlyMaster = isAudioOnly(master);
283
284 forEachMediaGroup(master, (properties, mediaType, groupKey, labelKey) => {
285 const groupId = `placeholder-uri-${mediaType}-${groupKey}-${labelKey}`;
286
287 // add a playlist array under properties
288 if (!properties.playlists || !properties.playlists.length) {
289 // If the manifest is audio only and this media group does not have a uri, check
290 // if the media group is located in the main list of playlists. If it is, don't add
291 // placeholder properties as it shouldn't be considered an alternate audio track.
292 if (audioOnlyMaster && mediaType === 'AUDIO' && !properties.uri) {
293 for (let i = 0; i < master.playlists.length; i++) {
294 const p = master.playlists[i];
295
296 if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) {
297 return;
298 }
299 }
300 }
301
302 properties.playlists = [Object.assign({}, properties)];
303 }
304
305 properties.playlists.forEach(function(p, i) {
306 const id = createPlaylistID(i, groupId);
307
308 if (p.uri) {
309 p.resolvedUri = p.resolvedUri || resolveUrl(master.uri, p.uri);
310 } else {
311 // DEPRECATED, this has been added to prevent a breaking change.
312 // previously we only ever had a single media group playlist, so
313 // we mark the first playlist uri without prepending the index as we used to
314 // ideally we would do all of the playlists the same way.
315 p.uri = i === 0 ? groupId : id;
316
317 // don't resolve a placeholder uri to an absolute url, just use
318 // the placeholder again
319 p.resolvedUri = p.uri;
320 }
321
322 p.id = p.id || id;
323
324 // add an empty attributes object, all playlists are
325 // expected to have this.
326 p.attributes = p.attributes || {};
327
328 // setup ID and URI references (URI for backwards compatibility)
329 master.playlists[p.id] = p;
330 master.playlists[p.uri] = p;
331 });
332
333 });
334
335 setupMediaPlaylists(master);
336 resolveMediaGroupUris(master);
337};