mujoco-react 9.3.0 → 9.5.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.
@@ -8,10 +8,12 @@
8
8
  import { useCallback, useState } from 'react';
9
9
  import { useMujoco } from '../core/MujocoSimProvider';
10
10
  import {
11
+ createMountedCameraFrameSequenceReadiness,
11
12
  createMountedCameraFrameSequencePlanFromApi,
12
13
  recordMountedCameraFrameSequence,
13
14
  type MountedCameraFrameSequencePlan,
14
15
  type MountedCameraFrameSequencePlanOptions,
16
+ type MountedCameraFrameSequenceReadiness,
15
17
  type MountedCameraFrameSequenceRecordOptions,
16
18
  type MountedCameraFrameSequenceRecordResult,
17
19
  } from '../rendering/cameraFrameSource';
@@ -26,13 +28,22 @@ export type MountedCameraSequenceRecordOptions =
26
28
  MountedCameraFrameSequenceRecordOptions;
27
29
  export type MountedCameraSequenceRecordResult =
28
30
  MountedCameraFrameSequenceRecordResult;
31
+ export type MountedCameraSequenceReadiness =
32
+ MountedCameraFrameSequenceReadiness;
29
33
 
30
34
  export interface MountedCameraSequenceRecorderAPI
31
35
  extends Omit<CameraFrameSequenceRecorderAPI, 'record'> {
36
+ plan: MountedCameraFrameSequencePlan | null;
37
+ readiness: MountedCameraSequenceReadiness | null;
38
+ result: MountedCameraSequenceRecordResult | null;
32
39
  createPlan: (
33
40
  cameraKeys: readonly string[],
34
41
  options?: MountedCameraSequencePlanOptions
35
42
  ) => MountedCameraFrameSequencePlan;
43
+ checkReadiness: (
44
+ cameraKeys: readonly string[],
45
+ options?: MountedCameraSequencePlanOptions
46
+ ) => MountedCameraSequenceReadiness;
36
47
  record: (
37
48
  options: MountedCameraSequenceRecordOptions
38
49
  ) => Promise<MountedCameraSequenceRecordResult>;
@@ -44,10 +55,19 @@ export function useMountedCameraSequenceRecorder(
44
55
  const mujoco = useMujoco();
45
56
  const [status, setStatus] = useState<FrameCaptureStatus>('idle');
46
57
  const [error, setError] = useState<Error | null>(null);
58
+ const [plan, setPlan] = useState<MountedCameraFrameSequencePlan | null>(null);
59
+ const [readiness, setReadiness] =
60
+ useState<MountedCameraSequenceReadiness | null>(null);
61
+ const [result, setResult] = useState<MountedCameraSequenceRecordResult | null>(
62
+ null
63
+ );
47
64
 
48
65
  const reset = useCallback(() => {
49
66
  setStatus('idle');
50
67
  setError(null);
68
+ setPlan(null);
69
+ setReadiness(null);
70
+ setResult(null);
51
71
  }, []);
52
72
 
53
73
  const createPlan = useCallback(
@@ -59,14 +79,34 @@ export function useMountedCameraSequenceRecorder(
59
79
  throw new Error('MuJoCo scene is not ready for mounted camera sequence planning.');
60
80
  }
61
81
 
62
- return createMountedCameraFrameSequencePlanFromApi(mujoco.api, cameraKeys, {
63
- ...defaultOptions,
64
- ...options,
65
- });
82
+ const nextPlan = createMountedCameraFrameSequencePlanFromApi(
83
+ mujoco.api,
84
+ cameraKeys,
85
+ {
86
+ ...defaultOptions,
87
+ ...options,
88
+ }
89
+ );
90
+ setPlan(nextPlan);
91
+ setReadiness(null);
92
+ return nextPlan;
66
93
  },
67
94
  [defaultOptions, mujoco.api]
68
95
  );
69
96
 
97
+ const checkReadiness = useCallback(
98
+ (
99
+ cameraKeys: readonly string[],
100
+ options: MountedCameraSequencePlanOptions = {}
101
+ ) => {
102
+ const nextPlan = createPlan(cameraKeys, options);
103
+ const nextReadiness = createMountedCameraFrameSequenceReadiness(nextPlan);
104
+ setReadiness(nextReadiness);
105
+ return nextReadiness;
106
+ },
107
+ [createPlan]
108
+ );
109
+
70
110
  const record = useCallback(
71
111
  async (options: MountedCameraSequenceRecordOptions) => {
72
112
  if (!mujoco.api) {
@@ -75,14 +115,18 @@ export function useMountedCameraSequenceRecorder(
75
115
 
76
116
  setStatus('capturing');
77
117
  setError(null);
118
+ setResult(null);
78
119
 
79
120
  try {
80
- const result = await recordMountedCameraFrameSequence(mujoco.api, {
121
+ const nextResult = await recordMountedCameraFrameSequence(mujoco.api, {
81
122
  ...defaultOptions,
82
123
  ...options,
83
124
  });
125
+ setPlan(nextResult.plan);
126
+ setReadiness(nextResult.readiness);
127
+ setResult(nextResult);
84
128
  setStatus('captured');
85
- return result;
129
+ return nextResult;
86
130
  } catch (nextError) {
87
131
  const error =
88
132
  nextError instanceof Error
@@ -99,8 +143,12 @@ export function useMountedCameraSequenceRecorder(
99
143
  return {
100
144
  status,
101
145
  error,
146
+ plan,
147
+ readiness,
148
+ result,
102
149
  isRecording: status === 'capturing',
103
150
  createPlan,
151
+ checkReadiness,
104
152
  record,
105
153
  reset,
106
154
  };
package/src/index.ts CHANGED
@@ -48,14 +48,32 @@ export {
48
48
  createPairedSplatEnvironment,
49
49
  createSparkSplatViewerUrl,
50
50
  createSplatEnvironmentUserData,
51
+ createSplatSceneConfig,
52
+ createVisualScenarioExecutionContext,
51
53
  getSplatEnvironmentReadiness,
52
54
  getScenarioBackground,
53
55
  getScenarioCameraPosition,
54
56
  useSplatEnvironment,
55
57
  useSplatSceneConfig,
58
+ useVisualScenarioExecutionContext,
56
59
  useVisualScenarioEffects,
57
60
  withSplatEnvironment,
58
61
  } from './components/VisualScenario';
62
+ export {
63
+ canFetchSplatCollisionProxyXml,
64
+ fetchSplatCollisionProxyXml,
65
+ parseSplatCollisionProxyGeoms,
66
+ SplatCollisionProxyPreview,
67
+ useSplatCollisionProxyGeoms,
68
+ } from './components/SplatCollisionProxyPreview';
69
+ export type {
70
+ SplatCollisionProxyGeomPreview,
71
+ SplatCollisionProxyGeomsState,
72
+ SplatCollisionProxyPreviewProps,
73
+ SplatCollisionProxyPreviewStatus,
74
+ SplatCollisionProxyPreviewVector3,
75
+ UseSplatCollisionProxyGeomsOptions,
76
+ } from './components/SplatCollisionProxyPreview';
59
77
  export { Debug } from './components/Debug';
60
78
  export { TendonRenderer } from './components/TendonRenderer';
61
79
  export { FlexRenderer } from './components/FlexRenderer';
@@ -90,18 +108,24 @@ export { useMountedCameraSequenceRecorder } from './hooks/useMountedCameraSequen
90
108
  export type {
91
109
  MountedCameraSequencePlanOptions,
92
110
  MountedCameraSequenceRecorderAPI,
111
+ MountedCameraSequenceReadiness,
93
112
  MountedCameraSequenceRecordOptions,
94
113
  MountedCameraSequenceRecordResult,
95
114
  } from './hooks/useMountedCameraSequenceRecorder';
96
115
  export {
116
+ CAPTURE_EXCLUDE_KEY,
97
117
  captureCameraFrame,
98
118
  captureCameraFrameBlob,
99
119
  createCameraFrameCaptureSession,
100
120
  renderCameraFrameToCanvas,
101
121
  } from './rendering/cameraFrameCapture';
102
122
  export {
123
+ createMountedCameraFrameSequenceManifest,
103
124
  createMountedCameraFrameSequenceReadiness,
125
+ createMountedCameraFrameSourceSuggestions,
126
+ MountedCameraFrameSequenceManifestStatus,
104
127
  MountedCameraFrameSequenceReadinessStatus,
128
+ MountedCameraFrameSourceSuggestionMatch,
105
129
  createMountedCameraFrameSequencePlanFromApi,
106
130
  createMountedCameraFrameSequencePlan,
107
131
  getCameraFrameCaptureSourceTarget,
@@ -112,8 +136,10 @@ export {
112
136
  } from './rendering/cameraFrameSource';
113
137
  export type {
114
138
  CameraFrameMountSelector,
139
+ CreateMountedCameraFrameSequenceManifestOptions,
115
140
  CreateMountedCameraFrameSequencePlanOptions,
116
141
  MountedCameraFrameCaptureSource,
142
+ MountedCameraFrameSequenceManifest,
117
143
  MountedCameraFrameSequencePlanOptions,
118
144
  MountedCameraFrameSequenceRecorderTarget,
119
145
  MountedCameraFrameSequenceCameraOptions,
@@ -123,6 +149,8 @@ export type {
123
149
  MountedCameraFrameSequenceRecordOptions,
124
150
  MountedCameraFrameSequenceRecordResult,
125
151
  MountedCameraFrameSequenceSourceReadiness,
152
+ MountedCameraFrameSequenceStreamSummary,
153
+ MountedCameraFrameSourceSuggestion,
126
154
  NamedCameraFrameResource,
127
155
  ResolveMountedCameraFrameSourceOptions,
128
156
  ResolvedMountedCameraFrameSource,
@@ -214,6 +242,10 @@ export type {
214
242
  SplatEnvironmentReadiness,
215
243
  SplatEnvironmentMetadataInput,
216
244
  SplatEnvironmentMetadata,
245
+ VisualScenarioExecutionContext,
246
+ VisualScenarioExecutionContextInput,
247
+ ResolvedScenarioCameraConfig,
248
+ ResolvedScenarioMaterialConfig,
217
249
  SplatSceneConfigInput,
218
250
  SplatSceneConfigState,
219
251
  SplatSceneInput,
@@ -24,11 +24,66 @@ export interface CameraFrameCaptureSession {
24
24
  height: number;
25
25
  source: CameraFrameCaptureSource;
26
26
  };
27
+ captureAsync(options?: CameraFrameCaptureOptions): Promise<{
28
+ canvas: HTMLCanvasElement;
29
+ camera: THREE.Camera;
30
+ width: number;
31
+ height: number;
32
+ source: CameraFrameCaptureSource;
33
+ }>;
27
34
  captureDataUrl(options?: CameraFrameCaptureOptions): CameraFrameCaptureResult;
35
+ captureDataUrlAsync(
36
+ options?: CameraFrameCaptureOptions
37
+ ): Promise<CameraFrameCaptureResult>;
28
38
  captureBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
29
39
  dispose(): void;
30
40
  }
31
41
 
42
+ export const CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY =
43
+ 'mujocoReactCameraFrameCaptureRender';
44
+ export const CAPTURE_EXCLUDE_KEY =
45
+ 'mujoco.capture.exclude';
46
+
47
+ export type CameraFrameCaptureRenderInput = {
48
+ renderer: THREE.WebGLRenderer;
49
+ scene: THREE.Scene;
50
+ camera: THREE.Camera;
51
+ target: THREE.WebGLRenderTarget;
52
+ width: number;
53
+ height: number;
54
+ };
55
+
56
+ export type CameraFrameCaptureRenderResult = {
57
+ pixels: Uint8Array;
58
+ width?: number;
59
+ height?: number;
60
+ flipY?: boolean;
61
+ };
62
+
63
+ type CameraFrameCaptureRender = (
64
+ input: CameraFrameCaptureRenderInput
65
+ ) =>
66
+ | CameraFrameCaptureRenderResult
67
+ | null
68
+ | undefined
69
+ | Promise<CameraFrameCaptureRenderResult | null | undefined>;
70
+
71
+ type RendererState = {
72
+ target: THREE.WebGLRenderTarget | null;
73
+ xrEnabled: boolean;
74
+ viewport: THREE.Vector4;
75
+ scissor: THREE.Vector4;
76
+ scissorTest: boolean;
77
+ clearColor: THREE.Color;
78
+ clearAlpha: number;
79
+ autoClear: boolean;
80
+ };
81
+
82
+ type VisibilityState = {
83
+ object: THREE.Object3D;
84
+ visible: boolean;
85
+ };
86
+
32
87
  function toVector3(
33
88
  value: CameraFrameCaptureVector3 | undefined,
34
89
  fallback: THREE.Vector3
@@ -136,21 +191,79 @@ function readRenderTargetToCanvas(
136
191
  pixels: Uint8Array,
137
192
  imageData: ImageData,
138
193
  width: number,
139
- height: number
194
+ height: number,
195
+ outputColorSpace: string
140
196
  ) {
141
197
  renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
142
198
 
143
199
  const rowBytes = width * 4;
200
+ const encodeSrgb = outputColorSpace === THREE.SRGBColorSpace;
144
201
  for (let y = 0; y < height; y += 1) {
145
202
  const sourceStart = (height - y - 1) * rowBytes;
146
203
  const targetStart = y * rowBytes;
204
+ const row = pixels.subarray(sourceStart, sourceStart + rowBytes);
205
+ if (!encodeSrgb) {
206
+ imageData.data.set(row, targetStart);
207
+ continue;
208
+ }
209
+
210
+ for (let x = 0; x < rowBytes; x += 4) {
211
+ const pixelOffset = targetStart + x;
212
+ imageData.data[pixelOffset] = linearByteToSrgbByte(row[x]);
213
+ imageData.data[pixelOffset + 1] = linearByteToSrgbByte(row[x + 1]);
214
+ imageData.data[pixelOffset + 2] = linearByteToSrgbByte(row[x + 2]);
215
+ imageData.data[pixelOffset + 3] = row[x + 3];
216
+ }
217
+ }
218
+ context.putImageData(imageData, 0, 0);
219
+ return canvas;
220
+ }
221
+
222
+ function linearByteToSrgbByte(value: number) {
223
+ const normalized = value / 255;
224
+ const encoded =
225
+ normalized <= 0.0031308
226
+ ? normalized * 12.92
227
+ : 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;
228
+ return Math.min(255, Math.max(0, Math.round(encoded * 255)));
229
+ }
230
+
231
+ function readPixelsToCanvas(
232
+ pixels: Uint8Array,
233
+ context: CanvasRenderingContext2D,
234
+ imageData: ImageData,
235
+ width: number,
236
+ height: number,
237
+ flipY = true
238
+ ) {
239
+ const rowBytes = width * 4;
240
+ for (let y = 0; y < height; y += 1) {
241
+ const sourceY = flipY ? height - y - 1 : y;
242
+ const sourceStart = sourceY * rowBytes;
243
+ const targetStart = y * rowBytes;
147
244
  imageData.data.set(
148
245
  pixels.subarray(sourceStart, sourceStart + rowBytes),
149
246
  targetStart
150
247
  );
151
248
  }
152
249
  context.putImageData(imageData, 0, 0);
153
- return canvas;
250
+ }
251
+
252
+ function hideExcludedCaptureObjects(scene: THREE.Scene): VisibilityState[] {
253
+ const hidden: VisibilityState[] = [];
254
+ scene.traverse((object) => {
255
+ if (!object.visible) return;
256
+ if (!object.userData[CAPTURE_EXCLUDE_KEY]) return;
257
+ hidden.push({ object, visible: object.visible });
258
+ object.visible = false;
259
+ });
260
+ return hidden;
261
+ }
262
+
263
+ function restoreObjectVisibility(hidden: VisibilityState[]) {
264
+ for (const { object, visible } of hidden) {
265
+ object.visible = visible;
266
+ }
154
267
  }
155
268
 
156
269
  function getCameraFrameCaptureSource(
@@ -173,6 +286,52 @@ function getCameraFrameCaptureSource(
173
286
  return { kind: 'fallback-camera' };
174
287
  }
175
288
 
289
+ function saveRendererState(renderer: THREE.WebGLRenderer): RendererState {
290
+ const viewport = new THREE.Vector4();
291
+ const scissor = new THREE.Vector4();
292
+ const clearColor = new THREE.Color();
293
+ renderer.getViewport(viewport);
294
+ renderer.getScissor(scissor);
295
+ renderer.getClearColor(clearColor);
296
+ return {
297
+ target: renderer.getRenderTarget(),
298
+ xrEnabled: renderer.xr.enabled,
299
+ viewport,
300
+ scissor,
301
+ scissorTest: renderer.getScissorTest(),
302
+ clearColor,
303
+ clearAlpha: renderer.getClearAlpha(),
304
+ autoClear: renderer.autoClear,
305
+ };
306
+ }
307
+
308
+ function restoreRendererState(
309
+ renderer: THREE.WebGLRenderer,
310
+ state: RendererState
311
+ ) {
312
+ renderer.setRenderTarget(state.target);
313
+ renderer.xr.enabled = state.xrEnabled;
314
+ renderer.setViewport(state.viewport);
315
+ renderer.setScissor(state.scissor);
316
+ renderer.setScissorTest(state.scissorTest);
317
+ renderer.setClearColor(state.clearColor, state.clearAlpha);
318
+ renderer.autoClear = state.autoClear;
319
+ }
320
+
321
+ function getCaptureRenderer(
322
+ scene: THREE.Scene
323
+ ): CameraFrameCaptureRender | null {
324
+ const renderers: CameraFrameCaptureRender[] = [];
325
+ scene.traverse((object) => {
326
+ if (renderers.length) return;
327
+ const render = object.userData[
328
+ CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY
329
+ ] as CameraFrameCaptureRender | undefined;
330
+ if (typeof render === 'function') renderers.push(render);
331
+ });
332
+ return renderers[0] ?? null;
333
+ }
334
+
176
335
  export function createCameraFrameCaptureSession(
177
336
  renderer: THREE.WebGLRenderer,
178
337
  scene: THREE.Scene,
@@ -198,7 +357,7 @@ export function createCameraFrameCaptureSession(
198
357
  const pixels = new Uint8Array(width * height * 4);
199
358
  const imageData = drawContext.createImageData(width, height);
200
359
 
201
- function capture(nextOptions: CameraFrameCaptureOptions = {}) {
360
+ function resolveCaptureOptions(nextOptions: CameraFrameCaptureOptions = {}) {
202
361
  const captureOptions = { ...options, ...nextOptions };
203
362
  const nextDimensions = getCaptureDimensions(renderer, captureOptions);
204
363
  if (
@@ -218,13 +377,20 @@ export function createCameraFrameCaptureSession(
218
377
  height
219
378
  );
220
379
 
221
- const previousTarget = renderer.getRenderTarget();
222
- const previousXrEnabled = renderer.xr.enabled;
380
+ return captureOptions;
381
+ }
382
+
383
+ function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
384
+ const previousState = saveRendererState(renderer);
385
+ const hidden = hideExcludedCaptureObjects(scene);
223
386
 
224
387
  scene.updateMatrixWorld(true);
225
388
  try {
226
389
  renderer.xr.enabled = false;
227
390
  renderer.setRenderTarget(target);
391
+ renderer.setViewport(0, 0, width, height);
392
+ renderer.setScissor(0, 0, width, height);
393
+ renderer.setScissorTest(false);
228
394
  renderer.clear();
229
395
  renderer.render(scene, camera);
230
396
  readRenderTargetToCanvas(
@@ -235,7 +401,8 @@ export function createCameraFrameCaptureSession(
235
401
  pixels,
236
402
  imageData,
237
403
  width,
238
- height
404
+ height,
405
+ renderer.outputColorSpace
239
406
  );
240
407
  return {
241
408
  canvas,
@@ -245,15 +412,69 @@ export function createCameraFrameCaptureSession(
245
412
  source: getCameraFrameCaptureSource(captureOptions),
246
413
  };
247
414
  } finally {
248
- renderer.setRenderTarget(previousTarget);
249
- renderer.xr.enabled = previousXrEnabled;
415
+ restoreObjectVisibility(hidden);
416
+ restoreRendererState(renderer, previousState);
417
+ }
418
+ }
419
+
420
+ function capture(nextOptions: CameraFrameCaptureOptions = {}) {
421
+ return renderPreparedCapture(resolveCaptureOptions(nextOptions));
422
+ }
423
+
424
+ async function captureAsync(nextOptions: CameraFrameCaptureOptions = {}) {
425
+ const captureOptions = resolveCaptureOptions(nextOptions);
426
+ scene.updateMatrixWorld(true);
427
+ const captureRenderer = getCaptureRenderer(scene);
428
+ if (captureRenderer) {
429
+ const previousState = saveRendererState(renderer);
430
+ const hidden = hideExcludedCaptureObjects(scene);
431
+ try {
432
+ renderer.xr.enabled = false;
433
+ const captureResult = await captureRenderer({
434
+ renderer,
435
+ scene,
436
+ camera,
437
+ target,
438
+ width,
439
+ height,
440
+ });
441
+ if (captureResult) {
442
+ const captureWidth = captureResult.width ?? width;
443
+ const captureHeight = captureResult.height ?? height;
444
+ if (captureWidth !== width || captureHeight !== height) {
445
+ throw new Error(
446
+ 'Camera frame capture renderer returned unexpected dimensions.'
447
+ );
448
+ }
449
+ readPixelsToCanvas(
450
+ captureResult.pixels,
451
+ drawContext,
452
+ imageData,
453
+ width,
454
+ height,
455
+ captureResult.flipY ?? true
456
+ );
457
+ return {
458
+ canvas,
459
+ camera,
460
+ width,
461
+ height,
462
+ source: getCameraFrameCaptureSource(captureOptions),
463
+ };
464
+ }
465
+ } finally {
466
+ restoreObjectVisibility(hidden);
467
+ restoreRendererState(renderer, previousState);
468
+ }
250
469
  }
470
+ return renderPreparedCapture(captureOptions);
251
471
  }
252
472
 
253
473
  return {
254
474
  width,
255
475
  height,
256
476
  capture,
477
+ captureAsync,
257
478
  captureDataUrl(nextOptions = {}) {
258
479
  const type = nextOptions.type ?? options.type ?? 'image/png';
259
480
  const result = capture(nextOptions);
@@ -266,9 +487,21 @@ export function createCameraFrameCaptureSession(
266
487
  type,
267
488
  };
268
489
  },
490
+ async captureDataUrlAsync(nextOptions = {}) {
491
+ const type = nextOptions.type ?? options.type ?? 'image/png';
492
+ const result = await captureAsync(nextOptions);
493
+ return {
494
+ ...result,
495
+ dataUrl: result.canvas.toDataURL(
496
+ type,
497
+ nextOptions.quality ?? options.quality
498
+ ),
499
+ type,
500
+ };
501
+ },
269
502
  async captureBlob(nextOptions = {}) {
270
503
  const type = nextOptions.type ?? options.type ?? 'image/png';
271
- const result = capture(nextOptions);
504
+ const result = await captureAsync(nextOptions);
272
505
  const blob = await new Promise<Blob>((resolve, reject) => {
273
506
  result.canvas.toBlob(
274
507
  (nextBlob) => {
@@ -313,17 +546,22 @@ export async function captureCameraFrame(
313
546
  options: CameraFrameCaptureOptions = {}
314
547
  ): Promise<CameraFrameCaptureResult> {
315
548
  const type = options.type ?? 'image/png';
316
- const result = renderCameraFrameToCanvas(
549
+ const session = createCameraFrameCaptureSession(
317
550
  renderer,
318
551
  scene,
319
552
  fallbackCamera,
320
553
  options
321
554
  );
322
- return {
323
- ...result,
324
- dataUrl: result.canvas.toDataURL(type, options.quality),
325
- type,
326
- };
555
+ try {
556
+ const result = await session.captureAsync();
557
+ return {
558
+ ...result,
559
+ dataUrl: result.canvas.toDataURL(type, options.quality),
560
+ type,
561
+ };
562
+ } finally {
563
+ session.dispose();
564
+ }
327
565
  }
328
566
 
329
567
  export async function captureCameraFrameBlob(
@@ -332,22 +570,15 @@ export async function captureCameraFrameBlob(
332
570
  fallbackCamera: THREE.Camera,
333
571
  options: CameraFrameCaptureOptions = {}
334
572
  ): Promise<CameraFrameCaptureBlobResult> {
335
- const type = options.type ?? 'image/png';
336
- const result = renderCameraFrameToCanvas(
573
+ const session = createCameraFrameCaptureSession(
337
574
  renderer,
338
575
  scene,
339
576
  fallbackCamera,
340
577
  options
341
578
  );
342
- const blob = await new Promise<Blob>((resolve, reject) => {
343
- result.canvas.toBlob(
344
- (nextBlob) => {
345
- if (nextBlob) resolve(nextBlob);
346
- else reject(new Error('Camera frame capture did not produce a Blob.'));
347
- },
348
- type,
349
- options.quality
350
- );
351
- });
352
- return { ...result, blob, type };
579
+ try {
580
+ return await session.captureBlob();
581
+ } finally {
582
+ session.dispose();
583
+ }
353
584
  }