ts-video-player 0.1.7 → 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 +50 -50
- package/dist/{chunk-24n6adw2.js → chunk-hvjcmbfn.js} +685 -103
- 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 +6 -0
- package/dist/providers/vimeo.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/player.ts +30 -13
- package/src/providers/vimeo.ts +15 -3
package/src/elements/utils.ts
CHANGED
|
@@ -6,6 +6,39 @@
|
|
|
6
6
|
* @module elements/utils
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { TranslationKey } from '../core/i18n'
|
|
10
|
+
import { englishStrings, getI18n } from '../core/i18n'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The language a control speaks: the nearest `lang` attribute above it,
|
|
14
|
+
* through shadow roots (`<html lang="de">`, or `lang` on the `<video-player>`
|
|
15
|
+
* itself), then the browser's. A German page gets German button names and
|
|
16
|
+
* screen-reader announcements without configuring the player.
|
|
17
|
+
*/
|
|
18
|
+
export function languageOf(el: Node | null | undefined): string {
|
|
19
|
+
let node: Node | null | undefined = el
|
|
20
|
+
while (node) {
|
|
21
|
+
const tagged = (node as Element).closest?.('[lang]')?.getAttribute('lang')
|
|
22
|
+
if (tagged) return tagged.toLowerCase()
|
|
23
|
+
const root = node.getRootNode?.()
|
|
24
|
+
node = root && (root as ShadowRoot).host ? (root as ShadowRoot).host : null
|
|
25
|
+
}
|
|
26
|
+
if (typeof navigator !== 'undefined' && navigator.language) return navigator.language.toLowerCase()
|
|
27
|
+
return 'en'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A control's words in its language (languageOf): the built-in translations
|
|
32
|
+
* plus any an app added with getI18n().addTranslations(), English for a
|
|
33
|
+
* language or key that has none.
|
|
34
|
+
*/
|
|
35
|
+
export function label(el: Node | null | undefined, key: TranslationKey, params?: Record<string, string | number>): string {
|
|
36
|
+
const lang = languageOf(el)
|
|
37
|
+
const i18n = getI18n()
|
|
38
|
+
const text = i18n.getTranslations(lang)?.[key] || i18n.getTranslations(lang.split('-')[0]!)?.[key] || englishStrings[key] || key
|
|
39
|
+
return params ? text.replace(/\{(\w+)\}/g, (match, name) => (params[name] !== undefined ? String(params[name]) : match)) : text
|
|
40
|
+
}
|
|
41
|
+
|
|
9
42
|
/**
|
|
10
43
|
* Format time in seconds to MM:SS or HH:MM:SS
|
|
11
44
|
*/
|
|
@@ -28,10 +61,11 @@ export function formatTime(seconds: number, guide = 0): string {
|
|
|
28
61
|
|
|
29
62
|
/**
|
|
30
63
|
* Format time as a human-readable phrase for screen readers.
|
|
31
|
-
* e.g., "1 minute, 30 seconds"
|
|
64
|
+
* e.g., "1 minute, 30 seconds", in the language of `el` when one is given.
|
|
32
65
|
*/
|
|
33
|
-
export function formatTimePhrase(seconds: number): string {
|
|
34
|
-
|
|
66
|
+
export function formatTimePhrase(seconds: number, el?: Node | null): string {
|
|
67
|
+
const say = (n: number, one: TranslationKey, other: TranslationKey): string => label(el, n === 1 ? one : other, { n })
|
|
68
|
+
if (!isFinite(seconds) || isNaN(seconds)) return say(0, 'secondOne', 'secondOther')
|
|
35
69
|
|
|
36
70
|
const s = Math.floor(seconds)
|
|
37
71
|
const h = Math.floor(s / 3600)
|
|
@@ -39,9 +73,9 @@ export function formatTimePhrase(seconds: number): string {
|
|
|
39
73
|
const sec = s % 60
|
|
40
74
|
|
|
41
75
|
const parts: string[] = []
|
|
42
|
-
if (h > 0) parts.push(
|
|
43
|
-
if (m > 0) parts.push(
|
|
44
|
-
if (sec > 0 || parts.length === 0) parts.push(
|
|
76
|
+
if (h > 0) parts.push(say(h, 'hourOne', 'hourOther'))
|
|
77
|
+
if (m > 0) parts.push(say(m, 'minuteOne', 'minuteOther'))
|
|
78
|
+
if (sec > 0 || parts.length === 0) parts.push(say(sec, 'secondOne', 'secondOther'))
|
|
45
79
|
|
|
46
80
|
return parts.join(', ')
|
|
47
81
|
}
|
package/src/player.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { createMediaSession, type MediaSessionOptions } from './core/media-sessi
|
|
|
22
22
|
import { lockOrientation, unlockOrientation } from './core/orientation'
|
|
23
23
|
import { defaultLoaders, findSourceCandidates } from './providers'
|
|
24
24
|
import { embedCover } from './core/cover'
|
|
25
|
+
import { label } from './elements/utils'
|
|
25
26
|
|
|
26
27
|
// =============================================================================
|
|
27
28
|
// Player Class
|
|
@@ -111,8 +112,8 @@ export class Player implements IPlayer {
|
|
|
111
112
|
this._el.classList.add('ts-video-player')
|
|
112
113
|
this._el.setAttribute('tabindex', '0')
|
|
113
114
|
this._el.setAttribute('role', 'region')
|
|
114
|
-
this._el.setAttribute('aria-roledescription',
|
|
115
|
-
this._el.setAttribute('aria-label', this._options.title ||
|
|
115
|
+
this._el.setAttribute('aria-roledescription', label(this._el, 'videoRole'))
|
|
116
|
+
this._el.setAttribute('aria-label', this._options.title || label(this._el, 'videoPlayer'))
|
|
116
117
|
this._el.setAttribute('aria-keyshortcuts', 'Space K M F I C ArrowLeft ArrowRight J L ArrowUp ArrowDown Home End 0 1 2 3 4 5 6 7 8 9')
|
|
117
118
|
|
|
118
119
|
// Create inner structure
|
|
@@ -158,7 +159,7 @@ export class Player implements IPlayer {
|
|
|
158
159
|
const cover = document.createElement('button')
|
|
159
160
|
cover.type = 'button'
|
|
160
161
|
cover.className = 'ts-video-player__cover'
|
|
161
|
-
cover.setAttribute('aria-label',
|
|
162
|
+
cover.setAttribute('aria-label', this._options.title ? label(this._el, 'playTitle', { title: this._options.title }) : label(this._el, 'play'))
|
|
162
163
|
cover.hidden = true
|
|
163
164
|
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'
|
|
164
165
|
const button = document.createElement('span')
|
|
@@ -463,8 +464,11 @@ export class Player implements IPlayer {
|
|
|
463
464
|
}
|
|
464
465
|
|
|
465
466
|
// Update state. A new source has not played yet, so its cover returns.
|
|
467
|
+
// With autoplay it starts once loaded: a player that follows a chosen
|
|
468
|
+
// video (`src` set again on <video-player>) plays each one it is given,
|
|
469
|
+
// not only the first.
|
|
466
470
|
this._coverDismissed = false
|
|
467
|
-
this._playRequested =
|
|
471
|
+
this._playRequested = !!this._options.autoplay
|
|
468
472
|
this._store.batch({
|
|
469
473
|
sources,
|
|
470
474
|
src: sources[0],
|
|
@@ -533,7 +537,7 @@ export class Player implements IPlayer {
|
|
|
533
537
|
})
|
|
534
538
|
if (this._playRequested) {
|
|
535
539
|
this._playRequested = false
|
|
536
|
-
void this.
|
|
540
|
+
void this.startRequestedPlay(this._provider)
|
|
537
541
|
}
|
|
538
542
|
return
|
|
539
543
|
}
|
|
@@ -591,13 +595,13 @@ export class Player implements IPlayer {
|
|
|
591
595
|
provider.on('play', () => {
|
|
592
596
|
this._store.batch({ paused: false, ended: false })
|
|
593
597
|
this._events.emit('play')
|
|
594
|
-
this.announce('
|
|
598
|
+
this.announce(label(this._el, 'playing'))
|
|
595
599
|
})
|
|
596
600
|
|
|
597
601
|
provider.on('pause', () => {
|
|
598
602
|
this._store.set('paused', true)
|
|
599
603
|
this._events.emit('pause')
|
|
600
|
-
this.announce('
|
|
604
|
+
this.announce(label(this._el, 'paused'))
|
|
601
605
|
})
|
|
602
606
|
|
|
603
607
|
provider.on('playing', () => {
|
|
@@ -634,10 +638,10 @@ export class Player implements IPlayer {
|
|
|
634
638
|
this._store.batch({ volume, muted })
|
|
635
639
|
this._events.emit('volumechange', volume, muted)
|
|
636
640
|
if (muted) {
|
|
637
|
-
this.announce('
|
|
641
|
+
this.announce(label(this._el, 'muted'))
|
|
638
642
|
// eslint-disable-next-line style/brace-style
|
|
639
643
|
} else {
|
|
640
|
-
this.announce(
|
|
644
|
+
this.announce(label(this._el, 'volumeLevel', { percent: Math.round(volume * 100) }))
|
|
641
645
|
}
|
|
642
646
|
})
|
|
643
647
|
|
|
@@ -657,7 +661,7 @@ export class Player implements IPlayer {
|
|
|
657
661
|
provider.on('ended', () => {
|
|
658
662
|
this._store.batch({ ended: true, playing: false, playbackState: 'ended' })
|
|
659
663
|
this._events.emit('ended')
|
|
660
|
-
this.announce('
|
|
664
|
+
this.announce(label(this._el, 'ended'))
|
|
661
665
|
|
|
662
666
|
// Handle loop
|
|
663
667
|
if (this.state.loop) {
|
|
@@ -669,7 +673,7 @@ export class Player implements IPlayer {
|
|
|
669
673
|
provider.on('error', (error) => {
|
|
670
674
|
this._store.batch({ loadingState: 'error', error })
|
|
671
675
|
this._events.emit('error', error)
|
|
672
|
-
this.announce(
|
|
676
|
+
this.announce(label(this._el, 'errorMessage', { message: error.message }))
|
|
673
677
|
})
|
|
674
678
|
|
|
675
679
|
provider.on('statechange', (state) => {
|
|
@@ -685,7 +689,7 @@ export class Player implements IPlayer {
|
|
|
685
689
|
provider.on('fullscreenchange', (fullscreen) => {
|
|
686
690
|
this._store.set('fullscreen', fullscreen)
|
|
687
691
|
this._events.emit('fullscreenchange', fullscreen)
|
|
688
|
-
this.announce(fullscreen ? '
|
|
692
|
+
this.announce(label(this._el, fullscreen ? 'enteredFullscreen' : 'exitedFullscreen'))
|
|
689
693
|
})
|
|
690
694
|
|
|
691
695
|
provider.on('pipchange', (pip) => {
|
|
@@ -703,7 +707,20 @@ export class Player implements IPlayer {
|
|
|
703
707
|
|
|
704
708
|
// === Playback ===
|
|
705
709
|
|
|
706
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Play what was asked for before the source was ready. A browser that
|
|
712
|
+
* refuses sound without a tap (code 1) gets it muted instead, as autoplay
|
|
713
|
+
* is everywhere, and the mute button turns it up.
|
|
714
|
+
*/
|
|
715
|
+
private async startRequestedPlay(provider: Provider): Promise<void> {
|
|
716
|
+
await provider.play().catch(() => {})
|
|
717
|
+
if (this._provider !== provider || this.state.error?.code !== 1 || this.state.muted) return
|
|
718
|
+
this._store.set('error', null)
|
|
719
|
+
this.setMuted(true)
|
|
720
|
+
await provider.play().catch(() => {})
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async play(): Promise<void> {
|
|
707
724
|
if (!this._provider) {
|
|
708
725
|
this._playRequested = true
|
|
709
726
|
return
|
package/src/providers/vimeo.ts
CHANGED
|
@@ -114,6 +114,7 @@ interface VimeoPlayer {
|
|
|
114
114
|
pause(): Promise<void>
|
|
115
115
|
unload(): Promise<void>
|
|
116
116
|
destroy(): Promise<void>
|
|
117
|
+
loadVideo(id: number | string | { id: number, h?: string }): Promise<number>
|
|
117
118
|
getCurrentTime(): Promise<number>
|
|
118
119
|
setCurrentTime(seconds: number): Promise<number>
|
|
119
120
|
getDuration(): Promise<number>
|
|
@@ -240,10 +241,14 @@ export class VimeoProvider extends BaseProvider {
|
|
|
240
241
|
return
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
//
|
|
244
|
+
// A player already here loads the next video in place. Destroying it and
|
|
245
|
+
// making a new one on the same element never became ready again: the SDK
|
|
246
|
+
// keeps its player per element, so a second video (or the same one
|
|
247
|
+
// chosen again) stayed "loading" for good.
|
|
244
248
|
if (this.player) {
|
|
245
|
-
await this.player.
|
|
246
|
-
this.
|
|
249
|
+
await this.player.loadVideo(parseInt(parsed.videoId, 10))
|
|
250
|
+
await this.announceLoaded()
|
|
251
|
+
return
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
// Build player options
|
|
@@ -281,6 +286,13 @@ export class VimeoProvider extends BaseProvider {
|
|
|
281
286
|
// Setup event listeners
|
|
282
287
|
this.attachVimeoEvents()
|
|
283
288
|
|
|
289
|
+
await this.announceLoaded()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** What a loaded video tells the player: ready, its length, qualities and shape. */
|
|
293
|
+
private async announceLoaded(): Promise<void> {
|
|
294
|
+
if (!this.player) return
|
|
295
|
+
|
|
284
296
|
// Emit ready
|
|
285
297
|
this.events.emit('loadedmetadata')
|
|
286
298
|
this.events.emit('canplay')
|