@octane-xplat/camera 0.13.0 → 0.14.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.
- package/README.md +63 -3
- package/dist/linux/CameraView.web.tsrx.js +337 -0
- package/dist/linux/host-movie.web.js +100 -0
- package/dist/linux/index.js +5 -0
- package/dist/linux/layout-child.web.js +29 -0
- package/dist/linux/movie-metadata.web.js +65 -0
- package/dist/linux/octane-client-build.json +8 -0
- package/dist/linux/session-backend.linux.js +479 -0
- package/dist/linux/session-core.js +368 -0
- package/dist/linux/session.js +29 -0
- package/dist/linux/synthetic-session.js +280 -0
- package/dist/linux/types.js +19 -0
- package/dist/native/CameraView.android.js +25 -1
- package/dist/native/CameraView.ios.js +24 -1
- package/dist/native/CameraView.windows.js +173 -0
- package/dist/native/camera-permission.windows.js +43 -0
- package/dist/native/camera-session.windows.js +78 -0
- package/dist/native/cx.windows.js +9 -0
- package/dist/native/index.js +4 -1
- package/dist/native/layout-child.windows.js +53 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/session-backend.android.js +234 -0
- package/dist/native/session-backend.ios.js +682 -0
- package/dist/native/session-backend.js +55 -0
- package/dist/native/session-backend.windows.js +905 -0
- package/dist/native/session-core.js +368 -0
- package/dist/native/session.js +29 -0
- package/dist/native/synthetic-session.js +280 -0
- package/dist/native/types.js +19 -0
- package/dist/web/CameraView.web.tsrx.js +27 -7
- package/dist/web/host-movie.web.js +100 -0
- package/dist/web/index.js +4 -1
- package/dist/web/movie-metadata.web.js +65 -0
- package/dist/web/movie-storage.web.js +119 -0
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/session-backend.web.js +490 -0
- package/dist/web/session-core.js +368 -0
- package/dist/web/session.js +29 -0
- package/dist/web/synthetic-session.js +280 -0
- package/dist/web/types.js +19 -0
- package/package.json +22 -4
- package/platforms/android/AndroidManifest.xml +2 -0
- package/platforms/android/include.gradle +1 -0
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +4 -0
- package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +363 -0
- package/platforms/ios/Info.plist +2 -0
- package/platforms/macos/src/XplatCameraHost.swift +513 -0
- package/src/CameraView.android.tsrx +21 -1
- package/src/CameraView.ios.tsrx +28 -2
- package/src/CameraView.macos.tsrx +93 -0
- package/src/CameraView.web.tsrx +20 -2
- package/src/CameraView.windows.tsrx +124 -0
- package/src/camera-permission.windows.ts +76 -0
- package/src/camera-session.macos.ts +75 -0
- package/src/camera-session.windows.ts +123 -0
- package/src/cx.macos.ts +5 -0
- package/src/cx.windows.ts +6 -0
- package/src/host-movie.web.ts +180 -0
- package/src/index.ts +31 -0
- package/src/index.web.ts +31 -0
- package/src/layout-child.windows.ts +79 -0
- package/src/movie-metadata.web.ts +84 -0
- package/src/movie-storage.web.ts +164 -0
- package/src/props.ts +7 -1
- package/src/session-backend.android.ts +397 -0
- package/src/session-backend.ios.ts +1092 -0
- package/src/session-backend.linux.ts +869 -0
- package/src/session-backend.macos.ts +624 -0
- package/src/session-backend.ts +57 -0
- package/src/session-backend.web.ts +874 -0
- package/src/session-backend.windows.ts +1517 -0
- package/src/session-core.ts +659 -0
- package/src/session.ts +37 -0
- package/src/synthetic-session.ts +367 -0
- package/src/types.ts +329 -0
- package/types/index.d.ts +43 -0
- package/types/props.d.ts +7 -1
- package/types/session-core.d.ts +129 -0
- package/types/synthetic-session.d.ts +48 -0
- package/types/types.d.ts +263 -0
- package/src/CameraView.windows.ts +0 -5
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { CameraCaptureError } from "./types.js";
|
|
2
|
+
import { cameraHost, commitHostMovie, openHostMovie, removeHostReservation, reserveHostMovie } from "./host-movie.web.js";
|
|
3
|
+
import { inspectMovie } from "./movie-metadata.web.js";
|
|
4
|
+
//#region src/session-backend.linux.ts
|
|
5
|
+
var mimeCandidates = [
|
|
6
|
+
"video/webm;codecs=vp8,opus",
|
|
7
|
+
"video/webm;codecs=vp9,opus",
|
|
8
|
+
"video/mp4;codecs=avc1.424028,mp4a.40.2",
|
|
9
|
+
"video/mp4"
|
|
10
|
+
];
|
|
11
|
+
var owner;
|
|
12
|
+
var error = (kind, operation, message, cause) => new CameraCaptureError({
|
|
13
|
+
kind,
|
|
14
|
+
operation,
|
|
15
|
+
message,
|
|
16
|
+
cause
|
|
17
|
+
});
|
|
18
|
+
var policyAllows = (kind) => {
|
|
19
|
+
return (document.permissionsPolicy ?? document.featurePolicy)?.allowsFeature(kind) !== false;
|
|
20
|
+
};
|
|
21
|
+
function createSessionBackend(config) {
|
|
22
|
+
let currentConfig = { ...config };
|
|
23
|
+
let observer;
|
|
24
|
+
let host;
|
|
25
|
+
let listener;
|
|
26
|
+
let stream;
|
|
27
|
+
let ready = false;
|
|
28
|
+
let released = false;
|
|
29
|
+
let generation = 0;
|
|
30
|
+
let recorder;
|
|
31
|
+
let sink;
|
|
32
|
+
let startedAt = 0;
|
|
33
|
+
let stoppedAt = 0;
|
|
34
|
+
let stopTimer;
|
|
35
|
+
let watchdog;
|
|
36
|
+
let recordingAudio = false;
|
|
37
|
+
let captureFailure;
|
|
38
|
+
let hostState = "unknown";
|
|
39
|
+
const hostProbe = cameraHost().then((host) => {
|
|
40
|
+
hostState = host ? "available" : "absent";
|
|
41
|
+
return host;
|
|
42
|
+
});
|
|
43
|
+
const lease = {};
|
|
44
|
+
const known = {
|
|
45
|
+
camera: "unknown",
|
|
46
|
+
microphone: "unknown"
|
|
47
|
+
};
|
|
48
|
+
const supported = () => typeof navigator !== "undefined" && globalThis.isSecureContext === true && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined" && !!mimeType() && hostState === "available";
|
|
49
|
+
const mimeType = () => typeof MediaRecorder === "undefined" ? "" : mimeCandidates.find((mime) => MediaRecorder.isTypeSupported(mime)) ?? "";
|
|
50
|
+
const markReady = (value) => {
|
|
51
|
+
ready = value;
|
|
52
|
+
observer?.previewReady(value);
|
|
53
|
+
};
|
|
54
|
+
const dropStream = () => {
|
|
55
|
+
if (host) host.srcObject = null;
|
|
56
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
57
|
+
stream = void 0;
|
|
58
|
+
markReady(false);
|
|
59
|
+
if (owner === lease) owner = void 0;
|
|
60
|
+
};
|
|
61
|
+
const onVisibility = () => {
|
|
62
|
+
if (document.visibilityState === "hidden") {
|
|
63
|
+
markReady(false);
|
|
64
|
+
observer?.interruption("began", "background");
|
|
65
|
+
} else {
|
|
66
|
+
observer?.interruption("ended", "background");
|
|
67
|
+
markReady(!!host && stream?.getVideoTracks()[0]?.readyState === "live");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const onPageHide = () => {
|
|
71
|
+
markReady(false);
|
|
72
|
+
observer?.interruption("began", "background");
|
|
73
|
+
};
|
|
74
|
+
const observeTracks = (target) => {
|
|
75
|
+
for (const track of target.getTracks()) {
|
|
76
|
+
track.addEventListener("ended", () => {
|
|
77
|
+
if (track.kind === "audio" && (!sink || !recordingAudio)) return;
|
|
78
|
+
if (stream !== target) return;
|
|
79
|
+
markReady(false);
|
|
80
|
+
observer?.availability(false);
|
|
81
|
+
observer?.interruption("began", "unknown");
|
|
82
|
+
});
|
|
83
|
+
track.addEventListener("mute", () => {
|
|
84
|
+
if (track.kind === "audio" && (!sink || !recordingAudio)) return;
|
|
85
|
+
if (stream === target) {
|
|
86
|
+
markReady(false);
|
|
87
|
+
observer?.interruption("began", "unknown");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
track.addEventListener("unmute", () => {
|
|
91
|
+
if (stream === target && document.visibilityState !== "hidden") {
|
|
92
|
+
observer?.interruption("ended", "unknown");
|
|
93
|
+
observer?.availability(true);
|
|
94
|
+
markReady(!!host);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const constraints = () => {
|
|
100
|
+
const camera = currentConfig.camera;
|
|
101
|
+
return {
|
|
102
|
+
width: { ideal: 1280 },
|
|
103
|
+
height: { ideal: 720 },
|
|
104
|
+
frameRate: { ideal: 30 },
|
|
105
|
+
...typeof camera === "object" ? { deviceId: { exact: camera.deviceId } } : camera === "front" || camera === "back" ? { facingMode: { exact: camera === "front" ? "user" : "environment" } } : {}
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
let acquisition;
|
|
109
|
+
const acquireCameraOnce = async () => {
|
|
110
|
+
if (stream?.getVideoTracks()[0]?.readyState === "live") return;
|
|
111
|
+
if (owner && owner !== lease) throw error("busy", "preview", "Another camera session still owns the source");
|
|
112
|
+
owner = lease;
|
|
113
|
+
const epoch = generation;
|
|
114
|
+
try {
|
|
115
|
+
const acquired = await navigator.mediaDevices.getUserMedia({
|
|
116
|
+
video: constraints(),
|
|
117
|
+
audio: false
|
|
118
|
+
});
|
|
119
|
+
if (released || epoch !== generation) {
|
|
120
|
+
acquired.getTracks().forEach((track) => track.stop());
|
|
121
|
+
if (!stream && owner === lease) owner = void 0;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
stream = acquired;
|
|
125
|
+
known.camera = "granted";
|
|
126
|
+
observeTracks(acquired);
|
|
127
|
+
observer?.availability(true);
|
|
128
|
+
} catch (cause) {
|
|
129
|
+
if (owner === lease) owner = void 0;
|
|
130
|
+
throw cause;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const acquireCamera = () => {
|
|
134
|
+
acquisition ??= acquireCameraOnce().finally(() => {
|
|
135
|
+
acquisition = void 0;
|
|
136
|
+
});
|
|
137
|
+
return acquisition;
|
|
138
|
+
};
|
|
139
|
+
const acquireCombined = async () => {
|
|
140
|
+
const epoch = generation;
|
|
141
|
+
const acquired = await navigator.mediaDevices.getUserMedia({
|
|
142
|
+
video: constraints(),
|
|
143
|
+
audio: true
|
|
144
|
+
});
|
|
145
|
+
if (released || epoch !== generation) {
|
|
146
|
+
acquired.getTracks().forEach((track) => track.stop());
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const previous = stream;
|
|
150
|
+
stream = acquired;
|
|
151
|
+
previous?.getTracks().forEach((track) => track.stop());
|
|
152
|
+
observeTracks(acquired);
|
|
153
|
+
observer?.availability(true);
|
|
154
|
+
};
|
|
155
|
+
const ensureAudio = async () => {
|
|
156
|
+
if (stream?.getAudioTracks().some((track) => track.readyState === "live")) return;
|
|
157
|
+
if (stream) {
|
|
158
|
+
await acquireCombined();
|
|
159
|
+
if (host) await showPreview();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
(await navigator.mediaDevices.getUserMedia({
|
|
163
|
+
audio: true,
|
|
164
|
+
video: false
|
|
165
|
+
})).getTracks().forEach((track) => track.stop());
|
|
166
|
+
};
|
|
167
|
+
const showPreview = async () => {
|
|
168
|
+
if (!host || released) return;
|
|
169
|
+
const element = host;
|
|
170
|
+
const epoch = generation;
|
|
171
|
+
if (await checkPermission("camera") !== "granted") {
|
|
172
|
+
listener?.onError?.(error("permissionBlocked", "preview", "Request camera access from a user action before activating this preview"));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await acquireCamera();
|
|
176
|
+
if (host !== element || !stream || released || epoch !== generation) return;
|
|
177
|
+
element.srcObject = stream;
|
|
178
|
+
element.muted = true;
|
|
179
|
+
const facing = stream.getVideoTracks()[0]?.getSettings().facingMode;
|
|
180
|
+
element.style.transform = facing === "user" ? "scaleX(-1)" : "";
|
|
181
|
+
await element.play();
|
|
182
|
+
if (host !== element || released || epoch !== generation) return;
|
|
183
|
+
markReady(document.visibilityState !== "hidden");
|
|
184
|
+
listener?.onReady?.();
|
|
185
|
+
};
|
|
186
|
+
const hostMediaKind = (kind) => kind === "microphone" ? "microphone" : "camera";
|
|
187
|
+
async function checkPermission(kind) {
|
|
188
|
+
if (globalThis.isSecureContext !== true || !navigator.mediaDevices?.getUserMedia) return "unavailable";
|
|
189
|
+
if (!policyAllows(kind)) return "blocked";
|
|
190
|
+
const desktop = await hostProbe;
|
|
191
|
+
if (!desktop) return "unavailable";
|
|
192
|
+
try {
|
|
193
|
+
const { status } = await desktop.call("camera", "permissionStatus", { kind: hostMediaKind(kind) });
|
|
194
|
+
if (status === "granted" || status === "notDetermined") return status;
|
|
195
|
+
if (status === "restricted") return "restricted";
|
|
196
|
+
if (status === "denied" || status === "blocked") return "denied";
|
|
197
|
+
return "unknown";
|
|
198
|
+
} catch {
|
|
199
|
+
return "unknown";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const settle = (value, expected = sink) => {
|
|
203
|
+
const target = sink;
|
|
204
|
+
if (!target || target !== expected) return;
|
|
205
|
+
sink = void 0;
|
|
206
|
+
recorder = void 0;
|
|
207
|
+
captureFailure = void 0;
|
|
208
|
+
clearTimeout(stopTimer);
|
|
209
|
+
clearTimeout(watchdog);
|
|
210
|
+
if (!host) dropStream();
|
|
211
|
+
target.settled(value);
|
|
212
|
+
};
|
|
213
|
+
const stop = () => {
|
|
214
|
+
if (!recorder || !sink) return;
|
|
215
|
+
stoppedAt ||= performance.now();
|
|
216
|
+
clearTimeout(watchdog);
|
|
217
|
+
const target = sink;
|
|
218
|
+
const fail = captureFailure;
|
|
219
|
+
watchdog = setTimeout(() => {
|
|
220
|
+
if (sink !== target) return;
|
|
221
|
+
dropStream();
|
|
222
|
+
fail?.(error("finalizationFailed", "stop", "Recorder finalization callback timed out"));
|
|
223
|
+
}, 3e4);
|
|
224
|
+
if (recorder.state === "inactive") return;
|
|
225
|
+
try {
|
|
226
|
+
recorder.stop();
|
|
227
|
+
} catch (cause) {
|
|
228
|
+
fail?.(error("captureFailed", "stop", "The recorder could not stop", cause));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
platform: "web",
|
|
233
|
+
get supported() {
|
|
234
|
+
return supported();
|
|
235
|
+
},
|
|
236
|
+
checkPermission,
|
|
237
|
+
requestPermission: async (kind) => {
|
|
238
|
+
const status = await checkPermission(kind);
|
|
239
|
+
if (status === "blocked" || status === "unavailable") return status;
|
|
240
|
+
const desktop = await hostProbe;
|
|
241
|
+
if (!desktop) return "unavailable";
|
|
242
|
+
const { status: hostStatus } = await desktop.call("camera", "requestPermission", { kind: hostMediaKind(kind) });
|
|
243
|
+
if (hostStatus !== "granted") {
|
|
244
|
+
known[kind] = hostStatus === "denied" || hostStatus === "blocked" ? "denied" : hostStatus === "restricted" ? "restricted" : "unknown";
|
|
245
|
+
return known[kind];
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
if (kind === "camera") await acquireCamera();
|
|
249
|
+
else if (sink) (await navigator.mediaDevices.getUserMedia({
|
|
250
|
+
audio: true,
|
|
251
|
+
video: false
|
|
252
|
+
})).getTracks().forEach((track) => track.stop());
|
|
253
|
+
else await ensureAudio();
|
|
254
|
+
known[kind] = "granted";
|
|
255
|
+
if (kind === "camera" && host) await showPreview();
|
|
256
|
+
return "granted";
|
|
257
|
+
} catch (cause) {
|
|
258
|
+
known[kind] = cause instanceof DOMException && (cause.name === "NotAllowedError" || cause.name === "SecurityError") ? "blocked" : "unavailable";
|
|
259
|
+
return known[kind];
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
getCapabilities: async () => {
|
|
263
|
+
const desktop = await hostProbe;
|
|
264
|
+
const devices = await navigator.mediaDevices?.enumerateDevices?.().catch(() => []) ?? [];
|
|
265
|
+
const settings = stream?.getVideoTracks()[0]?.getSettings();
|
|
266
|
+
const mime = mimeType();
|
|
267
|
+
const storageReady = !!desktop;
|
|
268
|
+
return {
|
|
269
|
+
supported: supported(),
|
|
270
|
+
available: ready && storageReady,
|
|
271
|
+
reason: !supported() ? "Secure media capture, a supported WebM/MP4 codec, and the packaged Linux host are required" : !storageReady ? "Movie output requires the packaged Linux WebKitGTK host" : !ready ? "An active permitted preview is required" : void 0,
|
|
272
|
+
cameras: devices.filter((device) => device.kind === "videoinput").map((device) => ({
|
|
273
|
+
id: device.deviceId,
|
|
274
|
+
label: device.label
|
|
275
|
+
})),
|
|
276
|
+
profiles: settings?.width && settings.height && settings.frameRate ? [{
|
|
277
|
+
profile: "standard",
|
|
278
|
+
width: settings.width,
|
|
279
|
+
height: settings.height,
|
|
280
|
+
frameRate: settings.frameRate
|
|
281
|
+
}] : [],
|
|
282
|
+
audio: devices.some((device) => device.kind === "audioinput"),
|
|
283
|
+
orientations: [],
|
|
284
|
+
durationLimit: "bestEffort",
|
|
285
|
+
elapsedTime: "estimated",
|
|
286
|
+
output: {
|
|
287
|
+
mimeType: mime,
|
|
288
|
+
container: !mime ? "" : mime.startsWith("video/mp4") ? "mp4" : "webm",
|
|
289
|
+
storage: desktop ? "appPrivateFile" : "originLocal",
|
|
290
|
+
destinationFileUrl: !!desktop
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
previewReady: () => ready && !!host && !released,
|
|
295
|
+
validateStart: (options) => {
|
|
296
|
+
if (hostState === "absent") throw error("destinationUnavailable", "startRecording", "Movie output requires the packaged Linux WebKitGTK host");
|
|
297
|
+
if (options.orientation && options.orientation !== "unspecified") throw error("unsupportedConfiguration", "startRecording", "This browser cannot lock a recording orientation");
|
|
298
|
+
},
|
|
299
|
+
attachPreview: (target, callbacks) => {
|
|
300
|
+
if (released || sink) throw error("invalidState", "attachPreview", "The camera is disposed or still finalizing");
|
|
301
|
+
host = target;
|
|
302
|
+
listener = callbacks;
|
|
303
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
304
|
+
globalThis.addEventListener("pagehide", onPageHide);
|
|
305
|
+
showPreview().catch((cause) => callbacks?.onError?.(error("unavailable", "preview", "Could not activate camera preview", cause)));
|
|
306
|
+
return () => {
|
|
307
|
+
if (host) host.srcObject = null;
|
|
308
|
+
host = void 0;
|
|
309
|
+
listener = void 0;
|
|
310
|
+
generation += 1;
|
|
311
|
+
markReady(false);
|
|
312
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
313
|
+
globalThis.removeEventListener("pagehide", onPageHide);
|
|
314
|
+
if (!sink) dropStream();
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
configure: (next) => {
|
|
318
|
+
const sourceChanged = JSON.stringify(next.camera) !== JSON.stringify(currentConfig.camera) || next.profile !== currentConfig.profile;
|
|
319
|
+
currentConfig = { ...next };
|
|
320
|
+
if (sourceChanged) {
|
|
321
|
+
generation += 1;
|
|
322
|
+
dropStream();
|
|
323
|
+
if (host) showPreview().catch((cause) => listener?.onError?.(error("unavailable", "configure", "Could not change camera", cause)));
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
startCapture: (request, target) => {
|
|
327
|
+
sink = target;
|
|
328
|
+
recordingAudio = request.audio;
|
|
329
|
+
startedAt = 0;
|
|
330
|
+
stoppedAt = 0;
|
|
331
|
+
let desktop = null;
|
|
332
|
+
let reservation;
|
|
333
|
+
let stage = "preparation";
|
|
334
|
+
const chunks = [];
|
|
335
|
+
let captureError;
|
|
336
|
+
let limitReached = false;
|
|
337
|
+
let failing = false;
|
|
338
|
+
let didStart = false;
|
|
339
|
+
captureFailure = fail;
|
|
340
|
+
(async () => {
|
|
341
|
+
try {
|
|
342
|
+
desktop = await hostProbe;
|
|
343
|
+
if (!desktop) throw error("destinationUnavailable", "startRecording", "Movie output requires the packaged Linux WebKitGTK host");
|
|
344
|
+
if (request.audio) {
|
|
345
|
+
if (await checkPermission("microphone") !== "granted") throw error("permissionDenied", "startRecording", "Requested microphone access is missing");
|
|
346
|
+
await ensureAudio().catch((cause) => {
|
|
347
|
+
throw error("captureFailed", "startRecording", "Could not acquire microphone for recording", cause);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const source = stream;
|
|
351
|
+
const video = source?.getVideoTracks()[0];
|
|
352
|
+
const audio = source?.getAudioTracks().filter((track) => track.readyState === "live") ?? [];
|
|
353
|
+
if (!video || request.audio && !audio.length) throw error(request.audio ? "permissionDenied" : "unavailable", "startRecording", request.audio ? "Requested microphone access is missing" : "Video source is unavailable");
|
|
354
|
+
const recordingStream = new MediaStream([video, ...request.audio ? audio : []]);
|
|
355
|
+
reservation = (await reserveHostMovie(desktop, {
|
|
356
|
+
destinationFileUrl: request.destinationPath ? `file://${request.destinationPath.split("/").map(encodeURIComponent).join("/")}` : void 0,
|
|
357
|
+
container: mimeType().startsWith("video/mp4") ? "mp4" : "webm"
|
|
358
|
+
})).fileUrl;
|
|
359
|
+
if (!source || video.readyState !== "live" || document.visibilityState === "hidden") throw error("unavailable", "startRecording", "Camera source became inactive before recording");
|
|
360
|
+
const activeRecorder = new MediaRecorder(recordingStream, { mimeType: mimeType() });
|
|
361
|
+
recorder = activeRecorder;
|
|
362
|
+
activeRecorder.ondataavailable = (event) => {
|
|
363
|
+
if (sink !== target || failing) return;
|
|
364
|
+
if (event.data.size) chunks.push(event.data);
|
|
365
|
+
};
|
|
366
|
+
activeRecorder.onerror = (event) => {
|
|
367
|
+
if (sink !== target || failing) return;
|
|
368
|
+
captureError = event;
|
|
369
|
+
stop();
|
|
370
|
+
};
|
|
371
|
+
activeRecorder.onstart = () => {
|
|
372
|
+
if (sink !== target || failing) return;
|
|
373
|
+
if (didStart) return;
|
|
374
|
+
didStart = true;
|
|
375
|
+
clearTimeout(watchdog);
|
|
376
|
+
startedAt = performance.now();
|
|
377
|
+
stage = "capture";
|
|
378
|
+
const settings = video.getSettings();
|
|
379
|
+
const effective = {
|
|
380
|
+
cameraId: settings.deviceId ?? "",
|
|
381
|
+
audio: request.audio,
|
|
382
|
+
width: settings.width ?? 0,
|
|
383
|
+
height: settings.height ?? 0,
|
|
384
|
+
frameRate: settings.frameRate ?? 0,
|
|
385
|
+
orientation: "unspecified",
|
|
386
|
+
mimeType: activeRecorder.mimeType,
|
|
387
|
+
container: activeRecorder.mimeType.startsWith("video/mp4") ? "mp4" : "webm"
|
|
388
|
+
};
|
|
389
|
+
if (request.maximumDurationMs) {
|
|
390
|
+
const limit = request.maximumDurationMs;
|
|
391
|
+
const checkLimit = () => {
|
|
392
|
+
if (sink !== target || failing) return;
|
|
393
|
+
const remaining = limit - (performance.now() - startedAt);
|
|
394
|
+
if (remaining > 0) stopTimer = setTimeout(checkLimit, Math.min(remaining, 2147483647));
|
|
395
|
+
else {
|
|
396
|
+
limitReached = true;
|
|
397
|
+
stop();
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
stopTimer = setTimeout(checkLimit, Math.min(limit, 2147483647));
|
|
401
|
+
}
|
|
402
|
+
target.started(effective);
|
|
403
|
+
};
|
|
404
|
+
activeRecorder.onstop = () => {
|
|
405
|
+
if (sink !== target || failing) return;
|
|
406
|
+
target.finishing(captureError ? "captureError" : limitReached ? "maximumDuration" : void 0);
|
|
407
|
+
stoppedAt ||= performance.now();
|
|
408
|
+
clearTimeout(stopTimer);
|
|
409
|
+
clearTimeout(watchdog);
|
|
410
|
+
(async () => {
|
|
411
|
+
try {
|
|
412
|
+
stage = "finalization";
|
|
413
|
+
if (!startedAt) throw error("noMedia", "finalize", "No recording start was observed");
|
|
414
|
+
const bytes = new Blob(chunks, { type: activeRecorder.mimeType });
|
|
415
|
+
if (globalThis.__xplatDebugMovie) globalThis.__xplatLastMovieBlob = bytes;
|
|
416
|
+
const metadata = await inspectMovie(bytes, request.audio);
|
|
417
|
+
if (sink !== target || failing) return;
|
|
418
|
+
stage = "storage";
|
|
419
|
+
const output = await commitHostMovie(desktop, reservation, bytes);
|
|
420
|
+
const clip = {
|
|
421
|
+
...metadata,
|
|
422
|
+
output
|
|
423
|
+
};
|
|
424
|
+
settle({
|
|
425
|
+
kind: "clip",
|
|
426
|
+
...clip,
|
|
427
|
+
limitReached,
|
|
428
|
+
warning: captureError ? error("captureFailed", "capture", "Recorder reported an error; finalized media was verified", captureError) : void 0
|
|
429
|
+
}, target);
|
|
430
|
+
} catch (cause) {
|
|
431
|
+
await fail(cause);
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
434
|
+
};
|
|
435
|
+
activeRecorder.start(1e3);
|
|
436
|
+
watchdog = setTimeout(() => {
|
|
437
|
+
if (!startedAt && sink === target) {
|
|
438
|
+
stop();
|
|
439
|
+
fail(error("captureFailed", "startRecording", "Recorder start callback timed out"));
|
|
440
|
+
}
|
|
441
|
+
}, 15e3);
|
|
442
|
+
} catch (cause) {
|
|
443
|
+
await fail(cause);
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
async function fail(cause) {
|
|
447
|
+
if (sink !== target || failing) return;
|
|
448
|
+
failing = true;
|
|
449
|
+
if (reservation && desktop) await removeHostReservation(desktop, reservation);
|
|
450
|
+
settle({
|
|
451
|
+
kind: "failed",
|
|
452
|
+
stage,
|
|
453
|
+
issue: cause instanceof CameraCaptureError ? cause : error(stage === "storage" ? "storageFailed" : stage === "preparation" || stage === "capture" ? "captureFailed" : "finalizationFailed", stage, "Movie recording could not complete", cause)
|
|
454
|
+
}, target);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
stopCapture: stop,
|
|
458
|
+
elapsedMs: () => startedAt ? Math.max(0, (stoppedAt || performance.now()) - startedAt) : 0,
|
|
459
|
+
openOutput: async (output) => {
|
|
460
|
+
if (output.kind !== "nativeFile") throw error("unavailable", "openOutput", "Only host file-backed output exists on this platform");
|
|
461
|
+
const desktop = await hostProbe;
|
|
462
|
+
if (!desktop) throw error("unavailable", "openOutput", "File-backed output requires the desktop host that recorded it");
|
|
463
|
+
return openHostMovie(desktop, output);
|
|
464
|
+
},
|
|
465
|
+
release: async () => {
|
|
466
|
+
released = true;
|
|
467
|
+
generation += 1;
|
|
468
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
469
|
+
globalThis.removeEventListener("pagehide", onPageHide);
|
|
470
|
+
dropStream();
|
|
471
|
+
host = void 0;
|
|
472
|
+
},
|
|
473
|
+
setObserver: (value) => {
|
|
474
|
+
observer = value;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
export { createSessionBackend };
|