@cycjimmy/jsmpeg-player 6.1.7 → 6.1.8

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.
@@ -233,10 +233,10 @@ var VideoElement = /*#__PURE__*/function () {
233
233
  this.els.playButton.classList.add(_style.hidden);
234
234
  }
235
235
 
236
- // Set up the unlock audio button for iOS devices. iOS only allows us to
237
- // play audio after a user action has initiated playing. For autoplay or
238
- // streaming players we set up a muted speaker icon as the button. For all
239
- // others, we can simply use the play button.
236
+ // Set up the unlock audio button for the browsers that do not allow audio
237
+ // before a user action (iOS, and Chrome/Safari under their autoplay
238
+ // policy). For autoplay or streaming players we set up a muted speaker
239
+ // icon as the button. For all others, we can simply use the play button.
240
240
  if (this.player.audioOut && !this.player.audioOut.unlocked) {
241
241
  var unlockAudioElement = this.els.wrapper;
242
242
  if (this.options.autoplay || this.player.options.streaming) {
@@ -249,6 +249,12 @@ var VideoElement = /*#__PURE__*/function () {
249
249
  this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement);
250
250
  unlockAudioElement.addEventListener('touchstart', this.unlockAudioBound, false);
251
251
  unlockAudioElement.addEventListener('click', this.unlockAudioBound, true);
252
+
253
+ // Any gesture on the page may unlock the audio, so the icon has to go
254
+ // away even when it was not the element that was clicked.
255
+ this.player.audioOut.onUnlock = function () {
256
+ return _this.onAudioUnlocked(unlockAudioElement);
257
+ };
252
258
  }
253
259
  };
254
260
  _proto.initPlayButton = function initPlayButton() {
@@ -275,6 +281,15 @@ var VideoElement = /*#__PURE__*/function () {
275
281
  });
276
282
  });
277
283
  };
