@swiftbrowser/react 0.28.0 → 0.28.2
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/README.md +80 -0
- package/dist/avAsset.d.ts +218 -0
- package/dist/avAsset.d.ts.map +1 -0
- package/dist/bitmaps.d.ts +80 -0
- package/dist/bitmaps.d.ts.map +1 -0
- package/dist/components/controls.d.ts +2 -1
- package/dist/components/controls.d.ts.map +1 -1
- package/dist/components/text.d.ts +12 -0
- package/dist/components/text.d.ts.map +1 -1
- package/dist/host/remote.d.ts.map +1 -1
- package/dist/host/tree.d.ts +4 -0
- package/dist/host/tree.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +785 -48
- package/dist/index.js.map +1 -1
- package/dist/videoRecorder.d.ts +65 -0
- package/dist/videoRecorder.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createContext, useContext, createElement, isValidElement, useMemo, useSyncExternalStore, useLayoutEffect, useRef, Fragment, useState, Children, useEffect, useId, cloneElement, useCallback } from "react";
|
|
2
2
|
import { DefaultEventPriority, ConcurrentRoot } from "react-reconciler/constants";
|
|
3
3
|
import Reconciler from "react-reconciler";
|
|
4
|
+
import { base64Decode, base64Encode } from "@swiftbrowser/web/src/base64";
|
|
5
|
+
import { addLiveBitmap, forgetLiveBitmap } from "@swiftbrowser/web/src/bitmaps";
|
|
4
6
|
import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime";
|
|
5
7
|
import { horizontalSizeClassFor } from "@swiftbrowser/web/src/protocol";
|
|
6
8
|
import { BrowserHistory } from "@swiftbrowser/web/src/browserHistory";
|
|
@@ -16,7 +18,6 @@ import { createRoot } from "react-dom/client";
|
|
|
16
18
|
import { flushSync } from "react-dom";
|
|
17
19
|
import { ViewRegistry } from "@swiftbrowser/web/src/viewRegistry";
|
|
18
20
|
import { chooseColor } from "@swiftbrowser/web/src/colorPicker";
|
|
19
|
-
import { base64Encode } from "@swiftbrowser/web/src/base64";
|
|
20
21
|
import { timingFor } from "@swiftbrowser/web/src/animation";
|
|
21
22
|
const ViewEnvironmentContext = createContext({});
|
|
22
23
|
function useViewEnvironment() {
|
|
@@ -63,6 +64,176 @@ function usePlatform() {
|
|
|
63
64
|
function useRedacted() {
|
|
64
65
|
return useContext(ViewEnvironmentContext).redacted === true;
|
|
65
66
|
}
|
|
67
|
+
const registry = /* @__PURE__ */ new Map();
|
|
68
|
+
const byURL = /* @__PURE__ */ new Map();
|
|
69
|
+
function bitmapSource(file, options = {}) {
|
|
70
|
+
if (typeof file === "string") {
|
|
71
|
+
const known = byURL.get(file);
|
|
72
|
+
if (known !== void 0) return known;
|
|
73
|
+
const match = /^data:([^;,]*)(?:;[^,]*)?;base64,(.*)$/s.exec(file);
|
|
74
|
+
const source = match ? register(base64Decode(match[2]), match[2], { mime: match[1] || void 0, ...options }) : null;
|
|
75
|
+
byURL.set(file, source);
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
return register(file, null, options);
|
|
79
|
+
}
|
|
80
|
+
function register(bytes, base64, options) {
|
|
81
|
+
const header = imageHeader(bytes);
|
|
82
|
+
const width = options.width ?? header?.width;
|
|
83
|
+
const height = options.height ?? header?.height;
|
|
84
|
+
const mime = header?.mime ?? options.mime ?? "";
|
|
85
|
+
if (width === void 0 || height === void 0) return null;
|
|
86
|
+
const key = `f${fnv1a64(bytes)}`;
|
|
87
|
+
if (!registry.has(key)) registry.set(key, { op: "imagedata", key, width, height, mime, data: base64 ?? base64Encode(bytes) });
|
|
88
|
+
return { key, width, height, mime };
|
|
89
|
+
}
|
|
90
|
+
function liveBitmapSource(pixels, width, height) {
|
|
91
|
+
if (globalThis.__sbNative !== void 0) return null;
|
|
92
|
+
return { key: addLiveBitmap(pixels, width, height), width, height, mime: "" };
|
|
93
|
+
}
|
|
94
|
+
function releaseBitmapSource(source) {
|
|
95
|
+
if (source.key.startsWith("l")) forgetLiveBitmap(source.key);
|
|
96
|
+
}
|
|
97
|
+
function bitmapSourceOp(key) {
|
|
98
|
+
return registry.get(key);
|
|
99
|
+
}
|
|
100
|
+
function registerBitmapSourceOp(op) {
|
|
101
|
+
if (!registry.has(op.key)) registry.set(op.key, op);
|
|
102
|
+
}
|
|
103
|
+
function fnv1a64(bytes) {
|
|
104
|
+
let hi = 3421674724;
|
|
105
|
+
let lo = 2216829733;
|
|
106
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
107
|
+
lo = (lo ^ bytes[i]) >>> 0;
|
|
108
|
+
const loMul = lo * 435;
|
|
109
|
+
const carry = Math.floor(loMul / 4294967296);
|
|
110
|
+
const newLo = loMul >>> 0;
|
|
111
|
+
const newHi = Math.imul(hi, 435) + carry + (lo << 8 >>> 0) >>> 0;
|
|
112
|
+
hi = newHi;
|
|
113
|
+
lo = newLo;
|
|
114
|
+
}
|
|
115
|
+
return hi.toString(16).padStart(8, "0") + lo.toString(16).padStart(8, "0");
|
|
116
|
+
}
|
|
117
|
+
function imageHeader(b) {
|
|
118
|
+
const u16 = (i, little = false) => little ? b[i] | b[i + 1] << 8 : b[i] << 8 | b[i + 1];
|
|
119
|
+
const u32 = (i) => (b[i] << 24 | b[i + 1] << 16 | b[i + 2] << 8 | b[i + 3]) >>> 0;
|
|
120
|
+
if (b.length >= 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
|
|
121
|
+
return { mime: "image/png", width: u32(16), height: u32(20) };
|
|
122
|
+
}
|
|
123
|
+
if (b.length >= 10 && b[0] === 71 && b[1] === 73 && b[2] === 70) {
|
|
124
|
+
return { mime: "image/gif", width: u16(6, true), height: u16(8, true) };
|
|
125
|
+
}
|
|
126
|
+
if (b.length >= 30 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) {
|
|
127
|
+
const chunk = String.fromCharCode(b[12], b[13], b[14], b[15]);
|
|
128
|
+
if (chunk === "VP8 ") return { mime: "image/webp", width: u16(26, true) & 16383, height: u16(28, true) & 16383 };
|
|
129
|
+
if (chunk === "VP8L") {
|
|
130
|
+
const bits = (b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24) >>> 0;
|
|
131
|
+
return { mime: "image/webp", width: (bits & 16383) + 1, height: (bits >>> 14 & 16383) + 1 };
|
|
132
|
+
}
|
|
133
|
+
if (chunk === "VP8X") return { mime: "image/webp", width: 1 + (b[24] | b[25] << 8 | b[26] << 16), height: 1 + (b[27] | b[28] << 8 | b[29] << 16) };
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (b.length >= 4 && b[0] === 255 && b[1] === 216) {
|
|
137
|
+
let orientation = 1;
|
|
138
|
+
let i = 2;
|
|
139
|
+
while (i + 4 <= b.length) {
|
|
140
|
+
if (b[i] !== 255) return null;
|
|
141
|
+
const marker = b[i + 1];
|
|
142
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
143
|
+
i += 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const length = u16(i + 2);
|
|
147
|
+
if (marker === 225 && length >= 16) orientation = exifOrientation(b, i + 4, length - 2) ?? orientation;
|
|
148
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
149
|
+
if (i + 9 > b.length) return null;
|
|
150
|
+
const height = u16(i + 5);
|
|
151
|
+
const width = u16(i + 7);
|
|
152
|
+
return orientation >= 5 && orientation <= 8 ? { mime: "image/jpeg", width: height, height: width } : { mime: "image/jpeg", width, height };
|
|
153
|
+
}
|
|
154
|
+
i += 2 + length;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function exifOrientation(b, start, length) {
|
|
160
|
+
if (b[start] !== 69 || b[start + 1] !== 120 || b[start + 2] !== 105 || b[start + 3] !== 102) return null;
|
|
161
|
+
const tiff = start + 6;
|
|
162
|
+
const end = Math.min(b.length, start + length);
|
|
163
|
+
const little = b[tiff] === 73;
|
|
164
|
+
const u16 = (i) => little ? b[i] | b[i + 1] << 8 : b[i] << 8 | b[i + 1];
|
|
165
|
+
const u32 = (i) => little ? (b[i] | b[i + 1] << 8 | b[i + 2] << 16 | b[i + 3] << 24) >>> 0 : (b[i] << 24 | b[i + 1] << 16 | b[i + 2] << 8 | b[i + 3]) >>> 0;
|
|
166
|
+
if (tiff + 8 > end) return null;
|
|
167
|
+
const ifd = tiff + u32(tiff + 4);
|
|
168
|
+
if (ifd + 2 > end) return null;
|
|
169
|
+
const count = u16(ifd);
|
|
170
|
+
for (let n = 0; n < count; n++) {
|
|
171
|
+
const entry = ifd + 2 + n * 12;
|
|
172
|
+
if (entry + 12 > end) return null;
|
|
173
|
+
if (u16(entry) === 274) return u16(entry + 8);
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function bitmapExtent(source, steps = []) {
|
|
178
|
+
let r2 = source ? { x: 0, y: 0, width: source.width, height: source.height } : { x: 0, y: 0, width: 0, height: 0 };
|
|
179
|
+
const infinite = () => !Number.isFinite(r2.width) || !Number.isFinite(r2.height);
|
|
180
|
+
for (const step of steps) {
|
|
181
|
+
switch (step.op) {
|
|
182
|
+
case "crop": {
|
|
183
|
+
if (infinite()) {
|
|
184
|
+
r2 = { x: step.x, y: step.y, width: step.width, height: step.height };
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
const x0 = Math.max(r2.x, step.x);
|
|
188
|
+
const y0 = Math.max(r2.y, step.y);
|
|
189
|
+
const x1 = Math.min(r2.x + r2.width, step.x + step.width);
|
|
190
|
+
const y1 = Math.min(r2.y + r2.height, step.y + step.height);
|
|
191
|
+
r2 = { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "render":
|
|
195
|
+
r2 = { x: 0, y: 0, width: step.width, height: step.height };
|
|
196
|
+
break;
|
|
197
|
+
case "clamp":
|
|
198
|
+
r2 = { x: -Infinity, y: -Infinity, width: Infinity, height: Infinity };
|
|
199
|
+
break;
|
|
200
|
+
case "gaussianBlur": {
|
|
201
|
+
if (infinite()) break;
|
|
202
|
+
const grow = Math.ceil(3 * step.radius);
|
|
203
|
+
r2 = { x: r2.x - grow, y: r2.y - grow, width: r2.width + 2 * grow, height: r2.height + 2 * grow };
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "place":
|
|
207
|
+
if (!(step.sx > 0) || !(step.sy > 0)) r2 = { x: step.tx, y: step.ty, width: 0, height: 0 };
|
|
208
|
+
else if (!infinite()) r2 = { x: r2.x * step.sx + step.tx, y: r2.y * step.sy + step.ty, width: r2.width * step.sx, height: r2.height * step.sy };
|
|
209
|
+
break;
|
|
210
|
+
case "transform": {
|
|
211
|
+
if (infinite()) break;
|
|
212
|
+
const { a, b, c, d, tx, ty } = step;
|
|
213
|
+
if (!(Math.abs(a * d - b * c) > 1e-12)) {
|
|
214
|
+
r2 = { x: tx, y: ty, width: 0, height: 0 };
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
const xs = [];
|
|
218
|
+
const ys = [];
|
|
219
|
+
for (const [x, y] of [
|
|
220
|
+
[r2.x, r2.y],
|
|
221
|
+
[r2.x + r2.width, r2.y],
|
|
222
|
+
[r2.x, r2.y + r2.height],
|
|
223
|
+
[r2.x + r2.width, r2.y + r2.height]
|
|
224
|
+
]) {
|
|
225
|
+
xs.push(a * x + c * y + tx);
|
|
226
|
+
ys.push(b * x + d * y + ty);
|
|
227
|
+
}
|
|
228
|
+
const x0 = Math.min(...xs);
|
|
229
|
+
const y0 = Math.min(...ys);
|
|
230
|
+
r2 = { x: x0, y: y0, width: Math.max(...xs) - x0, height: Math.max(...ys) - y0 };
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return r2;
|
|
236
|
+
}
|
|
66
237
|
const PREF_KIND = "$pref";
|
|
67
238
|
const HOIST_KIND = "$hoist";
|
|
68
239
|
function inheritTitleModes(screens) {
|
|
@@ -122,6 +293,8 @@ class HostTree {
|
|
|
122
293
|
committedColorScheme;
|
|
123
294
|
/** Remote: the root's props last sent. */
|
|
124
295
|
committedRootProps = {};
|
|
296
|
+
/** The bitmap sources (`imagedata` keys) this tree has sent: each goes once, before the first image that draws it. */
|
|
297
|
+
sentSources = /* @__PURE__ */ new Set();
|
|
125
298
|
/** Whether another tree draws this one (`createSwiftUIRoot(sink, { remote: true })`, for a `RemoteTree`). */
|
|
126
299
|
get remote() {
|
|
127
300
|
return this.options.remote === true;
|
|
@@ -221,6 +394,7 @@ class HostTree {
|
|
|
221
394
|
};
|
|
222
395
|
const visit = (child, sink, owner, ctx, hoistedTo) => {
|
|
223
396
|
if (child.hidden) return;
|
|
397
|
+
if (child.kind === PREF_KIND || child.kind === HOIST_KIND) child.stale = false;
|
|
224
398
|
if (child.kind === PREF_KIND) {
|
|
225
399
|
contribute({ kind: "pref", target: child.target, props: child.props }, ctx);
|
|
226
400
|
return;
|
|
@@ -303,6 +477,7 @@ class HostTree {
|
|
|
303
477
|
const current = parent.committedChildren.filter((child) => child.shown && child.effectiveParent === parent);
|
|
304
478
|
entry.children.forEach((child, index) => {
|
|
305
479
|
const childEntry = child.entry;
|
|
480
|
+
if (child.kind === "image") this.sendBitmapSource(childEntry.props, ops);
|
|
306
481
|
if (!child.created) {
|
|
307
482
|
ops.push({ op: "create", id: child.id, kind: child.kind, props: childEntry.props });
|
|
308
483
|
child.created = true;
|
|
@@ -352,6 +527,15 @@ class HostTree {
|
|
|
352
527
|
ops.push(commit);
|
|
353
528
|
return ops;
|
|
354
529
|
}
|
|
530
|
+
/** An `imagedata` op ahead of an image whose recipe's source this tree has not sent yet (bitmaps.ts). */
|
|
531
|
+
sendBitmapSource(props, ops) {
|
|
532
|
+
const key = props["bitmap"]?.source;
|
|
533
|
+
if (typeof key !== "string" || this.sentSources.has(key)) return;
|
|
534
|
+
const op = bitmapSourceOp(key);
|
|
535
|
+
if (!op) return;
|
|
536
|
+
this.sentSources.add(key);
|
|
537
|
+
ops.push(op);
|
|
538
|
+
}
|
|
355
539
|
/** Whether a pass is still animating with this completion serial. */
|
|
356
540
|
awaitsCompletion(serial) {
|
|
357
541
|
return this.completions.has(serial);
|
|
@@ -670,6 +854,9 @@ function createRemoteRoot() {
|
|
|
670
854
|
case "commit":
|
|
671
855
|
commit = op;
|
|
672
856
|
break;
|
|
857
|
+
case "imagedata":
|
|
858
|
+
registerBitmapSourceOp(op);
|
|
859
|
+
break;
|
|
673
860
|
}
|
|
674
861
|
}
|
|
675
862
|
if (!commit) return;
|
|
@@ -2138,6 +2325,21 @@ function openURL(url) {
|
|
|
2138
2325
|
}
|
|
2139
2326
|
function imageProps$1(props) {
|
|
2140
2327
|
if (props.systemName !== void 0) return { systemName: props.systemName };
|
|
2328
|
+
if (props.bitmap !== void 0) {
|
|
2329
|
+
const { source, steps = [], version } = props.bitmap;
|
|
2330
|
+
const extent = bitmapExtent(source, steps);
|
|
2331
|
+
const finite = Number.isFinite(extent.width) && Number.isFinite(extent.height);
|
|
2332
|
+
const width = finite ? Math.max(0, Math.round(extent.width)) : 0;
|
|
2333
|
+
const height = finite ? Math.max(0, Math.round(extent.height)) : 0;
|
|
2334
|
+
const scale = props.scale !== void 0 && props.scale > 0 ? props.scale : 1;
|
|
2335
|
+
return {
|
|
2336
|
+
bitmap: { source: source?.key ?? null, steps, width, height, ...version !== void 0 ? { version } : {} },
|
|
2337
|
+
resizable: props.resizable ?? false,
|
|
2338
|
+
width: width / scale,
|
|
2339
|
+
height: height / scale,
|
|
2340
|
+
...props.interpolation === "none" ? { interpolation: "none" } : {}
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2141
2343
|
if (props.url !== void 0) return { url: props.url, resizable: props.resizable ?? false, width: props.width ?? 0, height: props.height ?? 0 };
|
|
2142
2344
|
return { name: props.name ?? "", resizable: props.resizable ?? false };
|
|
2143
2345
|
}
|
|
@@ -4036,7 +4238,7 @@ function protocolRuns(text) {
|
|
|
4036
4238
|
}
|
|
4037
4239
|
function useAudioRecorder(options = {}) {
|
|
4038
4240
|
const interval = options.meterInterval ?? 50;
|
|
4039
|
-
const
|
|
4241
|
+
const host2 = browserEnvironment.audioRecorder;
|
|
4040
4242
|
const [status, setStatus] = useState("idle");
|
|
4041
4243
|
const [time, setTime] = useState(0);
|
|
4042
4244
|
const [levels, setLevels] = useState([]);
|
|
@@ -4045,13 +4247,13 @@ function useAudioRecorder(options = {}) {
|
|
|
4045
4247
|
statusRef.current = status;
|
|
4046
4248
|
const fail = (reason) => setError(reason instanceof Error ? reason.message : String(reason));
|
|
4047
4249
|
useEffect(() => {
|
|
4048
|
-
if (status !== "recording" || !
|
|
4250
|
+
if (status !== "recording" || !host2) return;
|
|
4049
4251
|
let reading = false;
|
|
4050
4252
|
let live = true;
|
|
4051
4253
|
const timer = setInterval(() => {
|
|
4052
4254
|
if (reading) return;
|
|
4053
4255
|
reading = true;
|
|
4054
|
-
|
|
4256
|
+
host2.meter().then((meter) => {
|
|
4055
4257
|
if (!live) return;
|
|
4056
4258
|
setTime(meter.time);
|
|
4057
4259
|
setLevels((previous) => [...previous, meter.level]);
|
|
@@ -4063,15 +4265,15 @@ function useAudioRecorder(options = {}) {
|
|
|
4063
4265
|
live = false;
|
|
4064
4266
|
clearInterval(timer);
|
|
4065
4267
|
};
|
|
4066
|
-
}, [status,
|
|
4268
|
+
}, [status, host2, interval]);
|
|
4067
4269
|
useEffect(
|
|
4068
4270
|
() => () => {
|
|
4069
|
-
if (statusRef.current !== "idle") void
|
|
4271
|
+
if (statusRef.current !== "idle") void host2?.cancel().catch(() => void 0);
|
|
4070
4272
|
},
|
|
4071
|
-
[
|
|
4273
|
+
[host2]
|
|
4072
4274
|
);
|
|
4073
4275
|
const record = useCallback(async () => {
|
|
4074
|
-
if (!
|
|
4276
|
+
if (!host2) {
|
|
4075
4277
|
setError("unsupported: this page cannot record");
|
|
4076
4278
|
return;
|
|
4077
4279
|
}
|
|
@@ -4081,28 +4283,28 @@ function useAudioRecorder(options = {}) {
|
|
|
4081
4283
|
setLevels([]);
|
|
4082
4284
|
setStatus("starting");
|
|
4083
4285
|
try {
|
|
4084
|
-
await
|
|
4286
|
+
await host2.start();
|
|
4085
4287
|
setStatus("recording");
|
|
4086
4288
|
} catch (reason) {
|
|
4087
4289
|
fail(reason);
|
|
4088
4290
|
setStatus("idle");
|
|
4089
4291
|
}
|
|
4090
|
-
}, [
|
|
4292
|
+
}, [host2]);
|
|
4091
4293
|
const pause = useCallback(async () => {
|
|
4092
|
-
if (!
|
|
4093
|
-
await
|
|
4294
|
+
if (!host2 || statusRef.current !== "recording") return;
|
|
4295
|
+
await host2.pause().catch(fail);
|
|
4094
4296
|
setStatus("paused");
|
|
4095
|
-
}, [
|
|
4297
|
+
}, [host2]);
|
|
4096
4298
|
const resume = useCallback(async () => {
|
|
4097
|
-
if (!
|
|
4098
|
-
await
|
|
4299
|
+
if (!host2 || statusRef.current !== "paused") return;
|
|
4300
|
+
await host2.resume().catch(fail);
|
|
4099
4301
|
setStatus("recording");
|
|
4100
|
-
}, [
|
|
4302
|
+
}, [host2]);
|
|
4101
4303
|
const stop = useCallback(async () => {
|
|
4102
|
-
if (!
|
|
4304
|
+
if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
|
|
4103
4305
|
setStatus("stopping");
|
|
4104
4306
|
try {
|
|
4105
|
-
const recording = await
|
|
4307
|
+
const recording = await host2.stop();
|
|
4106
4308
|
setTime(recording.duration);
|
|
4107
4309
|
return recording;
|
|
4108
4310
|
} catch (reason) {
|
|
@@ -4111,30 +4313,538 @@ function useAudioRecorder(options = {}) {
|
|
|
4111
4313
|
} finally {
|
|
4112
4314
|
setStatus("idle");
|
|
4113
4315
|
}
|
|
4114
|
-
}, [
|
|
4316
|
+
}, [host2]);
|
|
4115
4317
|
const cancel = useCallback(async () => {
|
|
4116
|
-
if (!
|
|
4117
|
-
await
|
|
4318
|
+
if (!host2 || statusRef.current === "idle") return;
|
|
4319
|
+
await host2.cancel().catch(fail);
|
|
4118
4320
|
setStatus("idle");
|
|
4119
4321
|
setTime(0);
|
|
4120
4322
|
setLevels([]);
|
|
4121
|
-
}, [
|
|
4323
|
+
}, [host2]);
|
|
4122
4324
|
const discard = useCallback(async (url) => {
|
|
4123
|
-
await
|
|
4124
|
-
}, [
|
|
4325
|
+
await host2?.discard(url).catch(fail);
|
|
4326
|
+
}, [host2]);
|
|
4125
4327
|
const trim = useCallback(
|
|
4126
4328
|
async (url, keep) => {
|
|
4127
|
-
if (!
|
|
4329
|
+
if (!host2) throw new Error("unsupported: this page cannot cut audio");
|
|
4128
4330
|
try {
|
|
4129
|
-
return await
|
|
4331
|
+
return await host2.trim(url, keep);
|
|
4130
4332
|
} catch (reason) {
|
|
4131
4333
|
fail(reason);
|
|
4132
4334
|
throw reason;
|
|
4133
4335
|
}
|
|
4134
4336
|
},
|
|
4135
|
-
[
|
|
4337
|
+
[host2]
|
|
4338
|
+
);
|
|
4339
|
+
return { available: host2 !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
|
|
4340
|
+
}
|
|
4341
|
+
function host() {
|
|
4342
|
+
const found = browserEnvironment.avAsset;
|
|
4343
|
+
if (!found) throw new Error("unsupported: this page cannot read or write video");
|
|
4344
|
+
return found;
|
|
4345
|
+
}
|
|
4346
|
+
class CMTime {
|
|
4347
|
+
constructor(value, timescale) {
|
|
4348
|
+
this.value = value;
|
|
4349
|
+
this.timescale = timescale;
|
|
4350
|
+
}
|
|
4351
|
+
value;
|
|
4352
|
+
timescale;
|
|
4353
|
+
static zero = new CMTime(0, 1);
|
|
4354
|
+
static invalid = new CMTime(0, 0);
|
|
4355
|
+
static positiveInfinity = new CMTime(Infinity, 1);
|
|
4356
|
+
/** `CMTime(value:timescale:)`. */
|
|
4357
|
+
static make(value, timescale) {
|
|
4358
|
+
return new CMTime(value, timescale);
|
|
4359
|
+
}
|
|
4360
|
+
/** `CMTime(seconds:preferredTimescale:)`: rounded to the timescale. */
|
|
4361
|
+
static seconds(seconds, preferredTimescale = 600) {
|
|
4362
|
+
if (!Number.isFinite(seconds)) return seconds > 0 ? CMTime.positiveInfinity : CMTime.invalid;
|
|
4363
|
+
return new CMTime(Math.round(seconds * preferredTimescale), preferredTimescale);
|
|
4364
|
+
}
|
|
4365
|
+
get isValid() {
|
|
4366
|
+
return this.timescale > 0;
|
|
4367
|
+
}
|
|
4368
|
+
/** `CMTimeGetSeconds`. */
|
|
4369
|
+
get seconds() {
|
|
4370
|
+
return this.timescale > 0 ? this.value / this.timescale : Number.NaN;
|
|
4371
|
+
}
|
|
4372
|
+
add(other) {
|
|
4373
|
+
return CMTime.seconds(this.seconds + other.seconds, Math.max(this.timescale, other.timescale, 1));
|
|
4374
|
+
}
|
|
4375
|
+
subtract(other) {
|
|
4376
|
+
return CMTime.seconds(this.seconds - other.seconds, Math.max(this.timescale, other.timescale, 1));
|
|
4377
|
+
}
|
|
4378
|
+
/** `CMTimeCompare`: -1, 0 or 1. */
|
|
4379
|
+
compare(other) {
|
|
4380
|
+
const a = this.seconds;
|
|
4381
|
+
const b = other.seconds;
|
|
4382
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
4383
|
+
}
|
|
4384
|
+
static minimum(a, b) {
|
|
4385
|
+
return a.compare(b) <= 0 ? a : b;
|
|
4386
|
+
}
|
|
4387
|
+
static maximum(a, b) {
|
|
4388
|
+
return a.compare(b) >= 0 ? a : b;
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
class CMTimeRange {
|
|
4392
|
+
constructor(start, duration) {
|
|
4393
|
+
this.start = start;
|
|
4394
|
+
this.duration = duration;
|
|
4395
|
+
}
|
|
4396
|
+
start;
|
|
4397
|
+
duration;
|
|
4398
|
+
static zero = new CMTimeRange(CMTime.zero, CMTime.zero);
|
|
4399
|
+
/** `CMTimeRange(start:end:)`. */
|
|
4400
|
+
static fromTo(start, end) {
|
|
4401
|
+
return new CMTimeRange(start, CMTime.maximum(end.subtract(start), CMTime.zero));
|
|
4402
|
+
}
|
|
4403
|
+
/** `CMTimeRange(start:duration:)` in seconds. */
|
|
4404
|
+
static seconds(start, duration, preferredTimescale = 600) {
|
|
4405
|
+
return new CMTimeRange(CMTime.seconds(start, preferredTimescale), CMTime.seconds(duration, preferredTimescale));
|
|
4406
|
+
}
|
|
4407
|
+
get end() {
|
|
4408
|
+
return this.start.add(this.duration);
|
|
4409
|
+
}
|
|
4410
|
+
get isEmpty() {
|
|
4411
|
+
return this.duration.seconds <= 0;
|
|
4412
|
+
}
|
|
4413
|
+
containsTime(time) {
|
|
4414
|
+
return time.compare(this.start) >= 0 && time.compare(this.end) < 0;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
const AVMediaType = { video: "vide", audio: "soun" };
|
|
4418
|
+
class AVAssetTrack {
|
|
4419
|
+
constructor(trackID, mediaType, naturalSize, nominalFrameRate, timeRange, codec) {
|
|
4420
|
+
this.trackID = trackID;
|
|
4421
|
+
this.mediaType = mediaType;
|
|
4422
|
+
this.naturalSize = naturalSize;
|
|
4423
|
+
this.nominalFrameRate = nominalFrameRate;
|
|
4424
|
+
this.timeRange = timeRange;
|
|
4425
|
+
this.codec = codec;
|
|
4426
|
+
}
|
|
4427
|
+
trackID;
|
|
4428
|
+
mediaType;
|
|
4429
|
+
naturalSize;
|
|
4430
|
+
nominalFrameRate;
|
|
4431
|
+
timeRange;
|
|
4432
|
+
codec;
|
|
4433
|
+
}
|
|
4434
|
+
class AVAsset {
|
|
4435
|
+
/** @internal */
|
|
4436
|
+
segmentsForExport() {
|
|
4437
|
+
return this.segments();
|
|
4438
|
+
}
|
|
4439
|
+
/** `loadTracks(withMediaType:)`. */
|
|
4440
|
+
async loadTracks(mediaType) {
|
|
4441
|
+
return (await this.load("tracks")).filter((track) => track.mediaType === mediaType);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
const tracksOf = (info) => info.tracks.map(
|
|
4445
|
+
(t) => new AVAssetTrack(
|
|
4446
|
+
t.trackID,
|
|
4447
|
+
t.mediaType === "video" ? AVMediaType.video : AVMediaType.audio,
|
|
4448
|
+
t.naturalSize,
|
|
4449
|
+
t.nominalFrameRate,
|
|
4450
|
+
CMTimeRange.seconds(t.timeRange.start, t.timeRange.duration),
|
|
4451
|
+
t.codec
|
|
4452
|
+
)
|
|
4453
|
+
);
|
|
4454
|
+
class AVURLAsset extends AVAsset {
|
|
4455
|
+
constructor(url) {
|
|
4456
|
+
super();
|
|
4457
|
+
this.url = url;
|
|
4458
|
+
}
|
|
4459
|
+
url;
|
|
4460
|
+
info = null;
|
|
4461
|
+
loaded() {
|
|
4462
|
+
if (!this.info) {
|
|
4463
|
+
this.info = host().load(this.url);
|
|
4464
|
+
this.info.catch(() => {
|
|
4465
|
+
this.info = null;
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
return this.info;
|
|
4469
|
+
}
|
|
4470
|
+
async segments() {
|
|
4471
|
+
return [{ url: this.url, start: 0, end: (await this.loaded()).duration }];
|
|
4472
|
+
}
|
|
4473
|
+
async load(key) {
|
|
4474
|
+
const info = await this.loaded();
|
|
4475
|
+
const values = {
|
|
4476
|
+
duration: CMTime.seconds(info.duration),
|
|
4477
|
+
tracks: tracksOf(info),
|
|
4478
|
+
naturalSize: info.naturalSize,
|
|
4479
|
+
isPlayable: info.tracks.length > 0,
|
|
4480
|
+
isExportable: info.tracks.length > 0
|
|
4481
|
+
};
|
|
4482
|
+
return values[key];
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
class AVMutableComposition extends AVAsset {
|
|
4486
|
+
parts = [];
|
|
4487
|
+
/** The composition's length. */
|
|
4488
|
+
get duration() {
|
|
4489
|
+
return CMTime.seconds(this.parts.reduce((sum, part) => sum + part.range.duration.seconds, 0));
|
|
4490
|
+
}
|
|
4491
|
+
/** `insertTimeRange(_:of:at:)`: `range` of `asset` (an `AVURLAsset`, or another composition's parts) placed at `at`. */
|
|
4492
|
+
async insertTimeRange(range, asset, at) {
|
|
4493
|
+
const inserted = [];
|
|
4494
|
+
if (asset instanceof AVURLAsset) {
|
|
4495
|
+
const length = (await asset.load("duration")).seconds;
|
|
4496
|
+
const start = Math.max(0, range.start.seconds);
|
|
4497
|
+
const end = Math.min(length, range.end.seconds);
|
|
4498
|
+
if (end - start > 1e-3) inserted.push({ asset, range: CMTimeRange.seconds(start, end - start) });
|
|
4499
|
+
} else if (asset instanceof AVMutableComposition) {
|
|
4500
|
+
inserted.push(...asset.partsIn(range.start.seconds, range.end.seconds));
|
|
4501
|
+
} else {
|
|
4502
|
+
throw new Error("unsupported: only an AVURLAsset or an AVMutableComposition can be inserted");
|
|
4503
|
+
}
|
|
4504
|
+
const time = Math.max(0, Math.min(at.seconds, this.duration.seconds));
|
|
4505
|
+
const [before, after] = this.splitAt(time);
|
|
4506
|
+
this.parts = [...before, ...inserted, ...after];
|
|
4507
|
+
}
|
|
4508
|
+
/** `removeTimeRange(_:)`: the media in `range` cut out, the rest closing up. */
|
|
4509
|
+
removeTimeRange(range) {
|
|
4510
|
+
const [before] = this.splitAt(range.start.seconds);
|
|
4511
|
+
const [, after] = this.splitAt(range.end.seconds);
|
|
4512
|
+
this.parts = [...before, ...after];
|
|
4513
|
+
}
|
|
4514
|
+
/** The parts' media from `start` to `end` of the composition's timeline. */
|
|
4515
|
+
partsIn(start, end) {
|
|
4516
|
+
const [, from] = this.splitAt(start);
|
|
4517
|
+
const out = [];
|
|
4518
|
+
let cursor = start;
|
|
4519
|
+
for (const part of from) {
|
|
4520
|
+
if (cursor >= end) break;
|
|
4521
|
+
const take = Math.min(part.range.duration.seconds, end - cursor);
|
|
4522
|
+
if (take > 1e-3) out.push({ asset: part.asset, range: CMTimeRange.seconds(part.range.start.seconds, take) });
|
|
4523
|
+
cursor += take;
|
|
4524
|
+
}
|
|
4525
|
+
return out;
|
|
4526
|
+
}
|
|
4527
|
+
/** The parts before `time` and from it, a part across it split in two. */
|
|
4528
|
+
splitAt(time) {
|
|
4529
|
+
const before = [];
|
|
4530
|
+
const after = [];
|
|
4531
|
+
let cursor = 0;
|
|
4532
|
+
for (const part of this.parts) {
|
|
4533
|
+
const length = part.range.duration.seconds;
|
|
4534
|
+
const start = part.range.start.seconds;
|
|
4535
|
+
if (cursor + length <= time + 1e-9) before.push(part);
|
|
4536
|
+
else if (cursor >= time - 1e-9) after.push(part);
|
|
4537
|
+
else {
|
|
4538
|
+
const head = time - cursor;
|
|
4539
|
+
before.push({ asset: part.asset, range: CMTimeRange.seconds(start, head) });
|
|
4540
|
+
after.push({ asset: part.asset, range: CMTimeRange.seconds(start + head, length - head) });
|
|
4541
|
+
}
|
|
4542
|
+
cursor += length;
|
|
4543
|
+
}
|
|
4544
|
+
return [before, after];
|
|
4545
|
+
}
|
|
4546
|
+
async segments() {
|
|
4547
|
+
return this.parts.map((part) => ({ url: part.asset.url, start: part.range.start.seconds, end: part.range.end.seconds }));
|
|
4548
|
+
}
|
|
4549
|
+
async load(key) {
|
|
4550
|
+
const first = this.parts[0]?.asset;
|
|
4551
|
+
const tracks = first ? await first.load("tracks") : [];
|
|
4552
|
+
const values = {
|
|
4553
|
+
duration: this.duration,
|
|
4554
|
+
tracks: tracks.map((t) => new AVAssetTrack(t.trackID, t.mediaType, t.naturalSize, t.nominalFrameRate, new CMTimeRange(CMTime.zero, this.duration), t.codec)),
|
|
4555
|
+
naturalSize: first ? await first.load("naturalSize") : { width: 0, height: 0 },
|
|
4556
|
+
isPlayable: this.parts.length > 0,
|
|
4557
|
+
isExportable: this.parts.length > 0
|
|
4558
|
+
};
|
|
4559
|
+
return values[key];
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
class AVAssetImageGenerator {
|
|
4563
|
+
constructor(asset) {
|
|
4564
|
+
this.asset = asset;
|
|
4565
|
+
}
|
|
4566
|
+
asset;
|
|
4567
|
+
maximumSize = { width: 0, height: 0 };
|
|
4568
|
+
/** Stills come upright (the track's rotation applied), as with `appliesPreferredTrackTransform = true`. */
|
|
4569
|
+
appliesPreferredTrackTransform = true;
|
|
4570
|
+
made = [];
|
|
4571
|
+
/** The file and the time in it that `time` on the asset's timeline falls on (a composition's part). */
|
|
4572
|
+
async source(time) {
|
|
4573
|
+
let cursor = 0;
|
|
4574
|
+
const segments = await this.asset.segmentsForExport();
|
|
4575
|
+
for (const segment of segments) {
|
|
4576
|
+
const length = segment.end - segment.start;
|
|
4577
|
+
if (time < cursor + length || segment === segments[segments.length - 1]) return { url: segment.url, time: segment.start + Math.max(0, Math.min(length, time - cursor)) };
|
|
4578
|
+
cursor += length;
|
|
4579
|
+
}
|
|
4580
|
+
throw new Error("empty: the asset has no media");
|
|
4581
|
+
}
|
|
4582
|
+
/** `image(at:)`: the still at `time`. */
|
|
4583
|
+
async image(at) {
|
|
4584
|
+
const { url, time } = await this.source(Math.max(0, at.seconds));
|
|
4585
|
+
const options = {
|
|
4586
|
+
...this.maximumSize.width > 0 ? { maxWidth: this.maximumSize.width } : {},
|
|
4587
|
+
...this.maximumSize.height > 0 ? { maxHeight: this.maximumSize.height } : {}
|
|
4588
|
+
};
|
|
4589
|
+
const still = await host().image(url, time, options);
|
|
4590
|
+
this.made.push(still.url);
|
|
4591
|
+
return { image: still.url, actualTime: CMTime.seconds(at.seconds + (still.actualTime - time)), width: still.width, height: still.height };
|
|
4592
|
+
}
|
|
4593
|
+
/** `images(for:)`: a still for each time, in turn. */
|
|
4594
|
+
async *images(times) {
|
|
4595
|
+
for (const time of times) yield { ...await this.image(time), requestedTime: time };
|
|
4596
|
+
}
|
|
4597
|
+
/** Lets go of the stills this generator made (their `blob:` URLs on a page). */
|
|
4598
|
+
async release() {
|
|
4599
|
+
const urls = this.made.splice(0);
|
|
4600
|
+
await Promise.all(urls.map((url) => host().release(url)));
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
const AVFileType = { mp4: "public.mpeg-4", mov: "com.apple.quicktime-movie", m4v: "com.apple.m4v-video", webm: "org.webmproject.webm" };
|
|
4604
|
+
const FILE_TYPES = {
|
|
4605
|
+
"public.mpeg-4": "mp4",
|
|
4606
|
+
"com.apple.m4v-video": "mp4",
|
|
4607
|
+
"com.apple.quicktime-movie": "mov",
|
|
4608
|
+
"org.webmproject.webm": "webm"
|
|
4609
|
+
};
|
|
4610
|
+
const AVAssetExportPresetPassthrough = "AVAssetExportPresetPassthrough";
|
|
4611
|
+
const AVAssetExportPresetHighestQuality = "AVAssetExportPresetHighestQuality";
|
|
4612
|
+
const AVAssetExportPresetMediumQuality = "AVAssetExportPresetMediumQuality";
|
|
4613
|
+
const AVAssetExportPresetLowQuality = "AVAssetExportPresetLowQuality";
|
|
4614
|
+
const AVAssetExportPreset640x480 = "AVAssetExportPreset640x480";
|
|
4615
|
+
const AVAssetExportPreset960x540 = "AVAssetExportPreset960x540";
|
|
4616
|
+
const AVAssetExportPreset1280x720 = "AVAssetExportPreset1280x720";
|
|
4617
|
+
const AVAssetExportPreset1920x1080 = "AVAssetExportPreset1920x1080";
|
|
4618
|
+
const AVAssetExportPreset3840x2160 = "AVAssetExportPreset3840x2160";
|
|
4619
|
+
let exports$1 = 0;
|
|
4620
|
+
class AVAssetExportSession {
|
|
4621
|
+
constructor(asset, presetName) {
|
|
4622
|
+
this.asset = asset;
|
|
4623
|
+
this.presetName = presetName;
|
|
4624
|
+
}
|
|
4625
|
+
asset;
|
|
4626
|
+
presetName;
|
|
4627
|
+
outputURL = null;
|
|
4628
|
+
outputFileType = AVFileType.mp4;
|
|
4629
|
+
timeRange = null;
|
|
4630
|
+
status = "unknown";
|
|
4631
|
+
progress = 0;
|
|
4632
|
+
error = null;
|
|
4633
|
+
/** Heard as `progress` changes (a few times a second). */
|
|
4634
|
+
onProgress = null;
|
|
4635
|
+
/** The written file's length and MIME type, once completed. */
|
|
4636
|
+
result = null;
|
|
4637
|
+
id = `export-${++exports$1}`;
|
|
4638
|
+
/** `AVAssetExportSession.allExportPresets()`. */
|
|
4639
|
+
static allExportPresets() {
|
|
4640
|
+
return [
|
|
4641
|
+
AVAssetExportPresetPassthrough,
|
|
4642
|
+
AVAssetExportPresetHighestQuality,
|
|
4643
|
+
AVAssetExportPresetMediumQuality,
|
|
4644
|
+
AVAssetExportPresetLowQuality,
|
|
4645
|
+
AVAssetExportPreset640x480,
|
|
4646
|
+
AVAssetExportPreset960x540,
|
|
4647
|
+
AVAssetExportPreset1280x720,
|
|
4648
|
+
AVAssetExportPreset1920x1080,
|
|
4649
|
+
AVAssetExportPreset3840x2160
|
|
4650
|
+
];
|
|
4651
|
+
}
|
|
4652
|
+
/** The asset's parts, cut to `timeRange` when it is set. */
|
|
4653
|
+
async parts() {
|
|
4654
|
+
const all = await this.asset.segmentsForExport();
|
|
4655
|
+
if (!this.timeRange) return all;
|
|
4656
|
+
const from = this.timeRange.start.seconds;
|
|
4657
|
+
const to = this.timeRange.end.seconds;
|
|
4658
|
+
const out = [];
|
|
4659
|
+
let cursor = 0;
|
|
4660
|
+
for (const segment of all) {
|
|
4661
|
+
const length = segment.end - segment.start;
|
|
4662
|
+
const start = Math.max(from, cursor);
|
|
4663
|
+
const end = Math.min(to, cursor + length);
|
|
4664
|
+
if (end - start > 1e-3) out.push({ url: segment.url, start: segment.start + (start - cursor), end: segment.start + (end - cursor) });
|
|
4665
|
+
cursor += length;
|
|
4666
|
+
}
|
|
4667
|
+
return out;
|
|
4668
|
+
}
|
|
4669
|
+
/** `export()`: writes the file and answers its URL (also `outputURL`); rejects with why it could not. */
|
|
4670
|
+
async export() {
|
|
4671
|
+
if (this.status === "exporting") throw new Error("busy: already exporting");
|
|
4672
|
+
this.status = "exporting";
|
|
4673
|
+
this.error = null;
|
|
4674
|
+
this.setProgress(0);
|
|
4675
|
+
const assets = host();
|
|
4676
|
+
const ticker = setInterval(() => {
|
|
4677
|
+
void assets.progress(this.id).then((p) => this.status === "exporting" && this.setProgress(p));
|
|
4678
|
+
}, 200);
|
|
4679
|
+
try {
|
|
4680
|
+
const result = await assets.export(this.id, {
|
|
4681
|
+
segments: await this.parts(),
|
|
4682
|
+
preset: this.presetName,
|
|
4683
|
+
fileType: FILE_TYPES[this.outputFileType] ?? "mp4",
|
|
4684
|
+
...this.outputURL ? { outputURL: this.outputURL } : {}
|
|
4685
|
+
});
|
|
4686
|
+
this.outputURL = result.url;
|
|
4687
|
+
this.result = { duration: result.duration, type: result.type };
|
|
4688
|
+
this.status = "completed";
|
|
4689
|
+
this.setProgress(1);
|
|
4690
|
+
return result.url;
|
|
4691
|
+
} catch (reason) {
|
|
4692
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
4693
|
+
this.error = error;
|
|
4694
|
+
this.status = error.message.startsWith("cancelled") ? "cancelled" : "failed";
|
|
4695
|
+
throw error;
|
|
4696
|
+
} finally {
|
|
4697
|
+
clearInterval(ticker);
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
/** `cancelExport()`. */
|
|
4701
|
+
cancelExport() {
|
|
4702
|
+
if (this.status !== "exporting") return;
|
|
4703
|
+
void host().cancel(this.id);
|
|
4704
|
+
}
|
|
4705
|
+
setProgress(progress) {
|
|
4706
|
+
if (progress === this.progress) return;
|
|
4707
|
+
this.progress = progress;
|
|
4708
|
+
this.onProgress?.(progress);
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
function useVideoRecorder(options = {}) {
|
|
4712
|
+
const interval = options.timeInterval ?? 100;
|
|
4713
|
+
const host2 = browserEnvironment.videoRecorder;
|
|
4714
|
+
const [status, setStatus] = useState("closed");
|
|
4715
|
+
const [stream, setStream] = useState(null);
|
|
4716
|
+
const [facing, setFacing] = useState("user");
|
|
4717
|
+
const [time, setTime] = useState(0);
|
|
4718
|
+
const [error, setError] = useState(null);
|
|
4719
|
+
const statusRef = useRef(status);
|
|
4720
|
+
statusRef.current = status;
|
|
4721
|
+
const facingRef = useRef(facing);
|
|
4722
|
+
facingRef.current = facing;
|
|
4723
|
+
const fail = (reason) => setError(reason instanceof Error ? reason.message : String(reason));
|
|
4724
|
+
useEffect(() => {
|
|
4725
|
+
if (status !== "recording" || !host2) return;
|
|
4726
|
+
let reading = false;
|
|
4727
|
+
let live = true;
|
|
4728
|
+
const timer = setInterval(() => {
|
|
4729
|
+
if (reading) return;
|
|
4730
|
+
reading = true;
|
|
4731
|
+
host2.time().then((seconds) => live && setTime(seconds)).catch(() => void 0).finally(() => {
|
|
4732
|
+
reading = false;
|
|
4733
|
+
});
|
|
4734
|
+
}, interval);
|
|
4735
|
+
return () => {
|
|
4736
|
+
live = false;
|
|
4737
|
+
clearInterval(timer);
|
|
4738
|
+
};
|
|
4739
|
+
}, [status, host2, interval]);
|
|
4740
|
+
useEffect(
|
|
4741
|
+
() => () => {
|
|
4742
|
+
if (statusRef.current !== "closed") void host2?.close().catch(() => void 0);
|
|
4743
|
+
},
|
|
4744
|
+
[host2]
|
|
4745
|
+
);
|
|
4746
|
+
const open = useCallback(
|
|
4747
|
+
async (wanted) => {
|
|
4748
|
+
if (!host2) {
|
|
4749
|
+
setError("unsupported: this page cannot record video");
|
|
4750
|
+
return;
|
|
4751
|
+
}
|
|
4752
|
+
if (statusRef.current === "recording" || statusRef.current === "paused" || statusRef.current === "stopping") return;
|
|
4753
|
+
setError(null);
|
|
4754
|
+
setStatus("opening");
|
|
4755
|
+
try {
|
|
4756
|
+
const opened = await host2.open({ facing: wanted ?? facingRef.current });
|
|
4757
|
+
setStream(opened.stream);
|
|
4758
|
+
setFacing(opened.facing);
|
|
4759
|
+
setStatus("ready");
|
|
4760
|
+
} catch (reason) {
|
|
4761
|
+
fail(reason);
|
|
4762
|
+
setStream(null);
|
|
4763
|
+
setStatus("closed");
|
|
4764
|
+
}
|
|
4765
|
+
},
|
|
4766
|
+
[host2]
|
|
4767
|
+
);
|
|
4768
|
+
const close = useCallback(async () => {
|
|
4769
|
+
if (!host2) return;
|
|
4770
|
+
await host2.close().catch(fail);
|
|
4771
|
+
setStream(null);
|
|
4772
|
+
setStatus("closed");
|
|
4773
|
+
setTime(0);
|
|
4774
|
+
}, [host2]);
|
|
4775
|
+
const flip = useCallback(async () => {
|
|
4776
|
+
if (statusRef.current !== "ready") return;
|
|
4777
|
+
await open(facingRef.current === "user" ? "environment" : "user");
|
|
4778
|
+
}, [open]);
|
|
4779
|
+
const record = useCallback(async () => {
|
|
4780
|
+
if (!host2 || statusRef.current !== "ready") return;
|
|
4781
|
+
setError(null);
|
|
4782
|
+
setTime(0);
|
|
4783
|
+
try {
|
|
4784
|
+
await host2.start();
|
|
4785
|
+
setStatus("recording");
|
|
4786
|
+
} catch (reason) {
|
|
4787
|
+
fail(reason);
|
|
4788
|
+
}
|
|
4789
|
+
}, [host2]);
|
|
4790
|
+
const pause = useCallback(async () => {
|
|
4791
|
+
if (!host2 || statusRef.current !== "recording") return;
|
|
4792
|
+
await host2.pause().catch(fail);
|
|
4793
|
+
setStatus("paused");
|
|
4794
|
+
}, [host2]);
|
|
4795
|
+
const resume = useCallback(async () => {
|
|
4796
|
+
if (!host2 || statusRef.current !== "paused") return;
|
|
4797
|
+
await host2.resume().catch(fail);
|
|
4798
|
+
setStatus("recording");
|
|
4799
|
+
}, [host2]);
|
|
4800
|
+
const stop = useCallback(async () => {
|
|
4801
|
+
if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
|
|
4802
|
+
setStatus("stopping");
|
|
4803
|
+
try {
|
|
4804
|
+
const recording = await host2.stop();
|
|
4805
|
+
setTime(recording.duration);
|
|
4806
|
+
return recording;
|
|
4807
|
+
} catch (reason) {
|
|
4808
|
+
fail(reason);
|
|
4809
|
+
return null;
|
|
4810
|
+
} finally {
|
|
4811
|
+
setStatus("ready");
|
|
4812
|
+
}
|
|
4813
|
+
}, [host2]);
|
|
4814
|
+
const cancel = useCallback(async () => {
|
|
4815
|
+
if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return;
|
|
4816
|
+
await host2.cancel().catch(fail);
|
|
4817
|
+
setStatus("ready");
|
|
4818
|
+
setTime(0);
|
|
4819
|
+
}, [host2]);
|
|
4820
|
+
const discard = useCallback(
|
|
4821
|
+
async (url) => {
|
|
4822
|
+
await host2?.discard(url).catch(fail);
|
|
4823
|
+
},
|
|
4824
|
+
[host2]
|
|
4825
|
+
);
|
|
4826
|
+
return { available: host2 !== void 0, status, stream, facing, time, error, open, close, flip, record, pause, resume, stop, cancel, discard };
|
|
4827
|
+
}
|
|
4828
|
+
function CameraPreview(props) {
|
|
4829
|
+
const { own, modifiers } = splitProps(props);
|
|
4830
|
+
const stream = own.recorder.stream;
|
|
4831
|
+
if (!stream) return applyModifiers(sb("color", { color: { r: 0, g: 0, b: 0, a: 1 } }), modifiers);
|
|
4832
|
+
return applyModifiers(
|
|
4833
|
+
sb("video", {
|
|
4834
|
+
url: null,
|
|
4835
|
+
playing: false,
|
|
4836
|
+
muted: true,
|
|
4837
|
+
volume: 0,
|
|
4838
|
+
rate: 1,
|
|
4839
|
+
loop: false,
|
|
4840
|
+
gravity: own.gravity ?? "resizeAspectFill",
|
|
4841
|
+
controls: false,
|
|
4842
|
+
seek: { time: 0, serial: 0 },
|
|
4843
|
+
stream,
|
|
4844
|
+
mirrored: own.mirrored ?? own.recorder.facing === "user"
|
|
4845
|
+
}),
|
|
4846
|
+
modifiers
|
|
4136
4847
|
);
|
|
4137
|
-
return { available: host !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
|
|
4138
4848
|
}
|
|
4139
4849
|
let nextId$1 = 1;
|
|
4140
4850
|
const newId = () => nextId$1++;
|
|
@@ -4143,9 +4853,9 @@ function unsupported() {
|
|
|
4143
4853
|
return new Error("unsupported: this page or app has no audio engine");
|
|
4144
4854
|
}
|
|
4145
4855
|
function perform$1(command2) {
|
|
4146
|
-
const
|
|
4147
|
-
if (!
|
|
4148
|
-
return
|
|
4856
|
+
const host2 = hostNow$1();
|
|
4857
|
+
if (!host2) return Promise.reject(unsupported());
|
|
4858
|
+
return host2.perform(command2);
|
|
4149
4859
|
}
|
|
4150
4860
|
class AVAudioTime {
|
|
4151
4861
|
spec;
|
|
@@ -4751,9 +5461,9 @@ function useAudioEngine(build) {
|
|
|
4751
5461
|
let nextId = 1;
|
|
4752
5462
|
const hostNow = () => browserEnvironment.languageModel;
|
|
4753
5463
|
function perform(command2) {
|
|
4754
|
-
const
|
|
4755
|
-
if (!
|
|
4756
|
-
return
|
|
5464
|
+
const host2 = hostNow();
|
|
5465
|
+
if (!host2) return Promise.reject(new GenerationError("assetsUnavailable", "This page or app has no language model."));
|
|
5466
|
+
return host2.perform(command2).catch((error) => {
|
|
4757
5467
|
throw GenerationError.from(error);
|
|
4758
5468
|
});
|
|
4759
5469
|
}
|
|
@@ -5236,8 +5946,9 @@ function ProgressView(props) {
|
|
|
5236
5946
|
}
|
|
5237
5947
|
function ToolbarItem(props) {
|
|
5238
5948
|
const hidden = props.sharedBackgroundVisibility === "hidden";
|
|
5239
|
-
const
|
|
5240
|
-
|
|
5949
|
+
const placement = toolbarPlacement(props.placement);
|
|
5950
|
+
const items = placement === "secondary" ? props.children : withEnvironment({ labelStyle: "iconOnly" }, props.children);
|
|
5951
|
+
return sb("toolbar", hidden ? { placement, sharedBackground: "hidden" } : { placement }, void 0, items);
|
|
5241
5952
|
}
|
|
5242
5953
|
const ToolbarItemGroup = ToolbarItem;
|
|
5243
5954
|
function ToolbarSpacer(props) {
|
|
@@ -5439,14 +6150,14 @@ function canSignInWithApple() {
|
|
|
5439
6150
|
return browserEnvironment.appleSignIn !== void 0;
|
|
5440
6151
|
}
|
|
5441
6152
|
async function signInWithApple(options = {}) {
|
|
5442
|
-
const
|
|
5443
|
-
if (!
|
|
5444
|
-
return
|
|
6153
|
+
const host2 = browserEnvironment.appleSignIn;
|
|
6154
|
+
if (!host2) throw new Error("unsupported: Sign in with Apple is not available here (configureSignInWithApple on a page)");
|
|
6155
|
+
return host2.signIn({ scopes: options.scopes ?? ["fullName", "email"], ...options.nonce ? { nonce: options.nonce } : {}, ...options.state ? { state: options.state } : {} });
|
|
5445
6156
|
}
|
|
5446
6157
|
async function appleCredentialState(user) {
|
|
5447
|
-
const
|
|
5448
|
-
if (!
|
|
5449
|
-
return
|
|
6158
|
+
const host2 = browserEnvironment.appleSignIn;
|
|
6159
|
+
if (!host2) return "unknown";
|
|
6160
|
+
return host2.credentialState(user);
|
|
5450
6161
|
}
|
|
5451
6162
|
const LABELS = { signIn: "Sign in with Apple", signUp: "Sign up with Apple", continue: "Continue with Apple" };
|
|
5452
6163
|
function SignInWithAppleButton(props) {
|
|
@@ -5777,16 +6488,16 @@ function hostData(contents) {
|
|
|
5777
6488
|
if (contents instanceof ArrayBuffer) return new Uint8Array(contents);
|
|
5778
6489
|
return contents;
|
|
5779
6490
|
}
|
|
5780
|
-
function createFileManager(
|
|
6491
|
+
function createFileManager(host2) {
|
|
5781
6492
|
const required = () => {
|
|
5782
|
-
const files =
|
|
6493
|
+
const files = host2();
|
|
5783
6494
|
if (!files) throw new Error("unsupported: this page keeps no files");
|
|
5784
6495
|
return files;
|
|
5785
6496
|
};
|
|
5786
|
-
const directories = () =>
|
|
6497
|
+
const directories = () => host2()?.directories ?? NONE;
|
|
5787
6498
|
return {
|
|
5788
6499
|
get available() {
|
|
5789
|
-
return
|
|
6500
|
+
return host2() !== void 0;
|
|
5790
6501
|
},
|
|
5791
6502
|
get documentsDirectory() {
|
|
5792
6503
|
return directories().documents;
|
|
@@ -5823,8 +6534,8 @@ function createFileManager(host) {
|
|
|
5823
6534
|
copyItem: async (from, to) => required().copy(from, to),
|
|
5824
6535
|
removeItem: async (url) => required().remove(url),
|
|
5825
6536
|
loadURL: async (url) => required().loadURL(url),
|
|
5826
|
-
iCloudDirectory: async () => await
|
|
5827
|
-
watch: (listener) =>
|
|
6537
|
+
iCloudDirectory: async () => await host2()?.iCloud?.() ?? null,
|
|
6538
|
+
watch: (listener) => host2()?.watch?.(listener) ?? (() => {
|
|
5828
6539
|
})
|
|
5829
6540
|
};
|
|
5830
6541
|
}
|
|
@@ -7420,6 +8131,19 @@ function KeyframeAnimator({
|
|
|
7420
8131
|
return children(value);
|
|
7421
8132
|
}
|
|
7422
8133
|
export {
|
|
8134
|
+
AVAsset,
|
|
8135
|
+
AVAssetExportPreset1280x720,
|
|
8136
|
+
AVAssetExportPreset1920x1080,
|
|
8137
|
+
AVAssetExportPreset3840x2160,
|
|
8138
|
+
AVAssetExportPreset640x480,
|
|
8139
|
+
AVAssetExportPreset960x540,
|
|
8140
|
+
AVAssetExportPresetHighestQuality,
|
|
8141
|
+
AVAssetExportPresetLowQuality,
|
|
8142
|
+
AVAssetExportPresetMediumQuality,
|
|
8143
|
+
AVAssetExportPresetPassthrough,
|
|
8144
|
+
AVAssetExportSession,
|
|
8145
|
+
AVAssetImageGenerator,
|
|
8146
|
+
AVAssetTrack,
|
|
7423
8147
|
AVAudioEngine,
|
|
7424
8148
|
AVAudioFile,
|
|
7425
8149
|
AVAudioInputNode,
|
|
@@ -7437,8 +8161,12 @@ export {
|
|
|
7437
8161
|
AVAudioUnitReverb,
|
|
7438
8162
|
AVAudioUnitSampler,
|
|
7439
8163
|
AVAudioUnitTimePitch,
|
|
8164
|
+
AVFileType,
|
|
8165
|
+
AVMediaType,
|
|
8166
|
+
AVMutableComposition,
|
|
7440
8167
|
AVPlayer,
|
|
7441
8168
|
AVQueuePlayer,
|
|
8169
|
+
AVURLAsset,
|
|
7442
8170
|
AngularGradient,
|
|
7443
8171
|
AreaMark,
|
|
7444
8172
|
AsyncImage,
|
|
@@ -7446,6 +8174,9 @@ export {
|
|
|
7446
8174
|
AudioPlayer,
|
|
7447
8175
|
BarMark,
|
|
7448
8176
|
Button,
|
|
8177
|
+
CMTime,
|
|
8178
|
+
CMTimeRange,
|
|
8179
|
+
CameraPreview,
|
|
7449
8180
|
Canvas,
|
|
7450
8181
|
Capsule,
|
|
7451
8182
|
Chart,
|
|
@@ -7550,6 +8281,8 @@ export {
|
|
|
7550
8281
|
answerLayout,
|
|
7551
8282
|
appStorage,
|
|
7552
8283
|
appleCredentialState,
|
|
8284
|
+
bitmapExtent,
|
|
8285
|
+
bitmapSource,
|
|
7553
8286
|
canMeasureText,
|
|
7554
8287
|
canSignInWithApple,
|
|
7555
8288
|
color,
|
|
@@ -7566,10 +8299,12 @@ export {
|
|
|
7566
8299
|
geometryProxy,
|
|
7567
8300
|
gradient,
|
|
7568
8301
|
hex,
|
|
8302
|
+
imageHeader,
|
|
7569
8303
|
injectIntoDevTools,
|
|
7570
8304
|
insets,
|
|
7571
8305
|
installTextMeasurer,
|
|
7572
8306
|
keyframeValues,
|
|
8307
|
+
liveBitmapSource,
|
|
7573
8308
|
measureText,
|
|
7574
8309
|
mount,
|
|
7575
8310
|
moveItems,
|
|
@@ -7583,6 +8318,7 @@ export {
|
|
|
7583
8318
|
plainText,
|
|
7584
8319
|
providingScrollSettings,
|
|
7585
8320
|
readBinding,
|
|
8321
|
+
releaseBitmapSource,
|
|
7586
8322
|
rgb,
|
|
7587
8323
|
routeOf,
|
|
7588
8324
|
samplerZones,
|
|
@@ -7617,6 +8353,7 @@ export {
|
|
|
7617
8353
|
useSpeechSynthesis,
|
|
7618
8354
|
useSystemLanguageModel,
|
|
7619
8355
|
useUndoManager,
|
|
8356
|
+
useVideoRecorder,
|
|
7620
8357
|
useViewEnvironment,
|
|
7621
8358
|
useWakeLock,
|
|
7622
8359
|
weighted,
|