@langchain/svelte 0.4.6 → 1.0.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.
Files changed (74) hide show
  1. package/README.md +37 -443
  2. package/dist/context.cjs +72 -0
  3. package/dist/context.cjs.map +1 -0
  4. package/dist/context.d.cts +72 -0
  5. package/dist/context.d.cts.map +1 -0
  6. package/dist/context.d.ts +72 -0
  7. package/dist/context.d.ts.map +1 -0
  8. package/dist/context.js +70 -0
  9. package/dist/context.js.map +1 -0
  10. package/dist/index.cjs +32 -316
  11. package/dist/index.d.cts +11 -97
  12. package/dist/index.d.ts +11 -97
  13. package/dist/index.js +10 -290
  14. package/dist/selectors.svelte.cjs +214 -0
  15. package/dist/selectors.svelte.cjs.map +1 -0
  16. package/dist/selectors.svelte.d.cts +146 -0
  17. package/dist/selectors.svelte.d.cts.map +1 -0
  18. package/dist/selectors.svelte.d.ts +146 -0
  19. package/dist/selectors.svelte.d.ts.map +1 -0
  20. package/dist/selectors.svelte.js +204 -0
  21. package/dist/selectors.svelte.js.map +1 -0
  22. package/dist/use-audio-player.svelte.cjs +608 -0
  23. package/dist/use-audio-player.svelte.cjs.map +1 -0
  24. package/dist/use-audio-player.svelte.d.cts +70 -0
  25. package/dist/use-audio-player.svelte.d.cts.map +1 -0
  26. package/dist/use-audio-player.svelte.d.ts +70 -0
  27. package/dist/use-audio-player.svelte.d.ts.map +1 -0
  28. package/dist/use-audio-player.svelte.js +608 -0
  29. package/dist/use-audio-player.svelte.js.map +1 -0
  30. package/dist/use-media-url.svelte.cjs +54 -0
  31. package/dist/use-media-url.svelte.cjs.map +1 -0
  32. package/dist/use-media-url.svelte.d.cts +29 -0
  33. package/dist/use-media-url.svelte.d.cts.map +1 -0
  34. package/dist/use-media-url.svelte.d.ts +29 -0
  35. package/dist/use-media-url.svelte.d.ts.map +1 -0
  36. package/dist/use-media-url.svelte.js +54 -0
  37. package/dist/use-media-url.svelte.js.map +1 -0
  38. package/dist/use-projection.svelte.cjs +62 -0
  39. package/dist/use-projection.svelte.cjs.map +1 -0
  40. package/dist/use-projection.svelte.d.cts +65 -0
  41. package/dist/use-projection.svelte.d.cts.map +1 -0
  42. package/dist/use-projection.svelte.d.ts +65 -0
  43. package/dist/use-projection.svelte.d.ts.map +1 -0
  44. package/dist/use-projection.svelte.js +62 -0
  45. package/dist/use-projection.svelte.js.map +1 -0
  46. package/dist/use-stream.svelte.cjs +193 -0
  47. package/dist/use-stream.svelte.cjs.map +1 -0
  48. package/dist/use-stream.svelte.d.cts +116 -0
  49. package/dist/use-stream.svelte.d.cts.map +1 -0
  50. package/dist/use-stream.svelte.d.ts +116 -0
  51. package/dist/use-stream.svelte.d.ts.map +1 -0
  52. package/dist/use-stream.svelte.js +191 -0
  53. package/dist/use-stream.svelte.js.map +1 -0
  54. package/dist/use-video-player.svelte.cjs +233 -0
  55. package/dist/use-video-player.svelte.cjs.map +1 -0
  56. package/dist/use-video-player.svelte.d.cts +66 -0
  57. package/dist/use-video-player.svelte.d.cts.map +1 -0
  58. package/dist/use-video-player.svelte.d.ts +66 -0
  59. package/dist/use-video-player.svelte.d.ts.map +1 -0
  60. package/dist/use-video-player.svelte.js +233 -0
  61. package/dist/use-video-player.svelte.js.map +1 -0
  62. package/package.json +9 -8
  63. package/dist/index.cjs.map +0 -1
  64. package/dist/index.d.cts.map +0 -1
  65. package/dist/index.d.ts.map +0 -1
  66. package/dist/index.js.map +0 -1
  67. package/dist/stream.custom.cjs +0 -122
  68. package/dist/stream.custom.cjs.map +0 -1
  69. package/dist/stream.custom.js +0 -122
  70. package/dist/stream.custom.js.map +0 -1
  71. package/dist/subagents.cjs +0 -81
  72. package/dist/subagents.cjs.map +0 -1
  73. package/dist/subagents.js +0 -81
  74. package/dist/subagents.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-stream.svelte.js","names":["ClientCtor"],"sources":["../src/use-stream.svelte.ts"],"sourcesContent":["import { onDestroy } from \"svelte\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport type { Client, Interrupt } from \"@langchain/langgraph-sdk\";\nimport {\n flushPendingHeadlessToolInterrupts,\n type AnyHeadlessToolImplementation,\n type OnToolCallback,\n} from \"@langchain/langgraph-sdk\";\nimport {\n Client as ClientCtor,\n type ClientConfig,\n type ThreadStream,\n} from \"@langchain/langgraph-sdk/client\";\nimport {\n StreamController,\n type AgentServerAdapter,\n type AgentServerOptions as StreamAgentServerOptions,\n type AssembledToolCall,\n type ChannelRegistry,\n type CustomAdapterOptions as StreamCustomAdapterOptions,\n type InferStateType,\n type InferSubagentStates,\n type RootSnapshot,\n type StateOf as StreamStateOf,\n type StreamSubmitOptions,\n type SubagentDiscoverySnapshot,\n type SubagentMap,\n type SubgraphByNodeMap,\n type SubgraphDiscoverySnapshot,\n type SubgraphMap,\n type UseStreamOptions as StreamUseStreamOptions,\n type WidenUpdateMessages,\n} from \"@langchain/langgraph-sdk/stream\";\n\n/** @deprecated Prefer {@link InferStateType}. */\nexport type StateOf<T> = StreamStateOf<T>;\n\n/**\n * A value that may be either a plain `T` or a getter `() => T`. Used\n * for reactive-capable option inputs (currently `threadId` only). When\n * a getter is passed the composable tracks it via `$effect` and\n * re-hydrates when the returned value changes.\n */\nexport type ValueOrGetter<T> = T | (() => T);\n\nfunction readValueOrGetter<T>(\n input: ValueOrGetter<T> | undefined\n): T | undefined {\n if (typeof input === \"function\") return (input as () => T)();\n return input;\n}\n\ntype SvelteThreadId = ValueOrGetter<string | null | undefined>;\n\nexport type AgentServerOptions<StateType extends object> =\n StreamAgentServerOptions<StateType, SvelteThreadId>;\n\nexport type CustomAdapterOptions<StateType extends object> =\n StreamCustomAdapterOptions<StateType, SvelteThreadId, string>;\n\nexport type UseStreamOptions<\n StateType extends object = Record<string, unknown>,\n> = StreamUseStreamOptions<\n StateType,\n SvelteThreadId,\n string | undefined,\n string | undefined,\n string\n>;\n\n/**\n * Private field on the handle that carries the {@link StreamController}\n * reference. Selector composables read this to reach the shared\n * {@link ChannelRegistry}. Use the selector composables\n * (`useMessages`, `useToolCalls`, `useValues`, …) instead of reading\n * this directly.\n */\nexport const STREAM_CONTROLLER: unique symbol = Symbol.for(\n \"@langchain/svelte/controller\"\n);\n\n/**\n * Svelte binding return type for {@link useStream}. Reactive\n * projections are exposed as getters on a stable object so templates\n * can read `stream.messages` directly without a `.value` / `.current`\n * hop and `$derived` wrappers auto-track the getter read.\n *\n * Destructuring (`const { messages } = stream`) breaks reactivity —\n * this is a Svelte 5 constraint and applies to every getter-object\n * pattern. Access fields through the live `stream` handle instead.\n */\nexport interface UseStreamReturn<\n T = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n StateType extends object = InferStateType<T>,\n SubagentStates = InferSubagentStates<T>,\n> {\n readonly values: StateType;\n readonly messages: BaseMessage[];\n readonly toolCalls: AssembledToolCall[];\n readonly interrupts: Interrupt<InterruptType>[];\n readonly interrupt: Interrupt<InterruptType> | undefined;\n readonly isLoading: boolean;\n readonly isThreadLoading: boolean;\n readonly error: unknown;\n readonly threadId: string | null;\n /**\n * Promise that settles when the current thread's initial hydration\n * completes. Useful in SvelteKit `load()` handlers (or any\n * async-init site) to block until the controller has reconciled\n * with server-held state.\n */\n readonly hydrationPromise: Promise<void>;\n\n readonly subagents: ReadonlyMap<\n keyof SubagentStates & string extends never\n ? string\n : keyof SubagentStates & string,\n SubagentDiscoverySnapshot\n >;\n readonly subgraphs: ReadonlyMap<string, SubgraphDiscoverySnapshot>;\n readonly subgraphsByNode: ReadonlyMap<\n string,\n readonly SubgraphDiscoverySnapshot[]\n >;\n\n submit(\n input: WidenUpdateMessages<Partial<StateType>> | null | undefined,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): Promise<void>;\n stop(): Promise<void>;\n respond(\n response: unknown,\n target?: { interruptId: string; namespace?: string[] }\n ): Promise<void>;\n\n readonly client: Client;\n readonly assistantId: string;\n\n /** v2 escape hatch — returns the bound {@link ThreadStream}. */\n getThread(): ThreadStream | undefined;\n\n /** @internal Used by selector composables. */\n readonly [STREAM_CONTROLLER]: StreamController<\n StateType,\n InterruptType,\n ConfigurableType\n >;\n}\n\n/**\n * Erased handle useful as a parameter type for helpers and wrapper\n * components that pass a `stream` through to selector composables\n * without reading `values` directly. Mirrors the React\n * `AnyStream` alias.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyStream = UseStreamReturn<any, any, any>;\n\n/**\n * Svelte 5 binding for the v2-native stream runtime.\n *\n * Returns a handle whose reactive fields are plain getters on a\n * stable object — templates can read `stream.messages` directly and\n * `$derived(stream.isLoading)` auto-tracks the getter.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useStream } from \"@langchain/svelte\";\n *\n * const stream = useStream({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n * </script>\n *\n * {#each stream.messages as msg (msg.id)}\n * <div>{msg.content}</div>\n * {/each}\n * <button onclick={() =>\n * stream.submit({ messages: [{ type: \"human\", content: \"Hi\" }] })\n * }>\n * Send\n * </button>\n * ```\n *\n * `assistantId`, `client`, and `transport` are captured at composable\n * init. To bind a new assistant/transport, remount the component.\n * Only `threadId` is treated as reactive — pass it as a getter\n * (`threadId: () => active`) to drive an in-place thread swap.\n */\nexport function useStream<\n T = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n>(\n options: UseStreamOptions<InferStateType<T>>\n): UseStreamReturn<T, InterruptType, ConfigurableType> {\n type StateType = InferStateType<T>;\n\n interface OptionsBag {\n assistantId?: string;\n threadId?: ValueOrGetter<string | null | undefined>;\n client?: Client;\n apiUrl?: string;\n apiKey?: string;\n callerOptions?: ClientConfig[\"callerOptions\"];\n defaultHeaders?: ClientConfig[\"defaultHeaders\"];\n transport?: \"sse\" | \"websocket\" | AgentServerAdapter;\n fetch?: typeof fetch;\n webSocketFactory?: (url: string) => WebSocket;\n onThreadId?: (threadId: string) => void;\n onCreated?: (meta: { run_id: string; thread_id: string }) => void;\n initialValues?: StateType;\n messagesKey?: string;\n tools?: AnyHeadlessToolImplementation[];\n onTool?: OnToolCallback;\n }\n const asBag = options as OptionsBag;\n\n const hasCustomAdapter =\n asBag.transport != null && typeof asBag.transport !== \"string\";\n const transport = asBag.transport;\n\n // Client construction — captured once at init. Consumers that need\n // to swap `apiUrl`/`apiKey` at runtime remount the owning component.\n const client: Client =\n asBag.client ??\n (new ClientCtor({\n apiUrl: asBag.apiUrl,\n apiKey: asBag.apiKey,\n callerOptions: asBag.callerOptions,\n defaultHeaders: asBag.defaultHeaders,\n }) as unknown as Client);\n\n // Custom adapters may omit `assistantId`; the controller still\n // requires one so it has something to forward to `threads.stream`.\n const sentinel = \"_\";\n const assistantId =\n \"assistantId\" in options ? (options.assistantId ?? sentinel) : sentinel;\n\n const initialThreadId = readValueOrGetter(asBag.threadId) ?? null;\n\n // Plain `let` binding, not `$state`: the controller holds maps of\n // listeners, Promises, and the live `ThreadStream`, none of which\n // survive Svelte's deep `$state` proxy wrapping.\n const controller = new StreamController<\n StateType,\n InterruptType,\n ConfigurableType\n >({\n assistantId,\n // `Client` is state-shape agnostic at runtime; the controller\n // advertises `Client<StateType>` on its public type for ergonomics.\n client: client as unknown as Client<StateType>,\n threadId: initialThreadId,\n transport,\n fetch: hasCustomAdapter ? undefined : asBag.fetch,\n webSocketFactory: hasCustomAdapter ? undefined : asBag.webSocketFactory,\n onThreadId: options.onThreadId,\n onCreated: options.onCreated,\n initialValues: options.initialValues,\n messagesKey: options.messagesKey,\n });\n\n // Deferred dispose: mirrors React's activate/dispose pattern so HMR\n // and other scope-reuse scenarios stay clean. `activate()` cancels\n // a pending dispose if the owning scope survives.\n const deactivate = controller.activate();\n onDestroy(deactivate);\n\n // ─── Reactive state bridges ─────────────────────────────────────────\n //\n // Each always-on `StreamStore` is wrapped in a runes `$state` slot\n // seeded from `getSnapshot()` and kept in sync via `store.subscribe`.\n // Subscriptions are torn down on component destroy.\n let rootSnapshot = $state<RootSnapshot<StateType, InterruptType>>(\n controller.rootStore.getSnapshot()\n );\n const unsubscribeRoot = controller.rootStore.subscribe(() => {\n rootSnapshot = controller.rootStore.getSnapshot();\n });\n onDestroy(unsubscribeRoot);\n\n let subagentSnapshot = $state<SubagentMap>(\n controller.subagentStore.getSnapshot()\n );\n const unsubscribeSubagents = controller.subagentStore.subscribe(() => {\n subagentSnapshot = controller.subagentStore.getSnapshot();\n });\n onDestroy(unsubscribeSubagents);\n\n let subgraphSnapshot = $state<SubgraphMap>(\n controller.subgraphStore.getSnapshot()\n );\n const unsubscribeSubgraphs = controller.subgraphStore.subscribe(() => {\n subgraphSnapshot = controller.subgraphStore.getSnapshot();\n });\n onDestroy(unsubscribeSubgraphs);\n\n let subgraphByNodeSnapshot = $state<SubgraphByNodeMap>(\n controller.subgraphByNodeStore.getSnapshot()\n );\n const unsubscribeSubgraphByNode = controller.subgraphByNodeStore.subscribe(\n () => {\n subgraphByNodeSnapshot = controller.subgraphByNodeStore.getSnapshot();\n }\n );\n onDestroy(unsubscribeSubgraphByNode);\n\n // ─── threadId reactivity ────────────────────────────────────────────\n //\n // Only matters when the caller passed a getter. The initial hydrate\n // already fired in the controller constructor, so skip the first\n // tick to avoid a redundant `thread.state.get()`.\n if (typeof asBag.threadId === \"function\") {\n const getThreadId = asBag.threadId;\n let previousThreadId = initialThreadId;\n $effect(() => {\n const next = (getThreadId() ?? null) as string | null;\n if (next === previousThreadId) return;\n previousThreadId = next;\n void controller.hydrate(next);\n });\n }\n\n // ─── Headless-tool handling ─────────────────────────────────────────\n //\n // Watch the root `values.__interrupt__` key plus the protocol-\n // surfaced interrupts for items targeting a registered tool, invoke\n // the handler, and resume the run with the handler's return value.\n // Dedup via an id set so rerenders don't replay a tool call twice.\n const tools = options.tools;\n const onTool = options.onTool;\n if (tools?.length) {\n const handledTools = new Set<string>();\n let handledForThreadId: string | null = initialThreadId;\n $effect(() => {\n // Reset dedup set when the active thread id changes — a fresh\n // thread may legitimately re-emit a tool-call id we've seen.\n const currentThreadId = rootSnapshot.threadId;\n if (currentThreadId !== handledForThreadId) {\n handledTools.clear();\n handledForThreadId = currentThreadId;\n }\n\n const valuesBag = rootSnapshot.values as unknown as Record<\n string,\n unknown\n >;\n const existing = Array.isArray(valuesBag?.__interrupt__)\n ? (valuesBag.__interrupt__ as Interrupt[])\n : [];\n const combined: Interrupt[] = [\n ...existing,\n ...(rootSnapshot.interrupts as unknown as Interrupt[]),\n ];\n if (combined.length === 0) return;\n flushPendingHeadlessToolInterrupts(\n { ...valuesBag, __interrupt__: combined },\n tools,\n handledTools,\n {\n onTool,\n defer: (run) => {\n void Promise.resolve().then(run);\n },\n resumeSubmit: (command) =>\n controller.submit(null, {\n command,\n } as StreamSubmitOptions<StateType, ConfigurableType>),\n }\n );\n });\n }\n\n // ─── Public handle ──────────────────────────────────────────────────\n //\n // Single stable object with getters. Getters read the runes\n // `$state` slots, which drives template reactivity automatically.\n const handle: UseStreamReturn<T, InterruptType, ConfigurableType> = {\n get values() {\n return rootSnapshot.values;\n },\n get messages() {\n return rootSnapshot.messages;\n },\n get toolCalls() {\n return rootSnapshot.toolCalls;\n },\n get interrupts() {\n return rootSnapshot.interrupts;\n },\n get interrupt() {\n return rootSnapshot.interrupt;\n },\n get isLoading() {\n return rootSnapshot.isLoading;\n },\n get isThreadLoading() {\n return rootSnapshot.isThreadLoading;\n },\n get error() {\n return rootSnapshot.error;\n },\n get threadId() {\n return rootSnapshot.threadId;\n },\n get hydrationPromise() {\n return controller.hydrationPromise;\n },\n get subagents() {\n return subagentSnapshot as UseStreamReturn<\n T,\n InterruptType,\n ConfigurableType\n >[\"subagents\"];\n },\n get subgraphs() {\n return subgraphSnapshot;\n },\n get subgraphsByNode() {\n return subgraphByNodeSnapshot;\n },\n submit: (input, submitOptions) => controller.submit(input, submitOptions),\n stop: () => controller.stop(),\n respond: (response, target) => controller.respond(response, target),\n getThread: () => controller.getThread(),\n client,\n assistantId,\n [STREAM_CONTROLLER]: controller,\n };\n\n return handle;\n}\n\n/** Convenience alias for the fully-resolved stream handle type. */\nexport type UseStreamResult<\n T = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n> = UseStreamReturn<T, InterruptType, ConfigurableType>;\n\n/**\n * Helper used by the selector composables to reach the underlying\n * {@link ChannelRegistry} from a stream handle. Kept internal —\n * application code should call `useMessages`, `useToolCalls`, etc.\n *\n * @internal\n */\nexport function getRegistry(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n stream: UseStreamReturn<any, any, any>\n): ChannelRegistry {\n return stream[STREAM_CONTROLLER].registry;\n}\n\nexport type { ThreadStream };\n"],"mappings":"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"}
