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
|
@@ -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'
|
|
@@ -54,9 +54,11 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
54
54
|
.item[aria-checked="true"]::before { content: '\\2713'; }
|
|
55
55
|
.item[aria-checked="false"]::before { content: ''; min-width: 12px; display: inline-block; }
|
|
56
56
|
.back { font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
|
57
|
+
/* 44px, the smallest a finger hits reliably, on a touch screen. */
|
|
58
|
+
@media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
|
|
57
59
|
</style>
|
|
58
60
|
<button class="trigger" part="trigger" type="button"
|
|
59
|
-
aria-label="
|
|
61
|
+
aria-label="${say(this, 'settings')}" aria-haspopup="true" aria-expanded="false">
|
|
60
62
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
|
|
61
63
|
<path d="${GEAR_ICON}"/>
|
|
62
64
|
</svg>
|
|
@@ -177,7 +179,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
177
179
|
const state = player.state
|
|
178
180
|
|
|
179
181
|
// Speed
|
|
180
|
-
this.addItem(menu,
|
|
182
|
+
this.addItem(menu, `${say(this, 'speed')}: ${state.playbackRate}x`, () => {
|
|
181
183
|
this._currentPanel = 'speed'
|
|
182
184
|
this.renderPanel()
|
|
183
185
|
})
|
|
@@ -185,8 +187,8 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
185
187
|
// Quality
|
|
186
188
|
if (state.qualities.length > 0) {
|
|
187
189
|
const current = state.qualities.find((q: any) => q.selected)
|
|
188
|
-
const label = state.autoQuality ? '
|
|
189
|
-
this.addItem(menu,
|
|
190
|
+
const label = state.autoQuality ? say(this, 'auto') : (current ? `${current.height}p` : say(this, 'auto'))
|
|
191
|
+
this.addItem(menu, `${say(this, 'quality')}: ${label}`, () => {
|
|
190
192
|
this._currentPanel = 'quality'
|
|
191
193
|
this.renderPanel()
|
|
192
194
|
})
|
|
@@ -195,7 +197,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
195
197
|
// Captions
|
|
196
198
|
if (state.textTracks.length > 0) {
|
|
197
199
|
const showing = state.textTracks.find((t: any) => t.mode === 'showing')
|
|
198
|
-
this.addItem(menu,
|
|
200
|
+
this.addItem(menu, `${say(this, 'captions')}: ${showing?.label || say(this, 'captionsOff')}`, () => {
|
|
199
201
|
this._currentPanel = 'captions'
|
|
200
202
|
this.renderPanel()
|
|
201
203
|
})
|
|
@@ -203,7 +205,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
203
205
|
|
|
204
206
|
if (state.audioTracks.length > 1) {
|
|
205
207
|
const selected = state.audioTracks.find((track: { selected: boolean }) => track.selected)
|
|
206
|
-
this.addItem(menu,
|
|
208
|
+
this.addItem(menu, `${say(this, 'audio')}: ${selected?.label || say(this, 'defaultTrack')}`, () => {
|
|
207
209
|
this._currentPanel = 'audio'
|
|
208
210
|
this.renderPanel()
|
|
209
211
|
})
|
|
@@ -211,7 +213,7 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
private renderSpeed(menu: Element, player: any): void {
|
|
214
|
-
this.addItem(menu, '
|
|
216
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
215
217
|
this._currentPanel = 'main'
|
|
216
218
|
this.renderPanel()
|
|
217
219
|
}, false, true)
|
|
@@ -226,13 +228,13 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
private renderQuality(menu: Element, player: any): void {
|
|
229
|
-
this.addItem(menu, '
|
|
231
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
230
232
|
this._currentPanel = 'main'
|
|
231
233
|
this.renderPanel()
|
|
232
234
|
}, false, true)
|
|
233
235
|
|
|
234
236
|
// Auto option
|
|
235
|
-
this.addItem(menu, '
|
|
237
|
+
this.addItem(menu, say(this, 'auto'), () => {
|
|
236
238
|
player.setQuality('auto')
|
|
237
239
|
this.close()
|
|
238
240
|
}, player.state.autoQuality)
|
|
@@ -246,14 +248,14 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
private renderCaptions(menu: Element, player: any): void {
|
|
249
|
-
this.addItem(menu, '
|
|
251
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
250
252
|
this._currentPanel = 'main'
|
|
251
253
|
this.renderPanel()
|
|
252
254
|
}, false, true)
|
|
253
255
|
|
|
254
256
|
// Off option
|
|
255
257
|
const anyShowing = player.state.textTracks.some((t: any) => t.mode === 'showing')
|
|
256
|
-
this.addItem(menu, '
|
|
258
|
+
this.addItem(menu, say(this, 'captionsOff'), () => {
|
|
257
259
|
for (const t of player.state.textTracks) {
|
|
258
260
|
if (t.mode === 'showing') player.setTextTrack(t.id, 'disabled')
|
|
259
261
|
}
|
|
@@ -271,13 +273,13 @@ export class MediaSettingsMenu extends HTMLElementBase {
|
|
|
271
273
|
}
|
|
272
274
|
|
|
273
275
|
private renderAudio(menu: Element, player: any): void {
|
|
274
|
-
this.addItem(menu, '
|
|
276
|
+
this.addItem(menu, say(this, 'back'), () => {
|
|
275
277
|
this._currentPanel = 'main'
|
|
276
278
|
this.renderPanel()
|
|
277
279
|
}, false, true)
|
|
278
280
|
|
|
279
281
|
for (const track of player.state.audioTracks) {
|
|
280
|
-
this.addItem(menu, track.label || track.language ||
|
|
282
|
+
this.addItem(menu, track.label || track.language || say(this, 'audioTrack'), () => {
|
|
281
283
|
player.setAudioTrack(track.id)
|
|
282
284
|
this.close()
|
|
283
285
|
}, 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
|
|
|
@@ -42,10 +42,12 @@ export class MediaVolumeSlider extends HTMLElementBase {
|
|
|
42
42
|
transition: transform 0.1s;
|
|
43
43
|
}
|
|
44
44
|
.container:hover .thumb { transform: translate(-50%, -50%) scale(1.2); }
|
|
45
|
+
/* A finger needs more than the 20px a mouse does. */
|
|
46
|
+
@media (pointer: coarse) { .container { height: 36px; } .thumb { width: 18px; height: 18px; } }
|
|
45
47
|
</style>
|
|
46
48
|
<div class="container" part="container" role="slider"
|
|
47
49
|
aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
|
|
48
|
-
aria-label="
|
|
50
|
+
aria-label="${label(this, 'volume')}" tabindex="0">
|
|
49
51
|
<div class="track" part="track">
|
|
50
52
|
<div class="fill" part="fill"></div>
|
|
51
53
|
<div class="thumb" part="thumb"></div>
|
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
|
}
|
|
@@ -31,7 +31,9 @@ export class VideoPlayerElement extends HTMLElementBase {
|
|
|
31
31
|
if (this._player) return
|
|
32
32
|
|
|
33
33
|
// Create shadow DOM
|
|
34
|
-
|
|
34
|
+
// delegatesFocus: focusing <video-player> (a dialog's first focus, an
|
|
35
|
+
// `autofocus`) puts focus on the player itself, where its keys work.
|
|
36
|
+
const shadow = this.shadowRoot ?? this.attachShadow({ mode: 'open', delegatesFocus: true })
|
|
35
37
|
|
|
36
38
|
// Container
|
|
37
39
|
this._container = document.createElement('div')
|
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')
|
|
@@ -262,6 +263,8 @@ export class Player implements IPlayer {
|
|
|
262
263
|
toggleCaptions: () => this.toggleCaptions(),
|
|
263
264
|
seekBackward: () => this.seekBy(-5),
|
|
264
265
|
seekForward: () => this.seekBy(5),
|
|
266
|
+
seekBackwardLong: () => this.seekBy(-10),
|
|
267
|
+
seekForwardLong: () => this.seekBy(10),
|
|
265
268
|
volumeUp: () => this.setVolume(Math.min(1, this.state.volume + 0.1)),
|
|
266
269
|
volumeDown: () => this.setVolume(Math.max(0, this.state.volume - 0.1)),
|
|
267
270
|
speedUp: () => this.setPlaybackRate(Math.min(4, this.state.playbackRate + 0.25)),
|
|
@@ -463,8 +466,11 @@ export class Player implements IPlayer {
|
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
// Update state. A new source has not played yet, so its cover returns.
|
|
469
|
+
// With autoplay it starts once loaded: a player that follows a chosen
|
|
470
|
+
// video (`src` set again on <video-player>) plays each one it is given,
|
|
471
|
+
// not only the first.
|
|
466
472
|
this._coverDismissed = false
|
|
467
|
-
this._playRequested =
|
|
473
|
+
this._playRequested = !!this._options.autoplay
|
|
468
474
|
this._store.batch({
|
|
469
475
|
sources,
|
|
470
476
|
src: sources[0],
|
|
@@ -533,7 +539,7 @@ export class Player implements IPlayer {
|
|
|
533
539
|
})
|
|
534
540
|
if (this._playRequested) {
|
|
535
541
|
this._playRequested = false
|
|
536
|
-
void this.
|
|
542
|
+
void this.startRequestedPlay(this._provider)
|
|
537
543
|
}
|
|
538
544
|
return
|
|
539
545
|
}
|
|
@@ -591,13 +597,16 @@ export class Player implements IPlayer {
|
|
|
591
597
|
provider.on('play', () => {
|
|
592
598
|
this._store.batch({ paused: false, ended: false })
|
|
593
599
|
this._events.emit('play')
|
|
594
|
-
this.announce('
|
|
600
|
+
this.announce(label(this._el, 'playing'))
|
|
595
601
|
})
|
|
596
602
|
|
|
597
603
|
provider.on('pause', () => {
|
|
598
|
-
|
|
604
|
+
// Not playing any more either: the controls hide only while playing,
|
|
605
|
+
// and a paused player that still said `playing` hid them over a still
|
|
606
|
+
// frame, and told everyone reading state.playing it was still going.
|
|
607
|
+
this._store.batch({ paused: true, playing: false, playbackState: 'paused' })
|
|
599
608
|
this._events.emit('pause')
|
|
600
|
-
this.announce('
|
|
609
|
+
this.announce(label(this._el, 'paused'))
|
|
601
610
|
})
|
|
602
611
|
|
|
603
612
|
provider.on('playing', () => {
|
|
@@ -634,10 +643,10 @@ export class Player implements IPlayer {
|
|
|
634
643
|
this._store.batch({ volume, muted })
|
|
635
644
|
this._events.emit('volumechange', volume, muted)
|
|
636
645
|
if (muted) {
|
|
637
|
-
this.announce('
|
|
646
|
+
this.announce(label(this._el, 'muted'))
|
|
638
647
|
// eslint-disable-next-line style/brace-style
|
|
639
648
|
} else {
|
|
640
|
-
this.announce(
|
|
649
|
+
this.announce(label(this._el, 'volumeLevel', { percent: Math.round(volume * 100) }))
|
|
641
650
|
}
|
|
642
651
|
})
|
|
643
652
|
|
|
@@ -657,7 +666,7 @@ export class Player implements IPlayer {
|
|
|
657
666
|
provider.on('ended', () => {
|
|
658
667
|
this._store.batch({ ended: true, playing: false, playbackState: 'ended' })
|
|
659
668
|
this._events.emit('ended')
|
|
660
|
-
this.announce('
|
|
669
|
+
this.announce(label(this._el, 'ended'))
|
|
661
670
|
|
|
662
671
|
// Handle loop
|
|
663
672
|
if (this.state.loop) {
|
|
@@ -669,7 +678,7 @@ export class Player implements IPlayer {
|
|
|
669
678
|
provider.on('error', (error) => {
|
|
670
679
|
this._store.batch({ loadingState: 'error', error })
|
|
671
680
|
this._events.emit('error', error)
|
|
672
|
-
this.announce(
|
|
681
|
+
this.announce(label(this._el, 'errorMessage', { message: error.message }))
|
|
673
682
|
})
|
|
674
683
|
|
|
675
684
|
provider.on('statechange', (state) => {
|
|
@@ -685,7 +694,7 @@ export class Player implements IPlayer {
|
|
|
685
694
|
provider.on('fullscreenchange', (fullscreen) => {
|
|
686
695
|
this._store.set('fullscreen', fullscreen)
|
|
687
696
|
this._events.emit('fullscreenchange', fullscreen)
|
|
688
|
-
this.announce(fullscreen ? '
|
|
697
|
+
this.announce(label(this._el, fullscreen ? 'enteredFullscreen' : 'exitedFullscreen'))
|
|
689
698
|
})
|
|
690
699
|
|
|
691
700
|
provider.on('pipchange', (pip) => {
|
|
@@ -703,7 +712,20 @@ export class Player implements IPlayer {
|
|
|
703
712
|
|
|
704
713
|
// === Playback ===
|
|
705
714
|
|
|
706
|
-
|
|
715
|
+
/**
|
|
716
|
+
* Play what was asked for before the source was ready. A browser that
|
|
717
|
+
* refuses sound without a tap (code 1) gets it muted instead, as autoplay
|
|
718
|
+
* is everywhere, and the mute button turns it up.
|
|
719
|
+
*/
|
|
720
|
+
private async startRequestedPlay(provider: Provider): Promise<void> {
|
|
721
|
+
await provider.play().catch(() => {})
|
|
722
|
+
if (this._provider !== provider || this.state.error?.code !== 1 || this.state.muted) return
|
|
723
|
+
this._store.set('error', null)
|
|
724
|
+
this.setMuted(true)
|
|
725
|
+
await provider.play().catch(() => {})
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async play(): Promise<void> {
|
|
707
729
|
if (!this._provider) {
|
|
708
730
|
this._playRequested = true
|
|
709
731
|
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')
|