ts-video-player 0.1.0
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/LICENSE.md +21 -0
- package/README.md +438 -0
- package/dist/cdn.d.ts +10 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.min.js +1046 -0
- package/dist/cdn.min.js.map +54 -0
- package/dist/cdn.test.d.ts +2 -0
- package/dist/cdn.test.d.ts.map +1 -0
- package/dist/chunk-36x4yhjn.js +765 -0
- package/dist/chunk-36x4yhjn.js.map +12 -0
- package/dist/chunk-eajts2jv.js +4243 -0
- package/dist/chunk-eajts2jv.js.map +42 -0
- package/dist/chunk-z1ehbz9f.js +380 -0
- package/dist/chunk-z1ehbz9f.js.map +19 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/core/audio-gain.d.ts +67 -0
- package/dist/core/audio-gain.d.ts.map +1 -0
- package/dist/core/caption-styles.d.ts +121 -0
- package/dist/core/caption-styles.d.ts.map +1 -0
- package/dist/core/chapters.d.ts +150 -0
- package/dist/core/chapters.d.ts.map +1 -0
- package/dist/core/error-overlay.d.ts +71 -0
- package/dist/core/error-overlay.d.ts.map +1 -0
- package/dist/core/events.d.ts +119 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/features.d.ts +64 -0
- package/dist/core/features.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +132 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/live.d.ts +130 -0
- package/dist/core/live.d.ts.map +1 -0
- package/dist/core/media-session.d.ts +38 -0
- package/dist/core/media-session.d.ts.map +1 -0
- package/dist/core/orientation.d.ts +22 -0
- package/dist/core/orientation.d.ts.map +1 -0
- package/dist/core/popover.d.ts +42 -0
- package/dist/core/popover.d.ts.map +1 -0
- package/dist/core/remote-playback.d.ts +195 -0
- package/dist/core/remote-playback.d.ts.map +1 -0
- package/dist/core/settings-menu.d.ts +132 -0
- package/dist/core/settings-menu.d.ts.map +1 -0
- package/dist/core/spinner.d.ts +67 -0
- package/dist/core/spinner.d.ts.map +1 -0
- package/dist/core/state.d.ts +142 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/text-tracks.d.ts +162 -0
- package/dist/core/text-tracks.d.ts.map +1 -0
- package/dist/core/themes.d.ts +213 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/thumbnails.d.ts +124 -0
- package/dist/core/thumbnails.d.ts.map +1 -0
- package/dist/core/tooltips.d.ts +100 -0
- package/dist/core/tooltips.d.ts.map +1 -0
- package/dist/elements/base.d.ts +3 -0
- package/dist/elements/base.d.ts.map +1 -0
- package/dist/elements/index.d.ts +33 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +55 -0
- package/dist/elements/index.js.map +9 -0
- package/dist/elements/media-airplay-button.d.ts +13 -0
- package/dist/elements/media-airplay-button.d.ts.map +1 -0
- package/dist/elements/media-error-display.d.ts +13 -0
- package/dist/elements/media-error-display.d.ts.map +1 -0
- package/dist/elements/media-fullscreen-button.d.ts +13 -0
- package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
- package/dist/elements/media-live-button.d.ts +13 -0
- package/dist/elements/media-live-button.d.ts.map +1 -0
- package/dist/elements/media-mute-button.d.ts +13 -0
- package/dist/elements/media-mute-button.d.ts.map +1 -0
- package/dist/elements/media-pip-button.d.ts +13 -0
- package/dist/elements/media-pip-button.d.ts.map +1 -0
- package/dist/elements/media-play-button.d.ts +14 -0
- package/dist/elements/media-play-button.d.ts.map +1 -0
- package/dist/elements/media-playback-rate-button.d.ts +10 -0
- package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
- package/dist/elements/media-preview-time.d.ts +18 -0
- package/dist/elements/media-preview-time.d.ts.map +1 -0
- package/dist/elements/media-progress-bar.d.ts +16 -0
- package/dist/elements/media-progress-bar.d.ts.map +1 -0
- package/dist/elements/media-remote-playback-button.d.ts +16 -0
- package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
- package/dist/elements/media-seek-button.d.ts +9 -0
- package/dist/elements/media-seek-button.d.ts.map +1 -0
- package/dist/elements/media-settings-menu.d.ts +32 -0
- package/dist/elements/media-settings-menu.d.ts.map +1 -0
- package/dist/elements/media-time-display.d.ts +21 -0
- package/dist/elements/media-time-display.d.ts.map +1 -0
- package/dist/elements/media-time-group.d.ts +22 -0
- package/dist/elements/media-time-group.d.ts.map +1 -0
- package/dist/elements/media-time-separator.d.ts +13 -0
- package/dist/elements/media-time-separator.d.ts.map +1 -0
- package/dist/elements/media-volume-slider.d.ts +16 -0
- package/dist/elements/media-volume-slider.d.ts.map +1 -0
- package/dist/elements/utils.d.ts +27 -0
- package/dist/elements/utils.d.ts.map +1 -0
- package/dist/elements/video-player.d.ts +23 -0
- package/dist/elements/video-player.d.ts.map +1 -0
- package/dist/elements/video-skin.d.ts +23 -0
- package/dist/elements/video-skin.d.ts.map +1 -0
- package/dist/features/captions.d.ts +8 -0
- package/dist/features/captions.d.ts.map +1 -0
- package/dist/features/create-player.d.ts +45 -0
- package/dist/features/create-player.d.ts.map +1 -0
- package/dist/features/fullscreen.d.ts +8 -0
- package/dist/features/fullscreen.d.ts.map +1 -0
- package/dist/features/gestures.d.ts +8 -0
- package/dist/features/gestures.d.ts.map +1 -0
- package/dist/features/index.d.ts +26 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +31 -0
- package/dist/features/index.js.map +9 -0
- package/dist/features/keyboard.d.ts +8 -0
- package/dist/features/keyboard.d.ts.map +1 -0
- package/dist/features/pip.d.ts +8 -0
- package/dist/features/pip.d.ts.map +1 -0
- package/dist/features/playback.d.ts +8 -0
- package/dist/features/playback.d.ts.map +1 -0
- package/dist/features/quality.d.ts +8 -0
- package/dist/features/quality.d.ts.map +1 -0
- package/dist/features/types.d.ts +26 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/volume.d.ts +8 -0
- package/dist/features/volume.d.ts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8149 -0
- package/dist/index.js.map +31 -0
- package/dist/layouts/index.d.ts +58 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/player.d.ts +90 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/plugins/ads.d.ts +311 -0
- package/dist/plugins/ads.d.ts.map +1 -0
- package/dist/plugins/analytics.d.ts +203 -0
- package/dist/plugins/analytics.d.ts.map +1 -0
- package/dist/plugins/end-screen.d.ts +184 -0
- package/dist/plugins/end-screen.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +20 -0
- package/dist/plugins/index.js.map +9 -0
- package/dist/plugins/skip-segments.d.ts +159 -0
- package/dist/plugins/skip-segments.d.ts.map +1 -0
- package/dist/plugins/watermark.d.ts +157 -0
- package/dist/plugins/watermark.d.ts.map +1 -0
- package/dist/providers/base.d.ts +56 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/dash.d.ts +94 -0
- package/dist/providers/dash.d.ts.map +1 -0
- package/dist/providers/hls.d.ts +182 -0
- package/dist/providers/hls.d.ts.map +1 -0
- package/dist/providers/html5.d.ts +64 -0
- package/dist/providers/html5.d.ts.map +1 -0
- package/dist/providers/index.d.ts +41 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/vimeo.d.ts +157 -0
- package/dist/providers/vimeo.d.ts.map +1 -0
- package/dist/providers/youtube.d.ts +134 -0
- package/dist/providers/youtube.d.ts.map +1 -0
- package/dist/stx/index.d.ts +59 -0
- package/dist/stx/index.d.ts.map +1 -0
- package/dist/stx/index.js +480 -0
- package/dist/stx/index.js.map +11 -0
- package/dist/types.d.ts +724 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/components.d.ts +121 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +16 -0
- package/dist/ui/index.js.map +9 -0
- package/package.json +109 -0
- package/src/cdn.test.ts +10 -0
- package/src/cdn.ts +60 -0
- package/src/config.ts +25 -0
- package/src/core/audio-gain.ts +171 -0
- package/src/core/caption-styles.ts +568 -0
- package/src/core/chapters.ts +551 -0
- package/src/core/error-overlay.ts +354 -0
- package/src/core/events.ts +552 -0
- package/src/core/features.ts +287 -0
- package/src/core/i18n.ts +590 -0
- package/src/core/index.ts +249 -0
- package/src/core/live.ts +472 -0
- package/src/core/media-session.ts +137 -0
- package/src/core/orientation.ts +57 -0
- package/src/core/popover.ts +143 -0
- package/src/core/remote-playback.ts +661 -0
- package/src/core/settings-menu.ts +630 -0
- package/src/core/spinner.ts +310 -0
- package/src/core/state.ts +476 -0
- package/src/core/text-tracks.ts +599 -0
- package/src/core/themes.ts +607 -0
- package/src/core/thumbnails.ts +457 -0
- package/src/core/tooltips.ts +370 -0
- package/src/elements/base.ts +2 -0
- package/src/elements/index.ts +82 -0
- package/src/elements/media-airplay-button.ts +78 -0
- package/src/elements/media-error-display.ts +47 -0
- package/src/elements/media-fullscreen-button.ts +72 -0
- package/src/elements/media-live-button.ts +65 -0
- package/src/elements/media-mute-button.ts +69 -0
- package/src/elements/media-pip-button.ts +66 -0
- package/src/elements/media-play-button.ts +69 -0
- package/src/elements/media-playback-rate-button.ts +59 -0
- package/src/elements/media-preview-time.ts +75 -0
- package/src/elements/media-progress-bar.ts +200 -0
- package/src/elements/media-remote-playback-button.ts +108 -0
- package/src/elements/media-seek-button.ts +50 -0
- package/src/elements/media-settings-menu.ts +302 -0
- package/src/elements/media-time-display.ts +99 -0
- package/src/elements/media-time-group.ts +41 -0
- package/src/elements/media-time-separator.ts +35 -0
- package/src/elements/media-volume-slider.ts +140 -0
- package/src/elements/utils.ts +82 -0
- package/src/elements/video-player.ts +175 -0
- package/src/elements/video-skin.ts +127 -0
- package/src/features/captions.ts +56 -0
- package/src/features/create-player.ts +91 -0
- package/src/features/fullscreen.ts +44 -0
- package/src/features/gestures.ts +44 -0
- package/src/features/index.ts +44 -0
- package/src/features/keyboard.ts +89 -0
- package/src/features/pip.ts +47 -0
- package/src/features/playback.ts +68 -0
- package/src/features/quality.ts +19 -0
- package/src/features/types.ts +28 -0
- package/src/features/volume.ts +46 -0
- package/src/index.ts +635 -0
- package/src/layouts/index.ts +672 -0
- package/src/player.ts +821 -0
- package/src/plugins/ads.ts +1045 -0
- package/src/plugins/analytics.ts +673 -0
- package/src/plugins/end-screen.ts +793 -0
- package/src/plugins/index.ts +61 -0
- package/src/plugins/skip-segments.ts +537 -0
- package/src/plugins/watermark.ts +537 -0
- package/src/providers/base.ts +197 -0
- package/src/providers/dash.ts +588 -0
- package/src/providers/hls.ts +624 -0
- package/src/providers/html5.ts +473 -0
- package/src/providers/index.ts +97 -0
- package/src/providers/vimeo.ts +533 -0
- package/src/providers/youtube.ts +542 -0
- package/src/stx/index.ts +606 -0
- package/src/types.ts +874 -0
- package/src/ui/components.ts +910 -0
- package/src/ui/index.ts +29 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fullscreen feature — fullscreen state + availability detection.
|
|
3
|
+
*
|
|
4
|
+
* @module features/fullscreen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
import { detectFullscreenAvailability } from '../core/features'
|
|
9
|
+
import { onFullscreenChange } from '../core/events'
|
|
10
|
+
|
|
11
|
+
export const fullscreen: Feature = {
|
|
12
|
+
name: 'fullscreen',
|
|
13
|
+
stateKeys: ['fullscreen', 'fullscreenAvailability', 'canFullscreen'],
|
|
14
|
+
|
|
15
|
+
setup(ctx: FeatureContext) {
|
|
16
|
+
const { store, container, getMediaElement } = ctx
|
|
17
|
+
|
|
18
|
+
const update = () => {
|
|
19
|
+
const video = getMediaElement() as HTMLVideoElement | null
|
|
20
|
+
const availability = detectFullscreenAvailability(container, video)
|
|
21
|
+
store.batch({
|
|
22
|
+
fullscreenAvailability: availability,
|
|
23
|
+
canFullscreen: availability === 'available',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update()
|
|
28
|
+
|
|
29
|
+
const cleanup = onFullscreenChange((fs) => {
|
|
30
|
+
store.set('fullscreen', fs)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Re-detect after metadata loaded
|
|
34
|
+
const onMeta = () => update()
|
|
35
|
+
const media = getMediaElement()
|
|
36
|
+
media?.addEventListener('loadedmetadata', onMeta)
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
cleanup()
|
|
40
|
+
const m = getMediaElement()
|
|
41
|
+
m?.removeEventListener('loadedmetadata', onMeta)
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gestures feature — touch gesture detection.
|
|
3
|
+
*
|
|
4
|
+
* @module features/gestures
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
import { createGestureDetector } from '../core/events'
|
|
9
|
+
|
|
10
|
+
export const gestures: Feature = {
|
|
11
|
+
name: 'gestures',
|
|
12
|
+
|
|
13
|
+
setup(ctx: FeatureContext) {
|
|
14
|
+
const { container, options } = ctx
|
|
15
|
+
|
|
16
|
+
if (options.gestures === false) return
|
|
17
|
+
|
|
18
|
+
const cleanup = createGestureDetector(container, {
|
|
19
|
+
onDoubleTap: (x) => {
|
|
20
|
+
// Double-tap left third: seek back, right third: seek forward
|
|
21
|
+
const rect = container.getBoundingClientRect()
|
|
22
|
+
const third = rect.width / 3
|
|
23
|
+
const relX = x - rect.left
|
|
24
|
+
const media = ctx.getMediaElement()
|
|
25
|
+
if (!media) return
|
|
26
|
+
if (relX < third) {
|
|
27
|
+
media.currentTime = Math.max(0, media.currentTime - 10)
|
|
28
|
+
} else if (relX > third * 2) {
|
|
29
|
+
media.currentTime = Math.min(media.duration, media.currentTime + 10)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
onSwipeUp: () => {
|
|
33
|
+
const media = ctx.getMediaElement()
|
|
34
|
+
if (media) media.volume = Math.min(1, media.volume + 0.1)
|
|
35
|
+
},
|
|
36
|
+
onSwipeDown: () => {
|
|
37
|
+
const media = ctx.getMediaElement()
|
|
38
|
+
if (media) media.volume = Math.max(0, media.volume - 0.1)
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return cleanup
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Composable Features
|
|
3
|
+
*
|
|
4
|
+
* Tree-shakeable feature composition system.
|
|
5
|
+
*
|
|
6
|
+
* @module features
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Feature implementations
|
|
10
|
+
export { playback } from './playback'
|
|
11
|
+
export { volume } from './volume'
|
|
12
|
+
export { fullscreen } from './fullscreen'
|
|
13
|
+
export { pip } from './pip'
|
|
14
|
+
export { captions } from './captions'
|
|
15
|
+
export { quality } from './quality'
|
|
16
|
+
export { keyboard } from './keyboard'
|
|
17
|
+
export { gestures } from './gestures'
|
|
18
|
+
|
|
19
|
+
// Types
|
|
20
|
+
export type { Feature, FeatureContext } from './types'
|
|
21
|
+
|
|
22
|
+
// Factory
|
|
23
|
+
export { createComposablePlayer } from './create-player'
|
|
24
|
+
export type { ComposablePlayer, ComposablePlayerOptions } from './create-player'
|
|
25
|
+
|
|
26
|
+
// Feature bundles
|
|
27
|
+
import { playback } from './playback'
|
|
28
|
+
import { volume } from './volume'
|
|
29
|
+
import { fullscreen } from './fullscreen'
|
|
30
|
+
import { pip } from './pip'
|
|
31
|
+
import { captions } from './captions'
|
|
32
|
+
import { quality } from './quality'
|
|
33
|
+
import { keyboard } from './keyboard'
|
|
34
|
+
import { gestures } from './gestures'
|
|
35
|
+
import type { Feature } from './types'
|
|
36
|
+
|
|
37
|
+
/** All features for video playback */
|
|
38
|
+
export const videoFeatures: Feature[] = [playback, volume, fullscreen, pip, captions, quality, keyboard, gestures]
|
|
39
|
+
|
|
40
|
+
/** Features for audio-only playback */
|
|
41
|
+
export const audioFeatures: Feature[] = [playback, volume, keyboard]
|
|
42
|
+
|
|
43
|
+
/** Minimal features (playback only) */
|
|
44
|
+
export const minimalFeatures: Feature[] = [playback]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard feature — keyboard shortcut handling.
|
|
3
|
+
*
|
|
4
|
+
* @module features/keyboard
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
import { createKeyboardHandler, type KeyboardActions } from '../core/events'
|
|
9
|
+
|
|
10
|
+
export const keyboard: Feature = {
|
|
11
|
+
name: 'keyboard',
|
|
12
|
+
|
|
13
|
+
setup(ctx: FeatureContext) {
|
|
14
|
+
const { container, options } = ctx
|
|
15
|
+
|
|
16
|
+
if (options.keyboard === false) return
|
|
17
|
+
|
|
18
|
+
const actions: KeyboardActions = {
|
|
19
|
+
togglePlay: () => {
|
|
20
|
+
const media = ctx.getMediaElement()
|
|
21
|
+
if (!media) return
|
|
22
|
+
if (media.paused) media.play()
|
|
23
|
+
else media.pause()
|
|
24
|
+
},
|
|
25
|
+
toggleMute: () => {
|
|
26
|
+
const media = ctx.getMediaElement()
|
|
27
|
+
if (media) media.muted = !media.muted
|
|
28
|
+
},
|
|
29
|
+
toggleFullscreen: () => {
|
|
30
|
+
// Handled at player level
|
|
31
|
+
},
|
|
32
|
+
togglePiP: () => {
|
|
33
|
+
// Handled at player level
|
|
34
|
+
},
|
|
35
|
+
toggleCaptions: () => {
|
|
36
|
+
// Handled at player level
|
|
37
|
+
},
|
|
38
|
+
seekBackward: () => {
|
|
39
|
+
const media = ctx.getMediaElement()
|
|
40
|
+
if (media) media.currentTime = Math.max(0, media.currentTime - 5)
|
|
41
|
+
},
|
|
42
|
+
seekForward: () => {
|
|
43
|
+
const media = ctx.getMediaElement()
|
|
44
|
+
if (media) media.currentTime = Math.min(media.duration, media.currentTime + 5)
|
|
45
|
+
},
|
|
46
|
+
volumeUp: () => {
|
|
47
|
+
const media = ctx.getMediaElement()
|
|
48
|
+
if (media) media.volume = Math.min(1, media.volume + 0.1)
|
|
49
|
+
},
|
|
50
|
+
volumeDown: () => {
|
|
51
|
+
const media = ctx.getMediaElement()
|
|
52
|
+
if (media) media.volume = Math.max(0, media.volume - 0.1)
|
|
53
|
+
},
|
|
54
|
+
speedUp: () => {
|
|
55
|
+
const media = ctx.getMediaElement()
|
|
56
|
+
if (media) media.playbackRate = Math.min(4, media.playbackRate + 0.25)
|
|
57
|
+
},
|
|
58
|
+
speedDown: () => {
|
|
59
|
+
const media = ctx.getMediaElement()
|
|
60
|
+
if (media) media.playbackRate = Math.max(0.25, media.playbackRate - 0.25)
|
|
61
|
+
},
|
|
62
|
+
seekStart: () => {
|
|
63
|
+
const media = ctx.getMediaElement()
|
|
64
|
+
if (media) media.currentTime = 0
|
|
65
|
+
},
|
|
66
|
+
seekEnd: () => {
|
|
67
|
+
const media = ctx.getMediaElement()
|
|
68
|
+
if (media) media.currentTime = media.duration
|
|
69
|
+
},
|
|
70
|
+
seekPercent: (percent) => {
|
|
71
|
+
const media = ctx.getMediaElement()
|
|
72
|
+
if (media) media.currentTime = (percent / 100) * media.duration
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const config = typeof options.keyboard === 'object' ? options.keyboard as Record<string, any> : {}
|
|
77
|
+
const handler = createKeyboardHandler(actions, {
|
|
78
|
+
seekStep: config.seekStep,
|
|
79
|
+
volumeStep: config.volumeStep,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const target = config.global ? document : container
|
|
83
|
+
target.addEventListener('keydown', handler as EventListener)
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
target.removeEventListener('keydown', handler as EventListener)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picture-in-Picture feature — PiP state + availability detection.
|
|
3
|
+
*
|
|
4
|
+
* @module features/pip
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
import { detectPipAvailability } from '../core/features'
|
|
9
|
+
import { onPiPChange } from '../core/events'
|
|
10
|
+
|
|
11
|
+
export const pip: Feature = {
|
|
12
|
+
name: 'pip',
|
|
13
|
+
stateKeys: ['pictureInPicture', 'pipAvailability', 'canPictureInPicture'],
|
|
14
|
+
|
|
15
|
+
setup(ctx: FeatureContext) {
|
|
16
|
+
const { store, getMediaElement } = ctx
|
|
17
|
+
|
|
18
|
+
const update = () => {
|
|
19
|
+
const video = getMediaElement() as HTMLVideoElement | null
|
|
20
|
+
const availability = detectPipAvailability(video)
|
|
21
|
+
store.batch({
|
|
22
|
+
pipAvailability: availability,
|
|
23
|
+
canPictureInPicture: availability === 'available',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update()
|
|
28
|
+
|
|
29
|
+
let cleanupPiP: (() => void) | null = null
|
|
30
|
+
const media = getMediaElement()
|
|
31
|
+
if (media instanceof HTMLVideoElement) {
|
|
32
|
+
cleanupPiP = onPiPChange(media, (isPiP) => {
|
|
33
|
+
store.set('pictureInPicture', isPiP)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Re-detect after metadata loaded
|
|
38
|
+
const onMeta = () => update()
|
|
39
|
+
media?.addEventListener('loadedmetadata', onMeta)
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
cleanupPiP?.()
|
|
43
|
+
const m = getMediaElement()
|
|
44
|
+
m?.removeEventListener('loadedmetadata', onMeta)
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playback feature — core playback state management.
|
|
3
|
+
*
|
|
4
|
+
* @module features/playback
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
|
|
9
|
+
export const playback: Feature = {
|
|
10
|
+
name: 'playback',
|
|
11
|
+
stateKeys: ['paused', 'playing', 'ended', 'seeking', 'waiting', 'currentTime', 'duration', 'playbackRate', 'loop', 'autoplay', 'playbackState'],
|
|
12
|
+
|
|
13
|
+
setup(ctx: FeatureContext) {
|
|
14
|
+
const { store, getMediaElement } = ctx
|
|
15
|
+
|
|
16
|
+
const onPlay = () => store.batch({ paused: false, ended: false })
|
|
17
|
+
const onPause = () => store.set('paused', true)
|
|
18
|
+
const onPlaying = () => store.batch({ playing: true, waiting: false, playbackState: 'playing' })
|
|
19
|
+
const onWaiting = () => store.batch({ waiting: true, playbackState: 'buffering' })
|
|
20
|
+
const onSeeking = () => store.set('seeking', true)
|
|
21
|
+
const onSeeked = () => store.set('seeking', false)
|
|
22
|
+
const onEnded = () => store.batch({ ended: true, playing: false, playbackState: 'ended' })
|
|
23
|
+
const onTimeUpdate = () => {
|
|
24
|
+
const media = getMediaElement()
|
|
25
|
+
if (media) store.set('currentTime', media.currentTime)
|
|
26
|
+
}
|
|
27
|
+
const onDurationChange = () => {
|
|
28
|
+
const media = getMediaElement()
|
|
29
|
+
if (media) store.set('duration', media.duration)
|
|
30
|
+
}
|
|
31
|
+
const onRateChange = () => {
|
|
32
|
+
const media = getMediaElement()
|
|
33
|
+
if (media) store.set('playbackRate', media.playbackRate)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const bind = () => {
|
|
37
|
+
const media = getMediaElement()
|
|
38
|
+
if (!media) return
|
|
39
|
+
media.addEventListener('play', onPlay)
|
|
40
|
+
media.addEventListener('pause', onPause)
|
|
41
|
+
media.addEventListener('playing', onPlaying)
|
|
42
|
+
media.addEventListener('waiting', onWaiting)
|
|
43
|
+
media.addEventListener('seeking', onSeeking)
|
|
44
|
+
media.addEventListener('seeked', onSeeked)
|
|
45
|
+
media.addEventListener('ended', onEnded)
|
|
46
|
+
media.addEventListener('timeupdate', onTimeUpdate)
|
|
47
|
+
media.addEventListener('durationchange', onDurationChange)
|
|
48
|
+
media.addEventListener('ratechange', onRateChange)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bind()
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
const media = getMediaElement()
|
|
55
|
+
if (!media) return
|
|
56
|
+
media.removeEventListener('play', onPlay)
|
|
57
|
+
media.removeEventListener('pause', onPause)
|
|
58
|
+
media.removeEventListener('playing', onPlaying)
|
|
59
|
+
media.removeEventListener('waiting', onWaiting)
|
|
60
|
+
media.removeEventListener('seeking', onSeeking)
|
|
61
|
+
media.removeEventListener('seeked', onSeeked)
|
|
62
|
+
media.removeEventListener('ended', onEnded)
|
|
63
|
+
media.removeEventListener('timeupdate', onTimeUpdate)
|
|
64
|
+
media.removeEventListener('durationchange', onDurationChange)
|
|
65
|
+
media.removeEventListener('ratechange', onRateChange)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality feature — quality selection state.
|
|
3
|
+
*
|
|
4
|
+
* @module features/quality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
|
|
9
|
+
export const quality: Feature = {
|
|
10
|
+
name: 'quality',
|
|
11
|
+
stateKeys: ['qualities', 'autoQuality'],
|
|
12
|
+
|
|
13
|
+
setup(_ctx: FeatureContext) {
|
|
14
|
+
// Quality management is provider-specific (HLS/DASH).
|
|
15
|
+
// This feature exists as a placeholder for the state keys
|
|
16
|
+
// and to allow tree-shaking when quality UI is not needed.
|
|
17
|
+
return undefined
|
|
18
|
+
},
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Feature Types
|
|
3
|
+
*
|
|
4
|
+
* @module features/types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PlayerState } from '../types'
|
|
8
|
+
import type { StateStore } from '../core/state'
|
|
9
|
+
|
|
10
|
+
export interface FeatureContext {
|
|
11
|
+
/** The player container element */
|
|
12
|
+
container: HTMLElement
|
|
13
|
+
/** The state store */
|
|
14
|
+
store: StateStore
|
|
15
|
+
/** Options passed to the player */
|
|
16
|
+
options: Record<string, unknown>
|
|
17
|
+
/** Get the current media element (may be null before load) */
|
|
18
|
+
getMediaElement: () => HTMLMediaElement | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Feature {
|
|
22
|
+
/** Unique feature name */
|
|
23
|
+
name: string
|
|
24
|
+
/** State keys this feature manages */
|
|
25
|
+
stateKeys?: (keyof PlayerState)[]
|
|
26
|
+
/** Initialize the feature. Returns a cleanup function. */
|
|
27
|
+
setup(ctx: FeatureContext): (() => void) | void
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volume feature — volume state + availability detection.
|
|
3
|
+
*
|
|
4
|
+
* @module features/volume
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureContext } from './types'
|
|
8
|
+
import { detectVolumeAvailability, probeVolumeAvailability } from '../core/features'
|
|
9
|
+
|
|
10
|
+
export const volume: Feature = {
|
|
11
|
+
name: 'volume',
|
|
12
|
+
stateKeys: ['volume', 'muted', 'volumeAvailability'],
|
|
13
|
+
|
|
14
|
+
setup(ctx: FeatureContext) {
|
|
15
|
+
const { store, getMediaElement } = ctx
|
|
16
|
+
|
|
17
|
+
// Initial sync detection
|
|
18
|
+
const media = getMediaElement()
|
|
19
|
+
store.set('volumeAvailability', detectVolumeAvailability(media))
|
|
20
|
+
|
|
21
|
+
const onVolumeChange = () => {
|
|
22
|
+
const m = getMediaElement()
|
|
23
|
+
if (m) store.batch({ volume: m.volume, muted: m.muted })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const onLoadedData = () => {
|
|
27
|
+
const m = getMediaElement()
|
|
28
|
+
if (!m) return
|
|
29
|
+
probeVolumeAvailability(m).then((availability) => {
|
|
30
|
+
store.set('volumeAvailability', availability)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (media) {
|
|
35
|
+
media.addEventListener('volumechange', onVolumeChange)
|
|
36
|
+
media.addEventListener('loadeddata', onLoadedData)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
const m = getMediaElement()
|
|
41
|
+
if (!m) return
|
|
42
|
+
m.removeEventListener('volumechange', onVolumeChange)
|
|
43
|
+
m.removeEventListener('loadeddata', onLoadedData)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}
|