@octane-xplat/video 0.0.0 → 0.7.1
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 +21 -0
- package/dist/native/Video.tsrx.js +248 -0
- package/dist/native/cx.mobile.js +11 -0
- package/dist/native/escape-props.js +39 -0
- package/dist/native/index.js +2 -0
- package/dist/native/layout-child.js +38 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/native/video-chrome.tsrx.js +359 -0
- package/dist/web/Video.web.tsrx.js +400 -0
- package/dist/web/escape-props.web.js +17 -0
- package/dist/web/index.js +2 -0
- package/dist/web/layout-child.web.js +47 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/video-chrome.web.tsrx.js +201 -0
- package/package.json +75 -5
- package/src/Video.macos.tsrx +7 -0
- package/src/Video.tsrx +162 -0
- package/src/Video.web.tsrx +147 -0
- package/src/cx.mobile.ts +8 -0
- package/src/escape-props.ts +65 -0
- package/src/escape-props.web.ts +25 -0
- package/src/index.macos.ts +2 -0
- package/src/index.ts +2 -0
- package/src/index.web.ts +2 -0
- package/src/layout-child.ts +57 -0
- package/src/layout-child.web.ts +108 -0
- package/src/props.ts +125 -0
- package/src/video-chrome.tsrx +117 -0
- package/src/video-chrome.web.tsrx +116 -0
- package/types/index.d.ts +5 -0
- package/types/index.macos.d.ts +4 -0
- package/types/props.d.ts +103 -0
package/src/props.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export interface LayoutChildProps {
|
|
2
|
+
row?: number
|
|
3
|
+
col?: number
|
|
4
|
+
rowSpan?: number
|
|
5
|
+
colSpan?: number
|
|
6
|
+
dock?: 'left' | 'top' | 'right' | 'bottom'
|
|
7
|
+
left?: number
|
|
8
|
+
top?: number
|
|
9
|
+
flexGrow?: number
|
|
10
|
+
flexShrink?: number
|
|
11
|
+
alignSelf?: string
|
|
12
|
+
order?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Flex-container props shared by View/Row/Pressable — RN vocabulary, applied
|
|
16
|
+
* to the host flexboxlayout natively and the element's style on web.
|
|
17
|
+
* `gap` is a dip number (px on web); NS supports it on FlexboxLayout only
|
|
18
|
+
* (GridLayout has no gap). */
|
|
19
|
+
export interface FlexContainerProps {
|
|
20
|
+
justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly'
|
|
21
|
+
alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
|
|
22
|
+
flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse'
|
|
23
|
+
gap?: number | string
|
|
24
|
+
rowGap?: number | string
|
|
25
|
+
columnGap?: number | string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Role =
|
|
29
|
+
| 'button'
|
|
30
|
+
| 'link'
|
|
31
|
+
| 'search'
|
|
32
|
+
| 'image'
|
|
33
|
+
| 'heading'
|
|
34
|
+
| 'adjustable'
|
|
35
|
+
| 'summary'
|
|
36
|
+
| 'text'
|
|
37
|
+
| 'none'
|
|
38
|
+
| 'progressbar'
|
|
39
|
+
| 'checkbox'
|
|
40
|
+
| 'switch'
|
|
41
|
+
| 'radio'
|
|
42
|
+
| 'spinbutton'
|
|
43
|
+
| 'tab'
|
|
44
|
+
|
|
45
|
+
export interface AccessibilityProps {
|
|
46
|
+
accessible?: boolean
|
|
47
|
+
accessibilityLabel?: string
|
|
48
|
+
accessibilityRole?: Role
|
|
49
|
+
accessibilityHint?: string
|
|
50
|
+
accessibilityValue?: string
|
|
51
|
+
accessibilityState?: {
|
|
52
|
+
disabled?: boolean
|
|
53
|
+
selected?: boolean
|
|
54
|
+
checked?: boolean
|
|
55
|
+
}
|
|
56
|
+
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type VideoFit = 'contain' | 'cover' | 'fill'
|
|
60
|
+
|
|
61
|
+
export interface VideoEvent {
|
|
62
|
+
/** Playback position in milliseconds. */
|
|
63
|
+
position?: number
|
|
64
|
+
/** Clip duration in milliseconds, once known (0 before then). */
|
|
65
|
+
duration?: number
|
|
66
|
+
/** Failure description. */
|
|
67
|
+
message?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface VideoHandle {
|
|
71
|
+
play(): void
|
|
72
|
+
pause(): void
|
|
73
|
+
/** Seek to a position in milliseconds. */
|
|
74
|
+
seekTo(ms: number): void
|
|
75
|
+
/** Current playback position in ms (0 before metadata loads). */
|
|
76
|
+
currentTime(): number
|
|
77
|
+
/** Clip duration in ms (0 before metadata loads). */
|
|
78
|
+
duration(): number
|
|
79
|
+
/** The platform surface (`HTMLVideoElement` / plugin `Video`). */
|
|
80
|
+
native: any
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Embedded video — surface-hosted bucket: the OS engine owns the pixels
|
|
84
|
+
* (web `<video>` / `@nstudio/nativescript-exoplayer` → AVPlayerViewController
|
|
85
|
+
* + ExoPlayer2), all chrome is self-drawn so `controls` looks identical on
|
|
86
|
+
* every target. Times are milliseconds on every platform. */
|
|
87
|
+
export interface VideoProps extends LayoutChildProps, AccessibilityProps {
|
|
88
|
+
className?: any
|
|
89
|
+
style?: any
|
|
90
|
+
id?: string
|
|
91
|
+
/** Remote URL; native also accepts `~/` bundle paths and absolute files. */
|
|
92
|
+
src: string
|
|
93
|
+
/** Frame shown until playback starts. */
|
|
94
|
+
poster?: string
|
|
95
|
+
/** Controlled playback — pair with `onPlayingChange`. Leave unset for
|
|
96
|
+
* uncontrolled playback (the transport and `bind` handle still work). */
|
|
97
|
+
playing?: boolean
|
|
98
|
+
/** Uncontrolled start-playing shorthand. Browsers block unmuted
|
|
99
|
+
* autoplay — pair with `muted` when `autoPlay` matters on web. */
|
|
100
|
+
autoPlay?: boolean
|
|
101
|
+
muted?: boolean
|
|
102
|
+
loop?: boolean
|
|
103
|
+
/** Self-drawn transport overlay (play/pause, scrubber, time, mute) —
|
|
104
|
+
* tap the surface to show/hide. Default true. */
|
|
105
|
+
controls?: boolean
|
|
106
|
+
/** Content fitting inside the frame: `contain` (letterbox, default),
|
|
107
|
+
* `cover` (crop to fill), `fill` (stretch). */
|
|
108
|
+
fit?: VideoFit
|
|
109
|
+
/** Playback metadata is loaded; `e.duration` is the clip length in ms. */
|
|
110
|
+
onReady?: (e: VideoEvent) => void
|
|
111
|
+
onEnded?: () => void
|
|
112
|
+
/** Fires on user toggles and controlled `playing` writes. */
|
|
113
|
+
onPlayingChange?: (playing: boolean) => void
|
|
114
|
+
/** Fires when the transport's mute button toggles. */
|
|
115
|
+
onMutedChange?: (muted: boolean) => void
|
|
116
|
+
/** Web only — the plugin players surface no error event on native. */
|
|
117
|
+
onError?: (e: VideoEvent) => void
|
|
118
|
+
/** Imperative handle — play/pause/seek/time plus `native` for anything
|
|
119
|
+
* the shared props don't cover. */
|
|
120
|
+
bind?: (h: VideoHandle) => void
|
|
121
|
+
/** Platform escape hatches, applied after the shared props. */
|
|
122
|
+
ios?: Record<string, any>
|
|
123
|
+
android?: Record<string, any>
|
|
124
|
+
web?: Record<string, any>
|
|
125
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { useEffect, useState } from 'octane';
|
|
3
|
+
import { Icon, Image, Pressable, Row, Slider, Text, View } from '@octane-xplat/ui';
|
|
4
|
+
|
|
5
|
+
/** Self-drawn video transport — the chrome half of the surface-hosted
|
|
6
|
+
* `Video` component. Identical structure to the native twin (only the
|
|
7
|
+
* pragma differs): every layer composes xplat primitives,
|
|
8
|
+
* so the transport draws the same pixels on every target — the leaf only
|
|
9
|
+
* supplies the surface. The layers stack inside the leaf's `vx-stack`
|
|
10
|
+
* grid (later = on top), so this fragment's children are each a
|
|
11
|
+
* full-frame layer. Times are milliseconds (matches both plugin players;
|
|
12
|
+
* the web leaf converts from `HTMLMediaElement`'s seconds). */
|
|
13
|
+
export interface VideoChromeProps {
|
|
14
|
+
/** Frame image shown until playback first starts. */
|
|
15
|
+
poster?: string
|
|
16
|
+
started: boolean
|
|
17
|
+
playing: boolean
|
|
18
|
+
/** Playback position in ms. */
|
|
19
|
+
position: number
|
|
20
|
+
/** Clip duration in ms (0 until known). */
|
|
21
|
+
duration: number
|
|
22
|
+
muted: boolean
|
|
23
|
+
onTogglePlay: () => void
|
|
24
|
+
onToggleMute: () => void
|
|
25
|
+
onSeek: (ms: number) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stamp(ms: number): string {
|
|
29
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
30
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function VideoChrome(props: VideoChromeProps) @{
|
|
34
|
+
const [visible, setVisible] = useState(true);
|
|
35
|
+
|
|
36
|
+
// Chrome auto-hides while playing; pausing or tapping brings it back.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!props.playing || !visible) return;
|
|
39
|
+
const t = setTimeout(() => setVisible(false), 2500);
|
|
40
|
+
return () => clearTimeout(t);
|
|
41
|
+
}, [props.playing, visible]);
|
|
42
|
+
|
|
43
|
+
<>
|
|
44
|
+
@if (props.poster && !props.started) {
|
|
45
|
+
// aspectFill matches the web leaf's object-fit: cover.
|
|
46
|
+
<Image
|
|
47
|
+
className="vx-video-poster"
|
|
48
|
+
src={props.poster}
|
|
49
|
+
ios={{ stretch: 'aspectFill' }}
|
|
50
|
+
android={{ stretch: 'aspectFill' }}
|
|
51
|
+
/>
|
|
52
|
+
}
|
|
53
|
+
<Pressable
|
|
54
|
+
className="vx-video-tap"
|
|
55
|
+
accessibilityRole="button"
|
|
56
|
+
accessibilityLabel={visible ? 'Hide video controls' : 'Show video controls'}
|
|
57
|
+
onPress={() => setVisible(props.playing ? !visible : true)}
|
|
58
|
+
/>
|
|
59
|
+
@if (visible) {
|
|
60
|
+
<>
|
|
61
|
+
@if (!props.playing) {
|
|
62
|
+
// While paused, tap anywhere on the frame is tap-to-play; the
|
|
63
|
+
// show/hide tap layer beneath is reachable once it starts.
|
|
64
|
+
<Pressable
|
|
65
|
+
className="vx-video-center"
|
|
66
|
+
justifyContent="center"
|
|
67
|
+
alignItems="center"
|
|
68
|
+
accessibilityRole="button"
|
|
69
|
+
accessibilityLabel="Play"
|
|
70
|
+
onPress={props.onTogglePlay}
|
|
71
|
+
>
|
|
72
|
+
<View className="vx-video-centerbtn">
|
|
73
|
+
<Icon name="xplat-play" size={28} color="#fff" />
|
|
74
|
+
</View>
|
|
75
|
+
</Pressable>
|
|
76
|
+
}
|
|
77
|
+
// Positioning wrappers cover the frame — the web-only CSS pass gives
|
|
78
|
+
// them pointer-events:none so taps reach the surface/tap layer; NS
|
|
79
|
+
// layout views never capture touches without handlers.
|
|
80
|
+
<View className="vx-video-barwrap" justifyContent="end">
|
|
81
|
+
<Row className="vx-video-bar" alignItems="center" gap={8}>
|
|
82
|
+
<Pressable
|
|
83
|
+
className="vx-video-btn"
|
|
84
|
+
accessibilityRole="button"
|
|
85
|
+
accessibilityLabel={props.playing ? 'Pause' : 'Play'}
|
|
86
|
+
onPress={props.onTogglePlay}
|
|
87
|
+
>
|
|
88
|
+
<Icon name={props.playing ? 'xplat-pause' : 'xplat-play'} size={20} color="#fff" />
|
|
89
|
+
</Pressable>
|
|
90
|
+
<Text className="vx-video-time">{stamp(props.position)}</Text>
|
|
91
|
+
<Slider
|
|
92
|
+
className="vx-video-slider"
|
|
93
|
+
style={{ flexGrow: 1 }}
|
|
94
|
+
value={props.position}
|
|
95
|
+
minValue={0}
|
|
96
|
+
maxValue={props.duration > 0 ? props.duration : 1}
|
|
97
|
+
onValueChange={props.onSeek}
|
|
98
|
+
/>
|
|
99
|
+
<Text className="vx-video-time">{stamp(props.duration)}</Text>
|
|
100
|
+
<Pressable
|
|
101
|
+
className="vx-video-btn"
|
|
102
|
+
accessibilityRole="button"
|
|
103
|
+
accessibilityLabel={props.muted ? 'Unmute' : 'Mute'}
|
|
104
|
+
onPress={props.onToggleMute}
|
|
105
|
+
>
|
|
106
|
+
<Icon
|
|
107
|
+
name={props.muted ? 'xplat-volume-off' : 'xplat-volume'}
|
|
108
|
+
size={20}
|
|
109
|
+
color="#fff"
|
|
110
|
+
/>
|
|
111
|
+
</Pressable>
|
|
112
|
+
</Row>
|
|
113
|
+
</View>
|
|
114
|
+
</>
|
|
115
|
+
}
|
|
116
|
+
</>
|
|
117
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useState } from 'octane';
|
|
2
|
+
import { Icon, Image, Pressable, Row, Slider, Text, View } from '@octane-xplat/ui';
|
|
3
|
+
|
|
4
|
+
/** Self-drawn video transport — the chrome half of the surface-hosted
|
|
5
|
+
* `Video` component. Identical structure to the native twin (only the
|
|
6
|
+
* pragma differs): every layer composes xplat primitives,
|
|
7
|
+
* so the transport draws the same pixels on every target — the leaf only
|
|
8
|
+
* supplies the surface. The layers stack inside the leaf's `vx-stack`
|
|
9
|
+
* grid (later = on top), so this fragment's children are each a
|
|
10
|
+
* full-frame layer. Times are milliseconds (matches both plugin players;
|
|
11
|
+
* the web leaf converts from `HTMLMediaElement`'s seconds). */
|
|
12
|
+
export interface VideoChromeProps {
|
|
13
|
+
/** Frame image shown until playback first starts. */
|
|
14
|
+
poster?: string
|
|
15
|
+
started: boolean
|
|
16
|
+
playing: boolean
|
|
17
|
+
/** Playback position in ms. */
|
|
18
|
+
position: number
|
|
19
|
+
/** Clip duration in ms (0 until known). */
|
|
20
|
+
duration: number
|
|
21
|
+
muted: boolean
|
|
22
|
+
onTogglePlay: () => void
|
|
23
|
+
onToggleMute: () => void
|
|
24
|
+
onSeek: (ms: number) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stamp(ms: number): string {
|
|
28
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
29
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function VideoChrome(props: VideoChromeProps) @{
|
|
33
|
+
const [visible, setVisible] = useState(true);
|
|
34
|
+
|
|
35
|
+
// Chrome auto-hides while playing; pausing or tapping brings it back.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!props.playing || !visible) return;
|
|
38
|
+
const t = setTimeout(() => setVisible(false), 2500);
|
|
39
|
+
return () => clearTimeout(t);
|
|
40
|
+
}, [props.playing, visible]);
|
|
41
|
+
|
|
42
|
+
<>
|
|
43
|
+
@if (props.poster && !props.started) {
|
|
44
|
+
// aspectFill matches the web leaf's object-fit: cover.
|
|
45
|
+
<Image
|
|
46
|
+
className="vx-video-poster"
|
|
47
|
+
src={props.poster}
|
|
48
|
+
ios={{ stretch: 'aspectFill' }}
|
|
49
|
+
android={{ stretch: 'aspectFill' }}
|
|
50
|
+
/>
|
|
51
|
+
}
|
|
52
|
+
<Pressable
|
|
53
|
+
className="vx-video-tap"
|
|
54
|
+
accessibilityRole="button"
|
|
55
|
+
accessibilityLabel={visible ? 'Hide video controls' : 'Show video controls'}
|
|
56
|
+
onPress={() => setVisible(props.playing ? !visible : true)}
|
|
57
|
+
/>
|
|
58
|
+
@if (visible) {
|
|
59
|
+
<>
|
|
60
|
+
@if (!props.playing) {
|
|
61
|
+
// While paused, tap anywhere on the frame is tap-to-play; the
|
|
62
|
+
// show/hide tap layer beneath is reachable once it starts.
|
|
63
|
+
<Pressable
|
|
64
|
+
className="vx-video-center"
|
|
65
|
+
justifyContent="center"
|
|
66
|
+
alignItems="center"
|
|
67
|
+
accessibilityRole="button"
|
|
68
|
+
accessibilityLabel="Play"
|
|
69
|
+
onPress={props.onTogglePlay}
|
|
70
|
+
>
|
|
71
|
+
<View className="vx-video-centerbtn">
|
|
72
|
+
<Icon name="xplat-play" size={28} color="#fff" />
|
|
73
|
+
</View>
|
|
74
|
+
</Pressable>
|
|
75
|
+
}
|
|
76
|
+
// Positioning wrappers cover the frame — the web-only CSS pass gives
|
|
77
|
+
// them pointer-events:none so taps reach the surface/tap layer; NS
|
|
78
|
+
// layout views never capture touches without handlers.
|
|
79
|
+
<View className="vx-video-barwrap" justifyContent="end">
|
|
80
|
+
<Row className="vx-video-bar" alignItems="center" gap={8}>
|
|
81
|
+
<Pressable
|
|
82
|
+
className="vx-video-btn"
|
|
83
|
+
accessibilityRole="button"
|
|
84
|
+
accessibilityLabel={props.playing ? 'Pause' : 'Play'}
|
|
85
|
+
onPress={props.onTogglePlay}
|
|
86
|
+
>
|
|
87
|
+
<Icon name={props.playing ? 'xplat-pause' : 'xplat-play'} size={20} color="#fff" />
|
|
88
|
+
</Pressable>
|
|
89
|
+
<Text className="vx-video-time">{stamp(props.position)}</Text>
|
|
90
|
+
<Slider
|
|
91
|
+
className="vx-video-slider"
|
|
92
|
+
style={{ flexGrow: 1 }}
|
|
93
|
+
value={props.position}
|
|
94
|
+
minValue={0}
|
|
95
|
+
maxValue={props.duration > 0 ? props.duration : 1}
|
|
96
|
+
onValueChange={props.onSeek}
|
|
97
|
+
/>
|
|
98
|
+
<Text className="vx-video-time">{stamp(props.duration)}</Text>
|
|
99
|
+
<Pressable
|
|
100
|
+
className="vx-video-btn"
|
|
101
|
+
accessibilityRole="button"
|
|
102
|
+
accessibilityLabel={props.muted ? 'Unmute' : 'Mute'}
|
|
103
|
+
onPress={props.onToggleMute}
|
|
104
|
+
>
|
|
105
|
+
<Icon
|
|
106
|
+
name={props.muted ? 'xplat-volume-off' : 'xplat-volume'}
|
|
107
|
+
size={20}
|
|
108
|
+
color="#fff"
|
|
109
|
+
/>
|
|
110
|
+
</Pressable>
|
|
111
|
+
</Row>
|
|
112
|
+
</View>
|
|
113
|
+
</>
|
|
114
|
+
}
|
|
115
|
+
</>
|
|
116
|
+
}
|
package/types/index.d.ts
ADDED
package/types/props.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface LayoutChildProps {
|
|
2
|
+
row?: number;
|
|
3
|
+
col?: number;
|
|
4
|
+
rowSpan?: number;
|
|
5
|
+
colSpan?: number;
|
|
6
|
+
dock?: 'left' | 'top' | 'right' | 'bottom';
|
|
7
|
+
left?: number;
|
|
8
|
+
top?: number;
|
|
9
|
+
flexGrow?: number;
|
|
10
|
+
flexShrink?: number;
|
|
11
|
+
alignSelf?: string;
|
|
12
|
+
order?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Flex-container props shared by View/Row/Pressable — RN vocabulary, applied
|
|
15
|
+
* to the host flexboxlayout natively and the element's style on web.
|
|
16
|
+
* `gap` is a dip number (px on web); NS supports it on FlexboxLayout only
|
|
17
|
+
* (GridLayout has no gap). */
|
|
18
|
+
export interface FlexContainerProps {
|
|
19
|
+
justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
|
|
20
|
+
alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
21
|
+
flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse';
|
|
22
|
+
gap?: number | string;
|
|
23
|
+
rowGap?: number | string;
|
|
24
|
+
columnGap?: number | string;
|
|
25
|
+
}
|
|
26
|
+
export type Role = 'button' | 'link' | 'search' | 'image' | 'heading' | 'adjustable' | 'summary' | 'text' | 'none' | 'progressbar' | 'checkbox' | 'switch' | 'radio' | 'spinbutton' | 'tab';
|
|
27
|
+
export interface AccessibilityProps {
|
|
28
|
+
accessible?: boolean;
|
|
29
|
+
accessibilityLabel?: string;
|
|
30
|
+
accessibilityRole?: Role;
|
|
31
|
+
accessibilityHint?: string;
|
|
32
|
+
accessibilityValue?: string;
|
|
33
|
+
accessibilityState?: {
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
selected?: boolean;
|
|
36
|
+
checked?: boolean;
|
|
37
|
+
};
|
|
38
|
+
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive';
|
|
39
|
+
}
|
|
40
|
+
export type VideoFit = 'contain' | 'cover' | 'fill';
|
|
41
|
+
export interface VideoEvent {
|
|
42
|
+
/** Playback position in milliseconds. */
|
|
43
|
+
position?: number;
|
|
44
|
+
/** Clip duration in milliseconds, once known (0 before then). */
|
|
45
|
+
duration?: number;
|
|
46
|
+
/** Failure description. */
|
|
47
|
+
message?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface VideoHandle {
|
|
50
|
+
play(): void;
|
|
51
|
+
pause(): void;
|
|
52
|
+
/** Seek to a position in milliseconds. */
|
|
53
|
+
seekTo(ms: number): void;
|
|
54
|
+
/** Current playback position in ms (0 before metadata loads). */
|
|
55
|
+
currentTime(): number;
|
|
56
|
+
/** Clip duration in ms (0 before metadata loads). */
|
|
57
|
+
duration(): number;
|
|
58
|
+
/** The platform surface (`HTMLVideoElement` / plugin `Video`). */
|
|
59
|
+
native: any;
|
|
60
|
+
}
|
|
61
|
+
/** Embedded video — surface-hosted bucket: the OS engine owns the pixels
|
|
62
|
+
* (web `<video>` / `@nstudio/nativescript-exoplayer` → AVPlayerViewController
|
|
63
|
+
* + ExoPlayer2), all chrome is self-drawn so `controls` looks identical on
|
|
64
|
+
* every target. Times are milliseconds on every platform. */
|
|
65
|
+
export interface VideoProps extends LayoutChildProps, AccessibilityProps {
|
|
66
|
+
className?: any;
|
|
67
|
+
style?: any;
|
|
68
|
+
id?: string;
|
|
69
|
+
/** Remote URL; native also accepts `~/` bundle paths and absolute files. */
|
|
70
|
+
src: string;
|
|
71
|
+
/** Frame shown until playback starts. */
|
|
72
|
+
poster?: string;
|
|
73
|
+
/** Controlled playback — pair with `onPlayingChange`. Leave unset for
|
|
74
|
+
* uncontrolled playback (the transport and `bind` handle still work). */
|
|
75
|
+
playing?: boolean;
|
|
76
|
+
/** Uncontrolled start-playing shorthand. Browsers block unmuted
|
|
77
|
+
* autoplay — pair with `muted` when `autoPlay` matters on web. */
|
|
78
|
+
autoPlay?: boolean;
|
|
79
|
+
muted?: boolean;
|
|
80
|
+
loop?: boolean;
|
|
81
|
+
/** Self-drawn transport overlay (play/pause, scrubber, time, mute) —
|
|
82
|
+
* tap the surface to show/hide. Default true. */
|
|
83
|
+
controls?: boolean;
|
|
84
|
+
/** Content fitting inside the frame: `contain` (letterbox, default),
|
|
85
|
+
* `cover` (crop to fill), `fill` (stretch). */
|
|
86
|
+
fit?: VideoFit;
|
|
87
|
+
/** Playback metadata is loaded; `e.duration` is the clip length in ms. */
|
|
88
|
+
onReady?: (e: VideoEvent) => void;
|
|
89
|
+
onEnded?: () => void;
|
|
90
|
+
/** Fires on user toggles and controlled `playing` writes. */
|
|
91
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
92
|
+
/** Fires when the transport's mute button toggles. */
|
|
93
|
+
onMutedChange?: (muted: boolean) => void;
|
|
94
|
+
/** Web only — the plugin players surface no error event on native. */
|
|
95
|
+
onError?: (e: VideoEvent) => void;
|
|
96
|
+
/** Imperative handle — play/pause/seek/time plus `native` for anything
|
|
97
|
+
* the shared props don't cover. */
|
|
98
|
+
bind?: (h: VideoHandle) => void;
|
|
99
|
+
/** Platform escape hatches, applied after the shared props. */
|
|
100
|
+
ios?: Record<string, any>;
|
|
101
|
+
android?: Record<string, any>;
|
|
102
|
+
web?: Record<string, any>;
|
|
103
|
+
}
|