@swiftbrowser/react 0.28.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/avAsset.d.ts +218 -0
- package/dist/avAsset.d.ts.map +1 -0
- package/dist/components/controls.d.ts +2 -1
- package/dist/components/controls.d.ts.map +1 -1
- package/dist/host/tree.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -47
- 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
|
@@ -221,6 +221,7 @@ class HostTree {
|
|
|
221
221
|
};
|
|
222
222
|
const visit = (child, sink, owner, ctx, hoistedTo) => {
|
|
223
223
|
if (child.hidden) return;
|
|
224
|
+
if (child.kind === PREF_KIND || child.kind === HOIST_KIND) child.stale = false;
|
|
224
225
|
if (child.kind === PREF_KIND) {
|
|
225
226
|
contribute({ kind: "pref", target: child.target, props: child.props }, ctx);
|
|
226
227
|
return;
|
|
@@ -4036,7 +4037,7 @@ function protocolRuns(text) {
|
|
|
4036
4037
|
}
|
|
4037
4038
|
function useAudioRecorder(options = {}) {
|
|
4038
4039
|
const interval = options.meterInterval ?? 50;
|
|
4039
|
-
const
|
|
4040
|
+
const host2 = browserEnvironment.audioRecorder;
|
|
4040
4041
|
const [status, setStatus] = useState("idle");
|
|
4041
4042
|
const [time, setTime] = useState(0);
|
|
4042
4043
|
const [levels, setLevels] = useState([]);
|
|
@@ -4045,13 +4046,13 @@ function useAudioRecorder(options = {}) {
|
|
|
4045
4046
|
statusRef.current = status;
|
|
4046
4047
|
const fail = (reason) => setError(reason instanceof Error ? reason.message : String(reason));
|
|
4047
4048
|
useEffect(() => {
|
|
4048
|
-
if (status !== "recording" || !
|
|
4049
|
+
if (status !== "recording" || !host2) return;
|
|
4049
4050
|
let reading = false;
|
|
4050
4051
|
let live = true;
|
|
4051
4052
|
const timer = setInterval(() => {
|
|
4052
4053
|
if (reading) return;
|
|
4053
4054
|
reading = true;
|
|
4054
|
-
|
|
4055
|
+
host2.meter().then((meter) => {
|
|
4055
4056
|
if (!live) return;
|
|
4056
4057
|
setTime(meter.time);
|
|
4057
4058
|
setLevels((previous) => [...previous, meter.level]);
|
|
@@ -4063,15 +4064,15 @@ function useAudioRecorder(options = {}) {
|
|
|
4063
4064
|
live = false;
|
|
4064
4065
|
clearInterval(timer);
|
|
4065
4066
|
};
|
|
4066
|
-
}, [status,
|
|
4067
|
+
}, [status, host2, interval]);
|
|
4067
4068
|
useEffect(
|
|
4068
4069
|
() => () => {
|
|
4069
|
-
if (statusRef.current !== "idle") void
|
|
4070
|
+
if (statusRef.current !== "idle") void host2?.cancel().catch(() => void 0);
|
|
4070
4071
|
},
|
|
4071
|
-
[
|
|
4072
|
+
[host2]
|
|
4072
4073
|
);
|
|
4073
4074
|
const record = useCallback(async () => {
|
|
4074
|
-
if (!
|
|
4075
|
+
if (!host2) {
|
|
4075
4076
|
setError("unsupported: this page cannot record");
|
|
4076
4077
|
return;
|
|
4077
4078
|
}
|
|
@@ -4081,28 +4082,28 @@ function useAudioRecorder(options = {}) {
|
|
|
4081
4082
|
setLevels([]);
|
|
4082
4083
|
setStatus("starting");
|
|
4083
4084
|
try {
|
|
4084
|
-
await
|
|
4085
|
+
await host2.start();
|
|
4085
4086
|
setStatus("recording");
|
|
4086
4087
|
} catch (reason) {
|
|
4087
4088
|
fail(reason);
|
|
4088
4089
|
setStatus("idle");
|
|
4089
4090
|
}
|
|
4090
|
-
}, [
|
|
4091
|
+
}, [host2]);
|
|
4091
4092
|
const pause = useCallback(async () => {
|
|
4092
|
-
if (!
|
|
4093
|
-
await
|
|
4093
|
+
if (!host2 || statusRef.current !== "recording") return;
|
|
4094
|
+
await host2.pause().catch(fail);
|
|
4094
4095
|
setStatus("paused");
|
|
4095
|
-
}, [
|
|
4096
|
+
}, [host2]);
|
|
4096
4097
|
const resume = useCallback(async () => {
|
|
4097
|
-
if (!
|
|
4098
|
-
await
|
|
4098
|
+
if (!host2 || statusRef.current !== "paused") return;
|
|
4099
|
+
await host2.resume().catch(fail);
|
|
4099
4100
|
setStatus("recording");
|
|
4100
|
-
}, [
|
|
4101
|
+
}, [host2]);
|
|
4101
4102
|
const stop = useCallback(async () => {
|
|
4102
|
-
if (!
|
|
4103
|
+
if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
|
|
4103
4104
|
setStatus("stopping");
|
|
4104
4105
|
try {
|
|
4105
|
-
const recording = await
|
|
4106
|
+
const recording = await host2.stop();
|
|
4106
4107
|
setTime(recording.duration);
|
|
4107
4108
|
return recording;
|
|
4108
4109
|
} catch (reason) {
|
|
@@ -4111,30 +4112,538 @@ function useAudioRecorder(options = {}) {
|
|
|
4111
4112
|
} finally {
|
|
4112
4113
|
setStatus("idle");
|
|
4113
4114
|
}
|
|
4114
|
-
}, [
|
|
4115
|
+
}, [host2]);
|
|
4115
4116
|
const cancel = useCallback(async () => {
|
|
4116
|
-
if (!
|
|
4117
|
-
await
|
|
4117
|
+
if (!host2 || statusRef.current === "idle") return;
|
|
4118
|
+
await host2.cancel().catch(fail);
|
|
4118
4119
|
setStatus("idle");
|
|
4119
4120
|
setTime(0);
|
|
4120
4121
|
setLevels([]);
|
|
4121
|
-
}, [
|
|
4122
|
+
}, [host2]);
|
|
4122
4123
|
const discard = useCallback(async (url) => {
|
|
4123
|
-
await
|
|
4124
|
-
}, [
|
|
4124
|
+
await host2?.discard(url).catch(fail);
|
|
4125
|
+
}, [host2]);
|
|
4125
4126
|
const trim = useCallback(
|
|
4126
4127
|
async (url, keep) => {
|
|
4127
|
-
if (!
|
|
4128
|
+
if (!host2) throw new Error("unsupported: this page cannot cut audio");
|
|
4128
4129
|
try {
|
|
4129
|
-
return await
|
|
4130
|
+
return await host2.trim(url, keep);
|
|
4130
4131
|
} catch (reason) {
|
|
4131
4132
|
fail(reason);
|
|
4132
4133
|
throw reason;
|
|
4133
4134
|
}
|
|
4134
4135
|
},
|
|
4135
|
-
[
|
|
4136
|
+
[host2]
|
|
4137
|
+
);
|
|
4138
|
+
return { available: host2 !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
|
|
4139
|
+
}
|
|
4140
|
+
function host() {
|
|
4141
|
+
const found = browserEnvironment.avAsset;
|
|
4142
|
+
if (!found) throw new Error("unsupported: this page cannot read or write video");
|
|
4143
|
+
return found;
|
|
4144
|
+
}
|
|
4145
|
+
class CMTime {
|
|
4146
|
+
constructor(value, timescale) {
|
|
4147
|
+
this.value = value;
|
|
4148
|
+
this.timescale = timescale;
|
|
4149
|
+
}
|
|
4150
|
+
value;
|
|
4151
|
+
timescale;
|
|
4152
|
+
static zero = new CMTime(0, 1);
|
|
4153
|
+
static invalid = new CMTime(0, 0);
|
|
4154
|
+
static positiveInfinity = new CMTime(Infinity, 1);
|
|
4155
|
+
/** `CMTime(value:timescale:)`. */
|
|
4156
|
+
static make(value, timescale) {
|
|
4157
|
+
return new CMTime(value, timescale);
|
|
4158
|
+
}
|
|
4159
|
+
/** `CMTime(seconds:preferredTimescale:)`: rounded to the timescale. */
|
|
4160
|
+
static seconds(seconds, preferredTimescale = 600) {
|
|
4161
|
+
if (!Number.isFinite(seconds)) return seconds > 0 ? CMTime.positiveInfinity : CMTime.invalid;
|
|
4162
|
+
return new CMTime(Math.round(seconds * preferredTimescale), preferredTimescale);
|
|
4163
|
+
}
|
|
4164
|
+
get isValid() {
|
|
4165
|
+
return this.timescale > 0;
|
|
4166
|
+
}
|
|
4167
|
+
/** `CMTimeGetSeconds`. */
|
|
4168
|
+
get seconds() {
|
|
4169
|
+
return this.timescale > 0 ? this.value / this.timescale : Number.NaN;
|
|
4170
|
+
}
|
|
4171
|
+
add(other) {
|
|
4172
|
+
return CMTime.seconds(this.seconds + other.seconds, Math.max(this.timescale, other.timescale, 1));
|
|
4173
|
+
}
|
|
4174
|
+
subtract(other) {
|
|
4175
|
+
return CMTime.seconds(this.seconds - other.seconds, Math.max(this.timescale, other.timescale, 1));
|
|
4176
|
+
}
|
|
4177
|
+
/** `CMTimeCompare`: -1, 0 or 1. */
|
|
4178
|
+
compare(other) {
|
|
4179
|
+
const a = this.seconds;
|
|
4180
|
+
const b = other.seconds;
|
|
4181
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
4182
|
+
}
|
|
4183
|
+
static minimum(a, b) {
|
|
4184
|
+
return a.compare(b) <= 0 ? a : b;
|
|
4185
|
+
}
|
|
4186
|
+
static maximum(a, b) {
|
|
4187
|
+
return a.compare(b) >= 0 ? a : b;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
class CMTimeRange {
|
|
4191
|
+
constructor(start, duration) {
|
|
4192
|
+
this.start = start;
|
|
4193
|
+
this.duration = duration;
|
|
4194
|
+
}
|
|
4195
|
+
start;
|
|
4196
|
+
duration;
|
|
4197
|
+
static zero = new CMTimeRange(CMTime.zero, CMTime.zero);
|
|
4198
|
+
/** `CMTimeRange(start:end:)`. */
|
|
4199
|
+
static fromTo(start, end) {
|
|
4200
|
+
return new CMTimeRange(start, CMTime.maximum(end.subtract(start), CMTime.zero));
|
|
4201
|
+
}
|
|
4202
|
+
/** `CMTimeRange(start:duration:)` in seconds. */
|
|
4203
|
+
static seconds(start, duration, preferredTimescale = 600) {
|
|
4204
|
+
return new CMTimeRange(CMTime.seconds(start, preferredTimescale), CMTime.seconds(duration, preferredTimescale));
|
|
4205
|
+
}
|
|
4206
|
+
get end() {
|
|
4207
|
+
return this.start.add(this.duration);
|
|
4208
|
+
}
|
|
4209
|
+
get isEmpty() {
|
|
4210
|
+
return this.duration.seconds <= 0;
|
|
4211
|
+
}
|
|
4212
|
+
containsTime(time) {
|
|
4213
|
+
return time.compare(this.start) >= 0 && time.compare(this.end) < 0;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
const AVMediaType = { video: "vide", audio: "soun" };
|
|
4217
|
+
class AVAssetTrack {
|
|
4218
|
+
constructor(trackID, mediaType, naturalSize, nominalFrameRate, timeRange, codec) {
|
|
4219
|
+
this.trackID = trackID;
|
|
4220
|
+
this.mediaType = mediaType;
|
|
4221
|
+
this.naturalSize = naturalSize;
|
|
4222
|
+
this.nominalFrameRate = nominalFrameRate;
|
|
4223
|
+
this.timeRange = timeRange;
|
|
4224
|
+
this.codec = codec;
|
|
4225
|
+
}
|
|
4226
|
+
trackID;
|
|
4227
|
+
mediaType;
|
|
4228
|
+
naturalSize;
|
|
4229
|
+
nominalFrameRate;
|
|
4230
|
+
timeRange;
|
|
4231
|
+
codec;
|
|
4232
|
+
}
|
|
4233
|
+
class AVAsset {
|
|
4234
|
+
/** @internal */
|
|
4235
|
+
segmentsForExport() {
|
|
4236
|
+
return this.segments();
|
|
4237
|
+
}
|
|
4238
|
+
/** `loadTracks(withMediaType:)`. */
|
|
4239
|
+
async loadTracks(mediaType) {
|
|
4240
|
+
return (await this.load("tracks")).filter((track) => track.mediaType === mediaType);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
const tracksOf = (info) => info.tracks.map(
|
|
4244
|
+
(t) => new AVAssetTrack(
|
|
4245
|
+
t.trackID,
|
|
4246
|
+
t.mediaType === "video" ? AVMediaType.video : AVMediaType.audio,
|
|
4247
|
+
t.naturalSize,
|
|
4248
|
+
t.nominalFrameRate,
|
|
4249
|
+
CMTimeRange.seconds(t.timeRange.start, t.timeRange.duration),
|
|
4250
|
+
t.codec
|
|
4251
|
+
)
|
|
4252
|
+
);
|
|
4253
|
+
class AVURLAsset extends AVAsset {
|
|
4254
|
+
constructor(url) {
|
|
4255
|
+
super();
|
|
4256
|
+
this.url = url;
|
|
4257
|
+
}
|
|
4258
|
+
url;
|
|
4259
|
+
info = null;
|
|
4260
|
+
loaded() {
|
|
4261
|
+
if (!this.info) {
|
|
4262
|
+
this.info = host().load(this.url);
|
|
4263
|
+
this.info.catch(() => {
|
|
4264
|
+
this.info = null;
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
return this.info;
|
|
4268
|
+
}
|
|
4269
|
+
async segments() {
|
|
4270
|
+
return [{ url: this.url, start: 0, end: (await this.loaded()).duration }];
|
|
4271
|
+
}
|
|
4272
|
+
async load(key) {
|
|
4273
|
+
const info = await this.loaded();
|
|
4274
|
+
const values = {
|
|
4275
|
+
duration: CMTime.seconds(info.duration),
|
|
4276
|
+
tracks: tracksOf(info),
|
|
4277
|
+
naturalSize: info.naturalSize,
|
|
4278
|
+
isPlayable: info.tracks.length > 0,
|
|
4279
|
+
isExportable: info.tracks.length > 0
|
|
4280
|
+
};
|
|
4281
|
+
return values[key];
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
class AVMutableComposition extends AVAsset {
|
|
4285
|
+
parts = [];
|
|
4286
|
+
/** The composition's length. */
|
|
4287
|
+
get duration() {
|
|
4288
|
+
return CMTime.seconds(this.parts.reduce((sum, part) => sum + part.range.duration.seconds, 0));
|
|
4289
|
+
}
|
|
4290
|
+
/** `insertTimeRange(_:of:at:)`: `range` of `asset` (an `AVURLAsset`, or another composition's parts) placed at `at`. */
|
|
4291
|
+
async insertTimeRange(range, asset, at) {
|
|
4292
|
+
const inserted = [];
|
|
4293
|
+
if (asset instanceof AVURLAsset) {
|
|
4294
|
+
const length = (await asset.load("duration")).seconds;
|
|
4295
|
+
const start = Math.max(0, range.start.seconds);
|
|
4296
|
+
const end = Math.min(length, range.end.seconds);
|
|
4297
|
+
if (end - start > 1e-3) inserted.push({ asset, range: CMTimeRange.seconds(start, end - start) });
|
|
4298
|
+
} else if (asset instanceof AVMutableComposition) {
|
|
4299
|
+
inserted.push(...asset.partsIn(range.start.seconds, range.end.seconds));
|
|
4300
|
+
} else {
|
|
4301
|
+
throw new Error("unsupported: only an AVURLAsset or an AVMutableComposition can be inserted");
|
|
4302
|
+
}
|
|
4303
|
+
const time = Math.max(0, Math.min(at.seconds, this.duration.seconds));
|
|
4304
|
+
const [before, after] = this.splitAt(time);
|
|
4305
|
+
this.parts = [...before, ...inserted, ...after];
|
|
4306
|
+
}
|
|
4307
|
+
/** `removeTimeRange(_:)`: the media in `range` cut out, the rest closing up. */
|
|
4308
|
+
removeTimeRange(range) {
|
|
4309
|
+
const [before] = this.splitAt(range.start.seconds);
|
|
4310
|
+
const [, after] = this.splitAt(range.end.seconds);
|
|
4311
|
+
this.parts = [...before, ...after];
|
|
4312
|
+
}
|
|
4313
|
+
/** The parts' media from `start` to `end` of the composition's timeline. */
|
|
4314
|
+
partsIn(start, end) {
|
|
4315
|
+
const [, from] = this.splitAt(start);
|
|
4316
|
+
const out = [];
|
|
4317
|
+
let cursor = start;
|
|
4318
|
+
for (const part of from) {
|
|
4319
|
+
if (cursor >= end) break;
|
|
4320
|
+
const take = Math.min(part.range.duration.seconds, end - cursor);
|
|
4321
|
+
if (take > 1e-3) out.push({ asset: part.asset, range: CMTimeRange.seconds(part.range.start.seconds, take) });
|
|
4322
|
+
cursor += take;
|
|
4323
|
+
}
|
|
4324
|
+
return out;
|
|
4325
|
+
}
|
|
4326
|
+
/** The parts before `time` and from it, a part across it split in two. */
|
|
4327
|
+
splitAt(time) {
|
|
4328
|
+
const before = [];
|
|
4329
|
+
const after = [];
|
|
4330
|
+
let cursor = 0;
|
|
4331
|
+
for (const part of this.parts) {
|
|
4332
|
+
const length = part.range.duration.seconds;
|
|
4333
|
+
const start = part.range.start.seconds;
|
|
4334
|
+
if (cursor + length <= time + 1e-9) before.push(part);
|
|
4335
|
+
else if (cursor >= time - 1e-9) after.push(part);
|
|
4336
|
+
else {
|
|
4337
|
+
const head = time - cursor;
|
|
4338
|
+
before.push({ asset: part.asset, range: CMTimeRange.seconds(start, head) });
|
|
4339
|
+
after.push({ asset: part.asset, range: CMTimeRange.seconds(start + head, length - head) });
|
|
4340
|
+
}
|
|
4341
|
+
cursor += length;
|
|
4342
|
+
}
|
|
4343
|
+
return [before, after];
|
|
4344
|
+
}
|
|
4345
|
+
async segments() {
|
|
4346
|
+
return this.parts.map((part) => ({ url: part.asset.url, start: part.range.start.seconds, end: part.range.end.seconds }));
|
|
4347
|
+
}
|
|
4348
|
+
async load(key) {
|
|
4349
|
+
const first = this.parts[0]?.asset;
|
|
4350
|
+
const tracks = first ? await first.load("tracks") : [];
|
|
4351
|
+
const values = {
|
|
4352
|
+
duration: this.duration,
|
|
4353
|
+
tracks: tracks.map((t) => new AVAssetTrack(t.trackID, t.mediaType, t.naturalSize, t.nominalFrameRate, new CMTimeRange(CMTime.zero, this.duration), t.codec)),
|
|
4354
|
+
naturalSize: first ? await first.load("naturalSize") : { width: 0, height: 0 },
|
|
4355
|
+
isPlayable: this.parts.length > 0,
|
|
4356
|
+
isExportable: this.parts.length > 0
|
|
4357
|
+
};
|
|
4358
|
+
return values[key];
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
class AVAssetImageGenerator {
|
|
4362
|
+
constructor(asset) {
|
|
4363
|
+
this.asset = asset;
|
|
4364
|
+
}
|
|
4365
|
+
asset;
|
|
4366
|
+
maximumSize = { width: 0, height: 0 };
|
|
4367
|
+
/** Stills come upright (the track's rotation applied), as with `appliesPreferredTrackTransform = true`. */
|
|
4368
|
+
appliesPreferredTrackTransform = true;
|
|
4369
|
+
made = [];
|
|
4370
|
+
/** The file and the time in it that `time` on the asset's timeline falls on (a composition's part). */
|
|
4371
|
+
async source(time) {
|
|
4372
|
+
let cursor = 0;
|
|
4373
|
+
const segments = await this.asset.segmentsForExport();
|
|
4374
|
+
for (const segment of segments) {
|
|
4375
|
+
const length = segment.end - segment.start;
|
|
4376
|
+
if (time < cursor + length || segment === segments[segments.length - 1]) return { url: segment.url, time: segment.start + Math.max(0, Math.min(length, time - cursor)) };
|
|
4377
|
+
cursor += length;
|
|
4378
|
+
}
|
|
4379
|
+
throw new Error("empty: the asset has no media");
|
|
4380
|
+
}
|
|
4381
|
+
/** `image(at:)`: the still at `time`. */
|
|
4382
|
+
async image(at) {
|
|
4383
|
+
const { url, time } = await this.source(Math.max(0, at.seconds));
|
|
4384
|
+
const options = {
|
|
4385
|
+
...this.maximumSize.width > 0 ? { maxWidth: this.maximumSize.width } : {},
|
|
4386
|
+
...this.maximumSize.height > 0 ? { maxHeight: this.maximumSize.height } : {}
|
|
4387
|
+
};
|
|
4388
|
+
const still = await host().image(url, time, options);
|
|
4389
|
+
this.made.push(still.url);
|
|
4390
|
+
return { image: still.url, actualTime: CMTime.seconds(at.seconds + (still.actualTime - time)), width: still.width, height: still.height };
|
|
4391
|
+
}
|
|
4392
|
+
/** `images(for:)`: a still for each time, in turn. */
|
|
4393
|
+
async *images(times) {
|
|
4394
|
+
for (const time of times) yield { ...await this.image(time), requestedTime: time };
|
|
4395
|
+
}
|
|
4396
|
+
/** Lets go of the stills this generator made (their `blob:` URLs on a page). */
|
|
4397
|
+
async release() {
|
|
4398
|
+
const urls = this.made.splice(0);
|
|
4399
|
+
await Promise.all(urls.map((url) => host().release(url)));
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
const AVFileType = { mp4: "public.mpeg-4", mov: "com.apple.quicktime-movie", m4v: "com.apple.m4v-video", webm: "org.webmproject.webm" };
|
|
4403
|
+
const FILE_TYPES = {
|
|
4404
|
+
"public.mpeg-4": "mp4",
|
|
4405
|
+
"com.apple.m4v-video": "mp4",
|
|
4406
|
+
"com.apple.quicktime-movie": "mov",
|
|
4407
|
+
"org.webmproject.webm": "webm"
|
|
4408
|
+
};
|
|
4409
|
+
const AVAssetExportPresetPassthrough = "AVAssetExportPresetPassthrough";
|
|
4410
|
+
const AVAssetExportPresetHighestQuality = "AVAssetExportPresetHighestQuality";
|
|
4411
|
+
const AVAssetExportPresetMediumQuality = "AVAssetExportPresetMediumQuality";
|
|
4412
|
+
const AVAssetExportPresetLowQuality = "AVAssetExportPresetLowQuality";
|
|
4413
|
+
const AVAssetExportPreset640x480 = "AVAssetExportPreset640x480";
|
|
4414
|
+
const AVAssetExportPreset960x540 = "AVAssetExportPreset960x540";
|
|
4415
|
+
const AVAssetExportPreset1280x720 = "AVAssetExportPreset1280x720";
|
|
4416
|
+
const AVAssetExportPreset1920x1080 = "AVAssetExportPreset1920x1080";
|
|
4417
|
+
const AVAssetExportPreset3840x2160 = "AVAssetExportPreset3840x2160";
|
|
4418
|
+
let exports$1 = 0;
|
|
4419
|
+
class AVAssetExportSession {
|
|
4420
|
+
constructor(asset, presetName) {
|
|
4421
|
+
this.asset = asset;
|
|
4422
|
+
this.presetName = presetName;
|
|
4423
|
+
}
|
|
4424
|
+
asset;
|
|
4425
|
+
presetName;
|
|
4426
|
+
outputURL = null;
|
|
4427
|
+
outputFileType = AVFileType.mp4;
|
|
4428
|
+
timeRange = null;
|
|
4429
|
+
status = "unknown";
|
|
4430
|
+
progress = 0;
|
|
4431
|
+
error = null;
|
|
4432
|
+
/** Heard as `progress` changes (a few times a second). */
|
|
4433
|
+
onProgress = null;
|
|
4434
|
+
/** The written file's length and MIME type, once completed. */
|
|
4435
|
+
result = null;
|
|
4436
|
+
id = `export-${++exports$1}`;
|
|
4437
|
+
/** `AVAssetExportSession.allExportPresets()`. */
|
|
4438
|
+
static allExportPresets() {
|
|
4439
|
+
return [
|
|
4440
|
+
AVAssetExportPresetPassthrough,
|
|
4441
|
+
AVAssetExportPresetHighestQuality,
|
|
4442
|
+
AVAssetExportPresetMediumQuality,
|
|
4443
|
+
AVAssetExportPresetLowQuality,
|
|
4444
|
+
AVAssetExportPreset640x480,
|
|
4445
|
+
AVAssetExportPreset960x540,
|
|
4446
|
+
AVAssetExportPreset1280x720,
|
|
4447
|
+
AVAssetExportPreset1920x1080,
|
|
4448
|
+
AVAssetExportPreset3840x2160
|
|
4449
|
+
];
|
|
4450
|
+
}
|
|
4451
|
+
/** The asset's parts, cut to `timeRange` when it is set. */
|
|
4452
|
+
async parts() {
|
|
4453
|
+
const all = await this.asset.segmentsForExport();
|
|
4454
|
+
if (!this.timeRange) return all;
|
|
4455
|
+
const from = this.timeRange.start.seconds;
|
|
4456
|
+
const to = this.timeRange.end.seconds;
|
|
4457
|
+
const out = [];
|
|
4458
|
+
let cursor = 0;
|
|
4459
|
+
for (const segment of all) {
|
|
4460
|
+
const length = segment.end - segment.start;
|
|
4461
|
+
const start = Math.max(from, cursor);
|
|
4462
|
+
const end = Math.min(to, cursor + length);
|
|
4463
|
+
if (end - start > 1e-3) out.push({ url: segment.url, start: segment.start + (start - cursor), end: segment.start + (end - cursor) });
|
|
4464
|
+
cursor += length;
|
|
4465
|
+
}
|
|
4466
|
+
return out;
|
|
4467
|
+
}
|
|
4468
|
+
/** `export()`: writes the file and answers its URL (also `outputURL`); rejects with why it could not. */
|
|
4469
|
+
async export() {
|
|
4470
|
+
if (this.status === "exporting") throw new Error("busy: already exporting");
|
|
4471
|
+
this.status = "exporting";
|
|
4472
|
+
this.error = null;
|
|
4473
|
+
this.setProgress(0);
|
|
4474
|
+
const assets = host();
|
|
4475
|
+
const ticker = setInterval(() => {
|
|
4476
|
+
void assets.progress(this.id).then((p) => this.status === "exporting" && this.setProgress(p));
|
|
4477
|
+
}, 200);
|
|
4478
|
+
try {
|
|
4479
|
+
const result = await assets.export(this.id, {
|
|
4480
|
+
segments: await this.parts(),
|
|
4481
|
+
preset: this.presetName,
|
|
4482
|
+
fileType: FILE_TYPES[this.outputFileType] ?? "mp4",
|
|
4483
|
+
...this.outputURL ? { outputURL: this.outputURL } : {}
|
|
4484
|
+
});
|
|
4485
|
+
this.outputURL = result.url;
|
|
4486
|
+
this.result = { duration: result.duration, type: result.type };
|
|
4487
|
+
this.status = "completed";
|
|
4488
|
+
this.setProgress(1);
|
|
4489
|
+
return result.url;
|
|
4490
|
+
} catch (reason) {
|
|
4491
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
4492
|
+
this.error = error;
|
|
4493
|
+
this.status = error.message.startsWith("cancelled") ? "cancelled" : "failed";
|
|
4494
|
+
throw error;
|
|
4495
|
+
} finally {
|
|
4496
|
+
clearInterval(ticker);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
/** `cancelExport()`. */
|
|
4500
|
+
cancelExport() {
|
|
4501
|
+
if (this.status !== "exporting") return;
|
|
4502
|
+
void host().cancel(this.id);
|
|
4503
|
+
}
|
|
4504
|
+
setProgress(progress) {
|
|
4505
|
+
if (progress === this.progress) return;
|
|
4506
|
+
this.progress = progress;
|
|
4507
|
+
this.onProgress?.(progress);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
function useVideoRecorder(options = {}) {
|
|
4511
|
+
const interval = options.timeInterval ?? 100;
|
|
4512
|
+
const host2 = browserEnvironment.videoRecorder;
|
|
4513
|
+
const [status, setStatus] = useState("closed");
|
|
4514
|
+
const [stream, setStream] = useState(null);
|
|
4515
|
+
const [facing, setFacing] = useState("user");
|
|
4516
|
+
const [time, setTime] = useState(0);
|
|
4517
|
+
const [error, setError] = useState(null);
|
|
4518
|
+
const statusRef = useRef(status);
|
|
4519
|
+
statusRef.current = status;
|
|
4520
|
+
const facingRef = useRef(facing);
|
|
4521
|
+
facingRef.current = facing;
|
|
4522
|
+
const fail = (reason) => setError(reason instanceof Error ? reason.message : String(reason));
|
|
4523
|
+
useEffect(() => {
|
|
4524
|
+
if (status !== "recording" || !host2) return;
|
|
4525
|
+
let reading = false;
|
|
4526
|
+
let live = true;
|
|
4527
|
+
const timer = setInterval(() => {
|
|
4528
|
+
if (reading) return;
|
|
4529
|
+
reading = true;
|
|
4530
|
+
host2.time().then((seconds) => live && setTime(seconds)).catch(() => void 0).finally(() => {
|
|
4531
|
+
reading = false;
|
|
4532
|
+
});
|
|
4533
|
+
}, interval);
|
|
4534
|
+
return () => {
|
|
4535
|
+
live = false;
|
|
4536
|
+
clearInterval(timer);
|
|
4537
|
+
};
|
|
4538
|
+
}, [status, host2, interval]);
|
|
4539
|
+
useEffect(
|
|
4540
|
+
() => () => {
|
|
4541
|
+
if (statusRef.current !== "closed") void host2?.close().catch(() => void 0);
|
|
4542
|
+
},
|
|
4543
|
+
[host2]
|
|
4544
|
+
);
|
|
4545
|
+
const open = useCallback(
|
|
4546
|
+
async (wanted) => {
|
|
4547
|
+
if (!host2) {
|
|
4548
|
+
setError("unsupported: this page cannot record video");
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
if (statusRef.current === "recording" || statusRef.current === "paused" || statusRef.current === "stopping") return;
|
|
4552
|
+
setError(null);
|
|
4553
|
+
setStatus("opening");
|
|
4554
|
+
try {
|
|
4555
|
+
const opened = await host2.open({ facing: wanted ?? facingRef.current });
|
|
4556
|
+
setStream(opened.stream);
|
|
4557
|
+
setFacing(opened.facing);
|
|
4558
|
+
setStatus("ready");
|
|
4559
|
+
} catch (reason) {
|
|
4560
|
+
fail(reason);
|
|
4561
|
+
setStream(null);
|
|
4562
|
+
setStatus("closed");
|
|
4563
|
+
}
|
|
4564
|
+
},
|
|
4565
|
+
[host2]
|
|
4566
|
+
);
|
|
4567
|
+
const close = useCallback(async () => {
|
|
4568
|
+
if (!host2) return;
|
|
4569
|
+
await host2.close().catch(fail);
|
|
4570
|
+
setStream(null);
|
|
4571
|
+
setStatus("closed");
|
|
4572
|
+
setTime(0);
|
|
4573
|
+
}, [host2]);
|
|
4574
|
+
const flip = useCallback(async () => {
|
|
4575
|
+
if (statusRef.current !== "ready") return;
|
|
4576
|
+
await open(facingRef.current === "user" ? "environment" : "user");
|
|
4577
|
+
}, [open]);
|
|
4578
|
+
const record = useCallback(async () => {
|
|
4579
|
+
if (!host2 || statusRef.current !== "ready") return;
|
|
4580
|
+
setError(null);
|
|
4581
|
+
setTime(0);
|
|
4582
|
+
try {
|
|
4583
|
+
await host2.start();
|
|
4584
|
+
setStatus("recording");
|
|
4585
|
+
} catch (reason) {
|
|
4586
|
+
fail(reason);
|
|
4587
|
+
}
|
|
4588
|
+
}, [host2]);
|
|
4589
|
+
const pause = useCallback(async () => {
|
|
4590
|
+
if (!host2 || statusRef.current !== "recording") return;
|
|
4591
|
+
await host2.pause().catch(fail);
|
|
4592
|
+
setStatus("paused");
|
|
4593
|
+
}, [host2]);
|
|
4594
|
+
const resume = useCallback(async () => {
|
|
4595
|
+
if (!host2 || statusRef.current !== "paused") return;
|
|
4596
|
+
await host2.resume().catch(fail);
|
|
4597
|
+
setStatus("recording");
|
|
4598
|
+
}, [host2]);
|
|
4599
|
+
const stop = useCallback(async () => {
|
|
4600
|
+
if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
|
|
4601
|
+
setStatus("stopping");
|
|
4602
|
+
try {
|
|
4603
|
+
const recording = await host2.stop();
|
|
4604
|
+
setTime(recording.duration);
|
|
4605
|
+
return recording;
|
|
4606
|
+
} catch (reason) {
|
|
4607
|
+
fail(reason);
|
|
4608
|
+
return null;
|
|
4609
|
+
} finally {
|
|
4610
|
+
setStatus("ready");
|
|
4611
|
+
}
|
|
4612
|
+
}, [host2]);
|
|
4613
|
+
const cancel = useCallback(async () => {
|
|
4614
|
+
if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return;
|
|
4615
|
+
await host2.cancel().catch(fail);
|
|
4616
|
+
setStatus("ready");
|
|
4617
|
+
setTime(0);
|
|
4618
|
+
}, [host2]);
|
|
4619
|
+
const discard = useCallback(
|
|
4620
|
+
async (url) => {
|
|
4621
|
+
await host2?.discard(url).catch(fail);
|
|
4622
|
+
},
|
|
4623
|
+
[host2]
|
|
4624
|
+
);
|
|
4625
|
+
return { available: host2 !== void 0, status, stream, facing, time, error, open, close, flip, record, pause, resume, stop, cancel, discard };
|
|
4626
|
+
}
|
|
4627
|
+
function CameraPreview(props) {
|
|
4628
|
+
const { own, modifiers } = splitProps(props);
|
|
4629
|
+
const stream = own.recorder.stream;
|
|
4630
|
+
if (!stream) return applyModifiers(sb("color", { color: { r: 0, g: 0, b: 0, a: 1 } }), modifiers);
|
|
4631
|
+
return applyModifiers(
|
|
4632
|
+
sb("video", {
|
|
4633
|
+
url: null,
|
|
4634
|
+
playing: false,
|
|
4635
|
+
muted: true,
|
|
4636
|
+
volume: 0,
|
|
4637
|
+
rate: 1,
|
|
4638
|
+
loop: false,
|
|
4639
|
+
gravity: own.gravity ?? "resizeAspectFill",
|
|
4640
|
+
controls: false,
|
|
4641
|
+
seek: { time: 0, serial: 0 },
|
|
4642
|
+
stream,
|
|
4643
|
+
mirrored: own.mirrored ?? own.recorder.facing === "user"
|
|
4644
|
+
}),
|
|
4645
|
+
modifiers
|
|
4136
4646
|
);
|
|
4137
|
-
return { available: host !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
|
|
4138
4647
|
}
|
|
4139
4648
|
let nextId$1 = 1;
|
|
4140
4649
|
const newId = () => nextId$1++;
|
|
@@ -4143,9 +4652,9 @@ function unsupported() {
|
|
|
4143
4652
|
return new Error("unsupported: this page or app has no audio engine");
|
|
4144
4653
|
}
|
|
4145
4654
|
function perform$1(command2) {
|
|
4146
|
-
const
|
|
4147
|
-
if (!
|
|
4148
|
-
return
|
|
4655
|
+
const host2 = hostNow$1();
|
|
4656
|
+
if (!host2) return Promise.reject(unsupported());
|
|
4657
|
+
return host2.perform(command2);
|
|
4149
4658
|
}
|
|
4150
4659
|
class AVAudioTime {
|
|
4151
4660
|
spec;
|
|
@@ -4751,9 +5260,9 @@ function useAudioEngine(build) {
|
|
|
4751
5260
|
let nextId = 1;
|
|
4752
5261
|
const hostNow = () => browserEnvironment.languageModel;
|
|
4753
5262
|
function perform(command2) {
|
|
4754
|
-
const
|
|
4755
|
-
if (!
|
|
4756
|
-
return
|
|
5263
|
+
const host2 = hostNow();
|
|
5264
|
+
if (!host2) return Promise.reject(new GenerationError("assetsUnavailable", "This page or app has no language model."));
|
|
5265
|
+
return host2.perform(command2).catch((error) => {
|
|
4757
5266
|
throw GenerationError.from(error);
|
|
4758
5267
|
});
|
|
4759
5268
|
}
|
|
@@ -5236,8 +5745,9 @@ function ProgressView(props) {
|
|
|
5236
5745
|
}
|
|
5237
5746
|
function ToolbarItem(props) {
|
|
5238
5747
|
const hidden = props.sharedBackgroundVisibility === "hidden";
|
|
5239
|
-
const
|
|
5240
|
-
|
|
5748
|
+
const placement = toolbarPlacement(props.placement);
|
|
5749
|
+
const items = placement === "secondary" ? props.children : withEnvironment({ labelStyle: "iconOnly" }, props.children);
|
|
5750
|
+
return sb("toolbar", hidden ? { placement, sharedBackground: "hidden" } : { placement }, void 0, items);
|
|
5241
5751
|
}
|
|
5242
5752
|
const ToolbarItemGroup = ToolbarItem;
|
|
5243
5753
|
function ToolbarSpacer(props) {
|
|
@@ -5439,14 +5949,14 @@ function canSignInWithApple() {
|
|
|
5439
5949
|
return browserEnvironment.appleSignIn !== void 0;
|
|
5440
5950
|
}
|
|
5441
5951
|
async function signInWithApple(options = {}) {
|
|
5442
|
-
const
|
|
5443
|
-
if (!
|
|
5444
|
-
return
|
|
5952
|
+
const host2 = browserEnvironment.appleSignIn;
|
|
5953
|
+
if (!host2) throw new Error("unsupported: Sign in with Apple is not available here (configureSignInWithApple on a page)");
|
|
5954
|
+
return host2.signIn({ scopes: options.scopes ?? ["fullName", "email"], ...options.nonce ? { nonce: options.nonce } : {}, ...options.state ? { state: options.state } : {} });
|
|
5445
5955
|
}
|
|
5446
5956
|
async function appleCredentialState(user) {
|
|
5447
|
-
const
|
|
5448
|
-
if (!
|
|
5449
|
-
return
|
|
5957
|
+
const host2 = browserEnvironment.appleSignIn;
|
|
5958
|
+
if (!host2) return "unknown";
|
|
5959
|
+
return host2.credentialState(user);
|
|
5450
5960
|
}
|
|
5451
5961
|
const LABELS = { signIn: "Sign in with Apple", signUp: "Sign up with Apple", continue: "Continue with Apple" };
|
|
5452
5962
|
function SignInWithAppleButton(props) {
|
|
@@ -5777,16 +6287,16 @@ function hostData(contents) {
|
|
|
5777
6287
|
if (contents instanceof ArrayBuffer) return new Uint8Array(contents);
|
|
5778
6288
|
return contents;
|
|
5779
6289
|
}
|
|
5780
|
-
function createFileManager(
|
|
6290
|
+
function createFileManager(host2) {
|
|
5781
6291
|
const required = () => {
|
|
5782
|
-
const files =
|
|
6292
|
+
const files = host2();
|
|
5783
6293
|
if (!files) throw new Error("unsupported: this page keeps no files");
|
|
5784
6294
|
return files;
|
|
5785
6295
|
};
|
|
5786
|
-
const directories = () =>
|
|
6296
|
+
const directories = () => host2()?.directories ?? NONE;
|
|
5787
6297
|
return {
|
|
5788
6298
|
get available() {
|
|
5789
|
-
return
|
|
6299
|
+
return host2() !== void 0;
|
|
5790
6300
|
},
|
|
5791
6301
|
get documentsDirectory() {
|
|
5792
6302
|
return directories().documents;
|
|
@@ -5823,8 +6333,8 @@ function createFileManager(host) {
|
|
|
5823
6333
|
copyItem: async (from, to) => required().copy(from, to),
|
|
5824
6334
|
removeItem: async (url) => required().remove(url),
|
|
5825
6335
|
loadURL: async (url) => required().loadURL(url),
|
|
5826
|
-
iCloudDirectory: async () => await
|
|
5827
|
-
watch: (listener) =>
|
|
6336
|
+
iCloudDirectory: async () => await host2()?.iCloud?.() ?? null,
|
|
6337
|
+
watch: (listener) => host2()?.watch?.(listener) ?? (() => {
|
|
5828
6338
|
})
|
|
5829
6339
|
};
|
|
5830
6340
|
}
|
|
@@ -7420,6 +7930,19 @@ function KeyframeAnimator({
|
|
|
7420
7930
|
return children(value);
|
|
7421
7931
|
}
|
|
7422
7932
|
export {
|
|
7933
|
+
AVAsset,
|
|
7934
|
+
AVAssetExportPreset1280x720,
|
|
7935
|
+
AVAssetExportPreset1920x1080,
|
|
7936
|
+
AVAssetExportPreset3840x2160,
|
|
7937
|
+
AVAssetExportPreset640x480,
|
|
7938
|
+
AVAssetExportPreset960x540,
|
|
7939
|
+
AVAssetExportPresetHighestQuality,
|
|
7940
|
+
AVAssetExportPresetLowQuality,
|
|
7941
|
+
AVAssetExportPresetMediumQuality,
|
|
7942
|
+
AVAssetExportPresetPassthrough,
|
|
7943
|
+
AVAssetExportSession,
|
|
7944
|
+
AVAssetImageGenerator,
|
|
7945
|
+
AVAssetTrack,
|
|
7423
7946
|
AVAudioEngine,
|
|
7424
7947
|
AVAudioFile,
|
|
7425
7948
|
AVAudioInputNode,
|
|
@@ -7437,8 +7960,12 @@ export {
|
|
|
7437
7960
|
AVAudioUnitReverb,
|
|
7438
7961
|
AVAudioUnitSampler,
|
|
7439
7962
|
AVAudioUnitTimePitch,
|
|
7963
|
+
AVFileType,
|
|
7964
|
+
AVMediaType,
|
|
7965
|
+
AVMutableComposition,
|
|
7440
7966
|
AVPlayer,
|
|
7441
7967
|
AVQueuePlayer,
|
|
7968
|
+
AVURLAsset,
|
|
7442
7969
|
AngularGradient,
|
|
7443
7970
|
AreaMark,
|
|
7444
7971
|
AsyncImage,
|
|
@@ -7446,6 +7973,9 @@ export {
|
|
|
7446
7973
|
AudioPlayer,
|
|
7447
7974
|
BarMark,
|
|
7448
7975
|
Button,
|
|
7976
|
+
CMTime,
|
|
7977
|
+
CMTimeRange,
|
|
7978
|
+
CameraPreview,
|
|
7449
7979
|
Canvas,
|
|
7450
7980
|
Capsule,
|
|
7451
7981
|
Chart,
|
|
@@ -7617,6 +8147,7 @@ export {
|
|
|
7617
8147
|
useSpeechSynthesis,
|
|
7618
8148
|
useSystemLanguageModel,
|
|
7619
8149
|
useUndoManager,
|
|
8150
|
+
useVideoRecorder,
|
|
7620
8151
|
useViewEnvironment,
|
|
7621
8152
|
useWakeLock,
|
|
7622
8153
|
weighted,
|