@bendyline/squisq-editor-react 2.11.3 → 2.11.5
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/dist/{chunk-PMNNJYFD.js → chunk-34FRFDQ6.js} +138 -30
- package/dist/{chunk-7RRYZULY.js → chunk-BJ3TXAIQ.js} +155 -15
- package/dist/{chunk-VH45VMPL.js → chunk-DN6XFOY2.js} +1 -1
- package/dist/chunk-DUO5XZQK.js +1653 -0
- package/dist/{chunk-UNOKKSAC.js → chunk-ENDRPV6T.js} +1 -1
- package/dist/{chunk-A2W4GYPG.js → chunk-ES6CPRCW.js} +5 -4
- package/dist/{chunk-SK3MQSHQ.js → chunk-UYKOTWJM.js} +1 -1
- package/dist/{chunk-AAIZHMVD.js → chunk-WMNH5ROU.js} +2802 -3764
- package/dist/index.d.ts +9 -57
- package/dist/index.js +19 -17
- package/dist/json-editor/index.js +2 -2
- package/dist/proofing/index.d.ts +2 -2
- package/dist/recorder/index.d.ts +2 -2
- package/dist/recorder/index.js +3 -3
- package/dist/{recorder-GrEh4xE8.d.ts → recorder-B6MtQH5u.d.ts} +4 -2
- package/dist/review/index.d.ts +285 -0
- package/dist/review/index.js +103 -0
- package/dist/shell/index.d.ts +4 -2
- package/dist/shell/index.js +9 -7
- package/dist/{shell-CJkHDwpz.d.ts → shell-Dwf_W4K4.d.ts} +116 -3
- package/dist/styles/index.css +152 -0
- package/dist/teleprompter/index.d.ts +2 -2
- package/dist/teleprompter/index.js +2 -2
- package/dist/types-C0puuw-M.d.ts +81 -0
- package/dist/{types-B_aHRuSc.d.ts → types-De4z-gGb.d.ts} +1 -1
- package/dist/{useNarrationStage-B6r9Zlq-.d.ts → useNarrationStage-D3Sgcf0L.d.ts} +19 -0
- package/package.json +10 -6
|
@@ -1344,6 +1344,17 @@ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef4
|
|
|
1344
1344
|
import {
|
|
1345
1345
|
alignNarration
|
|
1346
1346
|
} from "@bendyline/squisq/narration";
|
|
1347
|
+
|
|
1348
|
+
// src/recorder/recordingLimits.ts
|
|
1349
|
+
var DEFAULT_MAX_RECORDING_BYTES = 900 * 1024 * 1024;
|
|
1350
|
+
function resolveMaxRecordingBytes(value) {
|
|
1351
|
+
return value !== void 0 && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_MAX_RECORDING_BYTES;
|
|
1352
|
+
}
|
|
1353
|
+
function formatRecordingBytes(bytes) {
|
|
1354
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/teleprompter/recording/useNarrationRecorder.ts
|
|
1347
1358
|
var TRACE_INTERVAL_MS = 250;
|
|
1348
1359
|
function mixdownToMono(buffer) {
|
|
1349
1360
|
const channels = buffer.numberOfChannels;
|
|
@@ -1377,6 +1388,11 @@ function stopRecorder(recorder) {
|
|
|
1377
1388
|
function useNarrationRecorder(options) {
|
|
1378
1389
|
const [state, setState] = useState4("idle");
|
|
1379
1390
|
const [error, setError] = useState4(null);
|
|
1391
|
+
const [recordedBytes, setRecordedBytes] = useState4(0);
|
|
1392
|
+
const [limitReached, setLimitReached] = useState4(false);
|
|
1393
|
+
const maxRecordingBytes = resolveMaxRecordingBytes(options.maxRecordingBytes);
|
|
1394
|
+
const stopRef = useRef4(async () => {
|
|
1395
|
+
});
|
|
1380
1396
|
const [withCamera, setWithCamera] = useState4(false);
|
|
1381
1397
|
const [cameraStream, setCameraStream] = useState4(null);
|
|
1382
1398
|
const [take, setTake] = useState4(null);
|
|
@@ -1423,8 +1439,11 @@ function useNarrationRecorder(options) {
|
|
|
1423
1439
|
const superseded = () => generationRef.current !== generation;
|
|
1424
1440
|
startingRef.current = true;
|
|
1425
1441
|
setError(null);
|
|
1442
|
+
setRecordedBytes(0);
|
|
1443
|
+
setLimitReached(false);
|
|
1426
1444
|
applyTake(null);
|
|
1427
1445
|
setState("starting");
|
|
1446
|
+
const recordingByteLimit = resolveMaxRecordingBytes(opts.maxRecordingBytes);
|
|
1428
1447
|
let camera = null;
|
|
1429
1448
|
let audioRecorder = null;
|
|
1430
1449
|
let cameraRecorder = null;
|
|
@@ -1493,17 +1512,30 @@ function useNarrationRecorder(options) {
|
|
|
1493
1512
|
traceTimer: null,
|
|
1494
1513
|
startedAtMs: performance.now(),
|
|
1495
1514
|
audioStartMs: null,
|
|
1496
|
-
cameraStartMs: null
|
|
1515
|
+
cameraStartMs: null,
|
|
1516
|
+
recordedBytes: 0,
|
|
1517
|
+
limitReached: false
|
|
1518
|
+
};
|
|
1519
|
+
const retainChunk = (chunks, chunk) => {
|
|
1520
|
+
if (chunk.size === 0 || superseded()) return;
|
|
1521
|
+
chunks.push(chunk);
|
|
1522
|
+
capture.recordedBytes += chunk.size;
|
|
1523
|
+
setRecordedBytes(capture.recordedBytes);
|
|
1524
|
+
if (!capture.limitReached && capture.recordedBytes >= recordingByteLimit) {
|
|
1525
|
+
capture.limitReached = true;
|
|
1526
|
+
setLimitReached(true);
|
|
1527
|
+
if (captureRef.current === capture) void stopRef.current();
|
|
1528
|
+
}
|
|
1497
1529
|
};
|
|
1498
1530
|
audioRecorder.ondataavailable = (e) => {
|
|
1499
|
-
|
|
1531
|
+
retainChunk(capture.audioChunks, e.data);
|
|
1500
1532
|
};
|
|
1501
1533
|
audioRecorder.onstart = () => {
|
|
1502
1534
|
capture.audioStartMs = performance.now();
|
|
1503
1535
|
};
|
|
1504
1536
|
if (cameraRecorder) {
|
|
1505
1537
|
cameraRecorder.ondataavailable = (e) => {
|
|
1506
|
-
|
|
1538
|
+
retainChunk(capture.cameraChunks, e.data);
|
|
1507
1539
|
};
|
|
1508
1540
|
cameraRecorder.onstart = () => {
|
|
1509
1541
|
capture.cameraStartMs = performance.now();
|
|
@@ -1552,8 +1584,10 @@ function useNarrationRecorder(options) {
|
|
|
1552
1584
|
optionsRef.current.onRecordingStop?.();
|
|
1553
1585
|
processingRef.current = true;
|
|
1554
1586
|
setState("processing");
|
|
1555
|
-
await
|
|
1556
|
-
|
|
1587
|
+
await Promise.all([
|
|
1588
|
+
stopRecorder(capture.audioRecorder),
|
|
1589
|
+
...capture.cameraRecorder ? [stopRecorder(capture.cameraRecorder)] : []
|
|
1590
|
+
]);
|
|
1557
1591
|
for (const track of capture.cameraStream?.getTracks() ?? []) track.stop();
|
|
1558
1592
|
setCameraStream(null);
|
|
1559
1593
|
const audioMime = capture.audioRecorder.mimeType || capture.audioMime;
|
|
@@ -1613,16 +1647,21 @@ function useNarrationRecorder(options) {
|
|
|
1613
1647
|
});
|
|
1614
1648
|
setState("review");
|
|
1615
1649
|
}, [applyTake, cancelPendingStart]);
|
|
1650
|
+
stopRef.current = stop;
|
|
1616
1651
|
const retake = useCallback4(() => {
|
|
1617
1652
|
teardownCapture();
|
|
1618
1653
|
applyTake(null);
|
|
1619
1654
|
setError(null);
|
|
1655
|
+
setRecordedBytes(0);
|
|
1656
|
+
setLimitReached(false);
|
|
1620
1657
|
setState("idle");
|
|
1621
1658
|
}, [applyTake, teardownCapture]);
|
|
1622
1659
|
const discard = useCallback4(() => {
|
|
1623
1660
|
teardownCapture();
|
|
1624
1661
|
applyTake(null);
|
|
1625
1662
|
setError(null);
|
|
1663
|
+
setRecordedBytes(0);
|
|
1664
|
+
setLimitReached(false);
|
|
1626
1665
|
setState("idle");
|
|
1627
1666
|
}, [applyTake, teardownCapture]);
|
|
1628
1667
|
const beginSave = useCallback4(() => setState("saving"), []);
|
|
@@ -1631,6 +1670,8 @@ function useNarrationRecorder(options) {
|
|
|
1631
1670
|
if (ok) {
|
|
1632
1671
|
applyTake(null);
|
|
1633
1672
|
setError(null);
|
|
1673
|
+
setRecordedBytes(0);
|
|
1674
|
+
setLimitReached(false);
|
|
1634
1675
|
setState("idle");
|
|
1635
1676
|
} else {
|
|
1636
1677
|
setError(saveError ?? new Error("Save failed"));
|
|
@@ -1658,6 +1699,9 @@ function useNarrationRecorder(options) {
|
|
|
1658
1699
|
return {
|
|
1659
1700
|
state,
|
|
1660
1701
|
error,
|
|
1702
|
+
recordedBytes,
|
|
1703
|
+
maxRecordingBytes,
|
|
1704
|
+
limitReached,
|
|
1661
1705
|
withCamera,
|
|
1662
1706
|
setWithCamera,
|
|
1663
1707
|
cameraStream,
|
|
@@ -1818,6 +1862,7 @@ function useNarrationStage(opts) {
|
|
|
1818
1862
|
getWordPos: () => controllerRef.current.wordPos,
|
|
1819
1863
|
getMicDeviceId: () => controllerRef.current.prefs.micDeviceId,
|
|
1820
1864
|
cameraConstraints: opts.cameraConstraints,
|
|
1865
|
+
maxRecordingBytes: opts.maxRecordingBytes,
|
|
1821
1866
|
audioRecorderOptions: opts.audioRecorderOptions,
|
|
1822
1867
|
cameraRecorderOptions: opts.cameraRecorderOptions,
|
|
1823
1868
|
onRecordingStart: () => controllerRef.current.play(),
|
|
@@ -2379,7 +2424,7 @@ function TeleprompterControls({
|
|
|
2379
2424
|
}
|
|
2380
2425
|
|
|
2381
2426
|
// src/teleprompter/NarrationStage.tsx
|
|
2382
|
-
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef8 } from "react";
|
|
2427
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef8, useState as useState6 } from "react";
|
|
2383
2428
|
import { createPortal } from "react-dom";
|
|
2384
2429
|
|
|
2385
2430
|
// src/teleprompter/TeleprompterSelfView.tsx
|
|
@@ -2517,6 +2562,47 @@ function drawPrompterFrame(canvas, frame) {
|
|
|
2517
2562
|
|
|
2518
2563
|
// src/teleprompter/NarrationStage.tsx
|
|
2519
2564
|
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2565
|
+
function NarrationTakeBackups({ take, baseWpm }) {
|
|
2566
|
+
const [downloads, setDownloads] = useState6(
|
|
2567
|
+
[]
|
|
2568
|
+
);
|
|
2569
|
+
useEffect8(() => {
|
|
2570
|
+
const timing = buildNarrationSavePlan({
|
|
2571
|
+
script: take.script,
|
|
2572
|
+
alignment: take.alignment,
|
|
2573
|
+
durationSec: take.durationSec,
|
|
2574
|
+
audioExt: take.audioExt,
|
|
2575
|
+
cameraExt: take.cameraExt,
|
|
2576
|
+
cameraOffsetSec: take.cameraOffsetSec,
|
|
2577
|
+
baseWpm
|
|
2578
|
+
}).sidecarPayload;
|
|
2579
|
+
const assets = [
|
|
2580
|
+
{
|
|
2581
|
+
label: "Download audio backup",
|
|
2582
|
+
blob: take.audioBlob,
|
|
2583
|
+
filename: "narration" + take.audioExt
|
|
2584
|
+
},
|
|
2585
|
+
...take.cameraBlob ? [
|
|
2586
|
+
{
|
|
2587
|
+
label: "Download camera backup",
|
|
2588
|
+
blob: take.cameraBlob,
|
|
2589
|
+
filename: "narration-camera" + (take.cameraExt ?? ".webm")
|
|
2590
|
+
}
|
|
2591
|
+
] : [],
|
|
2592
|
+
{
|
|
2593
|
+
label: "Download timing backup",
|
|
2594
|
+
blob: new Blob([JSON.stringify(timing, null, 2)], { type: "application/json" }),
|
|
2595
|
+
filename: "narration" + take.audioExt + ".timing.json"
|
|
2596
|
+
}
|
|
2597
|
+
];
|
|
2598
|
+
const next = assets.map(({ blob, ...asset }) => ({ ...asset, url: URL.createObjectURL(blob) }));
|
|
2599
|
+
setDownloads(next);
|
|
2600
|
+
return () => {
|
|
2601
|
+
for (const download of next) URL.revokeObjectURL(download.url);
|
|
2602
|
+
};
|
|
2603
|
+
}, [take, baseWpm]);
|
|
2604
|
+
return downloads.map(({ label, url, filename }) => /* @__PURE__ */ jsx4("a", { href: url, download: filename, children: label }, label));
|
|
2605
|
+
}
|
|
2520
2606
|
function NarrationStage(props) {
|
|
2521
2607
|
const {
|
|
2522
2608
|
stage,
|
|
@@ -2632,32 +2718,45 @@ function NarrationStage(props) {
|
|
|
2632
2718
|
const script = controller.script;
|
|
2633
2719
|
const portalOpen = float.portalTarget !== null;
|
|
2634
2720
|
const busyRecording = recorder.state === "recording" || recorder.state === "starting";
|
|
2635
|
-
const recordSlot = recording && showRecordSlot ? /* @__PURE__ */
|
|
2636
|
-
/* @__PURE__ */
|
|
2637
|
-
"
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
children: "\u23FA Record"
|
|
2643
|
-
}
|
|
2644
|
-
),
|
|
2645
|
-
showCameraToggleInRecordSlot ? /* @__PURE__ */ jsxs3("label", { title: "Also capture your camera as a separate video file", children: [
|
|
2721
|
+
const recordSlot = recording && showRecordSlot ? /* @__PURE__ */ jsxs3("span", { className: "squisq-teleprompter-group", "data-testid": "teleprompter-record", children: [
|
|
2722
|
+
/* @__PURE__ */ jsxs3("span", { children: [
|
|
2723
|
+
"Auto-stops at ",
|
|
2724
|
+
formatRecordingBytes(recorder.maxRecordingBytes),
|
|
2725
|
+
"."
|
|
2726
|
+
] }),
|
|
2727
|
+
recorder.state === "idle" || recorder.state === "error" ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
2646
2728
|
/* @__PURE__ */ jsx4(
|
|
2647
|
-
"
|
|
2729
|
+
"button",
|
|
2648
2730
|
{
|
|
2649
|
-
type: "
|
|
2650
|
-
|
|
2651
|
-
|
|
2731
|
+
type: "button",
|
|
2732
|
+
onClick: () => void recorder.start(),
|
|
2733
|
+
title: "Record narration while you read",
|
|
2734
|
+
children: "\u23FA Record"
|
|
2652
2735
|
}
|
|
2653
2736
|
),
|
|
2654
|
-
"camera"
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2737
|
+
showCameraToggleInRecordSlot ? /* @__PURE__ */ jsxs3("label", { title: "Also capture your camera as a separate video file", children: [
|
|
2738
|
+
/* @__PURE__ */ jsx4(
|
|
2739
|
+
"input",
|
|
2740
|
+
{
|
|
2741
|
+
type: "checkbox",
|
|
2742
|
+
checked: recorder.withCamera,
|
|
2743
|
+
onChange: (e) => recorder.setWithCamera(e.target.checked)
|
|
2744
|
+
}
|
|
2745
|
+
),
|
|
2746
|
+
"camera"
|
|
2747
|
+
] }) : null,
|
|
2748
|
+
recorder.state === "error" ? /* @__PURE__ */ jsxs3("span", { title: recorder.error?.message, children: [
|
|
2749
|
+
"\u26A0 ",
|
|
2750
|
+
recorder.error?.message
|
|
2751
|
+
] }) : null
|
|
2752
|
+
] }) : busyRecording ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
2753
|
+
/* @__PURE__ */ jsxs3("span", { children: [
|
|
2754
|
+
formatRecordingBytes(recorder.recordedBytes),
|
|
2755
|
+
" recorded"
|
|
2756
|
+
] }),
|
|
2757
|
+
/* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void recorder.stop(), children: "\u23F9 Stop" })
|
|
2758
|
+
] }) : recorder.state === "processing" ? /* @__PURE__ */ jsx4("span", { children: "Aligning take\u2026" }) : recorder.state === "saving" ? /* @__PURE__ */ jsx4("span", { children: "Saving\u2026" }) : null
|
|
2759
|
+
] }) : null;
|
|
2661
2760
|
return /* @__PURE__ */ jsxs3("div", { ref: rootRef, className: "squisq-teleprompter-root", "data-testid": "teleprompter-view", children: [
|
|
2662
2761
|
/* @__PURE__ */ jsxs3("div", { className: "squisq-teleprompter-stage", children: [
|
|
2663
2762
|
portalOpen ? /* @__PURE__ */ jsxs3("div", { className: "squisq-teleprompter-float-note", children: [
|
|
@@ -2689,6 +2788,11 @@ function NarrationStage(props) {
|
|
|
2689
2788
|
) : null,
|
|
2690
2789
|
showSelfView ? /* @__PURE__ */ jsx4(TeleprompterSelfView, { stream: recorder.cameraStream }) : null
|
|
2691
2790
|
] }),
|
|
2791
|
+
recorder.limitReached ? /* @__PURE__ */ jsxs3("div", { className: "squisq-teleprompter-review", role: "status", children: [
|
|
2792
|
+
"Recording stopped at the ",
|
|
2793
|
+
formatRecordingBytes(recorder.maxRecordingBytes),
|
|
2794
|
+
" size limit. The complete take is retained; save it or download a backup before leaving."
|
|
2795
|
+
] }) : null,
|
|
2692
2796
|
recorder.state === "review" && recorder.take ? /* @__PURE__ */ jsxs3("div", { className: "squisq-teleprompter-review", "data-testid": "teleprompter-review", children: [
|
|
2693
2797
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
2694
2798
|
"Take: ",
|
|
@@ -2704,14 +2808,16 @@ function NarrationStage(props) {
|
|
|
2704
2808
|
onTimeUpdate: stage.handleReviewTimeUpdate
|
|
2705
2809
|
}
|
|
2706
2810
|
) : null,
|
|
2811
|
+
/* @__PURE__ */ jsx4(NarrationTakeBackups, { take: recorder.take, baseWpm: controller.prefs.baseWpm }),
|
|
2707
2812
|
showReviewActions ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
2708
2813
|
/* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void stage.handleSave(), children: "\u2713 Save narration" }),
|
|
2709
2814
|
/* @__PURE__ */ jsx4("button", { type: "button", onClick: stage.handleRetake, children: "\u21BA Retake" }),
|
|
2710
2815
|
/* @__PURE__ */ jsx4("button", { type: "button", onClick: stage.handleDiscard, children: "\u2715 Discard" })
|
|
2711
2816
|
] }) : null,
|
|
2712
|
-
recorder.error ? /* @__PURE__ */ jsxs3("span", { children: [
|
|
2817
|
+
recorder.error ? /* @__PURE__ */ jsxs3("span", { role: "alert", children: [
|
|
2713
2818
|
"\u26A0 ",
|
|
2714
|
-
recorder.error.message
|
|
2819
|
+
recorder.error.message,
|
|
2820
|
+
" Your take is still available. Download a backup before leaving."
|
|
2715
2821
|
] }) : null
|
|
2716
2822
|
] }) : null,
|
|
2717
2823
|
stage.saveNotice ? /* @__PURE__ */ jsxs3("div", { className: "squisq-teleprompter-review", "data-testid": "teleprompter-save-notice", children: [
|
|
@@ -2731,6 +2837,8 @@ function NarrationStage(props) {
|
|
|
2731
2837
|
}
|
|
2732
2838
|
|
|
2733
2839
|
export {
|
|
2840
|
+
resolveMaxRecordingBytes,
|
|
2841
|
+
formatRecordingBytes,
|
|
2734
2842
|
resolveFormat,
|
|
2735
2843
|
supportsMediaRecorder,
|
|
2736
2844
|
supportsUserMedia,
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
buildTimingJson,
|
|
8
8
|
encodeNarrationTimingJson,
|
|
9
9
|
encodeTimingJson,
|
|
10
|
+
formatRecordingBytes,
|
|
10
11
|
requestCameraStream,
|
|
11
12
|
requestMicStream,
|
|
12
13
|
resolveFormat,
|
|
14
|
+
resolveMaxRecordingBytes,
|
|
13
15
|
supportsDisplayMedia,
|
|
14
16
|
supportsMediaRecorder,
|
|
15
17
|
supportsSystemAudioCapture,
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
timingPathFor,
|
|
18
20
|
useNarrationStage,
|
|
19
21
|
useStreamPreview
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-34FRFDQ6.js";
|
|
21
23
|
|
|
22
24
|
// src/recorder/sources/screenStream.ts
|
|
23
25
|
function mixAudioTracks(streams) {
|
|
@@ -245,6 +247,9 @@ function getCaptureKind(source) {
|
|
|
245
247
|
}
|
|
246
248
|
function useMediaRecorder(options = {}) {
|
|
247
249
|
const [state, setState] = useState("idle");
|
|
250
|
+
const [recordedBytes, setRecordedBytes] = useState(0);
|
|
251
|
+
const [limitReached, setLimitReached] = useState(false);
|
|
252
|
+
const recordedBytesRef = useRef(0);
|
|
248
253
|
const [stream, setStream] = useState(null);
|
|
249
254
|
const [blob, setBlob] = useState(null);
|
|
250
255
|
const [format, setFormat] = useState(null);
|
|
@@ -320,7 +325,23 @@ function useMediaRecorder(options = {}) {
|
|
|
320
325
|
setCamera(null);
|
|
321
326
|
setCameraOffsetSec(null);
|
|
322
327
|
}, []);
|
|
328
|
+
const resetByteCount = useCallback(() => {
|
|
329
|
+
recordedBytesRef.current = 0;
|
|
330
|
+
setRecordedBytes(0);
|
|
331
|
+
setLimitReached(false);
|
|
332
|
+
}, []);
|
|
333
|
+
const recordChunk = useCallback((data, chunks) => {
|
|
334
|
+
if (data.size === 0) return;
|
|
335
|
+
chunks.push(data);
|
|
336
|
+
recordedBytesRef.current += data.size;
|
|
337
|
+
setRecordedBytes(recordedBytesRef.current);
|
|
338
|
+
if (stateRef.current === "recording" && recordedBytesRef.current >= resolveMaxRecordingBytes(optionsRef.current.maxRecordingBytes)) {
|
|
339
|
+
setLimitReached(true);
|
|
340
|
+
void stopFnRef.current?.();
|
|
341
|
+
}
|
|
342
|
+
}, []);
|
|
323
343
|
const reset = useCallback(() => {
|
|
344
|
+
resetByteCount();
|
|
324
345
|
setBlob(null);
|
|
325
346
|
setDurationMs(0);
|
|
326
347
|
setError(null);
|
|
@@ -336,8 +357,9 @@ function useMediaRecorder(options = {}) {
|
|
|
336
357
|
} else {
|
|
337
358
|
cancelFnRef.current?.();
|
|
338
359
|
}
|
|
339
|
-
}, [clearTicker, transition]);
|
|
360
|
+
}, [clearTicker, resetByteCount, transition]);
|
|
340
361
|
const cancel = useCallback(() => {
|
|
362
|
+
resetByteCount();
|
|
341
363
|
lifecycleRef.current += 1;
|
|
342
364
|
const rec = recorderRef.current;
|
|
343
365
|
if (rec && rec.state !== "inactive") {
|
|
@@ -363,7 +385,7 @@ function useMediaRecorder(options = {}) {
|
|
|
363
385
|
setDurationMs(0);
|
|
364
386
|
setError(null);
|
|
365
387
|
transition("idle");
|
|
366
|
-
}, [clearTicker, releaseStream, releaseSecondary, transition]);
|
|
388
|
+
}, [clearTicker, releaseStream, releaseSecondary, resetByteCount, transition]);
|
|
367
389
|
const request = useCallback(async () => {
|
|
368
390
|
if (requestPromiseRef.current) return requestPromiseRef.current;
|
|
369
391
|
if (recorderRef.current?.stream.active) return;
|
|
@@ -471,7 +493,8 @@ function useMediaRecorder(options = {}) {
|
|
|
471
493
|
stopPromiseRef.current = null;
|
|
472
494
|
};
|
|
473
495
|
primary.ondataavailable = (e) => {
|
|
474
|
-
if (
|
|
496
|
+
if (lifecycle !== lifecycleRef.current || recorderRef.current !== primary) return;
|
|
497
|
+
if (e.data) recordChunk(e.data, chunksRef.current);
|
|
475
498
|
};
|
|
476
499
|
primary.onstart = () => {
|
|
477
500
|
primaryStartMsRef.current = performance.now();
|
|
@@ -479,7 +502,9 @@ function useMediaRecorder(options = {}) {
|
|
|
479
502
|
primary.onstop = () => laneStopped(primary);
|
|
480
503
|
primary.onerror = (e) => laneError(primary, e);
|
|
481
504
|
secondary.ondataavailable = (e) => {
|
|
482
|
-
|
|
505
|
+
const lane = secondaryRef.current;
|
|
506
|
+
if (lifecycle !== lifecycleRef.current || lane?.recorder !== secondary) return;
|
|
507
|
+
if (e.data) recordChunk(e.data, lane.chunks);
|
|
483
508
|
};
|
|
484
509
|
secondary.onstart = () => {
|
|
485
510
|
secondaryStartMsRef.current = performance.now();
|
|
@@ -526,7 +551,8 @@ function useMediaRecorder(options = {}) {
|
|
|
526
551
|
}
|
|
527
552
|
const recorder = new MediaRecorder(nextStream, recorderOptions);
|
|
528
553
|
recorder.ondataavailable = (e) => {
|
|
529
|
-
if (
|
|
554
|
+
if (lifecycle !== lifecycleRef.current || recorderRef.current !== recorder) return;
|
|
555
|
+
if (e.data) recordChunk(e.data, chunksRef.current);
|
|
530
556
|
};
|
|
531
557
|
recorder.onstop = () => {
|
|
532
558
|
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
@@ -585,7 +611,7 @@ function useMediaRecorder(options = {}) {
|
|
|
585
611
|
})();
|
|
586
612
|
requestPromiseRef.current = requestPromise;
|
|
587
613
|
return requestPromise;
|
|
588
|
-
}, [clearTicker, deactivateCapture, transition]);
|
|
614
|
+
}, [clearTicker, deactivateCapture, recordChunk, transition]);
|
|
589
615
|
const start = useCallback(() => {
|
|
590
616
|
const rec = recorderRef.current;
|
|
591
617
|
if (!rec) {
|
|
@@ -595,6 +621,7 @@ function useMediaRecorder(options = {}) {
|
|
|
595
621
|
return;
|
|
596
622
|
}
|
|
597
623
|
if (rec.state === "recording") return;
|
|
624
|
+
resetByteCount();
|
|
598
625
|
chunksRef.current = [];
|
|
599
626
|
if (secondaryRef.current) secondaryRef.current.chunks = [];
|
|
600
627
|
setBlob(null);
|
|
@@ -634,7 +661,7 @@ function useMediaRecorder(options = {}) {
|
|
|
634
661
|
setDurationMs(Date.now() - startTimestampRef.current);
|
|
635
662
|
}
|
|
636
663
|
}, 100);
|
|
637
|
-
}, [clearTicker, deactivateCapture, transition]);
|
|
664
|
+
}, [clearTicker, deactivateCapture, resetByteCount, transition]);
|
|
638
665
|
const getElapsedMs = useCallback(() => {
|
|
639
666
|
if (stateRef.current !== "recording") return null;
|
|
640
667
|
const startedAt = startTimestampRef.current;
|
|
@@ -647,6 +674,10 @@ function useMediaRecorder(options = {}) {
|
|
|
647
674
|
return Promise.resolve(blob);
|
|
648
675
|
}
|
|
649
676
|
transition("stopping");
|
|
677
|
+
clearTicker();
|
|
678
|
+
if (startTimestampRef.current !== null) {
|
|
679
|
+
setDurationMs(Math.max(0, Date.now() - startTimestampRef.current));
|
|
680
|
+
}
|
|
650
681
|
const secondary = secondaryRef.current;
|
|
651
682
|
const secondaryActive = secondary != null && secondary.recorder.state !== "inactive";
|
|
652
683
|
pendingLanesRef.current = secondaryActive ? 2 : 1;
|
|
@@ -706,6 +737,9 @@ function useMediaRecorder(options = {}) {
|
|
|
706
737
|
};
|
|
707
738
|
}, [releaseStream, releaseSecondary, clearTicker]);
|
|
708
739
|
return {
|
|
740
|
+
recordedBytes,
|
|
741
|
+
maxRecordingBytes: resolveMaxRecordingBytes(options.maxRecordingBytes),
|
|
742
|
+
limitReached,
|
|
709
743
|
state,
|
|
710
744
|
stream,
|
|
711
745
|
blob,
|
|
@@ -2397,7 +2431,8 @@ function RecorderModal({
|
|
|
2397
2431
|
onSave,
|
|
2398
2432
|
narration = null,
|
|
2399
2433
|
slides = null,
|
|
2400
|
-
title = "Record media"
|
|
2434
|
+
title = "Record media",
|
|
2435
|
+
maxRecordingBytes
|
|
2401
2436
|
}) {
|
|
2402
2437
|
const initialToggles = toggleStateFromMode(initialMode);
|
|
2403
2438
|
const [micOn, setMicOn] = useState3(initialToggles.micOn);
|
|
@@ -2454,6 +2489,7 @@ function RecorderModal({
|
|
|
2454
2489
|
const encoding = deviceSettings.encoding;
|
|
2455
2490
|
const recorder = useMediaRecorder({
|
|
2456
2491
|
source,
|
|
2492
|
+
maxRecordingBytes,
|
|
2457
2493
|
includeMicrophone: cameraOn ? micOn : void 0,
|
|
2458
2494
|
audioConstraints,
|
|
2459
2495
|
videoConstraints: cameraConstraints,
|
|
@@ -2477,6 +2513,7 @@ function RecorderModal({
|
|
|
2477
2513
|
basenameRef.current = basename;
|
|
2478
2514
|
const stage = useNarrationStage({
|
|
2479
2515
|
doc: narration?.doc ?? null,
|
|
2516
|
+
maxRecordingBytes,
|
|
2480
2517
|
recording: narration?.recording ?? null,
|
|
2481
2518
|
getAudioBasename: () => basenameRef.current.trim() || void 0,
|
|
2482
2519
|
micConstraints: audioConstraints,
|
|
@@ -2681,6 +2718,13 @@ function RecorderModal({
|
|
|
2681
2718
|
}
|
|
2682
2719
|
}, [captureKey, recorder]);
|
|
2683
2720
|
const handleClose = useCallback4(() => {
|
|
2721
|
+
if (!narrationOn) {
|
|
2722
|
+
if (isSaving || recorder.state === "stopping" || recorder.state === "requesting") return;
|
|
2723
|
+
if ((recorder.blob || recorder.camera?.blob || recorder.state === "recording") && !window.confirm(
|
|
2724
|
+
"Discard this recording? Download or save it before closing to keep a copy."
|
|
2725
|
+
))
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2684
2728
|
const s = stageRef.current;
|
|
2685
2729
|
if (closeNeedsConfirm(narrationOn, s.recorder.state, s.recorder.take !== null)) {
|
|
2686
2730
|
if (!window.confirm("Discard the current narration take?")) return;
|
|
@@ -2694,7 +2738,7 @@ function RecorderModal({
|
|
|
2694
2738
|
}
|
|
2695
2739
|
recorder.cancel();
|
|
2696
2740
|
onClose();
|
|
2697
|
-
}, [narrationOn, recorder, onClose]);
|
|
2741
|
+
}, [narrationOn, recorder, onClose, isSaving]);
|
|
2698
2742
|
useModalDialog({
|
|
2699
2743
|
rootRef: overlayRef,
|
|
2700
2744
|
dialogRef,
|
|
@@ -2822,7 +2866,8 @@ function RecorderModal({
|
|
|
2822
2866
|
};
|
|
2823
2867
|
dualSaveProgressRef.current = {};
|
|
2824
2868
|
onSave?.(result);
|
|
2825
|
-
|
|
2869
|
+
recorder.cancel();
|
|
2870
|
+
onClose();
|
|
2826
2871
|
} catch (err) {
|
|
2827
2872
|
setSaveError(err instanceof Error ? err.message : "Failed to save recording");
|
|
2828
2873
|
} finally {
|
|
@@ -2876,7 +2921,8 @@ function RecorderModal({
|
|
|
2876
2921
|
result.sourceText = slideTiming ? void 0 : sourceText;
|
|
2877
2922
|
}
|
|
2878
2923
|
onSave?.(result);
|
|
2879
|
-
|
|
2924
|
+
recorder.cancel();
|
|
2925
|
+
onClose();
|
|
2880
2926
|
} catch (err) {
|
|
2881
2927
|
setSaveError(err instanceof Error ? err.message : "Failed to save recording");
|
|
2882
2928
|
} finally {
|
|
@@ -2895,7 +2941,7 @@ function RecorderModal({
|
|
|
2895
2941
|
mediaProvider,
|
|
2896
2942
|
container,
|
|
2897
2943
|
onSave,
|
|
2898
|
-
|
|
2944
|
+
onClose,
|
|
2899
2945
|
writeSlideTimingSidecar
|
|
2900
2946
|
]);
|
|
2901
2947
|
const handleDiscard = useCallback4(() => {
|
|
@@ -2927,8 +2973,47 @@ function RecorderModal({
|
|
|
2927
2973
|
() => slides?.doc && showTimingCheckbox ? advanceCoverage(slides.doc, advanceLog) : null,
|
|
2928
2974
|
[slides?.doc, showTimingCheckbox, advanceLog]
|
|
2929
2975
|
);
|
|
2976
|
+
const [timingDownloadUrl, setTimingDownloadUrl] = useState3(null);
|
|
2977
|
+
const downloadFilename = useMemo(
|
|
2978
|
+
() => !recorder.blob ? "" : buildFilename(
|
|
2979
|
+
source === "mic" ? "audio" : "video",
|
|
2980
|
+
recorder.extension ?? ".webm",
|
|
2981
|
+
isDual && basename.trim() ? `${basename.trim()}-screen` : basename,
|
|
2982
|
+
filenameSeed
|
|
2983
|
+
),
|
|
2984
|
+
[source, recorder.extension, recorder.blob, isDual, basename, filenameSeed]
|
|
2985
|
+
);
|
|
2986
|
+
const timingDownloadFilename = `${downloadFilename}.timing.json`;
|
|
2987
|
+
useEffect4(() => {
|
|
2988
|
+
if (narrationOn || !canSave) {
|
|
2989
|
+
setTimingDownloadUrl(null);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
const duration = recorder.durationMs / 1e3;
|
|
2993
|
+
const encoded = slidesOn && slidesCaptureTimings && applySlideTimings && slides?.doc && advanceLog.length > 0 ? encodeNarrationTimingJson(buildAdvanceTimingJson(slides.doc, advanceLog, duration)) : source === "mic" ? encodeTimingJson(buildTimingJson(sourceText, duration)) : null;
|
|
2994
|
+
if (encoded === null) {
|
|
2995
|
+
setTimingDownloadUrl(null);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const url = URL.createObjectURL(
|
|
2999
|
+
new Blob([new Uint8Array(encoded)], { type: "application/json" })
|
|
3000
|
+
);
|
|
3001
|
+
setTimingDownloadUrl(url);
|
|
3002
|
+
return () => URL.revokeObjectURL(url);
|
|
3003
|
+
}, [
|
|
3004
|
+
narrationOn,
|
|
3005
|
+
canSave,
|
|
3006
|
+
recorder.durationMs,
|
|
3007
|
+
slidesOn,
|
|
3008
|
+
slidesCaptureTimings,
|
|
3009
|
+
applySlideTimings,
|
|
3010
|
+
slides?.doc,
|
|
3011
|
+
advanceLog,
|
|
3012
|
+
source,
|
|
3013
|
+
sourceText
|
|
3014
|
+
]);
|
|
2930
3015
|
const slideCoverageWarning = slideCoverage ? unshownSlidesWarning(slideCoverage.total, slideCoverage.unshown) : null;
|
|
2931
|
-
const togglesLocked = recorder.state === "recording" || recorder.state === "requesting" || canSave;
|
|
3016
|
+
const togglesLocked = recorder.state === "recording" || recorder.state === "requesting" || recorder.state === "stopping" || canSave;
|
|
2932
3017
|
const toggleLockReason = canSave ? "Save or discard this recording before changing sources" : void 0;
|
|
2933
3018
|
const systemAudioHasCompanion = micOn || cameraOn || screenOn;
|
|
2934
3019
|
const simpleToggleProps = (key) => {
|
|
@@ -3110,7 +3195,25 @@ function RecorderModal({
|
|
|
3110
3195
|
}
|
|
3111
3196
|
),
|
|
3112
3197
|
!narrationOn && recorder.error && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: recorder.error.message }),
|
|
3113
|
-
!narrationOn && saveError && /* @__PURE__ */
|
|
3198
|
+
!narrationOn && saveError && /* @__PURE__ */ jsxs4("div", { role: "alert", style: errorStyle, children: [
|
|
3199
|
+
saveError,
|
|
3200
|
+
/* @__PURE__ */ jsx5("p", { style: { marginBottom: 0 }, children: "Your recording is still available below. Download a copy before closing, or try saving again." })
|
|
3201
|
+
] }),
|
|
3202
|
+
/* @__PURE__ */ jsxs4("p", { style: summaryStyle, children: [
|
|
3203
|
+
"Recording size:",
|
|
3204
|
+
" ",
|
|
3205
|
+
formatRecordingBytes(
|
|
3206
|
+
narrationOn ? stage.recorder.recordedBytes : recorder.recordedBytes
|
|
3207
|
+
),
|
|
3208
|
+
". Automatically stops at",
|
|
3209
|
+
" ",
|
|
3210
|
+
formatRecordingBytes(
|
|
3211
|
+
narrationOn ? stage.recorder.maxRecordingBytes : recorder.maxRecordingBytes
|
|
3212
|
+
),
|
|
3213
|
+
" ",
|
|
3214
|
+
"total. Final encoding may add more data."
|
|
3215
|
+
] }),
|
|
3216
|
+
!narrationOn && recorder.limitReached && /* @__PURE__ */ jsx5("p", { role: "status", style: recordingStatusStyle, children: "Recording stopped at the size limit. Your complete recording is retained; download a copy or save it to the document." }),
|
|
3114
3217
|
narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
|
|
3115
3218
|
narrationOn && narrationCameraStream && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5(
|
|
3116
3219
|
"video",
|
|
@@ -3226,6 +3329,43 @@ function RecorderModal({
|
|
|
3226
3329
|
] }),
|
|
3227
3330
|
/* @__PURE__ */ jsx5("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
|
|
3228
3331
|
] }),
|
|
3332
|
+
!narrationOn && canSave && playbackUrl && /* @__PURE__ */ jsxs4(
|
|
3333
|
+
"div",
|
|
3334
|
+
{
|
|
3335
|
+
style: {
|
|
3336
|
+
...buttonRowStyle,
|
|
3337
|
+
flexWrap: "wrap",
|
|
3338
|
+
justifyContent: "flex-start",
|
|
3339
|
+
marginBottom: 12
|
|
3340
|
+
},
|
|
3341
|
+
children: [
|
|
3342
|
+
/* @__PURE__ */ jsx5("a", { href: playbackUrl, download: downloadFilename, style: btnSecondary, children: isDual ? "Download screen recording" : "Download recording" }),
|
|
3343
|
+
isDual && cameraPlaybackUrl && /* @__PURE__ */ jsx5(
|
|
3344
|
+
"a",
|
|
3345
|
+
{
|
|
3346
|
+
href: cameraPlaybackUrl,
|
|
3347
|
+
download: buildFilename(
|
|
3348
|
+
"video",
|
|
3349
|
+
recorder.camera?.extension ?? ".webm",
|
|
3350
|
+
basename.trim() ? `${basename.trim()}-camera` : void 0,
|
|
3351
|
+
micOn ? "camera-audio" : "camera"
|
|
3352
|
+
),
|
|
3353
|
+
style: btnSecondary,
|
|
3354
|
+
children: "Download camera recording"
|
|
3355
|
+
}
|
|
3356
|
+
),
|
|
3357
|
+
timingDownloadUrl && /* @__PURE__ */ jsx5(
|
|
3358
|
+
"a",
|
|
3359
|
+
{
|
|
3360
|
+
href: timingDownloadUrl,
|
|
3361
|
+
download: timingDownloadFilename,
|
|
3362
|
+
style: btnSecondary,
|
|
3363
|
+
children: "Download recording timings"
|
|
3364
|
+
}
|
|
3365
|
+
)
|
|
3366
|
+
]
|
|
3367
|
+
}
|
|
3368
|
+
),
|
|
3229
3369
|
!narrationOn && !slidesTimingCapture && source === "mic" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
3230
3370
|
/* @__PURE__ */ jsx5("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
|
|
3231
3371
|
/* @__PURE__ */ jsx5(
|