@solidrt/core 0.0.59 → 0.0.60
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/package.json +2 -2
- package/src/audio.ts +4 -3
- package/src/video.ts +81 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"AGENTS.md"
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@solidrt/flux-types": "0.0.
|
|
34
|
+
"@solidrt/flux-types": "0.0.60"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@solidjs/signals": "2.0.0-rc.8",
|
package/src/audio.ts
CHANGED
|
@@ -218,9 +218,10 @@ export function createPcmSound(
|
|
|
218
218
|
* Streams a large track, decoding on demand instead of loading it into memory.
|
|
219
219
|
* Single-voice: each play() restarts it. Pass a path (resolved like flux:fs,
|
|
220
220
|
* relative to the process cwd) or a `file()` from flux:fs; a path is wrapped in
|
|
221
|
-
* `file()` for you
|
|
222
|
-
*
|
|
223
|
-
* disposed. For imperative use, call
|
|
221
|
+
* `file()` for you. The track is read from local storage (the app's assets in
|
|
222
|
+
* a packed app), not over the network. Owns the stream's lifecycle: stopped
|
|
223
|
+
* and released when the reactive owner is disposed. For imperative use, call
|
|
224
|
+
* stream()/play() from "flux:audio".
|
|
224
225
|
*/
|
|
225
226
|
export function createSoundStream(source: string | FluxFile, options: SoundStreamOptions = {}): Sound {
|
|
226
227
|
let src = typeof source === "string" ? file(source) : source
|
package/src/video.ts
CHANGED
|
@@ -5,74 +5,134 @@
|
|
|
5
5
|
// subtree effects - applies to video for free. A richer Video component
|
|
6
6
|
// composes on top of this in a higher layer, not in core.
|
|
7
7
|
//
|
|
8
|
+
// A plane player (`present: "plane"`, Android) has no texture: the platform
|
|
9
|
+
// composites the picture fullscreen beneath the UI on the video's own clock,
|
|
10
|
+
// and the UI simply draws over it. It exists from open until the owner is
|
|
11
|
+
// disposed, and it streams: the source may be a URL, read as it plays. On a
|
|
12
|
+
// platform without a plane the open fails (error() with kind "no-plane"),
|
|
13
|
+
// and an app that runs everywhere falls back to a texture player itself.
|
|
14
|
+
//
|
|
8
15
|
// The imperative primitive lives in the `flux:video` module; import { open }
|
|
9
16
|
// from "flux:video" for non-reactive use.
|
|
10
17
|
|
|
11
18
|
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
12
19
|
import type { TextureId } from "flux:gpu"
|
|
13
|
-
import { open } from "flux:video"
|
|
20
|
+
import { open, type VideoError, type VideoPlane, type VideoPlayer, type VideoSource } from "flux:video"
|
|
21
|
+
|
|
22
|
+
export type { VideoError, VideoErrorKind, VideoSource } from "flux:video"
|
|
14
23
|
|
|
15
24
|
export type VideoOptions = {
|
|
16
|
-
/** Start playback as soon as the
|
|
25
|
+
/** Start playback as soon as the source is open. */
|
|
17
26
|
autoplay?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Where the picture goes: a texture (default, displayed by the app) or the
|
|
29
|
+
* platform's video plane (fullscreen beneath the UI, Android only).
|
|
30
|
+
*/
|
|
31
|
+
present?: "texture" | "plane"
|
|
32
|
+
/** Plane only: letterboxed (default) or cropped to fill the window. */
|
|
33
|
+
fit?: "contain" | "cover"
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
/** An opened video as reactive accessors plus playback controls. */
|
|
21
37
|
export type VideoStream = {
|
|
22
|
-
/**
|
|
38
|
+
/** True once the source is open (and, for a plane, the plane exists). */
|
|
39
|
+
ready(): boolean
|
|
40
|
+
/** Texture id once open, undefined while opening and always for a plane player; render with <texture src={...}>. */
|
|
23
41
|
texture(): TextureId | undefined
|
|
24
42
|
/** Frame size, undefined while opening. */
|
|
25
43
|
width(): number | undefined
|
|
26
44
|
height(): number | undefined
|
|
27
|
-
/** Duration in seconds, undefined while opening. */
|
|
45
|
+
/** Duration in seconds, undefined while opening or when the source has no known end. */
|
|
28
46
|
duration(): number | undefined
|
|
29
|
-
/**
|
|
30
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The failure: opening failed (unreadable or unreachable source,
|
|
49
|
+
* unsupported codec, no plane on this platform), or playback stopped on
|
|
50
|
+
* one later. Its `kind` says which; an app's plane-to-texture fallback
|
|
51
|
+
* keys on "no-plane" only.
|
|
52
|
+
*/
|
|
53
|
+
error(): VideoError | undefined
|
|
31
54
|
/** Start or resume playback (before open resolves, playback starts on resolve). */
|
|
32
55
|
play(): void
|
|
33
56
|
/** Pause playback; the current frame stays displayed. */
|
|
34
57
|
pause(): void
|
|
58
|
+
/**
|
|
59
|
+
* Seek to a time in seconds. A no-op while opening, on a texture player,
|
|
60
|
+
* and on a source that cannot seek (see `seekable`). While paused the
|
|
61
|
+
* target frame is shown.
|
|
62
|
+
*/
|
|
63
|
+
seek(seconds: number): void
|
|
64
|
+
/** Whether `seek` does anything (plain read, not a signal). */
|
|
65
|
+
seekable(): boolean
|
|
35
66
|
/** Whether playback is running (plain read, not a signal). */
|
|
36
67
|
playing(): boolean
|
|
37
68
|
/** Presentation time of the displayed frame in seconds (plain read, not a signal). */
|
|
38
69
|
currentTime(): number
|
|
39
70
|
/** Whether the last frame has been displayed (plain read, not a signal). */
|
|
40
71
|
finished(): boolean
|
|
72
|
+
/** Whether playback is held for the source to catch up (plain read, not a signal). */
|
|
73
|
+
buffering(): boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type AnyPlayer = VideoPlayer | VideoPlane
|
|
77
|
+
|
|
78
|
+
function toVideoError(e: unknown): VideoError {
|
|
79
|
+
if (e instanceof Error && "kind" in e) return e as VideoError
|
|
80
|
+
let error = new Error(String(e)) as VideoError
|
|
81
|
+
error.kind = "decode"
|
|
82
|
+
return error
|
|
41
83
|
}
|
|
42
84
|
|
|
43
85
|
/**
|
|
44
|
-
* Opens a video
|
|
45
|
-
*
|
|
86
|
+
* Opens a video (WebM, VP9 + Opus) from a path, an http(s) URL (plane
|
|
87
|
+
* players only) or a file(), and exposes it as reactive signals: read
|
|
88
|
+
* texture() in JSX and the frames appear once playback starts. Closes
|
|
46
89
|
* automatically when the reactive owner is disposed (e.g. the component
|
|
47
|
-
* unmounts). For imperative use, call
|
|
90
|
+
* unmounts), abandoning an open still in flight. For imperative use, call
|
|
91
|
+
* open() from "flux:video" directly.
|
|
48
92
|
*/
|
|
49
|
-
export function createVideo(
|
|
93
|
+
export function createVideo(source: VideoSource, options: VideoOptions = {}): VideoStream {
|
|
94
|
+
let [ready, setReady] = createSignal(false)
|
|
50
95
|
let [texture, setTexture] = createSignal<TextureId | undefined>(undefined)
|
|
51
96
|
let [width, setWidth] = createSignal<number | undefined>(undefined)
|
|
52
97
|
let [height, setHeight] = createSignal<number | undefined>(undefined)
|
|
53
98
|
let [duration, setDuration] = createSignal<number | undefined>(undefined)
|
|
54
|
-
let [error, setError] = createSignal<
|
|
55
|
-
let player:
|
|
99
|
+
let [error, setError] = createSignal<VideoError | undefined>(undefined)
|
|
100
|
+
let player: AnyPlayer | undefined
|
|
56
101
|
let disposed = false
|
|
57
102
|
let wantPlay = options.autoplay ?? false
|
|
103
|
+
// Disposal aborts a pending open, so a player never appears after the
|
|
104
|
+
// owner is gone.
|
|
105
|
+
let abort = new AbortController()
|
|
58
106
|
|
|
59
|
-
|
|
107
|
+
let opened: Promise<AnyPlayer> =
|
|
108
|
+
options.present === "plane"
|
|
109
|
+
? open(source, { present: "plane", fit: options.fit, signal: abort.signal })
|
|
110
|
+
: open(source, { signal: abort.signal })
|
|
111
|
+
opened
|
|
60
112
|
.then((video) => {
|
|
61
113
|
if (disposed) {
|
|
62
114
|
video.close()
|
|
63
115
|
return
|
|
64
116
|
}
|
|
65
117
|
player = video
|
|
66
|
-
setTexture(video.texture)
|
|
118
|
+
if ("texture" in video) setTexture(video.texture)
|
|
67
119
|
setWidth(video.width)
|
|
68
120
|
setHeight(video.height)
|
|
69
121
|
setDuration(video.duration)
|
|
122
|
+
setReady(true)
|
|
70
123
|
if (wantPlay) video.play()
|
|
124
|
+
// A failure that stops playback later lands in the same signal.
|
|
125
|
+
video.failed.then((failure) => {
|
|
126
|
+
if (failure && !disposed) setError(failure)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
.catch((e) => {
|
|
130
|
+
if (!disposed) setError(toVideoError(e))
|
|
71
131
|
})
|
|
72
|
-
.catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
|
|
73
132
|
|
|
74
133
|
onCleanup(() => {
|
|
75
134
|
disposed = true
|
|
135
|
+
abort.abort()
|
|
76
136
|
if (player) {
|
|
77
137
|
player.close()
|
|
78
138
|
player = undefined
|
|
@@ -80,6 +140,7 @@ export function createVideo(path: string, options: VideoOptions = {}): VideoStre
|
|
|
80
140
|
})
|
|
81
141
|
|
|
82
142
|
return {
|
|
143
|
+
ready,
|
|
83
144
|
texture,
|
|
84
145
|
width,
|
|
85
146
|
height,
|
|
@@ -93,8 +154,13 @@ export function createVideo(path: string, options: VideoOptions = {}): VideoStre
|
|
|
93
154
|
wantPlay = false
|
|
94
155
|
player?.pause()
|
|
95
156
|
},
|
|
157
|
+
seek(seconds: number) {
|
|
158
|
+
player?.seek(seconds)
|
|
159
|
+
},
|
|
160
|
+
seekable: () => player?.seekable ?? false,
|
|
96
161
|
playing: () => player?.playing() ?? false,
|
|
97
162
|
currentTime: () => player?.currentTime() ?? 0,
|
|
98
163
|
finished: () => player?.finished() ?? false,
|
|
164
|
+
buffering: () => player?.buffering() ?? false,
|
|
99
165
|
}
|
|
100
166
|
}
|