@cycjimmy/jsmpeg-player 6.1.6 → 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.
- package/README.md +3 -3
- package/dist/jsmpeg-player.cjs +245 -30
- package/dist/jsmpeg-player.esm.js +245 -30
- package/dist/jsmpeg-player.umd.js +245 -30
- package/dist/jsmpeg-player.umd.min.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* **[jsmpeg-player](https://github.com/cycdpo/jsmpeg-player) has been renamed to @cycjimmy/jsmpeg-player for scoped NPM package.**
|
|
13
13
|
* JSMpeg player is based on [jsmpeg](https://github.com/phoboslab/jsmpeg).
|
|
14
14
|
* The video must be compressed into the TS format of MPEG1 / MP2.
|
|
15
|
-
* Apple device automatically plays without sound, you need to guide the user to click on the video in the lower right corner of the video icon to unlock the sound. (no similar problem in non-autoplay mode)
|
|
15
|
+
* Autoplay with sound is blocked by the browser until the page receives a user gesture: Apple device automatically plays without sound, you need to guide the user to click on the video in the lower right corner of the video icon to unlock the sound. Chrome and Safari follow the same autoplay policy, where the sound stays in sync with the picture and starts as soon as the user interacts with the page, or can be unlocked right away through the same icon. (no similar problem in non-autoplay mode)
|
|
16
16
|
* [Demo][github-pages-url]
|
|
17
17
|
|
|
18
18
|
## How to use
|
|
@@ -47,7 +47,7 @@ new JSMpeg.VideoElement(videoWrapper, videoUrl [, options] [, overlayOptions])
|
|
|
47
47
|
* `autoplay`: [Boolean] Whether to start playing immediately. Default `false`.
|
|
48
48
|
* `autoSetWrapperSize`: [Boolean] Whether to set the wrapper element size automatically when the video loaded. Default `false`.
|
|
49
49
|
* `loop`: [Boolean] Whether to loop the video (static files only). Default `false`.**[overwrite]**
|
|
50
|
-
* `control`: [Boolean] Whether the user can control. Default `true`.
|
|
50
|
+
* `control`: [Boolean] Whether the user can control. The built-in play button and click-to-toggle apply to static files only: `ws://` / `wss://` live sources have no built-in playback control and must be driven with `play()` / `pause()`. Default `true`.
|
|
51
51
|
* `decodeFirstFrame`: [Boolean] Whether to decode and display the first frame of the video. Default `true`.
|
|
52
52
|
* `picMode`: [Boolean] Picture mode (no playButton). Default `false`.
|
|
53
53
|
* `progressive`: [Boolean] whether to load data in chunks (static files only). Default `true`.
|
|
@@ -58,7 +58,7 @@ new JSMpeg.VideoElement(videoWrapper, videoUrl [, options] [, overlayOptions])
|
|
|
58
58
|
* `stop`: [Function] The hook function when the video stop.
|
|
59
59
|
* `load`: [Function] The hook function when the video established.
|
|
60
60
|
* `overlayOptions`: [Object] More options can view the [jsmpeg options](https://github.com/phoboslab/jsmpeg#usage)
|
|
61
|
-
* `protocols`: [String | Array<String>] WebSocket subprotocols, forwarded to `new WebSocket(url, protocols)` (`wss://` URLs only). Can carry a Bearer token. Default `undefined`. See [WebSocket authentication](#websocket-authentication).
|
|
61
|
+
* `protocols`: [String | Array<String>] WebSocket subprotocols, forwarded to `new WebSocket(url, protocols)` (`ws://` and `wss://` URLs only). Can carry a Bearer token. Default `undefined`. See [WebSocket authentication](#websocket-authentication).
|
|
62
62
|
* `preserveDrawingBuffer`: [Boolean] Keep the WebGL drawing buffer so the canvas can be read back, e.g. to take a snapshot with `canvas.toDataURL()`. Default `false`. See [Snapshot](#snapshot).
|
|
63
63
|
|
|
64
64
|
* `JSMpeg.VideoElement` instance supports the following methods:
|
package/dist/jsmpeg-player.cjs
CHANGED
|
@@ -235,10 +235,10 @@ var VideoElement = /*#__PURE__*/function () {
|
|
|
235
235
|
this.els.playButton.classList.add(_style.hidden);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
// Set up the unlock audio button for
|
|
239
|
-
//
|
|
240
|
-
// streaming players we set up a muted speaker
|
|
241
|
-
// others, we can simply use the play button.
|
|
238
|
+
// Set up the unlock audio button for the browsers that do not allow audio
|
|
239
|
+
// before a user action (iOS, and Chrome/Safari under their autoplay
|
|
240
|
+
// policy). For autoplay or streaming players we set up a muted speaker
|
|
241
|
+
// icon as the button. For all others, we can simply use the play button.
|
|
242
242
|
if (this.player.audioOut && !this.player.audioOut.unlocked) {
|
|
243
243
|
var unlockAudioElement = this.els.wrapper;
|
|
244
244
|
if (this.options.autoplay || this.player.options.streaming) {
|
|
@@ -251,6 +251,12 @@ var VideoElement = /*#__PURE__*/function () {
|
|
|
251
251
|
this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement);
|
|
252
252
|
unlockAudioElement.addEventListener('touchstart', this.unlockAudioBound, false);
|
|
253
253
|
unlockAudioElement.addEventListener('click', this.unlockAudioBound, true);
|
|
254
|
+
|
|
255
|
+
// Any gesture on the page may unlock the audio, so the icon has to go
|
|
256
|
+
// away even when it was not the element that was clicked.
|
|
257
|
+
this.player.audioOut.onUnlock = function () {
|
|
258
|
+
return _this.onAudioUnlocked(unlockAudioElement);
|
|
259
|
+
};
|
|
254
260
|
}
|
|
255
261
|
};
|
|
256
262
|
_proto.initPlayButton = function initPlayButton() {
|
|
@@ -277,6 +283,15 @@ var VideoElement = /*#__PURE__*/function () {
|
|
|
277
283
|
});
|
|
278
284
|
});
|
|
279
285
|
};
|
|
286
|
+
_proto.onAudioUnlocked = function onAudioUnlocked(element) {
|
|
287
|
+
if (this.els.unmuteButton) {
|
|
288
|
+
this.els.unmuteButton.classList.add(_style.hidden);
|
|
289
|
+
}
|
|
290
|
+
if (element && this.unlockAudioBound) {
|
|
291
|
+
element.removeEventListener('touchstart', this.unlockAudioBound);
|
|
292
|
+
element.removeEventListener('click', this.unlockAudioBound);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
280
295
|
_proto.onUnlockAudio = function onUnlockAudio(element, ev) {
|
|
281
296
|
var _this3 = this;
|
|
282
297
|
if (this.els.unmuteButton) {
|
|
@@ -284,11 +299,7 @@ var VideoElement = /*#__PURE__*/function () {
|
|
|
284
299
|
ev.stopPropagation();
|
|
285
300
|
}
|
|
286
301
|
this.player.audioOut.unlock(function () {
|
|
287
|
-
|
|
288
|
-
_this3.els.unmuteButton.classList.add(_style.hidden);
|
|
289
|
-
}
|
|
290
|
-
element.removeEventListener('touchstart', _this3.unlockAudioBound);
|
|
291
|
-
element.removeEventListener('click', _this3.unlockAudioBound);
|
|
302
|
+
return _this3.onAudioUnlocked(element);
|
|
292
303
|
});
|
|
293
304
|
};
|
|
294
305
|
_proto.onClick = function onClick() {
|
|
@@ -393,6 +404,11 @@ var AjaxSource = /*#__PURE__*/function () {
|
|
|
393
404
|
// Nothing to do here
|
|
394
405
|
};
|
|
395
406
|
_proto.destroy = function destroy() {
|
|
407
|
+
// destroy() may run before start(), e.g. when a player is torn down while
|
|
408
|
+
// the WASM module is still compiling and the source was never started.
|
|
409
|
+
if (!this.request) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
396
412
|
this.request.abort();
|
|
397
413
|
};
|
|
398
414
|
_proto.onProgress = function onProgress(ev) {
|
|
@@ -587,6 +603,12 @@ var WSSource = /*#__PURE__*/function () {
|
|
|
587
603
|
_proto.destroy = function destroy() {
|
|
588
604
|
clearTimeout(this.reconnectTimeoutId);
|
|
589
605
|
this.shouldAttemptReconnect = false;
|
|
606
|
+
|
|
607
|
+
// destroy() may run before start(), e.g. when a player is torn down while
|
|
608
|
+
// the WASM module is still compiling and the socket was never opened.
|
|
609
|
+
if (!this.socket) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
590
612
|
this.socket.close();
|
|
591
613
|
};
|
|
592
614
|
_proto.start = function start() {
|
|
@@ -4038,11 +4060,15 @@ var MP2WASM = /*#__PURE__*/function (_BaseDecoder) {
|
|
|
4038
4060
|
_this.bufferSize = options.audioBufferSize || 128 * 1024;
|
|
4039
4061
|
_this.bufferMode = options.streaming ? BitBuffer.MODE.EVICT : BitBuffer.MODE.EXPAND;
|
|
4040
4062
|
_this.sampleRate = 0;
|
|
4063
|
+
_this.destroyed = false;
|
|
4041
4064
|
return _this;
|
|
4042
4065
|
}
|
|
4043
4066
|
_inheritsLoose(MP2WASM, _BaseDecoder);
|
|
4044
4067
|
var _proto = MP2WASM.prototype;
|
|
4045
4068
|
_proto.initializeWasmDecoder = function initializeWasmDecoder() {
|
|
4069
|
+
if (this.destroyed) {
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4046
4072
|
if (!this.module.instance) {
|
|
4047
4073
|
console.warn('JSMpeg: WASM module not compiled yet');
|
|
4048
4074
|
return;
|
|
@@ -4052,10 +4078,23 @@ var MP2WASM = /*#__PURE__*/function (_BaseDecoder) {
|
|
|
4052
4078
|
this.decoder = this.functions._mp2_decoder_create(this.bufferSize, this.bufferMode);
|
|
4053
4079
|
};
|
|
4054
4080
|
_proto.destroy = function destroy() {
|
|
4081
|
+
// A source can still deliver one last chunk inside the very callback that
|
|
4082
|
+
// destroys the player, so nothing may reach the module from now on.
|
|
4083
|
+
this.destroyed = true;
|
|
4055
4084
|
if (!this.decoder) {
|
|
4056
4085
|
return;
|
|
4057
4086
|
}
|
|
4058
4087
|
this.functions._mp2_decoder_destroy(this.decoder);
|
|
4088
|
+
// The pointer refers to a freed block now, and the module may hand the same
|
|
4089
|
+
// address to the decoder of the next player: it must never be used again.
|
|
4090
|
+
this.decoder = null;
|
|
4091
|
+
this.canPlay = false;
|
|
4092
|
+
};
|
|
4093
|
+
_proto.write = function write(pts, buffers) {
|
|
4094
|
+
if (this.destroyed) {
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
_BaseDecoder.prototype.write.call(this, pts, buffers);
|
|
4059
4098
|
};
|
|
4060
4099
|
_proto.bufferGetIndex = function bufferGetIndex() {
|
|
4061
4100
|
if (!this.decoder) {
|
|
@@ -4071,9 +4110,16 @@ var MP2WASM = /*#__PURE__*/function (_BaseDecoder) {
|
|
|
4071
4110
|
this.functions._mp2_decoder_set_index(this.decoder, index);
|
|
4072
4111
|
};
|
|
4073
4112
|
_proto.bufferWrite = function bufferWrite(buffers) {
|
|
4113
|
+
if (this.destroyed) {
|
|
4114
|
+
return 0;
|
|
4115
|
+
}
|
|
4074
4116
|
if (!this.decoder) {
|
|
4075
4117
|
this.initializeWasmDecoder();
|
|
4076
4118
|
}
|
|
4119
|
+
if (!this.decoder) {
|
|
4120
|
+
// The module is not compiled yet; nothing can be written.
|
|
4121
|
+
return 0;
|
|
4122
|
+
}
|
|
4077
4123
|
var totalLength = 0;
|
|
4078
4124
|
for (var i = 0; i < buffers.length; i++) {
|
|
4079
4125
|
totalLength += buffers[i].length;
|
|
@@ -4430,6 +4476,9 @@ var CanvasRenderer = /*#__PURE__*/function () {
|
|
|
4430
4476
|
return CanvasRenderer;
|
|
4431
4477
|
}();
|
|
4432
4478
|
|
|
4479
|
+
// Chrome grants the user activation on pointerup/touchend, not on the
|
|
4480
|
+
// touchstart/pointerdown that precedes them, so the touch events are paired.
|
|
4481
|
+
var UNLOCK_EVENTS = ['pointerdown', 'pointerup', 'keydown', 'touchend'];
|
|
4433
4482
|
var WebAudioOut = /*#__PURE__*/function () {
|
|
4434
4483
|
function WebAudioOut() {
|
|
4435
4484
|
this.context = WebAudioOut.CachedContext = WebAudioOut.CachedContext || new (window.AudioContext || window.webkitAudioContext)();
|
|
@@ -4445,13 +4494,27 @@ var WebAudioOut = /*#__PURE__*/function () {
|
|
|
4445
4494
|
this.wallclockStartTime = 0;
|
|
4446
4495
|
this.volume = 1;
|
|
4447
4496
|
this.enabled = true;
|
|
4448
|
-
this.
|
|
4497
|
+
this.sources = [];
|
|
4498
|
+
this.skippedAudio = false;
|
|
4499
|
+
this.destroyed = false;
|
|
4500
|
+
this.gestureListener = null;
|
|
4501
|
+
this.unlockCallback = null;
|
|
4502
|
+
this.onUnlock = null;
|
|
4503
|
+
this.unlocked = !WebAudioOut.NeedsUnlocking() && WebAudioOut.IsRunning(this.context);
|
|
4449
4504
|
Object.defineProperty(this, 'enqueuedTime', {
|
|
4450
4505
|
get: this.getEnqueuedTime
|
|
4451
4506
|
});
|
|
4507
|
+
if (!this.unlocked) {
|
|
4508
|
+
this.listenForGesture();
|
|
4509
|
+
}
|
|
4452
4510
|
}
|
|
4453
4511
|
var _proto = WebAudioOut.prototype;
|
|
4454
4512
|
_proto.destroy = function destroy() {
|
|
4513
|
+
this.destroyed = true;
|
|
4514
|
+
this.unlockCallback = null;
|
|
4515
|
+
this.onUnlock = null;
|
|
4516
|
+
this.removeGestureListener();
|
|
4517
|
+
this.stopSources();
|
|
4455
4518
|
this.gain.disconnect();
|
|
4456
4519
|
this.context._connections--;
|
|
4457
4520
|
if (this.context._connections === 0) {
|
|
@@ -4460,20 +4523,46 @@ var WebAudioOut = /*#__PURE__*/function () {
|
|
|
4460
4523
|
}
|
|
4461
4524
|
};
|
|
4462
4525
|
_proto.play = function play(sampleRate, left, right) {
|
|
4463
|
-
|
|
4526
|
+
var _this = this;
|
|
4527
|
+
if (!this.enabled || this.destroyed) {
|
|
4464
4528
|
return;
|
|
4465
4529
|
}
|
|
4466
4530
|
|
|
4467
|
-
//
|
|
4468
|
-
//
|
|
4469
|
-
|
|
4531
|
+
// Browsers that need no explicit unlock resume the context by themselves
|
|
4532
|
+
// once the page has been interacted with, so the unlock does not necessarily
|
|
4533
|
+
// go through unlock(). iOS keeps requiring the gesture.
|
|
4534
|
+
if (!this.unlocked && !WebAudioOut.NeedsUnlocking() && WebAudioOut.IsRunning(this.context)) {
|
|
4535
|
+
this.onUnlocked();
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
// A context that is not running, or that has not been unlocked yet, cannot
|
|
4539
|
+
// play anything: the browser autoplay policy keeps it suspended until the
|
|
4540
|
+
// page has received a user gesture, and its currentTime does not advance
|
|
4541
|
+
// meanwhile. Scheduling buffers against it would queue all the audio
|
|
4542
|
+
// decoded in the meantime and play it back from the very beginning once
|
|
4543
|
+
// the context resumes. We simply advance the start time to "fake"
|
|
4544
|
+
// actually playing audio instead. This will keep the video in sync.
|
|
4545
|
+
if (!this.isPlayable()) {
|
|
4546
|
+
this.listenForGesture();
|
|
4470
4547
|
var ts = Now();
|
|
4471
4548
|
if (this.wallclockStartTime < ts) {
|
|
4472
4549
|
this.wallclockStartTime = ts;
|
|
4473
4550
|
}
|
|
4474
4551
|
this.wallclockStartTime += left.length / sampleRate;
|
|
4552
|
+
this.skippedAudio = true;
|
|
4475
4553
|
return;
|
|
4476
4554
|
}
|
|
4555
|
+
|
|
4556
|
+
// The audio is available again: whatever was queued while it could not be
|
|
4557
|
+
// played is dropped and the time base is rebuilt, so the audio joins the
|
|
4558
|
+
// picture at the current point in time instead of staying behind it. The
|
|
4559
|
+
// audio decoded meanwhile is lost once, which is what keeps it in sync
|
|
4560
|
+
// with the picture.
|
|
4561
|
+
if (this.skippedAudio) {
|
|
4562
|
+
this.skippedAudio = false;
|
|
4563
|
+
this.stopSources();
|
|
4564
|
+
this.resetEnqueuedTime();
|
|
4565
|
+
}
|
|
4477
4566
|
this.gain.gain.value = this.volume;
|
|
4478
4567
|
var buffer = this.context.createBuffer(2, left.length, sampleRate);
|
|
4479
4568
|
buffer.getChannelData(0).set(left);
|
|
@@ -4481,6 +4570,13 @@ var WebAudioOut = /*#__PURE__*/function () {
|
|
|
4481
4570
|
var source = this.context.createBufferSource();
|
|
4482
4571
|
source.buffer = buffer;
|
|
4483
4572
|
source.connect(this.destination);
|
|
4573
|
+
source.onended = function () {
|
|
4574
|
+
var index = _this.sources.indexOf(source);
|
|
4575
|
+
if (index !== -1) {
|
|
4576
|
+
_this.sources.splice(index, 1);
|
|
4577
|
+
}
|
|
4578
|
+
};
|
|
4579
|
+
this.sources.push(source);
|
|
4484
4580
|
var now = this.context.currentTime;
|
|
4485
4581
|
var duration = buffer.duration;
|
|
4486
4582
|
if (this.startTime < now) {
|
|
@@ -4492,11 +4588,13 @@ var WebAudioOut = /*#__PURE__*/function () {
|
|
|
4492
4588
|
this.wallclockStartTime += duration;
|
|
4493
4589
|
};
|
|
4494
4590
|
_proto.stop = function stop() {
|
|
4495
|
-
//
|
|
4496
|
-
//
|
|
4497
|
-
//
|
|
4498
|
-
//
|
|
4591
|
+
// Cut off everything that is still queued: the gain alone would only mute
|
|
4592
|
+
// the buffers that are already scheduled, and they would become audible
|
|
4593
|
+
// again as soon as play() restores it. The time base is rebuilt so the
|
|
4594
|
+
// next buffer joins the current context time instead of the old queue.
|
|
4499
4595
|
this.gain.gain.value = 0;
|
|
4596
|
+
this.stopSources();
|
|
4597
|
+
this.resetEnqueuedTime();
|
|
4500
4598
|
};
|
|
4501
4599
|
_proto.getEnqueuedTime = function getEnqueuedTime() {
|
|
4502
4600
|
// The AudioContext.currentTime is only updated every so often, so if we
|
|
@@ -4507,16 +4605,65 @@ var WebAudioOut = /*#__PURE__*/function () {
|
|
|
4507
4605
|
this.startTime = this.context.currentTime;
|
|
4508
4606
|
this.wallclockStartTime = Now();
|
|
4509
4607
|
};
|
|
4608
|
+
_proto.isPlayable = function isPlayable() {
|
|
4609
|
+
return this.unlocked && WebAudioOut.IsRunning(this.context);
|
|
4610
|
+
};
|
|
4611
|
+
_proto.stopSources = function stopSources() {
|
|
4612
|
+
var sources = this.sources;
|
|
4613
|
+
this.sources = [];
|
|
4614
|
+
sources.forEach(function (source) {
|
|
4615
|
+
try {
|
|
4616
|
+
source.stop();
|
|
4617
|
+
} catch (e) {
|
|
4618
|
+
// The source has already ended and cannot be stopped any more.
|
|
4619
|
+
}
|
|
4620
|
+
try {
|
|
4621
|
+
source.disconnect();
|
|
4622
|
+
} catch (e) {
|
|
4623
|
+
// It was never connected to anything.
|
|
4624
|
+
}
|
|
4625
|
+
});
|
|
4626
|
+
};
|
|
4627
|
+
_proto.listenForGesture = function listenForGesture() {
|
|
4628
|
+
var _this2 = this;
|
|
4629
|
+
if (this.gestureListener || this.isPlayable() || this.destroyed || WebAudioOut.NeedsUnlocking() || typeof document === 'undefined') {
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
// Chrome and desktop Safari keep the context suspended until it is resumed
|
|
4634
|
+
// inside a user gesture, and they do not resume it by themselves when the
|
|
4635
|
+
// page is tapped somewhere else. iOS keeps requiring the unlock icon.
|
|
4636
|
+
this.gestureListener = function () {
|
|
4637
|
+
return _this2.unlock();
|
|
4638
|
+
};
|
|
4639
|
+
UNLOCK_EVENTS.forEach(function (type) {
|
|
4640
|
+
document.addEventListener(type, _this2.gestureListener, true);
|
|
4641
|
+
});
|
|
4642
|
+
};
|
|
4643
|
+
_proto.removeGestureListener = function removeGestureListener() {
|
|
4644
|
+
var _this3 = this;
|
|
4645
|
+
if (!this.gestureListener) {
|
|
4646
|
+
return;
|
|
4647
|
+
}
|
|
4648
|
+
UNLOCK_EVENTS.forEach(function (type) {
|
|
4649
|
+
document.removeEventListener(type, _this3.gestureListener, true);
|
|
4650
|
+
});
|
|
4651
|
+
this.gestureListener = null;
|
|
4652
|
+
};
|
|
4510
4653
|
_proto.unlock = function unlock(callback) {
|
|
4511
|
-
|
|
4654
|
+
var _this4 = this;
|
|
4655
|
+
// Nothing to do while the audio plays; a context that was suspended again
|
|
4656
|
+
// still has to be resumed, and a destroyed one has nothing left to unlock.
|
|
4657
|
+
if (this.isPlayable() || this.destroyed || this.context.state === 'closed') {
|
|
4512
4658
|
if (callback) {
|
|
4513
4659
|
callback();
|
|
4514
4660
|
}
|
|
4515
4661
|
return;
|
|
4516
4662
|
}
|
|
4517
|
-
this.unlockCallback = callback;
|
|
4663
|
+
this.unlockCallback = callback || this.unlockCallback;
|
|
4518
4664
|
|
|
4519
|
-
// Create empty buffer and play it
|
|
4665
|
+
// Create empty buffer and play it, so the browsers that only start their
|
|
4666
|
+
// audio pipeline from within a user gesture (iOS Safari) really unlock.
|
|
4520
4667
|
var buffer = this.context.createBuffer(1, 1, 22050);
|
|
4521
4668
|
var source = this.context.createBufferSource();
|
|
4522
4669
|
source.buffer = buffer;
|
|
@@ -4528,23 +4675,73 @@ var WebAudioOut = /*#__PURE__*/function () {
|
|
|
4528
4675
|
} else {
|
|
4529
4676
|
source.noteOn(0);
|
|
4530
4677
|
}
|
|
4678
|
+
|
|
4679
|
+
// Chrome and desktop Safari keep the context suspended until it is resumed
|
|
4680
|
+
// explicitly, and resume() only takes effect inside a user gesture - which
|
|
4681
|
+
// is exactly where unlock() is called from.
|
|
4682
|
+
var resumed = null;
|
|
4683
|
+
try {
|
|
4684
|
+
resumed = this.context.resume && this.context.resume();
|
|
4685
|
+
} catch (e) {
|
|
4686
|
+
// An unsupported or interrupted context: fall through to the polling
|
|
4687
|
+
// check, which also covers the browsers that resume on their own.
|
|
4688
|
+
}
|
|
4689
|
+
if (resumed && resumed.then) {
|
|
4690
|
+
resumed.then(function () {
|
|
4691
|
+
return _this4.checkIfUnlocked(source, 0);
|
|
4692
|
+
}, function () {
|
|
4693
|
+
return _this4.checkIfUnlocked(source, 0);
|
|
4694
|
+
});
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4531
4697
|
setTimeout(this.checkIfUnlocked.bind(this, source, 0), 0);
|
|
4532
4698
|
};
|
|
4533
4699
|
_proto.checkIfUnlocked = function checkIfUnlocked(source, attempt) {
|
|
4534
|
-
if (
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4700
|
+
if (this.destroyed || this.context.state === 'closed') {
|
|
4701
|
+
// The player was destroyed while waiting for the unlock.
|
|
4702
|
+
this.unlockCallback = null;
|
|
4703
|
+
return;
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
// `playbackState` and its constants were removed from the Web Audio API,
|
|
4707
|
+
// and comparing two undefined values is always true, so the legacy check
|
|
4708
|
+
// may only be used when the browser actually exposes it.
|
|
4709
|
+
var legacyUnlocked = source.playbackState !== undefined && (source.playbackState === source.PLAYING_STATE || source.playbackState === source.FINISHED_STATE);
|
|
4710
|
+
var unlocked = this.context.state ? this.context.state === 'running' : legacyUnlocked;
|
|
4711
|
+
if (unlocked) {
|
|
4712
|
+
this.onUnlocked();
|
|
4540
4713
|
} else if (attempt < 10) {
|
|
4541
4714
|
// Jeez, what a shit show. Thanks iOS!
|
|
4542
4715
|
setTimeout(this.checkIfUnlocked.bind(this, source, attempt + 1), 100);
|
|
4543
4716
|
}
|
|
4544
4717
|
};
|
|
4718
|
+
_proto.onUnlocked = function onUnlocked() {
|
|
4719
|
+
if (this.destroyed) {
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
this.removeGestureListener();
|
|
4723
|
+
|
|
4724
|
+
// The output may be unlocked again after its context had been suspended, so
|
|
4725
|
+
// a pending callback is always served; only the onUnlock hook, which drives
|
|
4726
|
+
// the unlock UI, is limited to the first time.
|
|
4727
|
+
var firstUnlock = !this.unlocked;
|
|
4728
|
+
this.unlocked = true;
|
|
4729
|
+
var callbacks = [this.unlockCallback, firstUnlock ? this.onUnlock : null];
|
|
4730
|
+
this.unlockCallback = null;
|
|
4731
|
+
callbacks.forEach(function (callback) {
|
|
4732
|
+
if (callback) {
|
|
4733
|
+
callback();
|
|
4734
|
+
}
|
|
4735
|
+
});
|
|
4736
|
+
};
|
|
4545
4737
|
WebAudioOut.NeedsUnlocking = function NeedsUnlocking() {
|
|
4546
4738
|
return /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
4547
4739
|
};
|
|
4740
|
+
WebAudioOut.IsRunning = function IsRunning(context) {
|
|
4741
|
+
// Old WebKit versions expose no state at all; there, NeedsUnlocking()
|
|
4742
|
+
// alone decides whether the context has to be unlocked.
|
|
4743
|
+
return !context.state || context.state === 'running';
|
|
4744
|
+
};
|
|
4548
4745
|
WebAudioOut.IsSupported = function IsSupported() {
|
|
4549
4746
|
return window.AudioContext || window.webkitAudioContext;
|
|
4550
4747
|
};
|
|
@@ -4812,6 +5009,7 @@ var Player = /*#__PURE__*/function () {
|
|
|
4812
5009
|
set: this.setVolume
|
|
4813
5010
|
});
|
|
4814
5011
|
this.paused = true;
|
|
5012
|
+
this.destroyed = false;
|
|
4815
5013
|
this.unpauseOnShow = false;
|
|
4816
5014
|
if (options.pauseWhenHidden !== false) {
|
|
4817
5015
|
// Keep the bound reference so destroy() can remove this listener again,
|
|
@@ -4837,6 +5035,13 @@ var Player = /*#__PURE__*/function () {
|
|
|
4837
5035
|
}
|
|
4838
5036
|
var _proto = Player.prototype;
|
|
4839
5037
|
_proto.startLoading = function startLoading() {
|
|
5038
|
+
// The WASM module is compiled asynchronously, so this may still run through
|
|
5039
|
+
// the module's init callback after destroy() has already torn the player
|
|
5040
|
+
// down. Starting the source then would open a connection that nothing owns
|
|
5041
|
+
// any more and restart the decode loop.
|
|
5042
|
+
if (this.destroyed) {
|
|
5043
|
+
return;
|
|
5044
|
+
}
|
|
4840
5045
|
this.source.start();
|
|
4841
5046
|
if (this.autoplay) {
|
|
4842
5047
|
this.play();
|
|
@@ -4868,10 +5073,11 @@ var Player = /*#__PURE__*/function () {
|
|
|
4868
5073
|
this.isPlaying = false;
|
|
4869
5074
|
this.paused = true;
|
|
4870
5075
|
if (this.audio && this.audio.canPlay) {
|
|
4871
|
-
// Seek to the
|
|
4872
|
-
// further, so
|
|
5076
|
+
// Seek back to the frame that is being heard: stop() drops the queue that
|
|
5077
|
+
// was already enqueued further ahead, so the time has to be read first.
|
|
5078
|
+
var time = this.currentTime;
|
|
4873
5079
|
this.audioOut.stop();
|
|
4874
|
-
this.seek(
|
|
5080
|
+
this.seek(time);
|
|
4875
5081
|
}
|
|
4876
5082
|
if (this.hooks.pause) {
|
|
4877
5083
|
this.hooks.pause();
|
|
@@ -4899,6 +5105,9 @@ var Player = /*#__PURE__*/function () {
|
|
|
4899
5105
|
}
|
|
4900
5106
|
};
|
|
4901
5107
|
_proto.destroy = function destroy() {
|
|
5108
|
+
// Nothing may start the source from here on: the WASM init callback may
|
|
5109
|
+
// still fire later, and destroy() itself may run before the source started.
|
|
5110
|
+
this.destroyed = true;
|
|
4902
5111
|
this.pause();
|
|
4903
5112
|
this.source.destroy();
|
|
4904
5113
|
this.video && this.video.destroy();
|
|
@@ -5105,6 +5314,12 @@ var FetchSource = /*#__PURE__*/function () {
|
|
|
5105
5314
|
_proto.abort = function abort() {
|
|
5106
5315
|
this.aborted = true;
|
|
5107
5316
|
};
|
|
5317
|
+
_proto.destroy = function destroy() {
|
|
5318
|
+
// The player always calls source.destroy(); without this the Fetch source
|
|
5319
|
+
// would throw on teardown. The pump loop cancels the reader once it sees
|
|
5320
|
+
// the flag on the next chunk.
|
|
5321
|
+
this.aborted = true;
|
|
5322
|
+
};
|
|
5108
5323
|
return FetchSource;
|
|
5109
5324
|
}();
|
|
5110
5325
|
|