ts-video-player 0.1.6 → 0.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/dist/cdn.min.js +70 -50
- package/dist/{chunk-fvz33zn4.js → chunk-hvjcmbfn.js} +765 -114
- 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/index.js +28 -419
- package/dist/player.d.ts +12 -0
- package/dist/providers/vimeo.d.ts +6 -0
- package/dist/providers/youtube.d.ts +6 -0
- package/package.json +1 -1
- package/src/core/i18n.ts +173 -1
- package/src/elements/media-airplay-button.ts +2 -2
- package/src/elements/media-fullscreen-button.ts +3 -3
- package/src/elements/media-live-button.ts +2 -2
- package/src/elements/media-mute-button.ts +3 -3
- package/src/elements/media-pip-button.ts +3 -2
- package/src/elements/media-play-button.ts +3 -3
- package/src/elements/media-playback-rate-button.ts +3 -3
- package/src/elements/media-progress-bar.ts +3 -3
- package/src/elements/media-remote-playback-button.ts +2 -2
- package/src/elements/media-seek-button.ts +2 -3
- package/src/elements/media-settings-menu.ts +14 -14
- package/src/elements/media-time-display.ts +2 -2
- package/src/elements/media-volume-slider.ts +2 -2
- package/src/elements/utils.ts +40 -6
- package/src/elements/video-skin.ts +21 -1
- package/src/player.ts +76 -18
- package/src/providers/vimeo.ts +15 -3
- package/src/providers/youtube.ts +27 -5
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
|
|
|
@@ -28,7 +28,7 @@ export class MediaAirplayButton extends HTMLElementBase {
|
|
|
28
28
|
button:hover { opacity: 0.8; }
|
|
29
29
|
:host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
|
|
30
30
|
</style>
|
|
31
|
-
<button part="button" type="button" aria-label="
|
|
31
|
+
<button part="button" type="button" aria-label="${label(this, 'airPlayPlay')}">
|
|
32
32
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
|
|
33
33
|
</button>
|
|
34
34
|
`
|
|
@@ -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'
|
|
@@ -29,7 +29,7 @@ export class MediaFullscreenButton extends HTMLElementBase {
|
|
|
29
29
|
button:hover { opacity: 0.8; }
|
|
30
30
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
31
31
|
</style>
|
|
32
|
-
<button part="button" type="button" aria-label="
|
|
32
|
+
<button part="button" type="button" aria-label="${label(this, 'enterFullscreen')}">
|
|
33
33
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
34
34
|
<path d="${ENTER_FS}"/>
|
|
35
35
|
</svg>
|
|
@@ -62,7 +62,7 @@ export class MediaFullscreenButton extends HTMLElementBase {
|
|
|
62
62
|
btn.disabled = avail !== 'available'
|
|
63
63
|
|
|
64
64
|
const fs = state.fullscreen
|
|
65
|
-
btn.setAttribute('aria-label', fs ? '
|
|
65
|
+
btn.setAttribute('aria-label', label(this, fs ? 'exitFullscreen' : 'enterFullscreen'))
|
|
66
66
|
const path = this.shadowRoot!.querySelector('path')!
|
|
67
67
|
path.setAttribute('d', fs ? EXIT_FS : ENTER_FS)
|
|
68
68
|
})
|
|
@@ -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
|
|
@@ -21,7 +21,7 @@ export class MediaLiveButton extends HTMLElementBase {
|
|
|
21
21
|
i { width: 8px; height: 8px; border-radius: 9999px; background: #ef4444; }
|
|
22
22
|
:host(:not([at-live-edge])) button { opacity: 0.72; }
|
|
23
23
|
</style>
|
|
24
|
-
<button part="button" type="button" aria-label="
|
|
24
|
+
<button part="button" type="button" aria-label="${label(this, 'seekToLive')}"><i aria-hidden="true"></i><span>${label(this, 'live')}</span></button>
|
|
25
25
|
`
|
|
26
26
|
}
|
|
27
27
|
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'
|
|
@@ -34,7 +34,7 @@ export class MediaMuteButton extends HTMLElementBase {
|
|
|
34
34
|
}
|
|
35
35
|
button:hover { opacity: 0.8; }
|
|
36
36
|
</style>
|
|
37
|
-
<button part="button" type="button" aria-label="
|
|
37
|
+
<button part="button" type="button" aria-label="${label(this, 'mute')}">
|
|
38
38
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
39
39
|
<path d="${ICON_HIGH}"/>
|
|
40
40
|
</svg>
|
|
@@ -59,7 +59,7 @@ export class MediaMuteButton extends HTMLElementBase {
|
|
|
59
59
|
|
|
60
60
|
const unsub = player.subscribe((state: any) => {
|
|
61
61
|
const icon = getVolumeIcon(state.muted, state.volume)
|
|
62
|
-
btn.setAttribute('aria-label', state.muted ? '
|
|
62
|
+
btn.setAttribute('aria-label', label(this, state.muted ? 'unmute' : 'mute'))
|
|
63
63
|
const path = this.shadowRoot!.querySelector('path')!
|
|
64
64
|
path.setAttribute('d', icon)
|
|
65
65
|
})
|
|
@@ -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
|
|
|
@@ -28,7 +28,7 @@ export class MediaPipButton extends HTMLElementBase {
|
|
|
28
28
|
button:hover { opacity: 0.8; }
|
|
29
29
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
30
30
|
</style>
|
|
31
|
-
<button part="button" type="button" aria-label="
|
|
31
|
+
<button part="button" type="button" aria-label="${label(this, 'enterPiP')}">
|
|
32
32
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
33
33
|
<path d="${PIP_ICON}"/>
|
|
34
34
|
</svg>
|
|
@@ -59,6 +59,7 @@ export class MediaPipButton extends HTMLElementBase {
|
|
|
59
59
|
this.removeAttribute('hidden')
|
|
60
60
|
}
|
|
61
61
|
btn.disabled = avail !== 'available'
|
|
62
|
+
btn.setAttribute('aria-label', label(this, state.pictureInPicture ? 'exitPiP' : 'enterPiP'))
|
|
62
63
|
})
|
|
63
64
|
|
|
64
65
|
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'
|
|
@@ -28,7 +28,7 @@ export class MediaPlayButton extends HTMLElementBase {
|
|
|
28
28
|
}
|
|
29
29
|
button:hover { opacity: 0.8; }
|
|
30
30
|
</style>
|
|
31
|
-
<button part="button" type="button" aria-label="
|
|
31
|
+
<button part="button" type="button" aria-label="${label(this, 'play')}">
|
|
32
32
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
33
33
|
<path d="${PLAY_ICON}"/>
|
|
34
34
|
</svg>
|
|
@@ -58,7 +58,7 @@ export class MediaPlayButton extends HTMLElementBase {
|
|
|
58
58
|
|
|
59
59
|
const unsub = player.subscribe('paused', (state: any) => {
|
|
60
60
|
const paused = state.paused
|
|
61
|
-
btn.setAttribute('aria-label', paused ? '
|
|
61
|
+
btn.setAttribute('aria-label', label(this, paused ? 'play' : 'pause'))
|
|
62
62
|
btn.setAttribute('aria-pressed', String(!paused))
|
|
63
63
|
const path = this.shadowRoot!.querySelector('path')!
|
|
64
64
|
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]
|
|
@@ -19,7 +19,7 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
|
|
|
19
19
|
button:hover { opacity: .8; }
|
|
20
20
|
button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
|
|
21
21
|
</style>
|
|
22
|
-
<button part="button" type="button" aria-label="
|
|
22
|
+
<button part="button" type="button" aria-label="${label(this, 'speed')}"></button>
|
|
23
23
|
`
|
|
24
24
|
}
|
|
25
25
|
queueMicrotask(() => this.attach())
|
|
@@ -53,7 +53,7 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
|
|
|
53
53
|
button.addEventListener('click', this.onClick)
|
|
54
54
|
this.cleanup = player.subscribe('playbackRate', (state: PlayerState) => {
|
|
55
55
|
button.textContent = `${state.playbackRate}x`
|
|
56
|
-
button.setAttribute('aria-label',
|
|
56
|
+
button.setAttribute('aria-label', label(this, 'playbackRate', { rate: state.playbackRate }))
|
|
57
57
|
})
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -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
|
|
|
@@ -68,7 +68,7 @@ export class MediaProgressBar extends HTMLElementBase {
|
|
|
68
68
|
</style>
|
|
69
69
|
<div class="container" part="container" role="slider"
|
|
70
70
|
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
|
|
71
|
-
aria-label="
|
|
71
|
+
aria-label="${label(this, 'seek')}" tabindex="0">
|
|
72
72
|
<div class="tooltip" part="tooltip">0:00</div>
|
|
73
73
|
<div class="track" part="track">
|
|
74
74
|
<div class="waveform" part="waveform" aria-hidden="true"></div>
|
|
@@ -188,7 +188,7 @@ export class MediaProgressBar extends HTMLElementBase {
|
|
|
188
188
|
thumb.style.left = `${percent}%`
|
|
189
189
|
|
|
190
190
|
container.setAttribute('aria-valuenow', String(Math.round(percent)))
|
|
191
|
-
container.setAttribute('aria-valuetext',
|
|
191
|
+
container.setAttribute('aria-valuetext', label(this, 'timeOf', { current: formatTimePhrase(state.currentTime, this), duration: formatTimePhrase(duration, this) }))
|
|
192
192
|
|
|
193
193
|
// Buffered
|
|
194
194
|
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
|
|
|
@@ -33,7 +33,7 @@ export class MediaRemotePlaybackButton extends HTMLElementBase {
|
|
|
33
33
|
button:hover { opacity: 0.8; }
|
|
34
34
|
:host([connected]) button { color: var(--media-remote-active-color, #38bdf8); }
|
|
35
35
|
</style>
|
|
36
|
-
<button part="button" type="button" aria-label="
|
|
36
|
+
<button part="button" type="button" aria-label="${label(this, 'remotePlayback')}">
|
|
37
37
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${REMOTE_ICON}"/></svg>
|
|
38
38
|
</button>
|
|
39
39
|
`
|
|
@@ -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'
|
|
@@ -38,9 +38,8 @@ export class MediaSeekButton extends HTMLElementBase {
|
|
|
38
38
|
if (!player || !button) return
|
|
39
39
|
const seconds = Number.parseFloat(this.getAttribute('seconds') ?? '10')
|
|
40
40
|
const offset = Number.isFinite(seconds) && seconds !== 0 ? seconds : 10
|
|
41
|
-
const label = offset > 0 ? `Forward ${offset} seconds` : `Back ${Math.abs(offset)} seconds`
|
|
42
41
|
button.textContent = offset > 0 ? `+${offset}` : String(offset)
|
|
43
|
-
button.setAttribute('aria-label', label)
|
|
42
|
+
button.setAttribute('aria-label', label(this, offset > 0 ? 'seekForward' : 'seekBackward', { seconds: Math.abs(offset) }))
|
|
44
43
|
this.onClick = () => player.seekBy(offset)
|
|
45
44
|
button.addEventListener('click', this.onClick)
|
|
46
45
|
this.cleanup = player.subscribe((state: PlayerState) => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module elements/media-settings-menu
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { resolvePlayer } from './utils'
|
|
10
|
+
import { label as say, resolvePlayer } from './utils'
|
|
11
11
|
import { computePosition } from '../core/popover'
|
|
12
12
|
|
|
13
13
|
const GEAR_ICON = 'M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z'
|
|
@@ -56,7 +56,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
56
56
|
.back { font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
|
57
57
|
</style>
|
|
58
58
|
<button class="trigger" part="trigger" type="button"
|
|
59
|
-
aria-label="
|
|
59
|
+
aria-label="${say(this, 'settings')}" aria-haspopup="true" aria-expanded="false">
|
|
60
60
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
61
61
|
<path d="${GEAR_ICON}"/>
|
|
62
62
|
</svg>
|
|
@@ -177,7 +177,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
177
177
|
const state = player.state
|
|
178
178
|
|
|
179
179
|
// Speed
|
|
180
|
-
this.addItem(menu,
|
|
180
|
+
this.addItem(menu, `${say(this, 'speed')}: ${state.playbackRate}x`, () => {
|
|
181
181
|
this._currentPanel = 'speed'
|
|
182
182
|
this.renderPanel()
|
|
183
183
|
})
|
|
@@ -185,8 +185,8 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
185
185
|
// Quality
|
|
186
186
|
if (state.qualities.length > 0) {
|
|
187
187
|
const current = state.qualities.find((q: any) => q.selected)
|
|
188
|
-
const label = state.autoQuality ? '
|
|
189
|
-
this.addItem(menu,
|
|
188
|
+
const label = state.autoQuality ? say(this, 'auto') : (current ? `${current.height}p` : say(this, 'auto'))
|
|
189
|
+
this.addItem(menu, `${say(this, 'quality')}: ${label}`, () => {
|
|
190
190
|
this._currentPanel = 'quality'
|
|
191
191
|
this.renderPanel()
|
|
192
192
|
})
|
|
@@ -195,7 +195,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
195
195
|
// Captions
|
|
196
196
|
if (state.textTracks.length > 0) {
|
|
197
197
|
const showing = state.textTracks.find((t: any) => t.mode === 'showing')
|
|
198
|
-
this.addItem(menu,
|
|
198
|
+
this.addItem(menu, `${say(this, 'captions')}: ${showing?.label || say(this, 'captionsOff')}`, () => {
|
|
199
199
|
this._currentPanel = 'captions'
|
|
200
200
|
this.renderPanel()
|
|
201
201
|
})
|
|
@@ -203,7 +203,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
203
203
|
|
|
204
204
|
if (state.audioTracks.length > 1) {
|
|
205
205
|
const selected = state.audioTracks.find((track: { selected: boolean }) => track.selected)
|
|
206
|
-
this.addItem(menu,
|
|
206
|
+
this.addItem(menu, `${say(this, 'audio')}: ${selected?.label || say(this, 'defaultTrack')}`, () => {
|
|
207
207
|
this._currentPanel = 'audio'
|
|
208
208
|
this.renderPanel()
|
|
209
209
|
})
|
|
@@ -211,7 +211,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
private renderSpeed(menu: Element, player: any): void {
|
|
214
|
-
this.addItem(menu, '
|
|
214
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
215
215
|
this._currentPanel = 'main'
|
|
216
216
|
this.renderPanel()
|
|
217
217
|
}, false, true)
|
|
@@ -226,13 +226,13 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
private renderQuality(menu: Element, player: any): void {
|
|
229
|
-
this.addItem(menu, '
|
|
229
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
230
230
|
this._currentPanel = 'main'
|
|
231
231
|
this.renderPanel()
|
|
232
232
|
}, false, true)
|
|
233
233
|
|
|
234
234
|
// Auto option
|
|
235
|
-
this.addItem(menu, '
|
|
235
|
+
this.addItem(menu, say(this, 'auto'), () => {
|
|
236
236
|
player.setQuality('auto')
|
|
237
237
|
this.close()
|
|
238
238
|
}, player.state.autoQuality)
|
|
@@ -246,14 +246,14 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
private renderCaptions(menu: Element, player: any): void {
|
|
249
|
-
this.addItem(menu, '
|
|
249
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
250
250
|
this._currentPanel = 'main'
|
|
251
251
|
this.renderPanel()
|
|
252
252
|
}, false, true)
|
|
253
253
|
|
|
254
254
|
// Off option
|
|
255
255
|
const anyShowing = player.state.textTracks.some((t: any) => t.mode === 'showing')
|
|
256
|
-
this.addItem(menu, '
|
|
256
|
+
this.addItem(menu, say(this, 'captionsOff'), () => {
|
|
257
257
|
for (const t of player.state.textTracks) {
|
|
258
258
|
if (t.mode === 'showing') player.setTextTrack(t.id, 'disabled')
|
|
259
259
|
}
|
|
@@ -271,13 +271,13 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
private renderAudio(menu: Element, player: any): void {
|
|
274
|
-
this.addItem(menu, '
|
|
274
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
275
275
|
this._currentPanel = 'main'
|
|
276
276
|
this.renderPanel()
|
|
277
277
|
}, false, true)
|
|
278
278
|
|
|
279
279
|
for (const track of player.state.audioTracks) {
|
|
280
|
-
this.addItem(menu, track.label || track.language ||
|
|
280
|
+
this.addItem(menu, track.label || track.language || say(this, 'audioTrack'), () => {
|
|
281
281
|
player.setAudioTrack(track.id)
|
|
282
282
|
this.close()
|
|
283
283
|
}, track.selected)
|
|
@@ -39,7 +39,7 @@ export class MediaTimeDisplay extends HTMLElementBase {
|
|
|
39
39
|
time { color: inherit; }
|
|
40
40
|
.separator { margin: 0 2px; }
|
|
41
41
|
</style>
|
|
42
|
-
<time part="time" datetime="PT0S" aria-valuetext="0
|
|
42
|
+
<time part="time" datetime="PT0S" aria-valuetext="${formatTimePhrase(0, this)}">0:00</time>
|
|
43
43
|
`
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -94,6 +94,6 @@ export class MediaTimeDisplay extends HTMLElementBase {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
timeEl.setAttribute('datetime', toISODuration(seconds))
|
|
97
|
-
timeEl.setAttribute('aria-valuetext', formatTimePhrase(seconds))
|
|
97
|
+
timeEl.setAttribute('aria-valuetext', formatTimePhrase(seconds, this))
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module elements/media-volume-slider
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { resolvePlayer } from './utils'
|
|
9
|
+
import { label, resolvePlayer } from './utils'
|
|
10
10
|
|
|
11
11
|
import { HTMLElementBase } from './base'
|
|
12
12
|
|
|
@@ -45,7 +45,7 @@ export class MediaVolumeSlider extends HTMLElementBase {
|
|
|
45
45
|
</style>
|
|
46
46
|
<div class="container" part="container" role="slider"
|
|
47
47
|
aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
|
|
48
|
-
aria-label="
|
|
48
|
+
aria-label="${label(this, 'volume')}" tabindex="0">
|
|
49
49
|
<div class="track" part="track">
|
|
50
50
|
<div class="fill" part="fill"></div>
|
|
51
51
|
<div class="thumb" part="thumb"></div>
|