@@ -0,0 +1,233 @@
1
+ let svelte = require("svelte");
2
+ //#region src/use-video-player.svelte.ts
3
+ function unwrap(input) {
4
+ if (typeof input === "function") return input();
5
+ return input;
6
+ }
7
+ /**
8
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>`
9
+ * element.
10
+ *
11
+ * Svelte idioms:
12
+ * - `videoRef` accepts a raw `HTMLVideoElement`, a `$state` binding,
13
+ * or a getter. Use Svelte 5's `bind:this` to assign a template
14
+ * reference and pass it via a getter so the composable re-binds
15
+ * when the element first mounts:
16
+ * ```svelte
17
+ * <script lang="ts">
18
+ * let videoEl = $state<HTMLVideoElement>();
19
+ * const player = useVideoPlayer(() => videoEl, () => media);
20
+ * <\/script>
21
+ * <video bind:this={videoEl} />
22
+ * ```
23
+ * - The composable never injects DOM nor overrides layout.
24
+ * - On component destroy (or when `media` changes) the composable
25
+ * calls `media.revoke()` to free the object URL.
26
+ *
27
+ * @param videoRef - Reactive reference to the `<video>` element.
28
+ * @param media - Video handle from `useVideo`.
29
+ * @param options - Auto-play toggle.
30
+ */
31
+ function useVideoPlayer(videoRef, media, options) {
32
+ const autoPlay = options?.autoPlay ?? false;
33
+ let status = $state("idle");
34
+ let error = $state(void 0);
35
+ let currentTime = $state(0);
36
+ let duration = $state(void 0);
37
+ let shouldPlay = false;
38
+ let pendingResolve = null;
39
+ let pendingReject = null;
40
+ const resolvePending = () => {
41
+ const r = pendingResolve;
42
+ pendingResolve = null;
43
+ pendingReject = null;
44
+ r?.();
45
+ };
46
+ const rejectPending = (err) => {
47
+ const r = pendingReject;
48
+ pendingResolve = null;
49
+ pendingReject = null;
50
+ r?.(err);
51
+ };
52
+ $effect(() => {
53
+ const s = status;
54
+ if (s === "finished" || s === "paused" || s === "idle") resolvePending();
55
+ else if (s === "error") rejectPending(error ?? /* @__PURE__ */ new Error("playback error"));
56
+ });
57
+ const getVideo = () => unwrap(videoRef) ?? null;
58
+ const play = () => {
59
+ if (unwrap(media) == null) return;
60
+ if (status === "error") return;
61
+ shouldPlay = true;
62
+ const video = getVideo();
63
+ if (video == null) {
64
+ status = "buffering";
65
+ return;
66
+ }
67
+ video.play().catch((err) => {
68
+ error = err;
69
+ status = "error";
70
+ });
71
+ };
72
+ const pause = () => {
73
+ shouldPlay = false;
74
+ getVideo()?.pause();
75
+ if (status === "playing" || status === "buffering") status = "paused";
76
+ };
77
+ const stop = () => {
78
+ shouldPlay = false;
79
+ const video = getVideo();
80
+ if (video != null) {
81
+ video.pause();
82
+ video.currentTime = 0;
83
+ }
84
+ currentTime = 0;
85
+ status = unwrap(media) == null ? "idle" : "paused";
86
+ };
87
+ const reset = () => {
88
+ stop();
89
+ error = void 0;
90
+ duration = void 0;
91
+ status = "idle";
92
+ };
93
+ const toggle = () => {
94
+ if (status === "playing") pause();
95
+ else play();
96
+ };
97
+ const playToEnd = () => {
98
+ pendingResolve?.();
99
+ pendingResolve = null;
100
+ pendingReject = null;
101
+ return new Promise((resolve, reject) => {
102
+ pendingResolve = resolve;
103
+ pendingReject = reject;
104
+ play();
105
+ });
106
+ };
107
+ const seek = (seconds) => {
108
+ const video = getVideo();
109
+ if (video == null) return;
110
+ video.currentTime = seconds;
111
+ currentTime = seconds;
112
+ };
113
+ let detach = null;
114
+ const bind = (m, video) => {
115
+ error = void 0;
116
+ status = "buffering";
117
+ currentTime = 0;
118
+ duration = void 0;
119
+ let cancelled = false;
120
+ m.objectURL.then((resolved) => {
121
+ if (cancelled) return;
122
+ if (video == null) return;
123
+ video.src = resolved;
124
+ if (shouldPlay || autoPlay) video.play().catch((err) => {
125
+ error = err;
126
+ status = "error";
127
+ });
128
+ else status = "paused";
129
+ }, () => {
130
+ if (!cancelled) {
131
+ error = /* @__PURE__ */ new Error("media failed to materialise");
132
+ status = "error";
133
+ }
134
+ });
135
+ if (video == null) return () => {
136
+ cancelled = true;
137
+ try {
138
+ m.revoke();
139
+ } catch {}
140
+ };
141
+ const onPlay = () => {
142
+ if (status !== "error") status = "playing";
143
+ };
144
+ const onPause = () => {
145
+ if (video.ended) return;
146
+ if (status === "playing") status = "paused";
147
+ };
148
+ const onEnded = () => {
149
+ status = "finished";
150
+ };
151
+ const onTimeUpdate = () => {
152
+ currentTime = video.currentTime;
153
+ };
154
+ const onLoadedMetadata = () => {
155
+ if (Number.isFinite(video.duration)) duration = video.duration;
156
+ };
157
+ const onError = () => {
158
+ error = /* @__PURE__ */ new Error("HTMLVideoElement error");
159
+ status = "error";
160
+ };
161
+ video.addEventListener("play", onPlay);
162
+ video.addEventListener("pause", onPause);
163
+ video.addEventListener("ended", onEnded);
164
+ video.addEventListener("timeupdate", onTimeUpdate);
165
+ video.addEventListener("loadedmetadata", onLoadedMetadata);
166
+ video.addEventListener("error", onError);
167
+ return () => {
168
+ cancelled = true;
169
+ video.removeEventListener("play", onPlay);
170
+ video.removeEventListener("pause", onPause);
171
+ video.removeEventListener("ended", onEnded);
172
+ video.removeEventListener("timeupdate", onTimeUpdate);
173
+ video.removeEventListener("loadedmetadata", onLoadedMetadata);
174
+ video.removeEventListener("error", onError);
175
+ try {
176
+ video.pause();
177
+ video.removeAttribute("src");
178
+ video.load();
179
+ } catch {}
180
+ try {
181
+ m.revoke();
182
+ } catch {}
183
+ };
184
+ };
185
+ $effect(() => {
186
+ const m = unwrap(media);
187
+ const video = unwrap(videoRef) ?? null;
188
+ detach?.();
189
+ detach = null;
190
+ if (m == null) {
191
+ status = "idle";
192
+ error = void 0;
193
+ currentTime = 0;
194
+ duration = void 0;
195
+ return;
196
+ }
197
+ if (m.error != null) {
198
+ error = new Error(m.error.message);
199
+ status = "error";
200
+ return;
201
+ }
202
+ detach = bind(m, video);
203
+ });
204
+ (0, svelte.onDestroy)(() => {
205
+ detach?.();
206
+ detach = null;
207
+ });
208
+ return {
209
+ get status() {
210
+ return status;
211
+ },
212
+ play,
213
+ pause,
214
+ stop,
215
+ toggle,
216
+ reset,
217
+ playToEnd,
218
+ get currentTime() {
219
+ return currentTime;
220
+ },
221
+ get duration() {
222
+ return duration;
223
+ },
224
+ seek,
225
+ get error() {
226
+ return error;
227
+ }
228
+ };
229
+ }
230
+ //#endregion
231
+ exports.useVideoPlayer = useVideoPlayer;
232
+
233
+ //# sourceMappingURL=use-video-player.svelte.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-video-player.svelte.cjs","names":[],"sources":["../src/use-video-player.svelte.ts"],"sourcesContent":["import { onDestroy } from \"svelte\";\nimport type { VideoMedia } from \"@langchain/langgraph-sdk/stream\";\nimport type { ValueOrGetter } from \"./use-projection.svelte.js\";\nimport type { PlayerStatus } from \"./use-audio-player.svelte.js\";\n\nfunction unwrap<T>(input: ValueOrGetter<T>): T {\n if (typeof input === \"function\") return (input as () => T)();\n return input;\n}\n\n/** Options for {@link useVideoPlayer}. */\nexport interface UseVideoPlayerOptions {\n /**\n * Start playback as soon as the blob URL resolves. Subject to\n * browser autoplay policies — pair with `<video muted>` to bypass\n * the user-gesture requirement.\n */\n autoPlay?: boolean;\n}\n\n/**\n * Controls + live state returned by {@link useVideoPlayer}. Mirrors\n * `AudioPlayerHandle` on the shared subset so callers only ever\n * learn one shape.\n */\nexport interface VideoPlayerHandle {\n readonly status: PlayerStatus;\n play(): void;\n pause(): void;\n stop(): void;\n toggle(): void;\n reset(): void;\n /**\n * Resolve on the next terminal transition (`finished` / `paused` /\n * `idle`). Reject on transitions to `\"error\"`. Triggers `play()`\n * when called.\n */\n playToEnd(): Promise<void>;\n\n readonly currentTime: number;\n /** Total duration (seconds) once the element has parsed the blob. */\n readonly duration: number | undefined;\n seek(seconds: number): void;\n\n readonly error: Error | undefined;\n}\n\n/**\n * Bind a {@link VideoMedia} handle to a caller-owned `<video>`\n * element.\n *\n * Svelte idioms:\n * - `videoRef` accepts a raw `HTMLVideoElement`, a `$state` binding,\n * or a getter. Use Svelte 5's `bind:this` to assign a template\n * reference and pass it via a getter so the composable re-binds\n * when the element first mounts:\n * ```svelte\n * <script lang=\"ts\">\n * let videoEl = $state<HTMLVideoElement>();\n * const player = useVideoPlayer(() => videoEl, () => media);\n * </script>\n * <video bind:this={videoEl} />\n * ```\n * - The composable never injects DOM nor overrides layout.\n * - On component destroy (or when `media` changes) the composable\n * calls `media.revoke()` to free the object URL.\n *\n * @param videoRef - Reactive reference to the `<video>` element.\n * @param media - Video handle from `useVideo`.\n * @param options - Auto-play toggle.\n */\nexport function useVideoPlayer(\n videoRef: ValueOrGetter<HTMLVideoElement | null | undefined>,\n media: ValueOrGetter<VideoMedia | undefined>,\n options?: UseVideoPlayerOptions\n): VideoPlayerHandle {\n const autoPlay = options?.autoPlay ?? false;\n\n let status = $state<PlayerStatus>(\"idle\");\n let error = $state<Error | undefined>(undefined);\n let currentTime = $state(0);\n let duration = $state<number | undefined>(undefined);\n\n let shouldPlay = false;\n let pendingResolve: (() => void) | null = null;\n let pendingReject: ((err: Error) => void) | null = null;\n\n const resolvePending = () => {\n const r = pendingResolve;\n pendingResolve = null;\n pendingReject = null;\n r?.();\n };\n const rejectPending = (err: Error) => {\n const r = pendingReject;\n pendingResolve = null;\n pendingReject = null;\n r?.(err);\n };\n\n $effect(() => {\n const s = status;\n if (s === \"finished\" || s === \"paused\" || s === \"idle\") {\n resolvePending();\n } else if (s === \"error\") {\n rejectPending(error ?? new Error(\"playback error\"));\n }\n });\n\n const getVideo = () => unwrap(videoRef) ?? null;\n\n const play = () => {\n const m = unwrap(media);\n if (m == null) return;\n if (status === \"error\") return;\n shouldPlay = true;\n const video = getVideo();\n if (video == null) {\n status = \"buffering\";\n return;\n }\n video.play().catch((err) => {\n error = err as Error;\n status = \"error\";\n });\n };\n\n const pause = () => {\n shouldPlay = false;\n getVideo()?.pause();\n if (status === \"playing\" || status === \"buffering\") {\n status = \"paused\";\n }\n };\n\n const stop = () => {\n shouldPlay = false;\n const video = getVideo();\n if (video != null) {\n video.pause();\n video.currentTime = 0;\n }\n currentTime = 0;\n status = unwrap(media) == null ? \"idle\" : \"paused\";\n };\n\n const reset = () => {\n stop();\n error = undefined;\n duration = undefined;\n status = \"idle\";\n };\n\n const toggle = () => {\n if (status === \"playing\") pause();\n else play();\n };\n\n const playToEnd = (): Promise<void> => {\n pendingResolve?.();\n pendingResolve = null;\n pendingReject = null;\n return new Promise<void>((resolve, reject) => {\n pendingResolve = resolve;\n pendingReject = reject;\n play();\n });\n };\n\n const seek = (seconds: number) => {\n const video = getVideo();\n if (video == null) return;\n video.currentTime = seconds;\n currentTime = seconds;\n };\n\n let detach: (() => void) | null = null;\n\n const bind = (m: VideoMedia, video: HTMLVideoElement | null) => {\n error = undefined;\n status = \"buffering\";\n currentTime = 0;\n duration = undefined;\n\n let cancelled = false;\n\n m.objectURL.then(\n (resolved) => {\n if (cancelled) return;\n if (video == null) return;\n video.src = resolved;\n\n if (shouldPlay || autoPlay) {\n video.play().catch((err) => {\n error = err as Error;\n status = \"error\";\n });\n } else {\n status = \"paused\";\n }\n },\n () => {\n if (!cancelled) {\n error = new Error(\"media failed to materialise\");\n status = \"error\";\n }\n }\n );\n\n if (video == null) {\n return () => {\n cancelled = true;\n try {\n m.revoke();\n } catch {\n // best-effort\n }\n };\n }\n\n const onPlay = () => {\n if (status !== \"error\") status = \"playing\";\n };\n const onPause = () => {\n if (video.ended) return;\n if (status === \"playing\") status = \"paused\";\n };\n const onEnded = () => {\n status = \"finished\";\n };\n const onTimeUpdate = () => {\n currentTime = video.currentTime;\n };\n const onLoadedMetadata = () => {\n if (Number.isFinite(video.duration)) duration = video.duration;\n };\n const onError = () => {\n error = new Error(\"HTMLVideoElement error\");\n status = \"error\";\n };\n\n video.addEventListener(\"play\", onPlay);\n video.addEventListener(\"pause\", onPause);\n video.addEventListener(\"ended\", onEnded);\n video.addEventListener(\"timeupdate\", onTimeUpdate);\n video.addEventListener(\"loadedmetadata\", onLoadedMetadata);\n video.addEventListener(\"error\", onError);\n\n return () => {\n cancelled = true;\n video.removeEventListener(\"play\", onPlay);\n video.removeEventListener(\"pause\", onPause);\n video.removeEventListener(\"ended\", onEnded);\n video.removeEventListener(\"timeupdate\", onTimeUpdate);\n video.removeEventListener(\"loadedmetadata\", onLoadedMetadata);\n video.removeEventListener(\"error\", onError);\n try {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n } catch {\n // best-effort\n }\n try {\n m.revoke();\n } catch {\n // best-effort\n }\n };\n };\n\n $effect(() => {\n const m = unwrap(media);\n const video = unwrap(videoRef) ?? null;\n\n detach?.();\n detach = null;\n\n if (m == null) {\n status = \"idle\";\n error = undefined;\n currentTime = 0;\n duration = undefined;\n return;\n }\n\n if (m.error != null) {\n error = new Error(m.error.message);\n status = \"error\";\n return;\n }\n\n detach = bind(m, video);\n });\n\n onDestroy(() => {\n detach?.();\n detach = null;\n });\n\n return {\n get status() {\n return status;\n },\n play,\n pause,\n stop,\n toggle,\n reset,\n playToEnd,\n get currentTime() {\n return currentTime;\n },\n get duration() {\n return duration;\n },\n seek,\n get error() {\n return error;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { ValueOrGetter } from "./use-projection.svelte.cjs";
2
+ import { PlayerStatus } from "./use-audio-player.svelte.cjs";
3
+ import { VideoMedia } from "@langchain/langgraph-sdk/stream";
4
+
5
+ //#region src/use-video-player.svelte.d.ts
6
+ /** Options for {@link useVideoPlayer}. */
7
+ interface UseVideoPlayerOptions {
8
+ /**
9
+ * Start playback as soon as the blob URL resolves. Subject to
10
+ * browser autoplay policies — pair with `<video muted>` to bypass
11
+ * the user-gesture requirement.
12
+ */
13
+ autoPlay?: boolean;
14
+ }
15
+ /**
16
+ * Controls + live state returned by {@link useVideoPlayer}. Mirrors
17
+ * `AudioPlayerHandle` on the shared subset so callers only ever
18
+ * learn one shape.
19
+ */
20
+ interface VideoPlayerHandle {
21
+ readonly status: PlayerStatus;
22
+ play(): void;
23
+ pause(): void;
24
+ stop(): void;
25
+ toggle(): void;
26
+ reset(): void;
27
+ /**
28
+ * Resolve on the next terminal transition (`finished` / `paused` /
29
+ * `idle`). Reject on transitions to `"error"`. Triggers `play()`
30
+ * when called.
31
+ */
32
+ playToEnd(): Promise<void>;
33
+ readonly currentTime: number;
34
+ /** Total duration (seconds) once the element has parsed the blob. */
35
+ readonly duration: number | undefined;
36
+ seek(seconds: number): void;
37
+ readonly error: Error | undefined;
38
+ }
39
+ /**
40
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>`
41
+ * element.
42
+ *
43
+ * Svelte idioms:
44
+ * - `videoRef` accepts a raw `HTMLVideoElement`, a `$state` binding,
45
+ * or a getter. Use Svelte 5's `bind:this` to assign a template
46
+ * reference and pass it via a getter so the composable re-binds
47
+ * when the element first mounts:
48
+ * ```svelte
49
+ * <script lang="ts">
50
+ * let videoEl = $state<HTMLVideoElement>();
51
+ * const player = useVideoPlayer(() => videoEl, () => media);
52
+ * </script>
53
+ * <video bind:this={videoEl} />
54
+ * ```
55
+ * - The composable never injects DOM nor overrides layout.
56
+ * - On component destroy (or when `media` changes) the composable
57
+ * calls `media.revoke()` to free the object URL.
58
+ *
59
+ * @param videoRef - Reactive reference to the `<video>` element.
60
+ * @param media - Video handle from `useVideo`.
61
+ * @param options - Auto-play toggle.
62
+ */
63
+ declare function useVideoPlayer(videoRef: ValueOrGetter<HTMLVideoElement | null | undefined>, media: ValueOrGetter<VideoMedia | undefined>, options?: UseVideoPlayerOptions): VideoPlayerHandle;
64
+ //#endregion
65
+ export { UseVideoPlayerOptions, VideoPlayerHandle, useVideoPlayer };
66
+ //# sourceMappingURL=use-video-player.svelte.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-video-player.svelte.d.cts","names":[],"sources":["../src/use-video-player.svelte.ts"],"mappings":";;;;;;UAWiB,qBAAA;EAAA;;;;;EAMf,QAAA;AAAA;;;;;;UAQe,iBAAA;EAAA,SACN,MAAA,EAAQ,YAAA;EACjB,IAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;EAAA;;;;;EAMA,SAAA,IAAa,OAAA;EAAA,SAEJ,WAAA;EAKA;EAAA,SAHA,QAAA;EACT,IAAA,CAAK,OAAA;EAAA,SAEI,KAAA,EAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BF,cAAA,CACd,QAAA,EAAU,aAAA,CAAc,gBAAA,sBACxB,KAAA,EAAO,aAAA,CAAc,UAAA,eACrB,OAAA,GAAU,qBAAA,GACT,iBAAA"}
@@ -0,0 +1,66 @@
1
+ import { ValueOrGetter } from "./use-projection.svelte.js";
2
+ import { PlayerStatus } from "./use-audio-player.svelte.js";
3
+ import { VideoMedia } from "@langchain/langgraph-sdk/stream";
4
+
5
+ //#region src/use-video-player.svelte.d.ts
6
+ /** Options for {@link useVideoPlayer}. */
7
+ interface UseVideoPlayerOptions {
8
+ /**
9
+ * Start playback as soon as the blob URL resolves. Subject to
10
+ * browser autoplay policies — pair with `<video muted>` to bypass
11
+ * the user-gesture requirement.
12
+ */
13
+ autoPlay?: boolean;
14
+ }
15
+ /**
16
+ * Controls + live state returned by {@link useVideoPlayer}. Mirrors
17
+ * `AudioPlayerHandle` on the shared subset so callers only ever
18
+ * learn one shape.
19
+ */
20
+ interface VideoPlayerHandle {
21
+ readonly status: PlayerStatus;
22
+ play(): void;
23
+ pause(): void;
24
+ stop(): void;
25
+ toggle(): void;
26
+ reset(): void;
27
+ /**
28
+ * Resolve on the next terminal transition (`finished` / `paused` /
29
+ * `idle`). Reject on transitions to `"error"`. Triggers `play()`
30
+ * when called.
31
+ */
32
+ playToEnd(): Promise<void>;
33
+ readonly currentTime: number;
34
+ /** Total duration (seconds) once the element has parsed the blob. */
35
+ readonly duration: number | undefined;
36
+ seek(seconds: number): void;
37
+ readonly error: Error | undefined;
38
+ }
39
+ /**
40
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>`
41
+ * element.
42
+ *
43
+ * Svelte idioms:
44
+ * - `videoRef` accepts a raw `HTMLVideoElement`, a `$state` binding,
45
+ * or a getter. Use Svelte 5's `bind:this` to assign a template
46
+ * reference and pass it via a getter so the composable re-binds
47
+ * when the element first mounts:
48
+ * ```svelte
49
+ * <script lang="ts">
50
+ * let videoEl = $state<HTMLVideoElement>();
51
+ * const player = useVideoPlayer(() => videoEl, () => media);
52
+ * </script>
53
+ * <video bind:this={videoEl} />
54
+ * ```
55
+ * - The composable never injects DOM nor overrides layout.
56
+ * - On component destroy (or when `media` changes) the composable
57
+ * calls `media.revoke()` to free the object URL.
58
+ *
59
+ * @param videoRef - Reactive reference to the `<video>` element.
60
+ * @param media - Video handle from `useVideo`.
61
+ * @param options - Auto-play toggle.
62
+ */
63
+ declare function useVideoPlayer(videoRef: ValueOrGetter<HTMLVideoElement | null | undefined>, media: ValueOrGetter<VideoMedia | undefined>, options?: UseVideoPlayerOptions): VideoPlayerHandle;
64
+ //#endregion
65
+ export { UseVideoPlayerOptions, VideoPlayerHandle, useVideoPlayer };
66
+ //# sourceMappingURL=use-video-player.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-video-player.svelte.d.ts","names":[],"sources":["../src/use-video-player.svelte.ts"],"mappings":";;;;;;UAWiB,qBAAA;EAAA;;;;;EAMf,QAAA;AAAA;;;;;;UAQe,iBAAA;EAAA,SACN,MAAA,EAAQ,YAAA;EACjB,IAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;EAAA;;;;;EAMA,SAAA,IAAa,OAAA;EAAA,SAEJ,WAAA;EAKA;EAAA,SAHA,QAAA;EACT,IAAA,CAAK,OAAA;EAAA,SAEI,KAAA,EAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BF,cAAA,CACd,QAAA,EAAU,aAAA,CAAc,gBAAA,sBACxB,KAAA,EAAO,aAAA,CAAc,UAAA,eACrB,OAAA,GAAU,qBAAA,GACT,iBAAA"}
@@ -0,0 +1,233 @@
1
+ import { onDestroy } from "svelte";
2
+ //#region src/use-video-player.svelte.ts
3
+ function unwrap(input) {
4
+ if (typeof input === "function") return input();
5
+ return input;
6
+ }
7
+ /**
8
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>`
9
+ * element.
10
+ *
11
+ * Svelte idioms:
12
+ * - `videoRef` accepts a raw `HTMLVideoElement`, a `$state` binding,
13
+ * or a getter. Use Svelte 5's `bind:this` to assign a template
14
+ * reference and pass it via a getter so the composable re-binds
15
+ * when the element first mounts:
16
+ * ```svelte
17
+ * <script lang="ts">
18
+ * let videoEl = $state<HTMLVideoElement>();
19
+ * const player = useVideoPlayer(() => videoEl, () => media);
20
+ * <\/script>
21
+ * <video bind:this={videoEl} />
22
+ * ```
23
+ * - The composable never injects DOM nor overrides layout.
24
+ * - On component destroy (or when `media` changes) the composable
25
+ * calls `media.revoke()` to free the object URL.
26
+ *
27
+ * @param videoRef - Reactive reference to the `<video>` element.
28
+ * @param media - Video handle from `useVideo`.
29
+ * @param options - Auto-play toggle.
30
+ */
31
+ function useVideoPlayer(videoRef, media, options) {
32
+ const autoPlay = options?.autoPlay ?? false;
33
+ let status = $state("idle");
34
+ let error = $state(void 0);
35
+ let currentTime = $state(0);
36
+ let duration = $state(void 0);
37
+ let shouldPlay = false;
38
+ let pendingResolve = null;
39
+ let pendingReject = null;
40
+ const resolvePending = () => {
41
+ const r = pendingResolve;
42
+ pendingResolve = null;
43
+ pendingReject = null;
44
+ r?.();
45
+ };
46
+ const rejectPending = (err) => {
47
+ const r = pendingReject;
48
+ pendingResolve = null;
49
+ pendingReject = null;
50
+ r?.(err);
51
+ };
52
+ $effect(() => {
53
+ const s = status;
54
+ if (s === "finished" || s === "paused" || s === "idle") resolvePending();
55
+ else if (s === "error") rejectPending(error ?? /* @__PURE__ */ new Error("playback error"));
56
+ });
57
+ const getVideo = () => unwrap(videoRef) ?? null;
58
+ const play = () => {
59
+ if (unwrap(media) == null) return;
60
+ if (status === "error") return;
61
+ shouldPlay = true;
62
+ const video = getVideo();
63
+ if (video == null) {
64
+ status = "buffering";
65
+ return;
66
+ }
67
+ video.play().catch((err) => {
68
+ error = err;
69
+ status = "error";
70
+ });
71
+ };
72
+ const pause = () => {
73
+ shouldPlay = false;
74
+ getVideo()?.pause();
75
+ if (status === "playing" || status === "buffering") status = "paused";
76
+ };
77
+ const stop = () => {
78
+ shouldPlay = false;
79
+ const video = getVideo();
80
+ if (video != null) {
81
+ video.pause();
82
+ video.currentTime = 0;
83
+ }
84
+ currentTime = 0;
85
+ status = unwrap(media) == null ? "idle" : "paused";
86
+ };
87
+ const reset = () => {
88
+ stop();
89
+ error = void 0;
90
+ duration = void 0;
91
+ status = "idle";
92
+ };
93
+ const toggle = () => {
94
+ if (status === "playing") pause();
95
+ else play();
96
+ };
97
+ const playToEnd = () => {
98
+ pendingResolve?.();
99
+ pendingResolve = null;
100
+ pendingReject = null;
101
+ return new Promise((resolve, reject) => {
102
+ pendingResolve = resolve;
103
+ pendingReject = reject;
104
+ play();
105
+ });
106
+ };
107
+ const seek = (seconds) => {
108
+ const video = getVideo();
109
+ if (video == null) return;
110
+ video.currentTime = seconds;
111
+ currentTime = seconds;
112
+ };
113
+ let detach = null;
114
+ const bind = (m, video) => {
115
+ error = void 0;
116
+ status = "buffering";
117
+ currentTime = 0;
118
+ duration = void 0;
119
+ let cancelled = false;
120
+ m.objectURL.then((resolved) => {
121
+ if (cancelled) return;
122
+ if (video == null) return;
123
+ video.src = resolved;
124
+ if (shouldPlay || autoPlay) video.play().catch((err) => {
125
+ error = err;
126
+ status = "error";
127
+ });
128
+ else status = "paused";
129
+ }, () => {
130
+ if (!cancelled) {
131
+ error = /* @__PURE__ */ new Error("media failed to materialise");
132
+ status = "error";
133
+ }
134
+ });
135
+ if (video == null) return () => {
136
+ cancelled = true;
137
+ try {
138
+ m.revoke();
139
+ } catch {}
140
+ };
141
+ const onPlay = () => {
142
+ if (status !== "error") status = "playing";
143
+ };
144
+ const onPause = () => {
145
+ if (video.ended) return;
146
+ if (status === "playing") status = "paused";
147
+ };
148
+ const onEnded = () => {
149
+ status = "finished";
150
+ };
151
+ const onTimeUpdate = () => {
152
+ currentTime = video.currentTime;
153
+ };
154
+ const onLoadedMetadata = () => {
155
+ if (Number.isFinite(video.duration)) duration = video.duration;
156
+ };
157
+ const onError = () => {
158
+ error = /* @__PURE__ */ new Error("HTMLVideoElement error");
159
+ status = "error";
160
+ };
161
+ video.addEventListener("play", onPlay);
162
+ video.addEventListener("pause", onPause);
163
+ video.addEventListener("ended", onEnded);
164
+ video.addEventListener("timeupdate", onTimeUpdate);
165
+ video.addEventListener("loadedmetadata", onLoadedMetadata);
166
+ video.addEventListener("error", onError);
167
+ return () => {
168
+ cancelled = true;
169
+ video.removeEventListener("play", onPlay);
170
+ video.removeEventListener("pause", onPause);
171
+ video.removeEventListener("ended", onEnded);
172
+ video.removeEventListener("timeupdate", onTimeUpdate);
173
+ video.removeEventListener("loadedmetadata", onLoadedMetadata);
174
+ video.removeEventListener("error", onError);
175
+ try {
176
+ video.pause();
177
+ video.removeAttribute("src");
178
+ video.load();
179
+ } catch {}
180
+ try {
181
+ m.revoke();
182
+ } catch {}
183
+ };
184
+ };
185
+ $effect(() => {
186
+ const m = unwrap(media);
187
+ const video = unwrap(videoRef) ?? null;
188
+ detach?.();
189
+ detach = null;
190
+ if (m == null) {
191
+ status = "idle";
192
+ error = void 0;
193
+ currentTime = 0;
194
+ duration = void 0;
195
+ return;
196
+ }
197
+ if (m.error != null) {
198
+ error = new Error(m.error.message);
199
+ status = "error";
200
+ return;
201
+ }
202
+ detach = bind(m, video);
203
+ });
204
+ onDestroy(() => {
205
+ detach?.();
206
+ detach = null;
207
+ });
208
+ return {
209
+ get status() {
210
+ return status;
211
+ },
212
+ play,
213
+ pause,
214
+ stop,
215
+ toggle,
216
+ reset,
217
+ playToEnd,
218
+ get currentTime() {
219
+ return currentTime;
220
+ },
221
+ get duration() {
222
+ return duration;
223
+ },
224
+ seek,
225
+ get error() {
226
+ return error;
227
+ }
228
+ };
229
+ }
230
+ //#endregion
231
+ export { useVideoPlayer };
232
+
233
+ //# sourceMappingURL=use-video-player.svelte.js.map