ts-video-player 0.1.7 → 0.1.9
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 +74 -50
- package/dist/{chunk-24n6adw2.js → chunk-fvxawgp5.js} +714 -106
- package/dist/{chunk-ds1nj487.js → chunk-ndbfytd6.js} +1 -1
- package/dist/{chunk-rhykj0qy.js → chunk-pvq6e7gz.js} +17 -3
- package/dist/core/events.d.ts +3 -0
- package/dist/core/i18n.d.ts +24 -0
- package/dist/elements/index.js +2 -2
- package/dist/elements/utils.d.ts +16 -2
- package/dist/features/index.js +1 -1
- package/dist/index.js +30 -421
- package/dist/player.d.ts +6 -0
- package/dist/providers/vimeo.d.ts +6 -0
- package/package.json +1 -1
- package/src/core/events.ts +19 -3
- package/src/core/i18n.ts +173 -1
- package/src/elements/media-airplay-button.ts +4 -2
- package/src/elements/media-fullscreen-button.ts +5 -3
- package/src/elements/media-live-button.ts +4 -2
- package/src/elements/media-mute-button.ts +5 -3
- package/src/elements/media-pip-button.ts +5 -2
- package/src/elements/media-play-button.ts +5 -3
- package/src/elements/media-playback-rate-button.ts +5 -3
- package/src/elements/media-progress-bar.ts +5 -3
- package/src/elements/media-remote-playback-button.ts +4 -2
- package/src/elements/media-seek-button.ts +4 -3
- package/src/elements/media-settings-menu.ts +16 -14
- package/src/elements/media-time-display.ts +2 -2
- package/src/elements/media-volume-slider.ts +4 -2
- package/src/elements/utils.ts +40 -6
- package/src/elements/video-player.ts +3 -1
- package/src/player.ts +36 -14
- package/src/providers/vimeo.ts +15 -3
package/dist/player.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ export declare class Player implements IPlayer {
|
|
|
67
67
|
setPoster(url: string): void;
|
|
68
68
|
getSrc(): Src | null;
|
|
69
69
|
private attachProviderEvents;
|
|
70
|
+
/**
|
|
71
|
+
* Play what was asked for before the source was ready. A browser that
|
|
72
|
+
* refuses sound without a tap (code 1) gets it muted instead, as autoplay
|
|
73
|
+
* is everywhere, and the mute button turns it up.
|
|
74
|
+
*/
|
|
75
|
+
private startRequestedPlay;
|
|
70
76
|
play(): Promise<void>;
|
|
71
77
|
pause(): void;
|
|
72
78
|
togglePlay(): void;
|
|
@@ -48,6 +48,10 @@ interface VimeoPlayer {
|
|
|
48
48
|
pause(): Promise<void>;
|
|
49
49
|
unload(): Promise<void>;
|
|
50
50
|
destroy(): Promise<void>;
|
|
51
|
+
loadVideo(id: number | string | {
|
|
52
|
+
id: number;
|
|
53
|
+
h?: string;
|
|
54
|
+
}): Promise<number>;
|
|
51
55
|
getCurrentTime(): Promise<number>;
|
|
52
56
|
setCurrentTime(seconds: number): Promise<number>;
|
|
53
57
|
getDuration(): Promise<number>;
|
|
@@ -118,6 +122,8 @@ export declare class VimeoProvider extends BaseProvider {
|
|
|
118
122
|
protected cleanup(): void;
|
|
119
123
|
canPlay(src: Src): boolean;
|
|
120
124
|
load(src: Src): Promise<void>;
|
|
125
|
+
/** What a loaded video tells the player: ready, its length, qualities and shape. */
|
|
126
|
+
private announceLoaded;
|
|
121
127
|
private attachVimeoEvents;
|
|
122
128
|
private updateQualities;
|
|
123
129
|
play(): Promise<void>;
|
package/package.json
CHANGED
package/src/core/events.ts
CHANGED
|
@@ -208,7 +208,12 @@ export class MediaEventsNormalizer {
|
|
|
208
208
|
export function onFullscreenChange(handler: (fullscreen: boolean) => void): () => void {
|
|
209
209
|
const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']
|
|
210
210
|
|
|
211
|
-
const listener = () => {
|
|
211
|
+
const listener = (event?: Event) => {
|
|
212
|
+
// The browser's own change only. <video-player> re-dispatches the player's
|
|
213
|
+
// events on itself, bubbling and composed, under the same names; its
|
|
214
|
+
// "fullscreenchange" reached this document listener, which announced it
|
|
215
|
+
// again, and the two fed each other until the stack overflowed.
|
|
216
|
+
if (event && typeof CustomEvent !== 'undefined' && event instanceof CustomEvent) return
|
|
212
217
|
const fullscreen = !!(
|
|
213
218
|
document.fullscreenElement ||
|
|
214
219
|
(document as any).webkitFullscreenElement ||
|
|
@@ -261,8 +266,8 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: Record<string, string> = {
|
|
|
261
266
|
c: 'toggleCaptions',
|
|
262
267
|
ArrowLeft: 'seekBackward',
|
|
263
268
|
ArrowRight: 'seekForward',
|
|
264
|
-
j: '
|
|
265
|
-
l: '
|
|
269
|
+
j: 'seekBackwardLong',
|
|
270
|
+
l: 'seekForwardLong',
|
|
266
271
|
ArrowUp: 'volumeUp',
|
|
267
272
|
ArrowDown: 'volumeDown',
|
|
268
273
|
'>': 'speedUp',
|
|
@@ -292,6 +297,9 @@ export interface KeyboardActions {
|
|
|
292
297
|
toggleCaptions(): void
|
|
293
298
|
seekBackward(): void
|
|
294
299
|
seekForward(): void
|
|
300
|
+
/** J and L: ten seconds, as YouTube's keys go (the arrows are five). Falls back to seekBackward/seekForward. */
|
|
301
|
+
seekBackwardLong?(): void
|
|
302
|
+
seekForwardLong?(): void
|
|
295
303
|
volumeUp(): void
|
|
296
304
|
volumeDown(): void
|
|
297
305
|
speedUp(): void
|
|
@@ -351,6 +359,14 @@ export function createKeyboardHandler(
|
|
|
351
359
|
case 'seekForward':
|
|
352
360
|
actions.seekForward()
|
|
353
361
|
break
|
|
362
|
+
case 'seekBackwardLong':
|
|
363
|
+
if (actions.seekBackwardLong) actions.seekBackwardLong()
|
|
364
|
+
else actions.seekBackward()
|
|
365
|
+
break
|
|
366
|
+
case 'seekForwardLong':
|
|
367
|
+
if (actions.seekForwardLong) actions.seekForwardLong()
|
|
368
|
+
else actions.seekForward()
|
|
369
|
+
break
|
|
354
370
|
case 'volumeUp':
|
|
355
371
|
actions.volumeUp()
|
|
356
372
|
break
|
package/src/core/i18n.ts
CHANGED
|
@@ -74,6 +74,31 @@ export interface TranslationStrings {
|
|
|
74
74
|
close: string
|
|
75
75
|
menu: string
|
|
76
76
|
back: string
|
|
77
|
+
// The controls' own names and what a screen reader hears (elements/*)
|
|
78
|
+
seek: string
|
|
79
|
+
playbackRate: string
|
|
80
|
+
airPlayPlay: string
|
|
81
|
+
remotePlayback: string
|
|
82
|
+
videoPlayer: string
|
|
83
|
+
videoRole: string
|
|
84
|
+
playTitle: string
|
|
85
|
+
audio: string
|
|
86
|
+
defaultTrack: string
|
|
87
|
+
playing: string
|
|
88
|
+
paused: string
|
|
89
|
+
muted: string
|
|
90
|
+
volumeLevel: string
|
|
91
|
+
ended: string
|
|
92
|
+
enteredFullscreen: string
|
|
93
|
+
exitedFullscreen: string
|
|
94
|
+
errorMessage: string
|
|
95
|
+
timeOf: string
|
|
96
|
+
hourOne: string
|
|
97
|
+
hourOther: string
|
|
98
|
+
minuteOne: string
|
|
99
|
+
minuteOther: string
|
|
100
|
+
secondOne: string
|
|
101
|
+
secondOther: string
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
export type TranslationKey = keyof TranslationStrings
|
|
@@ -157,6 +182,30 @@ export const englishStrings: TranslationStrings = {
|
|
|
157
182
|
close: 'Close',
|
|
158
183
|
menu: 'Menu',
|
|
159
184
|
back: 'Back',
|
|
185
|
+
seek: 'Seek',
|
|
186
|
+
playbackRate: 'Playback speed {rate}x',
|
|
187
|
+
airPlayPlay: 'Play on AirPlay',
|
|
188
|
+
remotePlayback: 'Play on another device',
|
|
189
|
+
videoPlayer: 'Video player',
|
|
190
|
+
videoRole: 'video player',
|
|
191
|
+
playTitle: 'Play {title}',
|
|
192
|
+
audio: 'Audio',
|
|
193
|
+
defaultTrack: 'Default',
|
|
194
|
+
playing: 'Playing',
|
|
195
|
+
paused: 'Paused',
|
|
196
|
+
muted: 'Muted',
|
|
197
|
+
volumeLevel: 'Volume {percent}%',
|
|
198
|
+
ended: 'Playback ended',
|
|
199
|
+
enteredFullscreen: 'Entered fullscreen',
|
|
200
|
+
exitedFullscreen: 'Exited fullscreen',
|
|
201
|
+
errorMessage: 'Error: {message}',
|
|
202
|
+
timeOf: '{current} of {duration}',
|
|
203
|
+
hourOne: '{n} hour',
|
|
204
|
+
hourOther: '{n} hours',
|
|
205
|
+
minuteOne: '{n} minute',
|
|
206
|
+
minuteOther: '{n} minutes',
|
|
207
|
+
secondOne: '{n} second',
|
|
208
|
+
secondOther: '{n} seconds',
|
|
160
209
|
}
|
|
161
210
|
|
|
162
211
|
export const spanishStrings: TranslationStrings = {
|
|
@@ -209,6 +258,30 @@ export const spanishStrings: TranslationStrings = {
|
|
|
209
258
|
close: 'Cerrar',
|
|
210
259
|
menu: 'Menú',
|
|
211
260
|
back: 'Volver',
|
|
261
|
+
seek: 'Buscar',
|
|
262
|
+
playbackRate: 'Velocidad de reproducción {rate}x',
|
|
263
|
+
airPlayPlay: 'Reproducir con AirPlay',
|
|
264
|
+
remotePlayback: 'Reproducir en otro dispositivo',
|
|
265
|
+
videoPlayer: 'Reproductor de vídeo',
|
|
266
|
+
videoRole: 'reproductor de vídeo',
|
|
267
|
+
playTitle: 'Reproducir {title}',
|
|
268
|
+
audio: 'Audio',
|
|
269
|
+
defaultTrack: 'Predeterminado',
|
|
270
|
+
playing: 'Reproduciendo',
|
|
271
|
+
paused: 'En pausa',
|
|
272
|
+
muted: 'Silenciado',
|
|
273
|
+
volumeLevel: 'Volumen {percent}%',
|
|
274
|
+
ended: 'Reproducción finalizada',
|
|
275
|
+
enteredFullscreen: 'Pantalla completa activada',
|
|
276
|
+
exitedFullscreen: 'Pantalla completa desactivada',
|
|
277
|
+
errorMessage: 'Error: {message}',
|
|
278
|
+
timeOf: '{current} de {duration}',
|
|
279
|
+
hourOne: '{n} hora',
|
|
280
|
+
hourOther: '{n} horas',
|
|
281
|
+
minuteOne: '{n} minuto',
|
|
282
|
+
minuteOther: '{n} minutos',
|
|
283
|
+
secondOne: '{n} segundo',
|
|
284
|
+
secondOther: '{n} segundos',
|
|
212
285
|
}
|
|
213
286
|
|
|
214
287
|
export const frenchStrings: TranslationStrings = {
|
|
@@ -261,6 +334,30 @@ export const frenchStrings: TranslationStrings = {
|
|
|
261
334
|
close: 'Fermer',
|
|
262
335
|
menu: 'Menu',
|
|
263
336
|
back: 'Retour',
|
|
337
|
+
seek: 'Rechercher',
|
|
338
|
+
playbackRate: 'Vitesse de lecture {rate}x',
|
|
339
|
+
airPlayPlay: 'Lire avec AirPlay',
|
|
340
|
+
remotePlayback: 'Lire sur un autre appareil',
|
|
341
|
+
videoPlayer: 'Lecteur vidéo',
|
|
342
|
+
videoRole: 'lecteur vidéo',
|
|
343
|
+
playTitle: 'Lire {title}',
|
|
344
|
+
audio: 'Audio',
|
|
345
|
+
defaultTrack: 'Par défaut',
|
|
346
|
+
playing: 'Lecture',
|
|
347
|
+
paused: 'En pause',
|
|
348
|
+
muted: 'Son coupé',
|
|
349
|
+
volumeLevel: 'Volume {percent} %',
|
|
350
|
+
ended: 'Lecture terminée',
|
|
351
|
+
enteredFullscreen: 'Plein écran activé',
|
|
352
|
+
exitedFullscreen: 'Plein écran désactivé',
|
|
353
|
+
errorMessage: 'Erreur : {message}',
|
|
354
|
+
timeOf: '{current} sur {duration}',
|
|
355
|
+
hourOne: '{n} heure',
|
|
356
|
+
hourOther: '{n} heures',
|
|
357
|
+
minuteOne: '{n} minute',
|
|
358
|
+
minuteOther: '{n} minutes',
|
|
359
|
+
secondOne: '{n} seconde',
|
|
360
|
+
secondOther: '{n} secondes',
|
|
264
361
|
}
|
|
265
362
|
|
|
266
363
|
export const germanStrings: TranslationStrings = {
|
|
@@ -313,6 +410,30 @@ export const germanStrings: TranslationStrings = {
|
|
|
313
410
|
close: 'Schließen',
|
|
314
411
|
menu: 'Menü',
|
|
315
412
|
back: 'Zurück',
|
|
413
|
+
seek: 'Spulen',
|
|
414
|
+
playbackRate: 'Wiedergabegeschwindigkeit {rate}x',
|
|
415
|
+
airPlayPlay: 'Über AirPlay abspielen',
|
|
416
|
+
remotePlayback: 'Auf einem anderen Gerät abspielen',
|
|
417
|
+
videoPlayer: 'Videoplayer',
|
|
418
|
+
videoRole: 'Videoplayer',
|
|
419
|
+
playTitle: '{title} abspielen',
|
|
420
|
+
audio: 'Audio',
|
|
421
|
+
defaultTrack: 'Standard',
|
|
422
|
+
playing: 'Wird abgespielt',
|
|
423
|
+
paused: 'Pausiert',
|
|
424
|
+
muted: 'Stumm',
|
|
425
|
+
volumeLevel: 'Lautstärke {percent} %',
|
|
426
|
+
ended: 'Wiedergabe beendet',
|
|
427
|
+
enteredFullscreen: 'Vollbild an',
|
|
428
|
+
exitedFullscreen: 'Vollbild aus',
|
|
429
|
+
errorMessage: 'Fehler: {message}',
|
|
430
|
+
timeOf: '{current} von {duration}',
|
|
431
|
+
hourOne: '{n} Stunde',
|
|
432
|
+
hourOther: '{n} Stunden',
|
|
433
|
+
minuteOne: '{n} Minute',
|
|
434
|
+
minuteOther: '{n} Minuten',
|
|
435
|
+
secondOne: '{n} Sekunde',
|
|
436
|
+
secondOther: '{n} Sekunden',
|
|
316
437
|
}
|
|
317
438
|
|
|
318
439
|
export const japaneseStrings: TranslationStrings = {
|
|
@@ -365,6 +486,30 @@ export const japaneseStrings: TranslationStrings = {
|
|
|
365
486
|
close: '閉じる',
|
|
366
487
|
menu: 'メニュー',
|
|
367
488
|
back: '戻る',
|
|
489
|
+
seek: 'シーク',
|
|
490
|
+
playbackRate: '再生速度 {rate}x',
|
|
491
|
+
airPlayPlay: 'AirPlayで再生',
|
|
492
|
+
remotePlayback: '別のデバイスで再生',
|
|
493
|
+
videoPlayer: '動画プレーヤー',
|
|
494
|
+
videoRole: '動画プレーヤー',
|
|
495
|
+
playTitle: '{title}を再生',
|
|
496
|
+
audio: '音声',
|
|
497
|
+
defaultTrack: 'デフォルト',
|
|
498
|
+
playing: '再生中',
|
|
499
|
+
paused: '一時停止中',
|
|
500
|
+
muted: 'ミュート',
|
|
501
|
+
volumeLevel: '音量 {percent}%',
|
|
502
|
+
ended: '再生終了',
|
|
503
|
+
enteredFullscreen: '全画面表示',
|
|
504
|
+
exitedFullscreen: '全画面表示を終了',
|
|
505
|
+
errorMessage: 'エラー: {message}',
|
|
506
|
+
timeOf: '{current} / {duration}',
|
|
507
|
+
hourOne: '{n}時間',
|
|
508
|
+
hourOther: '{n}時間',
|
|
509
|
+
minuteOne: '{n}分',
|
|
510
|
+
minuteOther: '{n}分',
|
|
511
|
+
secondOne: '{n}秒',
|
|
512
|
+
secondOther: '{n}秒',
|
|
368
513
|
}
|
|
369
514
|
|
|
370
515
|
export const chineseStrings: TranslationStrings = {
|
|
@@ -417,6 +562,30 @@ export const chineseStrings: TranslationStrings = {
|
|
|
417
562
|
close: '关闭',
|
|
418
563
|
menu: '菜单',
|
|
419
564
|
back: '返回',
|
|
565
|
+
seek: '跳转',
|
|
566
|
+
playbackRate: '播放速度 {rate}x',
|
|
567
|
+
airPlayPlay: '通过 AirPlay 播放',
|
|
568
|
+
remotePlayback: '在其他设备上播放',
|
|
569
|
+
videoPlayer: '视频播放器',
|
|
570
|
+
videoRole: '视频播放器',
|
|
571
|
+
playTitle: '播放{title}',
|
|
572
|
+
audio: '音频',
|
|
573
|
+
defaultTrack: '默认',
|
|
574
|
+
playing: '正在播放',
|
|
575
|
+
paused: '已暂停',
|
|
576
|
+
muted: '已静音',
|
|
577
|
+
volumeLevel: '音量 {percent}%',
|
|
578
|
+
ended: '播放结束',
|
|
579
|
+
enteredFullscreen: '已进入全屏',
|
|
580
|
+
exitedFullscreen: '已退出全屏',
|
|
581
|
+
errorMessage: '错误:{message}',
|
|
582
|
+
timeOf: '{current} / {duration}',
|
|
583
|
+
hourOne: '{n}小时',
|
|
584
|
+
hourOther: '{n}小时',
|
|
585
|
+
minuteOne: '{n}分钟',
|
|
586
|
+
minuteOther: '{n}分钟',
|
|
587
|
+
secondOne: '{n}秒',
|
|
588
|
+
secondOther: '{n}秒',
|
|
420
589
|
}
|
|
421
590
|
|
|
422
591
|
export const defaultTranslations: Record<string, TranslationStrings> = {
|
|
@@ -439,9 +608,12 @@ export class I18n {
|
|
|
439
608
|
private interpolationPattern: RegExp
|
|
440
609
|
|
|
441
610
|
constructor(config: I18nConfig = {}) {
|
|
611
|
+
// The translations first: detectLanguage() looks the browser's language
|
|
612
|
+
// up in them, and before they were set it threw for every instance made
|
|
613
|
+
// without a language.
|
|
614
|
+
this.translations = { ...defaultTranslations }
|
|
442
615
|
this.language = config.language || this.detectLanguage() || 'en'
|
|
443
616
|
this.fallbackLanguage = config.fallbackLanguage || 'en'
|
|
444
|
-
this.translations = { ...defaultTranslations }
|
|
445
617
|
|
|
446
618
|
// Merge custom translations
|
|
447
619
|
if (config.translations) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { HTMLElementBase } from './base'
|
|
8
|
-
import { resolvePlayer } from './utils'
|
|
8
|
+
import { label, resolvePlayer } from './utils'
|
|
9
9
|
|
|
10
10
|
const AIRPLAY_ICON = 'M6.2 19h11.6L12 13.2 6.2 19zM19 3H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h2v-2H5V5h14v10h-2v2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'
|
|
11
11
|
|
|
@@ -27,8 +27,10 @@ export class MediaAirplayButton extends HTMLElementBase {
|
|
|
27
27
|
button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
|
|
28
28
|
button:hover { opacity: 0.8; }
|
|
29
29
|
:host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
|
|
30
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
31
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
30
32
|
</style>
|
|
31
|
-
<button part="button" type="button" aria-label="
|
|
33
|
+
<button part="button" type="button" aria-label="${label(this, 'airPlayPlay')}">
|
|
32
34
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
|
|
33
35
|
</button>
|
|
34
36
|
`
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module elements/media-fullscreen-button
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolvePlayer } from './utils'
|
|
7
|
+
import { label, resolvePlayer } from './utils'
|
|
8
8
|
|
|
9
9
|
const ENTER_FS = 'M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'
|
|
10
10
|
const EXIT_FS = 'M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z'
|
|
@@ -28,8 +28,10 @@ export class MediaFullscreenButton extends HTMLElementBase {
|
|
|
28
28
|
}
|
|
29
29
|
button:hover { opacity: 0.8; }
|
|
30
30
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
31
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
32
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
31
33
|
</style>
|
|
32
|
-
<button part="button" type="button" aria-label="
|
|
34
|
+
<button part="button" type="button" aria-label="${label(this, 'enterFullscreen')}">
|
|
33
35
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
34
36
|
<path d="${ENTER_FS}"/>
|
|
35
37
|
</svg>
|
|
@@ -62,7 +64,7 @@ export class MediaFullscreenButton extends HTMLElementBase {
|
|
|
62
64
|
btn.disabled = avail !== 'available'
|
|
63
65
|
|
|
64
66
|
const fs = state.fullscreen
|
|
65
|
-
btn.setAttribute('aria-label', fs ? '
|
|
67
|
+
btn.setAttribute('aria-label', label(this, fs ? 'exitFullscreen' : 'enterFullscreen'))
|
|
66
68
|
const path = this.shadowRoot!.querySelector('path')!
|
|
67
69
|
path.setAttribute('d', fs ? EXIT_FS : ENTER_FS)
|
|
68
70
|
})
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { HTMLElementBase } from './base'
|
|
8
|
-
import { resolvePlayer } from './utils'
|
|
8
|
+
import { label, resolvePlayer } from './utils'
|
|
9
9
|
|
|
10
10
|
export class MediaLiveButton extends HTMLElementBase {
|
|
11
11
|
private cleanup: (() => void) | null = null
|
|
@@ -20,8 +20,10 @@ export class MediaLiveButton extends HTMLElementBase {
|
|
|
20
20
|
button { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; }
|
|
21
21
|
i { width: 8px; height: 8px; border-radius: 9999px; background: #ef4444; }
|
|
22
22
|
:host(:not([at-live-edge])) button { opacity: 0.72; }
|
|
23
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
24
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
23
25
|
</style>
|
|
24
|
-
<button part="button" type="button" aria-label="
|
|
26
|
+
<button part="button" type="button" aria-label="${label(this, 'seekToLive')}"><i aria-hidden="true"></i><span>${label(this, 'live')}</span></button>
|
|
25
27
|
`
|
|
26
28
|
}
|
|
27
29
|
queueMicrotask(() => this.attach())
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module elements/media-mute-button
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolvePlayer } from './utils'
|
|
7
|
+
import { label, resolvePlayer } from './utils'
|
|
8
8
|
|
|
9
9
|
const ICON_MUTED = 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0 0 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z'
|
|
10
10
|
const ICON_LOW = 'M18.5 12A4.5 4.5 0 0 0 16 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z'
|
|
@@ -33,8 +33,10 @@ export class MediaMuteButton extends HTMLElementBase {
|
|
|
33
33
|
background: transparent; border: none; color: inherit; cursor: pointer;
|
|
34
34
|
}
|
|
35
35
|
button:hover { opacity: 0.8; }
|
|
36
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
37
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
36
38
|
</style>
|
|
37
|
-
<button part="button" type="button" aria-label="
|
|
39
|
+
<button part="button" type="button" aria-label="${label(this, 'mute')}">
|
|
38
40
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
39
41
|
<path d="${ICON_HIGH}"/>
|
|
40
42
|
</svg>
|
|
@@ -59,7 +61,7 @@ export class MediaMuteButton extends HTMLElementBase {
|
|
|
59
61
|
|
|
60
62
|
const unsub = player.subscribe((state: any) => {
|
|
61
63
|
const icon = getVolumeIcon(state.muted, state.volume)
|
|
62
|
-
btn.setAttribute('aria-label', state.muted ? '
|
|
64
|
+
btn.setAttribute('aria-label', label(this, state.muted ? 'unmute' : 'mute'))
|
|
63
65
|
const path = this.shadowRoot!.querySelector('path')!
|
|
64
66
|
path.setAttribute('d', icon)
|
|
65
67
|
})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module elements/media-pip-button
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolvePlayer } from './utils'
|
|
7
|
+
import { label, resolvePlayer } from './utils'
|
|
8
8
|
|
|
9
9
|
const PIP_ICON = 'M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z'
|
|
10
10
|
|
|
@@ -27,8 +27,10 @@ export class MediaPipButton extends HTMLElementBase {
|
|
|
27
27
|
}
|
|
28
28
|
button:hover { opacity: 0.8; }
|
|
29
29
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
30
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
31
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
30
32
|
</style>
|
|
31
|
-
<button part="button" type="button" aria-label="
|
|
33
|
+
<button part="button" type="button" aria-label="${label(this, 'enterPiP')}">
|
|
32
34
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
33
35
|
<path d="${PIP_ICON}"/>
|
|
34
36
|
</svg>
|
|
@@ -59,6 +61,7 @@ export class MediaPipButton extends HTMLElementBase {
|
|
|
59
61
|
this.removeAttribute('hidden')
|
|
60
62
|
}
|
|
61
63
|
btn.disabled = avail !== 'available'
|
|
64
|
+
btn.setAttribute('aria-label', label(this, state.pictureInPicture ? 'exitPiP' : 'enterPiP'))
|
|
62
65
|
})
|
|
63
66
|
|
|
64
67
|
this._cleanup = unsub
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module elements/media-play-button
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolvePlayer } from './utils'
|
|
7
|
+
import { label, resolvePlayer } from './utils'
|
|
8
8
|
|
|
9
9
|
const PLAY_ICON = 'M8 5v14l11-7z'
|
|
10
10
|
const PAUSE_ICON = 'M6 19h4V5H6v14zm8-14v14h4V5h-4z'
|
|
@@ -27,8 +27,10 @@ export class MediaPlayButton extends HTMLElementBase {
|
|
|
27
27
|
background: transparent; border: none; color: inherit; cursor: pointer;
|
|
28
28
|
}
|
|
29
29
|
button:hover { opacity: 0.8; }
|
|
30
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
31
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
30
32
|
</style>
|
|
31
|
-
<button part="button" type="button" aria-label="
|
|
33
|
+
<button part="button" type="button" aria-label="${label(this, 'play')}">
|
|
32
34
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
33
35
|
<path d="${PLAY_ICON}"/>
|
|
34
36
|
</svg>
|
|
@@ -58,7 +60,7 @@ export class MediaPlayButton extends HTMLElementBase {
|
|
|
58
60
|
|
|
59
61
|
const unsub = player.subscribe('paused', (state: any) => {
|
|
60
62
|
const paused = state.paused
|
|
61
|
-
btn.setAttribute('aria-label', paused ? '
|
|
63
|
+
btn.setAttribute('aria-label', label(this, paused ? 'play' : 'pause'))
|
|
62
64
|
btn.setAttribute('aria-pressed', String(!paused))
|
|
63
65
|
const path = this.shadowRoot!.querySelector('path')!
|
|
64
66
|
path.setAttribute('d', paused ? PLAY_ICON : PAUSE_ICON)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolvePlayer } from './utils'
|
|
1
|
+
import { label, resolvePlayer } from './utils'
|
|
2
2
|
import type { PlayerState } from '../types'
|
|
3
3
|
|
|
4
4
|
const defaultRates = [0.5, 0.75, 1, 1.25, 1.5, 2]
|
|
@@ -18,8 +18,10 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
|
|
|
18
18
|
button { min-width: 42px; height: 40px; padding: 0 8px; border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; font-variant-numeric: tabular-nums; }
|
|
19
19
|
button:hover { opacity: .8; }
|
|
20
20
|
button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
|
|
21
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
22
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
21
23
|
</style>
|
|
22
|
-
<button part="button" type="button" aria-label="
|
|
24
|
+
<button part="button" type="button" aria-label="${label(this, 'speed')}"></button>
|
|
23
25
|
`
|
|
24
26
|
}
|
|
25
27
|
queueMicrotask(() => this.attach())
|
|
@@ -53,7 +55,7 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
|
|
|
53
55
|
button.addEventListener('click', this.onClick)
|
|
54
56
|
this.cleanup = player.subscribe('playbackRate', (state: PlayerState) => {
|
|
55
57
|
button.textContent = `${state.playbackRate}x`
|
|
56
|
-
button.setAttribute('aria-label',
|
|
58
|
+
button.setAttribute('aria-label', label(this, 'playbackRate', { rate: state.playbackRate }))
|
|
57
59
|
})
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module elements/media-progress-bar
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { resolvePlayer, formatTime, formatTimePhrase } from './utils'
|
|
9
|
+
import { label, resolvePlayer, formatTime, formatTimePhrase } from './utils'
|
|
10
10
|
|
|
11
11
|
import { HTMLElementBase } from './base'
|
|
12
12
|
|
|
@@ -65,10 +65,12 @@ export class MediaProgressBar extends HTMLElementBase {
|
|
|
65
65
|
border-top-color: rgba(0,0,0,0.85);
|
|
66
66
|
}
|
|
67
67
|
.container:hover .tooltip { opacity: 1; }
|
|
68
|
+
/* A finger needs more than the 20px a mouse does. */
|
|
69
|
+
@media (pointer: coarse) { .container { height: 36px; } .thumb { width: 18px; height: 18px; } }
|
|
68
70
|
</style>
|
|
69
71
|
<div class="container" part="container" role="slider"
|
|
70
72
|
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
|
|
71
|
-
aria-label="
|
|
73
|
+
aria-label="${label(this, 'seek')}" tabindex="0">
|
|
72
74
|
<div class="tooltip" part="tooltip">0:00</div>
|
|
73
75
|
<div class="track" part="track">
|
|
74
76
|
<div class="waveform" part="waveform" aria-hidden="true"></div>
|
|
@@ -188,7 +190,7 @@ export class MediaProgressBar extends HTMLElementBase {
|
|
|
188
190
|
thumb.style.left = `${percent}%`
|
|
189
191
|
|
|
190
192
|
container.setAttribute('aria-valuenow', String(Math.round(percent)))
|
|
191
|
-
container.setAttribute('aria-valuetext',
|
|
193
|
+
container.setAttribute('aria-valuetext', label(this, 'timeOf', { current: formatTimePhrase(state.currentTime, this), duration: formatTimePhrase(duration, this) }))
|
|
192
194
|
|
|
193
195
|
// Buffered
|
|
194
196
|
const bufferedPct = (state.bufferedAmount || 0) * 100
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { HTMLElementBase } from './base'
|
|
11
|
-
import { resolvePlayer } from './utils'
|
|
11
|
+
import { label, resolvePlayer } from './utils'
|
|
12
12
|
|
|
13
13
|
const REMOTE_ICON = 'M1 18v3h3a3 3 0 0 0-3-3zm0-4v2a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7zm0-4v2c5 0 9 4 9 9h2c0-6.1-4.9-11-11-11zm18-7H5a2 2 0 0 0-2 2v3h2V5h14v10h-5v2h5a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'
|
|
14
14
|
|
|
@@ -32,8 +32,10 @@ export class MediaRemotePlaybackButton extends HTMLElementBase {
|
|
|
32
32
|
button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
|
|
33
33
|
button:hover { opacity: 0.8; }
|
|
34
34
|
:host([connected]) button { color: var(--media-remote-active-color, #38bdf8); }
|
|
35
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
36
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
35
37
|
</style>
|
|
36
|
-
<button part="button" type="button" aria-label="
|
|
38
|
+
<button part="button" type="button" aria-label="${label(this, 'remotePlayback')}">
|
|
37
39
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${REMOTE_ICON}"/></svg>
|
|
38
40
|
</button>
|
|
39
41
|
`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolvePlayer } from './utils'
|
|
1
|
+
import { label, resolvePlayer } from './utils'
|
|
2
2
|
import type { PlayerState } from '../types'
|
|
3
3
|
|
|
4
4
|
import { HTMLElementBase } from './base'
|
|
@@ -17,6 +17,8 @@ export class MediaSeekButton extends HTMLElementBase {
|
|
|
17
17
|
button:hover { opacity: .8; }
|
|
18
18
|
button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
|
|
19
19
|
button:disabled { cursor: default; opacity: .45; }
|
|
20
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
21
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
20
22
|
</style>
|
|
21
23
|
<button part="button" type="button"></button>
|
|
22
24
|
`
|
|
@@ -38,9 +40,8 @@ export class MediaSeekButton extends HTMLElementBase {
|
|
|
38
40
|
if (!player || !button) return
|
|
39
41
|
const seconds = Number.parseFloat(this.getAttribute('seconds') ?? '10')
|
|
40
42
|
const offset = Number.isFinite(seconds) && seconds !== 0 ? seconds : 10
|
|
41
|
-
const label = offset > 0 ? `Forward ${offset} seconds` : `Back ${Math.abs(offset)} seconds`
|
|
42
43
|
button.textContent = offset > 0 ? `+${offset}` : String(offset)
|
|
43
|
-
button.setAttribute('aria-label', label)
|
|
44
|
+
button.setAttribute('aria-label', label(this, offset > 0 ? 'seekForward' : 'seekBackward', { seconds: Math.abs(offset) }))
|
|
44
45
|
this.onClick = () => player.seekBy(offset)
|
|
45
46
|
button.addEventListener('click', this.onClick)
|
|
46
47
|
this.cleanup = player.subscribe((state: PlayerState) => {
|