ts-video-player 0.1.4 → 0.1.5
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 +32 -32
- package/dist/{chunk-hfd9k46d.js → chunk-z21ez1kz.js} +74 -1
- package/dist/core/cover.d.ts +23 -0
- package/dist/elements/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/player.d.ts +17 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/core/cover.ts +42 -0
- package/src/elements/video-player.ts +3 -0
- package/src/player.ts +72 -1
- package/src/types.ts +2 -0
|
@@ -2009,6 +2009,19 @@ function getPreconnectHints2(src, loaders = defaultLoaders2) {
|
|
|
2009
2009
|
return loader?.preconnect?.() || [];
|
|
2010
2010
|
}
|
|
2011
2011
|
|
|
2012
|
+
// src/core/cover.ts
|
|
2013
|
+
function embedCover(state, dismissed) {
|
|
2014
|
+
const embed = state.providerType === "youtube" || state.providerType === "vimeo";
|
|
2015
|
+
if (!embed || state.started || state.error || dismissed)
|
|
2016
|
+
return { show: false, image: "" };
|
|
2017
|
+
if (state.poster)
|
|
2018
|
+
return { show: true, image: state.poster };
|
|
2019
|
+
const src = state.src;
|
|
2020
|
+
const url = typeof src === "string" ? src : src?.src || "";
|
|
2021
|
+
const youtubeId = state.providerType === "youtube" ? extractYouTubeId2(url) : null;
|
|
2022
|
+
return { show: true, image: youtubeId ? `https://i.ytimg.com/vi/${youtubeId}/hqdefault.jpg` : "" };
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2012
2025
|
// src/player.ts
|
|
2013
2026
|
class Player2 {
|
|
2014
2027
|
_el;
|
|
@@ -2021,6 +2034,9 @@ class Player2 {
|
|
|
2021
2034
|
_destroyed = false;
|
|
2022
2035
|
_sourceLoadId = 0;
|
|
2023
2036
|
_announcer = null;
|
|
2037
|
+
_cover = null;
|
|
2038
|
+
_coverDismissed = false;
|
|
2039
|
+
_coverTimer = null;
|
|
2024
2040
|
constructor(container, options = {}) {
|
|
2025
2041
|
if (typeof container === "string") {
|
|
2026
2042
|
const el = document.querySelector(container);
|
|
@@ -2068,6 +2084,7 @@ class Player2 {
|
|
|
2068
2084
|
mediaContainer.className = "ts-video-player__container";
|
|
2069
2085
|
mediaContainer.style.cssText = "position:relative;width:100%;height:100%";
|
|
2070
2086
|
this._el.appendChild(mediaContainer);
|
|
2087
|
+
this.setupCover();
|
|
2071
2088
|
if (this._options.announcements !== false) {
|
|
2072
2089
|
const liveRegion = document.createElement("div");
|
|
2073
2090
|
liveRegion.className = "ts-video-player__announcer";
|
|
@@ -2081,6 +2098,53 @@ class Player2 {
|
|
|
2081
2098
|
const _el = this._el;
|
|
2082
2099
|
_el.__videoPlayer = this;
|
|
2083
2100
|
}
|
|
2101
|
+
setupCover() {
|
|
2102
|
+
const cover = document.createElement("button");
|
|
2103
|
+
cover.type = "button";
|
|
2104
|
+
cover.className = "ts-video-player__cover";
|
|
2105
|
+
cover.setAttribute("aria-label", `Play ${this._options.title || "video"}`);
|
|
2106
|
+
cover.hidden = true;
|
|
2107
|
+
cover.style.cssText = "position:absolute;inset:0;z-index:2;display:none;align-items:center;justify-content:center;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:inherit;background:#000 center/cover no-repeat;cursor:pointer;-webkit-tap-highlight-color:transparent";
|
|
2108
|
+
const button = document.createElement("span");
|
|
2109
|
+
button.setAttribute("aria-hidden", "true");
|
|
2110
|
+
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)";
|
|
2111
|
+
const triangle = document.createElement("span");
|
|
2112
|
+
triangle.style.cssText = "width:22px;height:24px;margin-left:5px;background:#0f172a;clip-path:polygon(0 0,100% 50%,0 100%)";
|
|
2113
|
+
button.appendChild(triangle);
|
|
2114
|
+
cover.appendChild(button);
|
|
2115
|
+
cover.addEventListener("click", (event) => {
|
|
2116
|
+
event.stopPropagation();
|
|
2117
|
+
this.play().catch(() => {});
|
|
2118
|
+
if (this._coverTimer)
|
|
2119
|
+
clearTimeout(this._coverTimer);
|
|
2120
|
+
this._coverTimer = setTimeout(() => {
|
|
2121
|
+
if (this.state.started)
|
|
2122
|
+
return;
|
|
2123
|
+
this._coverDismissed = true;
|
|
2124
|
+
this.updateCover(this.state);
|
|
2125
|
+
}, 2000);
|
|
2126
|
+
});
|
|
2127
|
+
this._el.appendChild(cover);
|
|
2128
|
+
this._cover = cover;
|
|
2129
|
+
this._cleanupFns.push(() => {
|
|
2130
|
+
if (this._coverTimer)
|
|
2131
|
+
clearTimeout(this._coverTimer);
|
|
2132
|
+
cover.remove();
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
updateCover(state) {
|
|
2136
|
+
const cover = this._cover;
|
|
2137
|
+
if (!cover)
|
|
2138
|
+
return;
|
|
2139
|
+
const { show, image } = embedCover(state, this._coverDismissed);
|
|
2140
|
+
if (show) {
|
|
2141
|
+
const background = image ? `url("${image.replace(/["\\]/g, "")}")` : "none";
|
|
2142
|
+
if (cover.style.backgroundImage !== background)
|
|
2143
|
+
cover.style.backgroundImage = background;
|
|
2144
|
+
}
|
|
2145
|
+
cover.hidden = !show;
|
|
2146
|
+
cover.style.display = show ? "flex" : "none";
|
|
2147
|
+
}
|
|
2084
2148
|
setupState() {
|
|
2085
2149
|
this._store.batch({
|
|
2086
2150
|
volume: this._options.volume ?? 1,
|
|
@@ -2095,6 +2159,7 @@ class Player2 {
|
|
|
2095
2159
|
});
|
|
2096
2160
|
const unsubscribe = this._store.subscribe((state) => {
|
|
2097
2161
|
this.updateAttributes(state);
|
|
2162
|
+
this.updateCover(state);
|
|
2098
2163
|
});
|
|
2099
2164
|
this._cleanupFns.push(unsubscribe);
|
|
2100
2165
|
}
|
|
@@ -2238,11 +2303,13 @@ class Player2 {
|
|
|
2238
2303
|
if (sources.length === 0) {
|
|
2239
2304
|
return;
|
|
2240
2305
|
}
|
|
2306
|
+
this._coverDismissed = false;
|
|
2241
2307
|
this._store.batch({
|
|
2242
2308
|
sources,
|
|
2243
2309
|
src: sources[0],
|
|
2244
2310
|
loadingState: "loading",
|
|
2245
|
-
error: null
|
|
2311
|
+
error: null,
|
|
2312
|
+
started: false
|
|
2246
2313
|
});
|
|
2247
2314
|
this._events.emit("sourceschange", sources);
|
|
2248
2315
|
const candidates = findSourceCandidates(sources, this._loaders);
|
|
@@ -2311,6 +2378,9 @@ class Player2 {
|
|
|
2311
2378
|
error: { code: 4, message: "Failed to load all compatible sources", details: lastError }
|
|
2312
2379
|
});
|
|
2313
2380
|
}
|
|
2381
|
+
setPoster(url) {
|
|
2382
|
+
this._store.set("poster", url);
|
|
2383
|
+
}
|
|
2314
2384
|
getSrc() {
|
|
2315
2385
|
return this.state.src;
|
|
2316
2386
|
}
|
|
@@ -2629,6 +2699,9 @@ class VideoPlayerElement2 extends HTMLElementBase {
|
|
|
2629
2699
|
if (value)
|
|
2630
2700
|
this._player.setSrc(value);
|
|
2631
2701
|
break;
|
|
2702
|
+
case "poster":
|
|
2703
|
+
this._player.setPoster(value || "");
|
|
2704
|
+
break;
|
|
2632
2705
|
case "muted":
|
|
2633
2706
|
this._player.setMuted(value !== null);
|
|
2634
2707
|
break;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The poster over an embedded video until it plays: when to show it, and
|
|
3
|
+
* with which image.
|
|
4
|
+
*
|
|
5
|
+
* @module core/cover
|
|
6
|
+
*/
|
|
7
|
+
import type { PlayerState } from '../types';
|
|
8
|
+
export interface EmbedCover {
|
|
9
|
+
show: boolean;
|
|
10
|
+
/** The poster to draw, or '' for a plain black cover. */
|
|
11
|
+
image: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A YouTube or Vimeo embed draws its own chrome before playback (a title bar,
|
|
15
|
+
* a play button, "Watch on YouTube", and on iOS the system's controls for the
|
|
16
|
+
* embed's own video), none of which can be styled from outside the iframe.
|
|
17
|
+
* Until the first play the player covers it with its poster instead: the one
|
|
18
|
+
* the page gave it, or else YouTube's thumbnail for the video.
|
|
19
|
+
*
|
|
20
|
+
* `dismissed` is set when a tap on the cover did not start playback, so the
|
|
21
|
+
* embed's own button is left reachable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function embedCover(state: Pick<PlayerState, 'providerType' | 'started' | 'error' | 'poster' | 'src'>, dismissed: boolean): EmbedCover;
|
package/dist/elements/index.js
CHANGED
package/dist/index.js
CHANGED
package/dist/player.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare class Player implements IPlayer {
|
|
|
20
20
|
private _destroyed;
|
|
21
21
|
private _sourceLoadId;
|
|
22
22
|
private _announcer;
|
|
23
|
+
private _cover;
|
|
24
|
+
private _coverDismissed;
|
|
25
|
+
private _coverTimer;
|
|
23
26
|
/**
|
|
24
27
|
* Create a new player instance
|
|
25
28
|
*/
|
|
@@ -29,6 +32,18 @@ export declare class Player implements IPlayer {
|
|
|
29
32
|
get provider(): Provider | null;
|
|
30
33
|
get ready(): boolean;
|
|
31
34
|
private setupElement;
|
|
35
|
+
/**
|
|
36
|
+
* The poster over an embedded video until it plays.
|
|
37
|
+
*
|
|
38
|
+
* A YouTube or Vimeo embed draws its own chrome before playback: a title
|
|
39
|
+
* bar, a play button, "Watch on YouTube", and on iOS the system's controls
|
|
40
|
+
* for the embed's own video. None of it can be styled from outside the
|
|
41
|
+
* iframe, and it stacks under the player's controls. Until the first play,
|
|
42
|
+
* the player shows its poster and one play button over the embed instead;
|
|
43
|
+
* once the video plays the embed's chrome is gone and the cover lifts.
|
|
44
|
+
*/
|
|
45
|
+
private setupCover;
|
|
46
|
+
private updateCover;
|
|
32
47
|
private setupState;
|
|
33
48
|
private setupKeyboard;
|
|
34
49
|
private setupActivity;
|
|
@@ -42,6 +57,8 @@ export declare class Player implements IPlayer {
|
|
|
42
57
|
private announce;
|
|
43
58
|
destroy(): void;
|
|
44
59
|
setSrc(src: Src | Src[]): Promise<void>;
|
|
60
|
+
/** The image shown before playback, and on an embed's cover. */
|
|
61
|
+
setPoster(url: string): void;
|
|
45
62
|
getSrc(): Src | null;
|
|
46
63
|
private attachProviderEvents;
|
|
47
64
|
play(): Promise<void>;
|
package/dist/types.d.ts
CHANGED
|
@@ -632,6 +632,8 @@ export interface Player {
|
|
|
632
632
|
destroy(): void;
|
|
633
633
|
setSrc(src: Src | Src[]): void;
|
|
634
634
|
getSrc(): Src | null;
|
|
635
|
+
/** The image shown before playback, and on an embed's cover */
|
|
636
|
+
setPoster(url: string): void;
|
|
635
637
|
play(): Promise<void>;
|
|
636
638
|
pause(): void;
|
|
637
639
|
togglePlay(): void;
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The poster over an embedded video until it plays: when to show it, and
|
|
3
|
+
* with which image.
|
|
4
|
+
*
|
|
5
|
+
* @module core/cover
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PlayerState } from '../types'
|
|
9
|
+
import { extractYouTubeId } from '../providers/youtube'
|
|
10
|
+
|
|
11
|
+
export interface EmbedCover {
|
|
12
|
+
show: boolean
|
|
13
|
+
/** The poster to draw, or '' for a plain black cover. */
|
|
14
|
+
image: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A YouTube or Vimeo embed draws its own chrome before playback (a title bar,
|
|
19
|
+
* a play button, "Watch on YouTube", and on iOS the system's controls for the
|
|
20
|
+
* embed's own video), none of which can be styled from outside the iframe.
|
|
21
|
+
* Until the first play the player covers it with its poster instead: the one
|
|
22
|
+
* the page gave it, or else YouTube's thumbnail for the video.
|
|
23
|
+
*
|
|
24
|
+
* `dismissed` is set when a tap on the cover did not start playback, so the
|
|
25
|
+
* embed's own button is left reachable.
|
|
26
|
+
*/
|
|
27
|
+
export function embedCover(
|
|
28
|
+
state: Pick<PlayerState, 'providerType' | 'started' | 'error' | 'poster' | 'src'>,
|
|
29
|
+
dismissed: boolean,
|
|
30
|
+
): EmbedCover {
|
|
31
|
+
const embed = state.providerType === 'youtube' || state.providerType === 'vimeo'
|
|
32
|
+
if (!embed || state.started || state.error || dismissed)
|
|
33
|
+
return { show: false, image: '' }
|
|
34
|
+
|
|
35
|
+
if (state.poster)
|
|
36
|
+
return { show: true, image: state.poster }
|
|
37
|
+
|
|
38
|
+
const src = state.src
|
|
39
|
+
const url = typeof src === 'string' ? src : src?.src || ''
|
|
40
|
+
const youtubeId = state.providerType === 'youtube' ? extractYouTubeId(url) : null
|
|
41
|
+
return { show: true, image: youtubeId ? `https://i.ytimg.com/vi/${youtubeId}/hqdefault.jpg` : '' }
|
|
42
|
+
}
|
package/src/player.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { EventEmitter, createKeyboardHandler, createActivityDetector, type Keybo
|
|
|
21
21
|
import { createMediaSession, type MediaSessionOptions } from './core/media-session'
|
|
22
22
|
import { lockOrientation, unlockOrientation } from './core/orientation'
|
|
23
23
|
import { defaultLoaders, findSourceCandidates } from './providers'
|
|
24
|
+
import { embedCover } from './core/cover'
|
|
24
25
|
|
|
25
26
|
// =============================================================================
|
|
26
27
|
// Player Class
|
|
@@ -40,6 +41,9 @@ export class Player implements IPlayer {
|
|
|
40
41
|
private _destroyed = false
|
|
41
42
|
private _sourceLoadId = 0
|
|
42
43
|
private _announcer: HTMLElement | null = null
|
|
44
|
+
private _cover: HTMLButtonElement | null = null
|
|
45
|
+
private _coverDismissed = false
|
|
46
|
+
private _coverTimer: ReturnType<typeof setTimeout> | null = null
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* Create a new player instance
|
|
@@ -114,6 +118,8 @@ export class Player implements IPlayer {
|
|
|
114
118
|
mediaContainer.style.cssText = 'position:relative;width:100%;height:100%'
|
|
115
119
|
this._el.appendChild(mediaContainer)
|
|
116
120
|
|
|
121
|
+
this.setupCover()
|
|
122
|
+
|
|
117
123
|
// Create ARIA live region for screen reader announcements
|
|
118
124
|
if (this._options.announcements !== false) {
|
|
119
125
|
const liveRegion = document.createElement('div')
|
|
@@ -132,6 +138,63 @@ export class Player implements IPlayer {
|
|
|
132
138
|
_el.__videoPlayer = this
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The poster over an embedded video until it plays.
|
|
143
|
+
*
|
|
144
|
+
* A YouTube or Vimeo embed draws its own chrome before playback: a title
|
|
145
|
+
* bar, a play button, "Watch on YouTube", and on iOS the system's controls
|
|
146
|
+
* for the embed's own video. None of it can be styled from outside the
|
|
147
|
+
* iframe, and it stacks under the player's controls. Until the first play,
|
|
148
|
+
* the player shows its poster and one play button over the embed instead;
|
|
149
|
+
* once the video plays the embed's chrome is gone and the cover lifts.
|
|
150
|
+
*/
|
|
151
|
+
private setupCover(): void {
|
|
152
|
+
const cover = document.createElement('button')
|
|
153
|
+
cover.type = 'button'
|
|
154
|
+
cover.className = 'ts-video-player__cover'
|
|
155
|
+
cover.setAttribute('aria-label', `Play ${this._options.title || 'video'}`)
|
|
156
|
+
cover.hidden = true
|
|
157
|
+
cover.style.cssText = 'position:absolute;inset:0;z-index:2;display:none;align-items:center;justify-content:center;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:inherit;background:#000 center/cover no-repeat;cursor:pointer;-webkit-tap-highlight-color:transparent'
|
|
158
|
+
const button = document.createElement('span')
|
|
159
|
+
button.setAttribute('aria-hidden', 'true')
|
|
160
|
+
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
|
+
const triangle = document.createElement('span')
|
|
162
|
+
triangle.style.cssText = 'width:22px;height:24px;margin-left:5px;background:#0f172a;clip-path:polygon(0 0,100% 50%,0 100%)'
|
|
163
|
+
button.appendChild(triangle)
|
|
164
|
+
cover.appendChild(button)
|
|
165
|
+
cover.addEventListener('click', (event) => {
|
|
166
|
+
event.stopPropagation()
|
|
167
|
+
void this.play().catch(() => {})
|
|
168
|
+
// 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.
|
|
170
|
+
if (this._coverTimer) clearTimeout(this._coverTimer)
|
|
171
|
+
this._coverTimer = setTimeout(() => {
|
|
172
|
+
if (this.state.started) return
|
|
173
|
+
this._coverDismissed = true
|
|
174
|
+
this.updateCover(this.state)
|
|
175
|
+
}, 2000)
|
|
176
|
+
})
|
|
177
|
+
this._el.appendChild(cover)
|
|
178
|
+
this._cover = cover
|
|
179
|
+
this._cleanupFns.push(() => {
|
|
180
|
+
if (this._coverTimer) clearTimeout(this._coverTimer)
|
|
181
|
+
cover.remove()
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private updateCover(state: PlayerState): void {
|
|
186
|
+
const cover = this._cover
|
|
187
|
+
if (!cover) return
|
|
188
|
+
const { show, image } = embedCover(state, this._coverDismissed)
|
|
189
|
+
if (show) {
|
|
190
|
+
const background = image ? `url("${image.replace(/["\\]/g, '')}")` : 'none'
|
|
191
|
+
if (cover.style.backgroundImage !== background) cover.style.backgroundImage = background
|
|
192
|
+
}
|
|
193
|
+
// `hidden` alone would not do it: the inline display wins over it.
|
|
194
|
+
cover.hidden = !show
|
|
195
|
+
cover.style.display = show ? 'flex' : 'none'
|
|
196
|
+
}
|
|
197
|
+
|
|
135
198
|
private setupState(): void {
|
|
136
199
|
// Initial state from options
|
|
137
200
|
this._store.batch({
|
|
@@ -150,6 +213,7 @@ export class Player implements IPlayer {
|
|
|
150
213
|
const unsubscribe = this._store.subscribe((state) => {
|
|
151
214
|
// Update DOM attributes
|
|
152
215
|
this.updateAttributes(state)
|
|
216
|
+
this.updateCover(state)
|
|
153
217
|
})
|
|
154
218
|
|
|
155
219
|
this._cleanupFns.push(unsubscribe)
|
|
@@ -366,12 +430,14 @@ export class Player implements IPlayer {
|
|
|
366
430
|
return
|
|
367
431
|
}
|
|
368
432
|
|
|
369
|
-
// Update state
|
|
433
|
+
// Update state. A new source has not played yet, so its cover returns.
|
|
434
|
+
this._coverDismissed = false
|
|
370
435
|
this._store.batch({
|
|
371
436
|
sources,
|
|
372
437
|
src: sources[0],
|
|
373
438
|
loadingState: 'loading',
|
|
374
439
|
error: null,
|
|
440
|
+
started: false,
|
|
375
441
|
})
|
|
376
442
|
|
|
377
443
|
this._events.emit('sourceschange', sources)
|
|
@@ -451,6 +517,11 @@ export class Player implements IPlayer {
|
|
|
451
517
|
})
|
|
452
518
|
}
|
|
453
519
|
|
|
520
|
+
/** The image shown before playback, and on an embed's cover. */
|
|
521
|
+
setPoster(url: string): void {
|
|
522
|
+
this._store.set('poster', url)
|
|
523
|
+
}
|
|
524
|
+
|
|
454
525
|
getSrc(): Src | null {
|
|
455
526
|
return this.state.src
|
|
456
527
|
}
|
package/src/types.ts
CHANGED