ts-video-player 0.1.6 → 0.1.7
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/dist/cdn.min.js +40 -20
- package/dist/{chunk-fvz33zn4.js → chunk-24n6adw2.js} +82 -13
- package/dist/elements/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/player.d.ts +6 -0
- package/dist/providers/youtube.d.ts +6 -0
- package/package.json +1 -1
- package/src/elements/video-skin.ts +21 -1
- package/src/player.ts +48 -7
- package/src/providers/youtube.ts +27 -5
|
@@ -625,6 +625,7 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
625
625
|
endTime;
|
|
626
626
|
timeUpdateInterval = null;
|
|
627
627
|
lastState = -1;
|
|
628
|
+
pendingPlay = false;
|
|
628
629
|
get mediaElement() {
|
|
629
630
|
return this.iframe;
|
|
630
631
|
}
|
|
@@ -702,6 +703,7 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
702
703
|
this.player?.destroy();
|
|
703
704
|
this.player = null;
|
|
704
705
|
this.playerReady = false;
|
|
706
|
+
this.pendingPlay = false;
|
|
705
707
|
this.iframe = null;
|
|
706
708
|
}
|
|
707
709
|
onReady() {
|
|
@@ -714,8 +716,12 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
714
716
|
this.events.emit("loadedmetadata");
|
|
715
717
|
this.events.emit("canplay");
|
|
716
718
|
if (this.pendingVideoId) {
|
|
717
|
-
|
|
719
|
+
const videoId = this.pendingVideoId;
|
|
718
720
|
this.pendingVideoId = null;
|
|
721
|
+
this.loadVideo(videoId, this.startTime, this.endTime);
|
|
722
|
+
} else if (this.pendingPlay) {
|
|
723
|
+
this.pendingPlay = false;
|
|
724
|
+
this.player?.playVideo();
|
|
719
725
|
}
|
|
720
726
|
}
|
|
721
727
|
onStateChange(event) {
|
|
@@ -727,6 +733,8 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
727
733
|
this.events.emit("loadstart");
|
|
728
734
|
break;
|
|
729
735
|
case PlayerState.PLAYING:
|
|
736
|
+
if (this.getDuration() > 0)
|
|
737
|
+
this.events.emit("durationchange", this.getDuration());
|
|
730
738
|
this.events.emit("play");
|
|
731
739
|
this.events.emit("playing");
|
|
732
740
|
this.events.emit("statechange", { paused: false, playing: true, waiting: false });
|
|
@@ -787,7 +795,8 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
787
795
|
loadVideo(videoId, startTime, endTime) {
|
|
788
796
|
if (!this.player)
|
|
789
797
|
return;
|
|
790
|
-
if (this.options.autoplay) {
|
|
798
|
+
if (this.options.autoplay || this.pendingPlay) {
|
|
799
|
+
this.pendingPlay = false;
|
|
791
800
|
this.player.loadVideoById({
|
|
792
801
|
videoId,
|
|
793
802
|
startSeconds: startTime,
|
|
@@ -802,13 +811,21 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
802
811
|
}
|
|
803
812
|
}
|
|
804
813
|
async play() {
|
|
805
|
-
this.player
|
|
814
|
+
if (!this.player || !this.playerReady || this.pendingVideoId) {
|
|
815
|
+
this.pendingPlay = true;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
this.player.playVideo();
|
|
806
819
|
}
|
|
807
820
|
pause() {
|
|
808
|
-
this.
|
|
821
|
+
this.pendingPlay = false;
|
|
822
|
+
if (this.playerReady)
|
|
823
|
+
this.player?.pauseVideo();
|
|
809
824
|
}
|
|
810
825
|
stop() {
|
|
811
|
-
this.
|
|
826
|
+
this.pendingPlay = false;
|
|
827
|
+
if (this.playerReady)
|
|
828
|
+
this.player?.stopVideo();
|
|
812
829
|
}
|
|
813
830
|
seekTo(time) {
|
|
814
831
|
this.player?.seekTo(time, true);
|
|
@@ -2040,6 +2057,7 @@ class Player2 {
|
|
|
2040
2057
|
_cover = null;
|
|
2041
2058
|
_coverDismissed = false;
|
|
2042
2059
|
_coverTimer = null;
|
|
2060
|
+
_playRequested = false;
|
|
2043
2061
|
constructor(container, options = {}) {
|
|
2044
2062
|
if (typeof container === "string") {
|
|
2045
2063
|
const el = document.querySelector(container);
|
|
@@ -2112,20 +2130,36 @@ class Player2 {
|
|
|
2112
2130
|
button.setAttribute("aria-hidden", "true");
|
|
2113
2131
|
button.style.cssText = "display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,0.92);box-shadow:0 4px 18px rgba(0,0,0,0.35)";
|
|
2114
2132
|
const triangle = document.createElement("span");
|
|
2133
|
+
triangle.className = "ts-video-player__cover-play";
|
|
2115
2134
|
triangle.style.cssText = "width:22px;height:24px;margin-left:5px;background:#0f172a;clip-path:polygon(0 0,100% 50%,0 100%)";
|
|
2116
2135
|
button.appendChild(triangle);
|
|
2117
2136
|
cover.appendChild(button);
|
|
2118
2137
|
cover.addEventListener("click", (event) => {
|
|
2119
2138
|
event.stopPropagation();
|
|
2139
|
+
if (!cover.dataset.loading) {
|
|
2140
|
+
cover.dataset.loading = "";
|
|
2141
|
+
triangle.style.display = "none";
|
|
2142
|
+
const ring = document.createElement("span");
|
|
2143
|
+
ring.className = "ts-video-player__cover-spinner";
|
|
2144
|
+
ring.style.cssText = "width:26px;height:26px;border-radius:50%;border:3px solid rgba(15,23,42,0.2);border-top-color:#0f172a";
|
|
2145
|
+
button.appendChild(ring);
|
|
2146
|
+
ring.animate?.([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], { duration: 800, iterations: Infinity });
|
|
2147
|
+
}
|
|
2120
2148
|
this.play().catch(() => {});
|
|
2121
2149
|
if (this._coverTimer)
|
|
2122
2150
|
clearTimeout(this._coverTimer);
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2151
|
+
let waits = 0;
|
|
2152
|
+
const settle = () => {
|
|
2153
|
+
this._coverTimer = setTimeout(() => {
|
|
2154
|
+
if (this.state.started)
|
|
2155
|
+
return;
|
|
2156
|
+
if (!this.state.canPlay && ++waits < 10)
|
|
2157
|
+
return settle();
|
|
2158
|
+
this._coverDismissed = true;
|
|
2159
|
+
this.updateCover(this.state);
|
|
2160
|
+
}, 2000);
|
|
2161
|
+
};
|
|
2162
|
+
settle();
|
|
2129
2163
|
});
|
|
2130
2164
|
this._el.appendChild(cover);
|
|
2131
2165
|
this._cover = cover;
|
|
@@ -2145,6 +2179,13 @@ class Player2 {
|
|
|
2145
2179
|
if (cover.style.backgroundImage !== background)
|
|
2146
2180
|
cover.style.backgroundImage = background;
|
|
2147
2181
|
}
|
|
2182
|
+
if (!show && cover.dataset.loading !== undefined) {
|
|
2183
|
+
delete cover.dataset.loading;
|
|
2184
|
+
cover.querySelector(".ts-video-player__cover-spinner")?.remove();
|
|
2185
|
+
const triangle = cover.querySelector(".ts-video-player__cover-play");
|
|
2186
|
+
if (triangle)
|
|
2187
|
+
triangle.style.display = "";
|
|
2188
|
+
}
|
|
2148
2189
|
cover.hidden = !show;
|
|
2149
2190
|
cover.style.display = show ? "flex" : "none";
|
|
2150
2191
|
}
|
|
@@ -2307,6 +2348,7 @@ class Player2 {
|
|
|
2307
2348
|
return;
|
|
2308
2349
|
}
|
|
2309
2350
|
this._coverDismissed = false;
|
|
2351
|
+
this._playRequested = false;
|
|
2310
2352
|
this._store.batch({
|
|
2311
2353
|
sources,
|
|
2312
2354
|
src: sources[0],
|
|
@@ -2364,6 +2406,10 @@ class Player2 {
|
|
|
2364
2406
|
mediaType: loader.mediaType(candidate.src),
|
|
2365
2407
|
providerType: loader.type
|
|
2366
2408
|
});
|
|
2409
|
+
if (this._playRequested) {
|
|
2410
|
+
this._playRequested = false;
|
|
2411
|
+
this._provider.play().catch(() => {});
|
|
2412
|
+
}
|
|
2367
2413
|
return;
|
|
2368
2414
|
} catch (error) {
|
|
2369
2415
|
lastError = error;
|
|
@@ -2502,11 +2548,14 @@ class Player2 {
|
|
|
2502
2548
|
});
|
|
2503
2549
|
}
|
|
2504
2550
|
async play() {
|
|
2505
|
-
if (!this._provider)
|
|
2551
|
+
if (!this._provider) {
|
|
2552
|
+
this._playRequested = true;
|
|
2506
2553
|
return;
|
|
2554
|
+
}
|
|
2507
2555
|
await this._provider.play();
|
|
2508
2556
|
}
|
|
2509
2557
|
pause() {
|
|
2558
|
+
this._playRequested = false;
|
|
2510
2559
|
this._provider?.pause();
|
|
2511
2560
|
}
|
|
2512
2561
|
togglePlay() {
|
|
@@ -2896,6 +2945,7 @@ class VideoSkin2 extends HTMLElementBase {
|
|
|
2896
2945
|
color: white;
|
|
2897
2946
|
font-family: system-ui, -apple-system, sans-serif;
|
|
2898
2947
|
font-size: 14px;
|
|
2948
|
+
container-type: inline-size;
|
|
2899
2949
|
}
|
|
2900
2950
|
|
|
2901
2951
|
/* Click-to-play overlay */
|
|
@@ -2923,6 +2973,25 @@ class VideoSkin2 extends HTMLElementBase {
|
|
|
2923
2973
|
visibility: hidden;
|
|
2924
2974
|
}
|
|
2925
2975
|
|
|
2976
|
+
/* The time and the progress bar. One row with the buttons on a
|
|
2977
|
+
wide player; a phone-width one gives the progress bar a row of
|
|
2978
|
+
its own, where it would otherwise be squeezed to a dot between
|
|
2979
|
+
the buttons. */
|
|
2980
|
+
.center { display: contents; }
|
|
2981
|
+
@container (max-width: 520px) {
|
|
2982
|
+
.controls { flex-wrap: wrap; row-gap: 0; padding: 4px 10px 6px; }
|
|
2983
|
+
.center { display: flex; order: -1; flex: 1 0 100%; align-items: center; gap: 10px; min-width: 0; font-size: 12px; }
|
|
2984
|
+
/* Speed is in the settings menu too; one button fewer keeps the
|
|
2985
|
+
rest on one row. */
|
|
2986
|
+
::slotted(media-playback-rate-button) { display: none; }
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/* A touch screen sets the volume with its own buttons, and iOS
|
|
2990
|
+
ignores a page's volume altogether: no slider there. */
|
|
2991
|
+
@media (pointer: coarse) {
|
|
2992
|
+
::slotted(media-volume-slider) { display: none; }
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2926
2995
|
/* Spacer pushes right-aligned controls */
|
|
2927
2996
|
.spacer {
|
|
2928
2997
|
flex: 1;
|
|
@@ -2938,7 +3007,7 @@ class VideoSkin2 extends HTMLElementBase {
|
|
|
2938
3007
|
</div>
|
|
2939
3008
|
<div class="controls" part="controls">
|
|
2940
3009
|
<slot name="left"></slot>
|
|
2941
|
-
<slot name="center"></slot>
|
|
3010
|
+
<div class="center" part="center"><slot name="center"></slot></div>
|
|
2942
3011
|
<div class="spacer"></div>
|
|
2943
3012
|
<slot name="right"></slot>
|
|
2944
3013
|
<slot></slot>
|
package/dist/elements/index.js
CHANGED
package/dist/index.js
CHANGED
package/dist/player.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare class Player implements IPlayer {
|
|
|
23
23
|
private _cover;
|
|
24
24
|
private _coverDismissed;
|
|
25
25
|
private _coverTimer;
|
|
26
|
+
/**
|
|
27
|
+
* A play asked for before there was a provider to ask: a tap on the cover
|
|
28
|
+
* while the source was still loading. Kept and honoured once the source has
|
|
29
|
+
* loaded, so one tap plays the video rather than one tap per loading step.
|
|
30
|
+
*/
|
|
31
|
+
private _playRequested;
|
|
26
32
|
/**
|
|
27
33
|
* Create a new player instance
|
|
28
34
|
*/
|
|
@@ -96,6 +96,12 @@ export declare class YouTubeProvider extends BaseProvider {
|
|
|
96
96
|
private endTime?;
|
|
97
97
|
private timeUpdateInterval;
|
|
98
98
|
private lastState;
|
|
99
|
+
/**
|
|
100
|
+
* A play asked for before YouTube's player could take it. Its methods only
|
|
101
|
+
* exist after its ready event, and the video may still be pending then, so
|
|
102
|
+
* the play waits for both and the video loads playing.
|
|
103
|
+
*/
|
|
104
|
+
private pendingPlay;
|
|
99
105
|
get mediaElement(): HTMLIFrameElement | null;
|
|
100
106
|
protected initialize(): Promise<void>;
|
|
101
107
|
private loadYouTubeAPI;
|
package/package.json
CHANGED
|
@@ -36,6 +36,7 @@ export class VideoSkin extends HTMLElementBase {
|
|
|
36
36
|
color: white;
|
|
37
37
|
font-family: system-ui, -apple-system, sans-serif;
|
|
38
38
|
font-size: 14px;
|
|
39
|
+
container-type: inline-size;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/* Click-to-play overlay */
|
|
@@ -63,6 +64,25 @@ export class VideoSkin extends HTMLElementBase {
|
|
|
63
64
|
visibility: hidden;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/* The time and the progress bar. One row with the buttons on a
|
|
68
|
+
wide player; a phone-width one gives the progress bar a row of
|
|
69
|
+
its own, where it would otherwise be squeezed to a dot between
|
|
70
|
+
the buttons. */
|
|
71
|
+
.center { display: contents; }
|
|
72
|
+
@container (max-width: 520px) {
|
|
73
|
+
.controls { flex-wrap: wrap; row-gap: 0; padding: 4px 10px 6px; }
|
|
74
|
+
.center { display: flex; order: -1; flex: 1 0 100%; align-items: center; gap: 10px; min-width: 0; font-size: 12px; }
|
|
75
|
+
/* Speed is in the settings menu too; one button fewer keeps the
|
|
76
|
+
rest on one row. */
|
|
77
|
+
::slotted(media-playback-rate-button) { display: none; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* A touch screen sets the volume with its own buttons, and iOS
|
|
81
|
+
ignores a page's volume altogether: no slider there. */
|
|
82
|
+
@media (pointer: coarse) {
|
|
83
|
+
::slotted(media-volume-slider) { display: none; }
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
/* Spacer pushes right-aligned controls */
|
|
67
87
|
.spacer {
|
|
68
88
|
flex: 1;
|
|
@@ -78,7 +98,7 @@ export class VideoSkin extends HTMLElementBase {
|
|
|
78
98
|
</div>
|
|
79
99
|
<div class="controls" part="controls">
|
|
80
100
|
<slot name="left"></slot>
|
|
81
|
-
<slot name="center"></slot>
|
|
101
|
+
<div class="center" part="center"><slot name="center"></slot></div>
|
|
82
102
|
<div class="spacer"></div>
|
|
83
103
|
<slot name="right"></slot>
|
|
84
104
|
<slot></slot>
|
package/src/player.ts
CHANGED
|
@@ -44,6 +44,12 @@ export class Player implements IPlayer {
|
|
|
44
44
|
private _cover: HTMLButtonElement | null = null
|
|
45
45
|
private _coverDismissed = false
|
|
46
46
|
private _coverTimer: ReturnType<typeof setTimeout> | null = null
|
|
47
|
+
/**
|
|
48
|
+
* A play asked for before there was a provider to ask: a tap on the cover
|
|
49
|
+
* while the source was still loading. Kept and honoured once the source has
|
|
50
|
+
* loaded, so one tap plays the video rather than one tap per loading step.
|
|
51
|
+
*/
|
|
52
|
+
private _playRequested = false
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
55
|
* Create a new player instance
|
|
@@ -159,20 +165,39 @@ export class Player implements IPlayer {
|
|
|
159
165
|
button.setAttribute('aria-hidden', 'true')
|
|
160
166
|
button.style.cssText = 'display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,0.92);box-shadow:0 4px 18px rgba(0,0,0,0.35)'
|
|
161
167
|
const triangle = document.createElement('span')
|
|
168
|
+
triangle.className = 'ts-video-player__cover-play'
|
|
162
169
|
triangle.style.cssText = 'width:22px;height:24px;margin-left:5px;background:#0f172a;clip-path:polygon(0 0,100% 50%,0 100%)'
|
|
163
170
|
button.appendChild(triangle)
|
|
164
171
|
cover.appendChild(button)
|
|
165
172
|
cover.addEventListener('click', (event) => {
|
|
166
173
|
event.stopPropagation()
|
|
174
|
+
// The embed can take a few seconds to load on a phone: the tap is
|
|
175
|
+
// answered at once, with the play button turning into a spinner.
|
|
176
|
+
if (!cover.dataset.loading) {
|
|
177
|
+
cover.dataset.loading = ''
|
|
178
|
+
triangle.style.display = 'none'
|
|
179
|
+
const ring = document.createElement('span')
|
|
180
|
+
ring.className = 'ts-video-player__cover-spinner'
|
|
181
|
+
ring.style.cssText = 'width:26px;height:26px;border-radius:50%;border:3px solid rgba(15,23,42,0.2);border-top-color:#0f172a'
|
|
182
|
+
button.appendChild(ring)
|
|
183
|
+
ring.animate?.([{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }], { duration: 800, iterations: Infinity })
|
|
184
|
+
}
|
|
167
185
|
void this.play().catch(() => {})
|
|
168
186
|
// Some browsers only let a tap inside the embed start it. If this one
|
|
169
|
-
// did not, get out of the way so the embed's own button can.
|
|
187
|
+
// did not, get out of the way so the embed's own button can. Only once
|
|
188
|
+
// the embed is ready, though: before then nothing could have started,
|
|
189
|
+
// and lifting the cover early showed the embed half-loaded.
|
|
170
190
|
if (this._coverTimer) clearTimeout(this._coverTimer)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
let waits = 0
|
|
192
|
+
const settle = () => {
|
|
193
|
+
this._coverTimer = setTimeout(() => {
|
|
194
|
+
if (this.state.started) return
|
|
195
|
+
if (!this.state.canPlay && ++waits < 10) return settle()
|
|
196
|
+
this._coverDismissed = true
|
|
197
|
+
this.updateCover(this.state)
|
|
198
|
+
}, 2000)
|
|
199
|
+
}
|
|
200
|
+
settle()
|
|
176
201
|
})
|
|
177
202
|
this._el.appendChild(cover)
|
|
178
203
|
this._cover = cover
|
|
@@ -190,6 +215,13 @@ export class Player implements IPlayer {
|
|
|
190
215
|
const background = image ? `url("${image.replace(/["\\]/g, '')}")` : 'none'
|
|
191
216
|
if (cover.style.backgroundImage !== background) cover.style.backgroundImage = background
|
|
192
217
|
}
|
|
218
|
+
// Back to a play button once it is no longer waiting on a tap's play.
|
|
219
|
+
if (!show && cover.dataset.loading !== undefined) {
|
|
220
|
+
delete cover.dataset.loading
|
|
221
|
+
cover.querySelector('.ts-video-player__cover-spinner')?.remove()
|
|
222
|
+
const triangle = cover.querySelector<HTMLElement>('.ts-video-player__cover-play')
|
|
223
|
+
if (triangle) triangle.style.display = ''
|
|
224
|
+
}
|
|
193
225
|
// `hidden` alone would not do it: the inline display wins over it.
|
|
194
226
|
cover.hidden = !show
|
|
195
227
|
cover.style.display = show ? 'flex' : 'none'
|
|
@@ -432,6 +464,7 @@ export class Player implements IPlayer {
|
|
|
432
464
|
|
|
433
465
|
// Update state. A new source has not played yet, so its cover returns.
|
|
434
466
|
this._coverDismissed = false
|
|
467
|
+
this._playRequested = false
|
|
435
468
|
this._store.batch({
|
|
436
469
|
sources,
|
|
437
470
|
src: sources[0],
|
|
@@ -498,6 +531,10 @@ export class Player implements IPlayer {
|
|
|
498
531
|
mediaType: loader.mediaType(candidate.src),
|
|
499
532
|
providerType: loader.type,
|
|
500
533
|
})
|
|
534
|
+
if (this._playRequested) {
|
|
535
|
+
this._playRequested = false
|
|
536
|
+
void this._provider.play().catch(() => {})
|
|
537
|
+
}
|
|
501
538
|
return
|
|
502
539
|
}
|
|
503
540
|
catch (error) {
|
|
@@ -667,11 +704,15 @@ export class Player implements IPlayer {
|
|
|
667
704
|
// === Playback ===
|
|
668
705
|
|
|
669
706
|
async play(): Promise<void> {
|
|
670
|
-
if (!this._provider)
|
|
707
|
+
if (!this._provider) {
|
|
708
|
+
this._playRequested = true
|
|
709
|
+
return
|
|
710
|
+
}
|
|
671
711
|
await this._provider.play()
|
|
672
712
|
}
|
|
673
713
|
|
|
674
714
|
pause(): void {
|
|
715
|
+
this._playRequested = false
|
|
675
716
|
this._provider?.pause()
|
|
676
717
|
}
|
|
677
718
|
|
package/src/providers/youtube.ts
CHANGED
|
@@ -192,6 +192,12 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
192
192
|
private endTime?: number
|
|
193
193
|
private timeUpdateInterval: ReturnType<typeof setInterval> | null = null
|
|
194
194
|
private lastState = -1
|
|
195
|
+
/**
|
|
196
|
+
* A play asked for before YouTube's player could take it. Its methods only
|
|
197
|
+
* exist after its ready event, and the video may still be pending then, so
|
|
198
|
+
* the play waits for both and the video loads playing.
|
|
199
|
+
*/
|
|
200
|
+
private pendingPlay = false
|
|
195
201
|
|
|
196
202
|
get mediaElement(): HTMLIFrameElement | null {
|
|
197
203
|
return this.iframe
|
|
@@ -303,6 +309,7 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
303
309
|
this.player?.destroy()
|
|
304
310
|
this.player = null
|
|
305
311
|
this.playerReady = false
|
|
312
|
+
this.pendingPlay = false
|
|
306
313
|
this.iframe = null
|
|
307
314
|
}
|
|
308
315
|
|
|
@@ -324,8 +331,13 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
324
331
|
|
|
325
332
|
// Load pending video if any
|
|
326
333
|
if (this.pendingVideoId) {
|
|
327
|
-
|
|
334
|
+
const videoId = this.pendingVideoId
|
|
328
335
|
this.pendingVideoId = null
|
|
336
|
+
this.loadVideo(videoId, this.startTime, this.endTime)
|
|
337
|
+
}
|
|
338
|
+
else if (this.pendingPlay) {
|
|
339
|
+
this.pendingPlay = false
|
|
340
|
+
this.player?.playVideo()
|
|
329
341
|
}
|
|
330
342
|
}
|
|
331
343
|
|
|
@@ -340,6 +352,9 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
340
352
|
this.events.emit('loadstart')
|
|
341
353
|
break
|
|
342
354
|
case PlayerState.PLAYING:
|
|
355
|
+
// A video loaded playing never passes through CUED, where the
|
|
356
|
+
// duration is otherwise announced.
|
|
357
|
+
if (this.getDuration() > 0) this.events.emit('durationchange', this.getDuration())
|
|
343
358
|
this.events.emit('play')
|
|
344
359
|
this.events.emit('playing')
|
|
345
360
|
this.events.emit('statechange', { paused: false, playing: true, waiting: false })
|
|
@@ -415,7 +430,8 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
415
430
|
private loadVideo(videoId: string, startTime?: number, endTime?: number): void {
|
|
416
431
|
if (!this.player) return
|
|
417
432
|
|
|
418
|
-
if (this.options.autoplay) {
|
|
433
|
+
if (this.options.autoplay || this.pendingPlay) {
|
|
434
|
+
this.pendingPlay = false
|
|
419
435
|
this.player.loadVideoById({
|
|
420
436
|
videoId,
|
|
421
437
|
startSeconds: startTime,
|
|
@@ -433,15 +449,21 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
433
449
|
// === Playback ===
|
|
434
450
|
|
|
435
451
|
async play(): Promise<void> {
|
|
436
|
-
this.player
|
|
452
|
+
if (!this.player || !this.playerReady || this.pendingVideoId) {
|
|
453
|
+
this.pendingPlay = true
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
this.player.playVideo()
|
|
437
457
|
}
|
|
438
458
|
|
|
439
459
|
pause(): void {
|
|
440
|
-
this.
|
|
460
|
+
this.pendingPlay = false
|
|
461
|
+
if (this.playerReady) this.player?.pauseVideo()
|
|
441
462
|
}
|
|
442
463
|
|
|
443
464
|
stop(): void {
|
|
444
|
-
this.
|
|
465
|
+
this.pendingPlay = false
|
|
466
|
+
if (this.playerReady) this.player?.stopVideo()
|
|
445
467
|
}
|
|
446
468
|
|
|
447
469
|
// === Seeking ===
|