@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.59",
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.59"
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, so a dev-server-proxied file streams over the proxy. Owns
222
- * the stream's lifecycle: stopped and released when the reactive owner is
223
- * disposed. For imperative use, call stream()/play() from "flux:audio".
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 file is open. */
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
- /** Texture id once the file is open, undefined while opening; render with <texture src={...}>. */
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
- /** Set if opening failed (unreadable file, unsupported codec). */
30
- error(): Error | undefined
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 file (MP4, VP9 + AAC) and exposes it as reactive signals:
45
- * read texture() in JSX and the frames appear once playback starts. Closes
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 open() from "flux:video" directly.
90
+ * unmounts), abandoning an open still in flight. For imperative use, call
91
+ * open() from "flux:video" directly.
48
92
  */
49
- export function createVideo(path: string, options: VideoOptions = {}): VideoStream {
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<Error | undefined>(undefined)
55
- let player: Awaited<ReturnType<typeof open>> | undefined
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
- open(path)
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
  }