284
+ _proto.onAudioUnlocked = function onAudioUnlocked(element) {
285
+ if (this.els.unmuteButton) {
286
+ this.els.unmuteButton.classList.add(_style.hidden);
287
+ }
288
+ if (element && this.unlockAudioBound) {
289
+ element.removeEventListener('touchstart', this.unlockAudioBound);
290
+ element.removeEventListener('click', this.unlockAudioBound);
291
+ }
292
+ };
278
293
  _proto.onUnlockAudio = function onUnlockAudio(element, ev) {
279
294
  var _this3 = this;
280
295
  if (this.els.unmuteButton) {
@@ -282,11 +297,7 @@ var VideoElement = /*#__PURE__*/function () {
282
297
  ev.stopPropagation();
283
298
  }
284
299
  this.player.audioOut.unlock(function () {
285
- if (_this3.els.unmuteButton) {
286
- _this3.els.unmuteButton.classList.add(_style.hidden);
287
- }
288
- element.removeEventListener('touchstart', _this3.unlockAudioBound);
289
- element.removeEventListener('click', _this3.unlockAudioBound);
300
+ return _this3.onAudioUnlocked(element);
290
301
  });
291
302
  };
292
303
  _proto.onClick = function onClick() {
@@ -4047,11 +4058,15 @@ var MP2WASM = /*#__PURE__*/function (_BaseDecoder) {
4047
4058
  _this.bufferSize = options.audioBufferSize || 128 * 1024;
4048
4059
  _this.bufferMode = options.streaming ? BitBuffer.MODE.EVICT : BitBuffer.MODE.EXPAND;
4049
4060
  _this.sampleRate = 0;
4061
+ _this.destroyed = false;
4050
4062
  return _this;
4051
4063
  }
4052
4064
  _inheritsLoose(MP2WASM, _BaseDecoder);
4053
4065
  var _proto = MP2WASM.prototype;
4054
4066
  _proto.initializeWasmDecoder = function initializeWasmDecoder() {
4067
+ if (this.destroyed) {
4068
+ return;
4069
+ }
4055
4070
  if (!this.module.instance) {
4056
4071
  console.warn('JSMpeg: WASM module not compiled yet');
4057
4072
  return;
@@ -4061,10 +4076,23 @@ var MP2WASM = /*#__PURE__*/function (_BaseDecoder) {
4061
4076
  this.decoder = this.functions._mp2_decoder_create(this.bufferSize, this.bufferMode);
4062
4077
  };
4063
4078
  _proto.destroy = function destroy() {
4079
+ // A source can still deliver one last chunk inside the very callback that
4080
+ // destroys the player, so nothing may reach the module from now on.
4081
+ this.destroyed = true;
4064
4082
  if (!this.decoder) {
4065
4083
  return;
4066
4084
  }
4067
4085
  this.functions._mp2_decoder_destroy(this.decoder);
4086
+ // The pointer refers to a freed block now, and the module may hand the same
4087
+ // address to the decoder of the next player: it must never be used again.
4088
+ this.decoder = null;
4089
+ this.canPlay = false;
4090
+ };
4091
+ _proto.write = function write(pts, buffers) {
4092
+ if (this.destroyed) {
4093
+ return;
4094
+ }
4095
+ _BaseDecoder.prototype.write.call(this, pts, buffers);
4068
4096
  };
4069
4097
  _proto.bufferGetIndex = function bufferGetIndex() {
4070
4098
  if (!this.decoder) {
@@ -4080,9 +4108,16 @@ var MP2WASM = /*#__PURE__*/function (_BaseDecoder) {
4080
4108
  this.functions._mp2_decoder_set_index(this.decoder, index);
4081
4109
  };
4082
4110
  _proto.bufferWrite = function bufferWrite(buffers) {
4111
+ if (this.destroyed) {
4112
+ return 0;
4113
+ }
4083
4114
  if (!this.decoder) {
4084
4115
  this.initializeWasmDecoder();
4085
4116
  }
4117
+ if (!this.decoder) {
4118
+ // The module is not compiled yet; nothing can be written.
4119
+ return 0;
4120
+ }
4086
4121
  var totalLength = 0;
4087
4122
  for (var i = 0; i < buffers.length; i++) {
4088
4123
  totalLength += buffers[i].length;
@@ -4439,6 +4474,9 @@ var CanvasRenderer = /*#__PURE__*/function () {
4439
4474
  return CanvasRenderer;
4440
4475
  }();
4441
4476
 
4477
+ // Chrome grants the user activation on pointerup/touchend, not on the
4478
+ // touchstart/pointerdown that precedes them, so the touch events are paired.
4479
+ var UNLOCK_EVENTS = ['pointerdown', 'pointerup', 'keydown', 'touchend'];
4442
4480
  var WebAudioOut = /*#__PURE__*/function () {
4443
4481
  function WebAudioOut() {
4444
4482
  this.context = WebAudioOut.CachedContext = WebAudioOut.CachedContext || new (window.AudioContext || window.webkitAudioContext)();
@@ -4454,13 +4492,27 @@ var WebAudioOut = /*#__PURE__*/function () {
4454
4492
  this.wallclockStartTime = 0;
4455
4493
  this.volume = 1;
4456
4494
  this.enabled = true;
4457
- this.unlocked = !WebAudioOut.NeedsUnlocking();
4495
+ this.sources = [];
4496
+ this.skippedAudio = false;
4497
+ this.destroyed = false;
4498
+ this.gestureListener = null;
4499
+ this.unlockCallback = null;
4500
+ this.onUnlock = null;
4501
+ this.unlocked = !WebAudioOut.NeedsUnlocking() && WebAudioOut.IsRunning(this.context);
4458
4502
  Object.defineProperty(this, 'enqueuedTime', {
4459
4503
  get: this.getEnqueuedTime
4460
4504
  });
4505
+ if (!this.unlocked) {
4506
+ this.listenForGesture();
4507
+ }
4461
4508
  }
4462
4509
  var _proto = WebAudioOut.prototype;
4463
4510
  _proto.destroy = function destroy() {
4511
+ this.destroyed = true;
4512
+ this.unlockCallback = null;
4513
+ this.onUnlock = null;
4514
+ this.removeGestureListener();
4515
+ this.stopSources();
4464
4516
  this.gain.disconnect();
4465
4517
  this.context._connections--;
4466
4518
  if (this.context._connections === 0) {
@@ -4469,20 +4521,46 @@ var WebAudioOut = /*#__PURE__*/function () {
4469
4521
  }
4470
4522
  };
4471
4523
  _proto.play = function play(sampleRate, left, right) {
4472
- if (!this.enabled) {
4524
+ var _this = this;
4525
+ if (!this.enabled || this.destroyed) {
4473
4526
  return;
4474
4527
  }
4475
4528
 
4476
- // If the context is not unlocked yet, we simply advance the start time
4477
- // to "fake" actually playing audio. This will keep the video in sync.
4478
- if (!this.unlocked) {
4529
+ // Browsers that need no explicit unlock resume the context by themselves
4530
+ // once the page has been interacted with, so the unlock does not necessarily
4531
+ // go through unlock(). iOS keeps requiring the gesture.
4532
+ if (!this.unlocked && !WebAudioOut.NeedsUnlocking() && WebAudioOut.IsRunning(this.context)) {
4533
+ this.onUnlocked();
4534
+ }
4535
+
4536
+ // A context that is not running, or that has not been unlocked yet, cannot
4537
+ // play anything: the browser autoplay policy keeps it suspended until the
4538
+ // page has received a user gesture, and its currentTime does not advance
4539
+ // meanwhile. Scheduling buffers against it would queue all the audio
4540
+ // decoded in the meantime and play it back from the very beginning once
4541
+ // the context resumes. We simply advance the start time to "fake"
4542
+ // actually playing audio instead. This will keep the video in sync.
4543
+ if (!this.isPlayable()) {
4544
+ this.listenForGesture();
4479
4545
  var ts = Now();
4480
4546
  if (this.wallclockStartTime < ts) {
4481
4547
  this.wallclockStartTime = ts;
4482
4548
  }
4483
4549
  this.wallclockStartTime += left.length / sampleRate;
4550
+ this.skippedAudio = true;
4484
4551
  return;
4485
4552
  }
4553
+
4554
+ // The audio is available again: whatever was queued while it could not be
4555
+ // played is dropped and the time base is rebuilt, so the audio joins the
4556
+ // picture at the current point in time instead of staying behind it. The
4557
+ // audio decoded meanwhile is lost once, which is what keeps it in sync
4558
+ // with the picture.
4559
+ if (this.skippedAudio) {
4560
+ this.skippedAudio = false;
4561
+ this.stopSources();
4562
+ this.resetEnqueuedTime();
4563
+ }
4486
4564
  this.gain.gain.value = this.volume;
4487
4565
  var buffer = this.context.createBuffer(2, left.length, sampleRate);
4488
4566
  buffer.getChannelData(0).set(left);
@@ -4490,6 +4568,13 @@ var WebAudioOut = /*#__PURE__*/function () {
4490
4568
  var source = this.context.createBufferSource();
4491
4569
  source.buffer = buffer;
4492
4570
  source.connect(this.destination);
4571
+ source.onended = function () {
4572
+ var index = _this.sources.indexOf(source);
4573
+ if (index !== -1) {
4574
+ _this.sources.splice(index, 1);
4575
+ }
4576
+ };
4577
+ this.sources.push(source);
4493
4578
  var now = this.context.currentTime;
4494
4579
  var duration = buffer.duration;
4495
4580
  if (this.startTime < now) {
@@ -4501,11 +4586,13 @@ var WebAudioOut = /*#__PURE__*/function () {
4501
4586
  this.wallclockStartTime += duration;
4502
4587
  };
4503
4588
  _proto.stop = function stop() {
4504
- // Meh; there seems to be no simple way to get a list of currently
4505
- // active source nodes from the Audio Context, and maintaining this
4506
- // list ourselfs would be a pain, so we just set the gain to 0
4507
- // to cut off all enqueued audio instantly.
4589
+ // Cut off everything that is still queued: the gain alone would only mute
4590
+ // the buffers that are already scheduled, and they would become audible
4591
+ // again as soon as play() restores it. The time base is rebuilt so the
4592
+ // next buffer joins the current context time instead of the old queue.
4508
4593
  this.gain.gain.value = 0;
4594
+ this.stopSources();
4595
+ this.resetEnqueuedTime();
4509
4596
  };
4510
4597
  _proto.getEnqueuedTime = function getEnqueuedTime() {
4511
4598
  // The AudioContext.currentTime is only updated every so often, so if we
@@ -4516,16 +4603,65 @@ var WebAudioOut = /*#__PURE__*/function () {
4516
4603
  this.startTime = this.context.currentTime;
4517
4604
  this.wallclockStartTime = Now();
4518
4605
  };
4606
+ _proto.isPlayable = function isPlayable() {
4607
+ return this.unlocked && WebAudioOut.IsRunning(this.context);
4608
+ };
4609
+ _proto.stopSources = function stopSources() {
4610
+ var sources = this.sources;
4611
+ this.sources = [];
4612
+ sources.forEach(function (source) {
4613
+ try {
4614
+ source.stop();
4615
+ } catch (e) {
4616
+ // The source has already ended and cannot be stopped any more.
4617
+ }
4618
+ try {
4619
+ source.disconnect();
4620
+ } catch (e) {
4621
+ // It was never connected to anything.
4622
+ }
4623
+ });
4624
+ };
4625
+ _proto.listenForGesture = function listenForGesture() {
4626
+ var _this2 = this;
4627
+ if (this.gestureListener || this.isPlayable() || this.destroyed || WebAudioOut.NeedsUnlocking() || typeof document === 'undefined') {
4628
+ return;
4629
+ }
4630
+
4631
+ // Chrome and desktop Safari keep the context suspended until it is resumed
4632
+ // inside a user gesture, and they do not resume it by themselves when the
4633
+ // page is tapped somewhere else. iOS keeps requiring the unlock icon.
4634
+ this.gestureListener = function () {
4635
+ return _this2.unlock();
4636
+ };
4637
+ UNLOCK_EVENTS.forEach(function (type) {
4638
+ document.addEventListener(type, _this2.gestureListener, true);
4639
+ });
4640
+ };
4641
+ _proto.removeGestureListener = function removeGestureListener() {
4642
+ var _this3 = this;
4643
+ if (!this.gestureListener) {
4644
+ return;
4645
+ }
4646
+ UNLOCK_EVENTS.forEach(function (type) {
4647
+ document.removeEventListener(type, _this3.gestureListener, true);
4648
+ });
4649
+ this.gestureListener = null;
4650
+ };
4519
4651
  _proto.unlock = function unlock(callback) {
4520
- if (this.unlocked) {
4652
+ var _this4 = this;
4653
+ // Nothing to do while the audio plays; a context that was suspended again
4654
+ // still has to be resumed, and a destroyed one has nothing left to unlock.
4655
+ if (this.isPlayable() || this.destroyed || this.context.state === 'closed') {
4521
4656
  if (callback) {
4522
4657
  callback();
4523
4658
  }
4524
4659
  return;
4525
4660
  }
4526
- this.unlockCallback = callback;
4661
+ this.unlockCallback = callback || this.unlockCallback;
4527
4662
 
4528
- // Create empty buffer and play it
4663
+ // Create empty buffer and play it, so the browsers that only start their
4664
+ // audio pipeline from within a user gesture (iOS Safari) really unlock.
4529
4665
  var buffer = this.context.createBuffer(1, 1, 22050);
4530
4666
  var source = this.context.createBufferSource();
4531
4667
  source.buffer = buffer;
@@ -4537,23 +4673,73 @@ var WebAudioOut = /*#__PURE__*/function () {
4537
4673
  } else {
4538
4674
  source.noteOn(0);
4539
4675
  }
4676
+
4677
+ // Chrome and desktop Safari keep the context suspended until it is resumed
4678
+ // explicitly, and resume() only takes effect inside a user gesture - which
4679
+ // is exactly where unlock() is called from.
4680
+ var resumed = null;
4681
+ try {
4682
+ resumed = this.context.resume && this.context.resume();
4683
+ } catch (e) {
4684
+ // An unsupported or interrupted context: fall through to the polling
4685
+ // check, which also covers the browsers that resume on their own.
4686
+ }
4687
+ if (resumed && resumed.then) {
4688
+ resumed.then(function () {
4689
+ return _this4.checkIfUnlocked(source, 0);
4690
+ }, function () {
4691
+ return _this4.checkIfUnlocked(source, 0);
4692
+ });
4693
+ return;
4694
+ }
4540
4695
  setTimeout(this.checkIfUnlocked.bind(this, source, 0), 0);
4541
4696
  };
4542
4697
  _proto.checkIfUnlocked = function checkIfUnlocked(source, attempt) {
4543
- if (source.playbackState === source.PLAYING_STATE || source.playbackState === source.FINISHED_STATE) {
4544
- this.unlocked = true;
4545
- if (this.unlockCallback) {
4546
- this.unlockCallback();
4547
- this.unlockCallback = null;
4548
- }
4698
+ if (this.destroyed || this.context.state === 'closed') {
4699
+ // The player was destroyed while waiting for the unlock.
4700
+ this.unlockCallback = null;
4701
+ return;
4702
+ }
4703
+
4704
+ // `playbackState` and its constants were removed from the Web Audio API,
4705
+ // and comparing two undefined values is always true, so the legacy check
4706
+ // may only be used when the browser actually exposes it.
4707
+ var legacyUnlocked = source.playbackState !== undefined && (source.playbackState === source.PLAYING_STATE || source.playbackState === source.FINISHED_STATE);
4708
+ var unlocked = this.context.state ? this.context.state === 'running' : legacyUnlocked;
4709
+ if (unlocked) {
4710
+ this.onUnlocked();
4549
4711
  } else if (attempt < 10) {
4550
4712
  // Jeez, what a shit show. Thanks iOS!
4551
4713
  setTimeout(this.checkIfUnlocked.bind(this, source, attempt + 1), 100);
4552
4714
  }
4553
4715
  };
4716
+ _proto.onUnlocked = function onUnlocked() {
4717
+ if (this.destroyed) {
4718
+ return;
4719
+ }
4720
+ this.removeGestureListener();
4721
+
4722
+ // The output may be unlocked again after its context had been suspended, so
4723
+ // a pending callback is always served; only the onUnlock hook, which drives
4724
+ // the unlock UI, is limited to the first time.
4725
+ var firstUnlock = !this.unlocked;
4726
+ this.unlocked = true;
4727
+ var callbacks = [this.unlockCallback, firstUnlock ? this.onUnlock : null];
4728
+ this.unlockCallback = null;
4729
+ callbacks.forEach(function (callback) {
4730
+ if (callback) {
4731
+ callback();
4732
+ }
4733
+ });
4734
+ };
4554
4735
  WebAudioOut.NeedsUnlocking = function NeedsUnlocking() {
4555
4736
  return /iPhone|iPad|iPod/i.test(navigator.userAgent);
4556
4737
  };
4738
+ WebAudioOut.IsRunning = function IsRunning(context) {
4739
+ // Old WebKit versions expose no state at all; there, NeedsUnlocking()
4740
+ // alone decides whether the context has to be unlocked.
4741
+ return !context.state || context.state === 'running';
4742
+ };
4557
4743
  WebAudioOut.IsSupported = function IsSupported() {
4558
4744
  return window.AudioContext || window.webkitAudioContext;
4559
4745
  };
@@ -4885,10 +5071,11 @@ var Player = /*#__PURE__*/function () {
4885
5071
  this.isPlaying = false;
4886
5072
  this.paused = true;
4887
5073
  if (this.audio && this.audio.canPlay) {
4888
- // Seek to the currentTime again - audio may already be enqueued a bit
4889
- // further, so we have to rewind it.
5074
+ // Seek back to the frame that is being heard: stop() drops the queue that
5075
+ // was already enqueued further ahead, so the time has to be read first.
5076
+ var time = this.currentTime;
4890
5077
  this.audioOut.stop();
4891
- this.seek(this.currentTime);
5078
+ this.seek(time);
4892
5079
  }
4893
5080
  if (this.hooks.pause) {
4894
5081
  this.hooks.pause();