UNPKG

16.5 kBJavaScriptView Raw
1'use strict';
2
3exports.__esModule = true;
4
5var _video = require('video.js');
6
7var _video2 = _interopRequireDefault(_video);
8
9var _window = require('global/window');
10
11var _window2 = _interopRequireDefault(_window);
12
13var _captionStream = require('mux.js/lib/m2ts/caption-stream');
14
15var _metadataStream = require('mux.js/lib/m2ts/metadata-stream');
16
17var _metadataStream2 = _interopRequireDefault(_metadataStream);
18
19var _representations = require('./representations.js');
20
21var _flashlsAudioTracks = require('./flashlsAudioTracks.js');
22
23function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24
25function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
26
27/**
28 * Define properties on a cue for backwards compatability,
29 * but warn the user that the way that they are using it
30 * is depricated and will be removed at a later date.
31 *
32 * @param {Cue} cue the cue to add the properties on
33 * @private
34 */
35var deprecateOldCue = function deprecateOldCue(cue) {
36 Object.defineProperties(cue.frame, {
37 id: {
38 get: function get() {
39 _video2.default.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.');
40 return cue.value.key;
41 }
42 },
43 value: {
44 get: function get() {
45 _video2.default.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.');
46 return cue.value.data;
47 }
48 },
49 privateData: {
50 get: function get() {
51 _video2.default.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.');
52 return cue.value.data;
53 }
54 }
55 });
56};
57
58/**
59 * Remove text track from tech
60 */
61var removeExistingTrack = function removeExistingTrack(tech, kind, label) {
62 var tracks = tech.remoteTextTracks() || [];
63
64 for (var i = 0; i < tracks.length; i++) {
65 var track = tracks[i];
66
67 if (track.kind === kind && track.label === label) {
68 tech.removeRemoteTextTrack(track);
69 }
70 }
71};
72
73/**
74 * convert a string to a byte array of char codes
75 */
76var stringToByteArray = function stringToByteArray(data) {
77 var bytes = new Uint8Array(data.length);
78
79 for (var i = 0; i < data.length; i++) {
80 bytes[i] = data.charCodeAt(i);
81 }
82
83 return bytes;
84};
85
86// see CEA-708-D, section 4.4
87var parseCaptionPackets = function parseCaptionPackets(pts, userData) {
88 var results = [];
89 var i = void 0;
90 var count = void 0;
91 var offset = void 0;
92 var data = void 0;
93
94 // if this is just filler, return immediately
95 if (!(userData[0] & 0x40)) {
96 return results;
97 }
98
99 // parse out the cc_data_1 and cc_data_2 fields
100 count = userData[0] & 0x1f;
101 for (i = 0; i < count; i++) {
102 offset = i * 3;
103 data = {
104 type: userData[offset + 2] & 0x03,
105 pts: pts
106 };
107
108 // capture cc data when cc_valid is 1
109 if (userData[offset + 2] & 0x04) {
110 data.ccData = userData[offset + 3] << 8 | userData[offset + 4];
111 results.push(data);
112 }
113 }
114 return results;
115};
116
117/**
118 * Remove cues from a track on video.js.
119 *
120 * @param {Double} start start of where we should remove the cue
121 * @param {Double} end end of where the we should remove the cue
122 * @param {Object} track the text track to remove the cues from
123 * @private
124 */
125var removeCuesFromTrack = function removeCuesFromTrack(start, end, track) {
126 var i = void 0;
127 var cue = void 0;
128
129 if (!track) {
130 return;
131 }
132
133 if (!track.cues) {
134 return;
135 }
136
137 i = track.cues.length;
138
139 while (i--) {
140 cue = track.cues[i];
141
142 // Remove any overlapping cue
143 if (cue.startTime <= end && cue.endTime >= start) {
144 track.removeCue(cue);
145 }
146 }
147};
148
149/**
150 * Removes cues from the track that come before the start of the buffer
151 *
152 * @param {TimeRanges} buffered state of the buffer
153 * @param {TextTrack} track track to remove cues from
154 * @private
155 * @function removeOldCues
156 */
157var removeOldCues = function removeOldCues(buffered, track) {
158 if (buffered.length) {
159 removeCuesFromTrack(0, buffered.start(0), track);
160 }
161};
162
163/**
164 * Updates the selected index of the quality levels list and triggers a change event
165 *
166 * @param {QualityLevelList} qualityLevels
167 * The quality levels list
168 * @param {String} id
169 * The id of the new active quality level
170 * @function updateSelectedIndex
171 */
172var updateSelectedIndex = function updateSelectedIndex(qualityLevels, id) {
173 var selectedIndex = -1;
174
175 for (var i = 0; i < qualityLevels.length; i++) {
176 if (qualityLevels[i].id === id) {
177 selectedIndex = i;
178 break;
179 }
180 }
181
182 qualityLevels.selectedIndex_ = selectedIndex;
183 qualityLevels.trigger({
184 selectedIndex: selectedIndex,
185 type: 'change'
186 });
187};
188
189var FlashlsHandler = function () {
190 function FlashlsHandler(source, tech, options) {
191 var _this = this;
192
193 _classCallCheck(this, FlashlsHandler);
194
195 // tech.player() is deprecated but setup a reference to HLS for
196 // backwards-compatibility
197 if (tech.options_ && tech.options_.playerId) {
198 var _player = (0, _video2.default)(tech.options_.playerId);
199
200 if (!_player.hasOwnProperty('hls')) {
201 Object.defineProperty(_player, 'hls', {
202 get: function get() {
203 _video2.default.log.warn('player.hls is deprecated. Use player.tech_.hls instead.');
204 tech.trigger({ type: 'usage', name: 'flashls-player-access' });
205 return _this;
206 }
207 });
208 }
209 }
210
211 this.tech_ = tech;
212 this.metadataTrack_ = null;
213 this.inbandTextTrack_ = null;
214 this.metadataStream_ = new _metadataStream2.default();
215 this.cea608Stream_ = new _captionStream.Cea608Stream();
216 this.captionPackets_ = [];
217
218 // bind event listeners to this context
219 this.onLoadedmetadata_ = this.onLoadedmetadata_.bind(this);
220 this.onSeeked_ = this.onSeeked_.bind(this);
221 this.onId3updated_ = this.onId3updated_.bind(this);
222 this.onCaptionData_ = this.onCaptionData_.bind(this);
223 this.onMetadataStreamData_ = this.onMetadataStreamData_.bind(this);
224 this.onCea608StreamData_ = this.onCea608StreamData_.bind(this);
225 this.onLevelSwitch_ = this.onLevelSwitch_.bind(this);
226 this.onAudioTrackChanged = this.onAudioTrackChanged.bind(this);
227
228 this.tech_.on('loadedmetadata', this.onLoadedmetadata_);
229 this.tech_.on('seeked', this.onSeeked_);
230 this.tech_.on('id3updated', this.onId3updated_);
231 this.tech_.on('captiondata', this.onCaptionData_);
232
233 this.metadataStream_.on('data', this.onMetadataStreamData_);
234 this.cea608Stream_.on('data', this.onCea608StreamData_);
235 }
236
237 FlashlsHandler.prototype.src = function src(source) {
238 // do nothing if source is falsey
239 if (!source) {
240 return;
241 }
242 this.tech_.setSrc(source.src);
243 };
244
245 /**
246 * Calculates the interval of time that is currently seekable.
247 *
248 * @return {TimeRange}
249 * Returns the time ranges that can be seeked to.
250 */
251
252
253 FlashlsHandler.prototype.seekable = function seekable() {
254 var seekableStart = this.tech_.el_.vjs_getProperty('seekableStart');
255 var seekableEnd = this.tech_.el_.vjs_getProperty('seekableEnd');
256
257 if (seekableEnd === 0) {
258 return _video2.default.createTimeRange();
259 }
260
261 return _video2.default.createTimeRange(seekableStart, seekableEnd);
262 };
263
264 /**
265 * Event listener for the loadedmetadata event. This sets up the representations api
266 * and populates the quality levels list if it is available on the player
267 */
268
269
270 FlashlsHandler.prototype.onLoadedmetadata_ = function onLoadedmetadata_() {
271 var _this2 = this;
272
273 this.representations = (0, _representations.createRepresentations)(this.tech_);
274
275 var player = _video2.default.players[this.tech_.options_.playerId];
276
277 if (player && player.qualityLevels) {
278 this.qualityLevels_ = player.qualityLevels();
279 this.representations().forEach(function (representation) {
280 _this2.qualityLevels_.addQualityLevel(representation);
281 });
282
283 this.tech_.on('levelswitch', this.onLevelSwitch_);
284
285 // update initial selected index
286 updateSelectedIndex(this.qualityLevels_, this.tech_.el_.vjs_getProperty('level') + '');
287 }
288
289 (0, _flashlsAudioTracks.setupAudioTracks)(this.tech_);
290 this.tech_.audioTracks().on('change', this.onAudioTrackChanged);
291 };
292
293 /**
294 * Event listener for the change event. This will update the selected index of the
295 * audio track list with the new active track.
296 */
297
298
299 FlashlsHandler.prototype.onAudioTrackChanged = function onAudioTrackChanged() {
300 (0, _flashlsAudioTracks.updateAudioTrack)(this.tech_);
301 };
302
303 /**
304 * Event listener for the levelswitch event. This will update the selected index of the
305 * quality levels list with the new active level.
306 *
307 * @param {Object} event
308 * event object for the levelswitch event
309 * @param {Array} level
310 * The active level will be the first element of the array
311 */
312
313
314 FlashlsHandler.prototype.onLevelSwitch_ = function onLevelSwitch_(event, level) {
315 updateSelectedIndex(this.qualityLevels_, level[0].levelIndex + '');
316 };
317
318 /**
319 * Event listener for the seeked event. This will remove cues from the metadata track
320 * and inband text track after seeks
321 */
322
323
324 FlashlsHandler.prototype.onSeeked_ = function onSeeked_() {
325 removeCuesFromTrack(0, Infinity, this.metadataTrack_);
326
327 var buffered = this.tech_.buffered();
328
329 if (buffered.length === 1) {
330 removeCuesFromTrack(0, buffered.start(0), this.inbandTextTrack_);
331 removeCuesFromTrack(buffered.end(0), Infinity, this.inbandTextTrack_);
332 } else {
333 removeCuesFromTrack(0, Infinity, this.inbandTextTrack_);
334 }
335 };
336
337 /**
338 * Event listener for the id3updated event. This will store id3 tags recevied by flashls
339 *
340 * @param {Object} event
341 * event object for the levelswitch event
342 * @param {Array} data
343 * The id3 tag base64 encoded will be the first element of the array
344 */
345
346
347 FlashlsHandler.prototype.onId3updated_ = function onId3updated_(event, data) {
348 var id3tag = _window2.default.atob(data[0]);
349 var bytes = stringToByteArray(id3tag);
350 var chunk = {
351 type: 'timed-metadata',
352 dataAlignmentIndicator: true,
353 data: bytes
354 };
355
356 this.metadataStream_.push(chunk);
357 };
358
359 /**
360 * Event listener for the data event from the metadata stream. This will create cues
361 * for each frame in the metadata tag and add them to the metadata track
362 *
363 * @param {Object} tag
364 * The metadata tag
365 */
366
367
368 FlashlsHandler.prototype.onMetadataStreamData_ = function onMetadataStreamData_(tag) {
369 var _this3 = this;
370
371 if (!this.metadataTrack_) {
372 this.metadataTrack_ = this.tech_.addRemoteTextTrack({
373 kind: 'metadata',
374 label: 'Timed Metadata'
375 }, false).track;
376
377 this.metadataTrack_.inBandMetadataTrackDispatchType = '';
378 }
379
380 removeOldCues(this.tech_.buffered(), this.metadataTrack_);
381
382 var time = this.tech_.currentTime();
383
384 tag.frames.forEach(function (frame) {
385 var cue = new _window2.default.VTTCue(time, time + 0.1, frame.value || frame.url || frame.data || '');
386
387 cue.frame = frame;
388 cue.value = frame;
389
390 deprecateOldCue(cue);
391 _this3.metadataTrack_.addCue(cue);
392 });
393
394 if (this.metadataTrack_.cues && this.metadataTrack_.cues.length) {
395 var cues = this.metadataTrack_.cues;
396 var cuesArray = [];
397 var duration = this.tech_.duration();
398
399 if (isNaN(duration) || Math.abs(duration) === Infinity) {
400 duration = Number.MAX_VALUE;
401 }
402
403 for (var i = 0; i < cues.length; i++) {
404 cuesArray.push(cues[i]);
405 }
406
407 cuesArray.sort(function (a, b) {
408 return a.startTime - b.startTime;
409 });
410
411 for (var _i = 0; _i < cuesArray.length - 1; _i++) {
412 if (cuesArray[_i].endTime !== cuesArray[_i + 1].startTime) {
413 cuesArray[_i].endTime = cuesArray[_i + 1].startTime;
414 }
415 }
416 cuesArray[cuesArray.length - 1].endTime = duration;
417 }
418 };
419
420 /**
421 * Event listener for the captiondata event from FlasHLS. This will parse out the
422 * caption data and feed it to the CEA608 caption stream.
423 *
424 * @param {Object} event
425 * The captiondata event object
426 * @param {Array} data
427 * The caption packets array will be the first element of data.
428 */
429
430
431 FlashlsHandler.prototype.onCaptionData_ = function onCaptionData_(event, data) {
432 var _this4 = this;
433
434 var captions = data[0].map(function (d) {
435 return {
436 pts: d.pos * 90000,
437 bytes: stringToByteArray(_window2.default.atob(d.data))
438 };
439 });
440
441 captions.forEach(function (caption) {
442 _this4.captionPackets_ = _this4.captionPackets_.concat(parseCaptionPackets(caption.pts, caption.bytes));
443 });
444
445 if (this.captionPackets_.length) {
446 // In Chrome, the Array#sort function is not stable so add a
447 // presortIndex that we can use to ensure we get a stable-sort
448 this.captionPackets_.forEach(function (elem, idx) {
449 elem.presortIndex = idx;
450 });
451
452 // sort caption byte-pairs based on their PTS values
453 this.captionPackets_.sort(function (a, b) {
454 if (a.pts === b.pts) {
455 return a.presortIndex - b.presortIndex;
456 }
457 return a.pts - b.pts;
458 });
459
460 // Push each caption into Cea608Stream
461 this.captionPackets_.forEach(this.cea608Stream_.push, this.cea608Stream_);
462 this.captionPackets_.length = 0;
463 this.cea608Stream_.flush();
464 }
465 };
466
467 /**
468 * Event listener for the data event from the CEA608 caption stream. This will create
469 * cues for the captions received from the stream and add them to the inband text track
470 *
471 * @param {Object} caption
472 * The caption object
473 */
474
475
476 FlashlsHandler.prototype.onCea608StreamData_ = function onCea608StreamData_(caption) {
477 if (caption) {
478 if (!this.inbandTextTrack_) {
479 removeExistingTrack(this.tech_, 'captions', 'cc1');
480 this.inbandTextTrack_ = this.tech_.addRemoteTextTrack({
481 kind: 'captions',
482 label: 'cc1'
483 }, false).track;
484 }
485
486 removeOldCues(this.tech_.buffered(), this.inbandTextTrack_);
487
488 this.inbandTextTrack_.addCue(new _window2.default.VTTCue(caption.startPts / 90000, caption.endPts / 90000, caption.text));
489 }
490 };
491
492 FlashlsHandler.prototype.dispose = function dispose() {
493 this.tech_.off('loadedmetadata', this.onLoadedmetadata_);
494 this.tech_.off('seeked', this.onSeeked_);
495 this.tech_.off('id3updated', this.onId3updated_);
496 this.tech_.off('captiondata', this.onCaptionData_);
497 this.tech_.audioTracks().off('change', this.onAudioTrackChanged);
498
499 if (this.qualityLevels_) {
500 this.qualityLevels_.dispose();
501 this.tech_.off('levelswitch', this.onLevelSwitch_);
502 }
503 };
504
505 return FlashlsHandler;
506}();
507
508/*
509 * Registers the SWF as a handler for HLS video.
510 *
511 * @property {Tech~SourceObject} source
512 * The source object
513 *
514 * @property {Flash} tech
515 * The instance of the Flash tech
516 */
517
518
519var FlashlsSourceHandler = {};
520
521var mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
522
523/**
524 * Reports that Flash can play HLS.
525 *
526 * @param {string} type
527 * The mimetype to check
528 *
529 * @return {string}
530 * 'maybe', or '' (empty string)
531 */
532FlashlsSourceHandler.canPlayType = function (type) {
533 return mpegurlRE.test(type) ? 'maybe' : '';
534};
535
536/**
537 * Returns true if the source type indicates HLS content.
538 *
539 * @param {Tech~SourceObject} source
540 * The source object
541 *
542 * @param {Object} [options]
543 * Options to be passed to the tech.
544 *
545 * @return {string}
546 * 'maybe', or '' (empty string).
547 */
548FlashlsSourceHandler.canHandleSource = function (source, options) {
549 return FlashlsSourceHandler.canPlayType(source.type) === 'maybe';
550};
551
552/**
553 * Pass the source to the swf.
554 *
555 * @param {Tech~SourceObject} source
556 * The source object
557 *
558 * @param {Flash} tech
559 * The instance of the Flash tech
560 *
561 * @param {Object} [options]
562 * The options to pass to the source
563 */
564FlashlsSourceHandler.handleSource = function (source, tech, options) {
565 tech.hls = new FlashlsHandler(source, tech, options);
566
567 tech.hls.src(source);
568 return tech.hls;
569};
570
571// Register the source handler and make sure it takes precedence over
572// any other Flash source handlers for HLS
573_video2.default.getTech('Flash').registerSourceHandler(FlashlsSourceHandler, 0);
574
575// Include the version number.
576FlashlsSourceHandler.VERSION = '__VERSION__';
577
578exports.default = FlashlsSourceHandler;
\No newline at end of file