@clappr/hlsjs-playback 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clappr/hlsjs-playback",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "HLS Playback based on hls.js",
5
5
  "main": "./dist/hlsjs-playback.js",
6
6
  "module": "./dist/hlsjs-playback.esm.js",
@@ -15,7 +15,8 @@
15
15
  "test:debug": "node --inspect node_modules/.bin/jest ./src --runInBand",
16
16
  "test:watch": "jest ./src --watch",
17
17
  "lint": "eslint *.js ./src",
18
- "lint:fix": "yarn lint -- --fix"
18
+ "lint:fix": "yarn lint -- --fix",
19
+ "prepublishOnly": "npm run release"
19
20
  },
20
21
  "files": [
21
22
  "/dist",
package/src/hls.js CHANGED
@@ -5,9 +5,9 @@
5
5
  import { Events, HTML5Video, Log, Playback, PlayerError, Utils } from '@clappr/core'
6
6
  import HLSJS from 'hls.js'
7
7
 
8
- const { now, assign, listContainsIgnoreCase } = Utils
9
-
8
+ const { now, listContainsIgnoreCase } = Utils
10
9
  const AUTO = -1
10
+ const DEFAULT_RECOVER_ATTEMPTS = 16
11
11
 
12
12
  Events.register('PLAYBACK_FRAGMENT_CHANGED')
13
13
  Events.register('PLAYBACK_FRAGMENT_PARSING_METADATA')
@@ -40,6 +40,14 @@ export default class HlsjsPlayback extends HTML5Video {
40
40
  this._hls.currentLevel = this._currentLevel
41
41
  }
42
42
 
43
+ get latency() {
44
+ return this._hls.latency
45
+ }
46
+
47
+ get currentProgramDateTime() {
48
+ return this._hls.playingDate
49
+ }
50
+
43
51
  get _startTime() {
44
52
  if (this._playbackType === Playback.LIVE && this._playlistType !== 'EVENT')
45
53
  return this._extrapolatedStartTime
@@ -68,12 +76,10 @@ export default class HlsjsPlayback extends HTML5Video {
68
76
  // extrapolated to increase in real time (instead of jumping as segments are added)
69
77
  get _extrapolatedEndTime() {
70
78
  let actualEndTime = this._playableRegionStartTime + this._playableRegionDuration
71
- if (!this._localEndTimeCorrelation)
72
- return actualEndTime
73
-
74
- let corr = this._localEndTimeCorrelation
75
- let timePassed = this._now - corr.local
76
- let extrapolatedEndTime = (corr.remote + timePassed) / 1000
79
+ if (!this._localEndTimeCorrelation) return actualEndTime
80
+ const correlation = this._localEndTimeCorrelation
81
+ const timePassed = this._now - correlation.local
82
+ const extrapolatedEndTime = (correlation.remote + timePassed) / 1000
77
83
  return Math.max(actualEndTime - this._extrapolatedWindowDuration, Math.min(extrapolatedEndTime, actualEndTime))
78
84
  }
79
85
 
@@ -117,15 +123,21 @@ export default class HlsjsPlayback extends HTML5Video {
117
123
  return this.options.hlsPlayback && this.options.hlsPlayback.customListeners || []
118
124
  }
119
125
 
126
+ get sourceMedia() {
127
+ return this.options.src
128
+ }
129
+
120
130
  static get HLSJS() {
121
131
  return HLSJS
122
132
  }
123
133
 
124
134
  constructor(...args) {
125
135
  super(...args)
126
- // backwards compatibility (TODO: remove on 0.3.0)
127
- this.options.playback = { ...this.options, ...this.options.playback }
128
136
  this.options.hlsPlayback = { ...this.defaultOptions, ...this.options.hlsPlayback }
137
+ this._setInitialState()
138
+ }
139
+
140
+ _setInitialState() {
129
141
  this._minDvrSize = typeof (this.options.hlsMinimumDvrSize) === 'undefined' ? 60 : this.options.hlsMinimumDvrSize
130
142
  // The size of the start time extrapolation window measured as a multiple of segments.
131
143
  // Should be 2 or higher, or 0 to disable. Should only need to be increased above 2 if more than one segment is
@@ -163,15 +175,38 @@ export default class HlsjsPlayback extends HTML5Video {
163
175
  this._segmentTargetDuration = null
164
176
  // #EXT-X-PLAYLIST-TYPE
165
177
  this._playlistType = null
166
- this._recoverAttemptsRemaining = this.options.hlsRecoverAttempts || 16
178
+ this._recoverAttemptsRemaining = this.options.hlsRecoverAttempts || DEFAULT_RECOVER_ATTEMPTS
167
179
  }
168
180
 
169
181
  _setup() {
182
+ this._destroyHLSInstance()
183
+ this._createHLSInstance()
184
+ this._listenHLSEvents()
185
+ this._attachHLSMedia()
186
+ }
187
+
188
+ _destroyHLSInstance() {
189
+ if (!this._hls) return
170
190
  this._manifestParsed = false
171
191
  this._ccIsSetup = false
172
192
  this._ccTracksUpdated = false
173
- this._hls && this._hls.destroy()
174
- this._hls = new HLSJS(assign({}, this.options.playback.hlsjsConfig))
193
+ this._setInitialState()
194
+ this._hls.destroy()
195
+ this._hls = null
196
+ }
197
+
198
+ _createHLSInstance() {
199
+ const config = { ...this.options.playback.hlsjsConfig }
200
+ this._hls = new HLSJS(config)
201
+ }
202
+
203
+ _attachHLSMedia() {
204
+ if (!this._hls) return
205
+ this._hls.attachMedia(this.el)
206
+ }
207
+
208
+ _listenHLSEvents() {
209
+ if (!this._hls) return
175
210
  this._hls.once(HLSJS.Events.MEDIA_ATTACHED, () => { this.options.hlsPlayback.preload && this._hls.loadSource(this.options.src) })
176
211
  this._hls.on(HLSJS.Events.MANIFEST_PARSED, () => this._manifestParsed = true)
177
212
  this._hls.on(HLSJS.Events.LEVEL_LOADED, (evt, data) => this._updatePlaybackType(evt, data))
@@ -183,10 +218,7 @@ export default class HlsjsPlayback extends HTML5Video {
183
218
  this._hls.on(HLSJS.Events.ERROR, (evt, data) => this._onHLSJSError(evt, data))
184
219
  this._hls.on(HLSJS.Events.SUBTITLE_TRACK_LOADED, (evt, data) => this._onSubtitleLoaded(evt, data))
185
220
  this._hls.on(HLSJS.Events.SUBTITLE_TRACKS_UPDATED, () => this._ccTracksUpdated = true)
186
-
187
221
  this.bindCustomListeners()
188
-
189
- this._hls.attachMedia(this.el)
190
222
  }
191
223
 
192
224
  bindCustomListeners() {
@@ -238,13 +270,11 @@ export default class HlsjsPlayback extends HTML5Video {
238
270
  }
239
271
 
240
272
  // override
241
- _setupSrc(srcUrl) { // eslint-disable-line no-unused-vars
242
- // this playback manages the src on the video element itself
243
- }
273
+ // this playback manages the src on the video element itself
274
+ _setupSrc(srcUrl) {} // eslint-disable-line no-unused-vars
244
275
 
245
276
  _startTimeUpdateTimer() {
246
277
  if (this._timeUpdateTimer) return
247
-
248
278
  this._timeUpdateTimer = setInterval(() => {
249
279
  this._onDurationChange()
250
280
  this._onTimeUpdate()
@@ -253,7 +283,6 @@ export default class HlsjsPlayback extends HTML5Video {
253
283
 
254
284
  _stopTimeUpdateTimer() {
255
285
  if (!this._timeUpdateTimer) return
256
-
257
286
  clearInterval(this._timeUpdateTimer)
258
287
  this._timeUpdateTimer = null
259
288
  }
@@ -261,6 +290,7 @@ export default class HlsjsPlayback extends HTML5Video {
261
290
  getProgramDateTime() {
262
291
  return this._programDateTime
263
292
  }
293
+
264
294
  // the duration on the video element itself should not be used
265
295
  // as this does not necesarily represent the duration of the stream
266
296
  // https://github.com/clappr/clappr/issues/668#issuecomment-157036678
@@ -283,10 +313,9 @@ export default class HlsjsPlayback extends HTML5Video {
283
313
  }
284
314
 
285
315
  seekPercentage(percentage) {
286
- let seekTo = this._duration
287
- if (percentage > 0)
288
- seekTo = this._duration * (percentage / 100)
289
-
316
+ const seekTo = (percentage > 0)
317
+ ? this._duration * (percentage / 100)
318
+ : this._duration
290
319
  this.seek(seekTo)
291
320
  }
292
321
 
@@ -402,30 +431,24 @@ export default class HlsjsPlayback extends HTML5Video {
402
431
  }
403
432
 
404
433
  _onTimeUpdate() {
405
- let update = { current: this.getCurrentTime(), total: this.getDuration(), firstFragDateTime: this.getProgramDateTime() }
406
- let isSame = this._lastTimeUpdate && (
434
+ const update = { current: this.getCurrentTime(), total: this.getDuration(), firstFragDateTime: this.getProgramDateTime() }
435
+ const isSame = this._lastTimeUpdate && (
407
436
  update.current === this._lastTimeUpdate.current &&
408
- update.total === this._lastTimeUpdate.total)
409
- if (isSame)
410
- return
411
-
437
+ update.total === this._lastTimeUpdate.total)
438
+ if (isSame) return
412
439
  this._lastTimeUpdate = update
413
440
  this.trigger(Events.PLAYBACK_TIMEUPDATE, update, this.name)
414
441
  }
415
442
 
416
443
  _onDurationChange() {
417
- let duration = this.getDuration()
418
- if (this._lastDuration === duration)
419
- return
420
-
444
+ const duration = this.getDuration()
445
+ if (this._lastDuration === duration) return
421
446
  this._lastDuration = duration
422
447
  super._onDurationChange()
423
448
  }
424
449
 
425
450
  _onProgress() {
426
- if (!this.el.buffered.length)
427
- return
428
-
451
+ if (!this.el.buffered.length) return
429
452
  let buffered = []
430
453
  let bufferedPos = 0
431
454
  for (let i = 0; i < this.el.buffered.length; i++) {
@@ -446,10 +469,15 @@ export default class HlsjsPlayback extends HTML5Video {
446
469
  this.trigger(Events.PLAYBACK_PROGRESS, progress, buffered)
447
470
  }
448
471
 
472
+ load(url) {
473
+ this._stopTimeUpdateTimer()
474
+ this.options.src = url
475
+ this._setup()
476
+ }
477
+
449
478
  play() {
450
479
  !this._hls && this._setup()
451
480
  !this._manifestParsed && !this.options.hlsPlayback.preload && this._hls.loadSource(this.options.src)
452
-
453
481
  super.play()
454
482
  this._startTimeUpdateTimer()
455
483
  }
@@ -462,26 +490,19 @@ export default class HlsjsPlayback extends HTML5Video {
462
490
 
463
491
  stop() {
464
492
  this._stopTimeUpdateTimer()
465
- if (this._hls) {
466
- super.stop()
467
- this._hls.destroy()
468
- delete this._hls
469
- }
493
+ if (this._hls) super.stop()
494
+ this._destroyHLSInstance()
470
495
  }
471
496
 
472
497
  destroy() {
473
498
  this._stopTimeUpdateTimer()
474
- if (this._hls) {
475
- this._hls.destroy()
476
- delete this._hls
477
- }
499
+ this._destroyHLSInstance()
478
500
  super.destroy()
479
501
  }
480
502
 
481
503
  _updatePlaybackType(evt, data) {
482
504
  this._playbackType = data.details.live ? Playback.LIVE : Playback.VOD
483
505
  this._onLevelUpdated(evt, data)
484
-
485
506
  // Live stream subtitle tracks detection hack (may not immediately available)
486
507
  if (this._ccTracksUpdated && this._playbackType === Playback.LIVE && this.hasClosedCaptionsTracks)
487
508
  this._onSubtitleLoaded()
@@ -498,22 +519,15 @@ export default class HlsjsPlayback extends HTML5Video {
498
519
  _onLevelUpdated(evt, data) {
499
520
  this._segmentTargetDuration = data.details.targetduration
500
521
  this._playlistType = data.details.type || null
501
-
502
522
  let startTimeChanged = false
503
523
  let durationChanged = false
504
524
  let fragments = data.details.fragments
505
525
  let previousPlayableRegionStartTime = this._playableRegionStartTime
506
526
  let previousPlayableRegionDuration = this._playableRegionDuration
507
-
508
- if (fragments.length === 0)
509
- return
510
-
511
-
527
+ if (fragments.length === 0) return
512
528
  // #EXT-X-PROGRAM-DATE-TIME
513
529
  if (fragments[0].rawProgramDateTime)
514
530
  this._programDateTime = fragments[0].rawProgramDateTime
515
-
516
-
517
531
  if (this._playableRegionStartTime !== fragments[0].start) {
518
532
  startTimeChanged = true
519
533
  this._playableRegionStartTime = fragments[0].start
@@ -552,7 +566,7 @@ export default class HlsjsPlayback extends HTML5Video {
552
566
  }
553
567
  }
554
568
  }
555
-
569
+
556
570
  let newDuration = data.details.totalduration
557
571
  // if it's a live stream then shorten the duration to remove access
558
572
  // to the area after hlsjs's live sync point
@@ -568,12 +582,10 @@ export default class HlsjsPlayback extends HTML5Video {
568
582
  } else { this._durationExcludesAfterLiveSyncPoint = false }
569
583
 
570
584
  }
571
-
572
585
  if (newDuration !== this._playableRegionDuration) {
573
586
  durationChanged = true
574
587
  this._playableRegionDuration = newDuration
575
588
  }
576
-
577
589
  // Note the end time is not the playableRegionDuration
578
590
  // The end time will always increase even if content is removed from the beginning
579
591
  let endTime = fragments[0].start + newDuration
@@ -642,9 +654,7 @@ export default class HlsjsPlayback extends HTML5Video {
642
654
  }
643
655
 
644
656
  _onLevelSwitch(evt, data) {
645
- if (!this.levels.length)
646
- this._fillLevels()
647
-
657
+ if (!this.levels.length) this._fillLevels()
648
658
  this.trigger(Events.PLAYBACK_LEVEL_SWITCH_END)
649
659
  this.trigger(Events.PLAYBACK_LEVEL_SWITCH, data)
650
660
  let currentLevel = this._hls.levels[data.level]
@@ -682,6 +692,5 @@ export default class HlsjsPlayback extends HTML5Video {
682
692
  HlsjsPlayback.canPlay = function(resource, mimeType) {
683
693
  const resourceParts = resource.split('?')[0].match(/.*\.(.*)$/) || []
684
694
  const isHls = ((resourceParts.length > 1 && resourceParts[1].toLowerCase() === 'm3u8') || listContainsIgnoreCase(mimeType, ['application/vnd.apple.mpegurl', 'application/x-mpegURL']))
685
-
686
695
  return !!(HLSJS.isSupported() && isHls)
687
696
  }
package/src/hls.test.js CHANGED
@@ -262,6 +262,18 @@ describe('HlsjsPlayback', () => {
262
262
  })
263
263
  })
264
264
 
265
+ describe('load method', () => {
266
+ test('loads a new source when called', () => {
267
+ const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8', hlsPlayback: { preload: true } })
268
+ const url = 'http://clappr.io/foo2.m3u8'
269
+ playback.load(url)
270
+ jest.spyOn(playback._hls, 'loadSource')
271
+ playback._hls.trigger(HLSJS.Events.MEDIA_ATTACHED, { media: playback.el })
272
+ expect(playback.options.src).toBe(url)
273
+ expect(playback._hls.loadSource).toHaveBeenCalledWith(url)
274
+ })
275
+ })
276
+
265
277
  describe('bindCustomListeners method', () => {
266
278
  test('creates listeners for each item configured on customListeners array', () => {
267
279
  const cb = jest.fn()