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