@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/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
+ }
@@ -0,0 +1,5 @@
1
+ import type { UniversalComponent } from 'octane/universal'
2
+ import type { VideoProps } from './props'
3
+
4
+ export declare const Video: UniversalComponent<VideoProps>
5
+ export type { VideoEvent, VideoFit, VideoHandle, VideoProps } from './props'
@@ -0,0 +1,4 @@
1
+ import type { VideoProps } from './props'
2
+
3
+ export type { VideoEvent, VideoFit, VideoHandle, VideoProps } from './props'
4
+ export declare function Video(props: VideoProps): unknown
@@ -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
+ }