@ttt-productions/file-input 0.11.16 → 0.11.18
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/lib/image-utils.d.ts +0 -1
- package/dist/lib/image-utils.d.ts.map +1 -1
- package/dist/lib/image-utils.js +1 -2
- package/dist/lib/image-utils.js.map +1 -1
- package/dist/lib/infer-content-type.d.ts +0 -23
- package/dist/lib/infer-content-type.d.ts.map +1 -1
- package/dist/lib/infer-content-type.js +2 -56
- package/dist/lib/infer-content-type.js.map +1 -1
- package/dist/lib/read-media-meta.d.ts.map +1 -1
- package/dist/lib/read-media-meta.js +5 -15
- package/dist/lib/read-media-meta.js.map +1 -1
- package/dist/lib/validate-media-duration.d.ts +1 -0
- package/dist/lib/validate-media-duration.d.ts.map +1 -1
- package/dist/lib/validate-media-duration.js +11 -11
- package/dist/lib/validate-media-duration.js.map +1 -1
- package/dist/lib/validate-upload-kind.d.ts +0 -34
- package/dist/lib/validate-upload-kind.d.ts.map +1 -1
- package/dist/lib/validate-upload-kind.js +0 -17
- package/dist/lib/validate-upload-kind.js.map +1 -1
- package/dist/react/components/audio-input.d.ts +1 -1
- package/dist/react/components/audio-input.d.ts.map +1 -1
- package/dist/react/components/audio-input.js.map +1 -1
- package/dist/react/components/file-input.d.ts +22 -1
- package/dist/react/components/file-input.d.ts.map +1 -1
- package/dist/react/components/file-input.js +2 -6
- package/dist/react/components/file-input.js.map +1 -1
- package/dist/react/components/image-cropper-modal.d.ts +1 -2
- package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
- package/dist/react/components/image-cropper-modal.js +1 -4
- package/dist/react/components/image-cropper-modal.js.map +1 -1
- package/dist/react/components/image-input.d.ts +1 -1
- package/dist/react/components/image-input.d.ts.map +1 -1
- package/dist/react/components/image-input.js.map +1 -1
- package/dist/react/components/media-constraints-hint.d.ts.map +1 -1
- package/dist/react/components/media-constraints-hint.js +0 -1
- package/dist/react/components/media-constraints-hint.js.map +1 -1
- package/dist/react/components/media-input.d.ts.map +1 -1
- package/dist/react/components/media-input.js +6 -73
- package/dist/react/components/media-input.js.map +1 -1
- package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
- package/dist/react/components/photo-capture-modal.js +5 -8
- package/dist/react/components/photo-capture-modal.js.map +1 -1
- package/dist/react/components/record-dialog.d.ts +0 -4
- package/dist/react/components/record-dialog.d.ts.map +1 -1
- package/dist/react/components/record-dialog.js +35 -106
- package/dist/react/components/record-dialog.js.map +1 -1
- package/dist/react/components/video-input.d.ts +1 -1
- package/dist/react/components/video-input.d.ts.map +1 -1
- package/dist/react/components/video-input.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/types.d.ts +0 -75
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
|
@@ -6,17 +6,23 @@ import { startWaveformLoop } from '@ttt-productions/media-viewer';
|
|
|
6
6
|
import { MediaPreview } from '@ttt-productions/media-viewer/react';
|
|
7
7
|
import { ensureFileWithContentType } from '../../lib/infer-content-type.js';
|
|
8
8
|
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Spinner, useAsyncAction, } from '@ttt-productions/ui-core/react';
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// supported type keeps the recorded container deterministic. Safari has no
|
|
13
|
-
// webm support and falls through to mp4. When isTypeSupported is unavailable
|
|
14
|
-
// or nothing matches (test environments), we fall back to the no-argument
|
|
15
|
-
// constructor and normalize whatever the recorder produced.
|
|
9
|
+
import { releaseObjectUrl } from '../../lib/validate-media-duration.js';
|
|
10
|
+
// An explicit supported container keeps mr.mimeType deterministic; Safari has no webm and falls
|
|
11
|
+
// through to mp4.
|
|
16
12
|
const RECORDER_MIME_CANDIDATES = {
|
|
17
13
|
audio: ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'],
|
|
18
14
|
video: ['video/webm;codecs=vp9,opus', 'video/webm', 'video/mp4'],
|
|
19
15
|
};
|
|
16
|
+
function stopIfRecording(recorder) {
|
|
17
|
+
if (!recorder || recorder.state !== 'recording')
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
recorder.stop();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// A recorder that stopped between the state check and this call throws; it has stopped.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
20
26
|
function pickRecorderMimeType(kind) {
|
|
21
27
|
if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') {
|
|
22
28
|
return undefined;
|
|
@@ -27,21 +33,13 @@ function pickRecorderMimeType(kind) {
|
|
|
27
33
|
return candidate;
|
|
28
34
|
}
|
|
29
35
|
catch {
|
|
30
|
-
//
|
|
36
|
+
// A browser that cannot parse a candidate throws; it counts as unsupported.
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
return undefined;
|
|
34
40
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// extension-based fallback downstream). The mp4 container holds either audio
|
|
38
|
-
// (.m4a → audio/mp4) or video (.mp4 → video/mp4), and — unlike webm/ogg — mp4
|
|
39
|
-
// is NOT in the extension-inference ambiguous map (a user-picked .mp4 is trusted
|
|
40
|
-
// as video). Here the RecordDialog's selected `kind` IS authoritative: an audio
|
|
41
|
-
// recording uses an audio-only, `video: false` getUserMedia stream, so no video
|
|
42
|
-
// track can exist. A surprising `video/mp4` mimeType on that audio recording is
|
|
43
|
-
// therefore still named `.m4a`; ensureFileWithContentType distrusts the
|
|
44
|
-
// kind-mismatched video/mp4 type and the `.m4a` extension yields audio/mp4.
|
|
41
|
+
// mp4 is not in the ambiguous-extension map, so an audio recording is named .m4a whatever
|
|
42
|
+
// container type the recorder reports.
|
|
45
43
|
function recordingFileName(mime, kind) {
|
|
46
44
|
const base = mime.split(';')[0].trim().toLowerCase();
|
|
47
45
|
const ext = base === 'audio/mp4' || base === 'video/mp4'
|
|
@@ -53,30 +51,9 @@ function recordingFileName(mime, kind) {
|
|
|
53
51
|
: 'webm';
|
|
54
52
|
return `recording.${ext}`;
|
|
55
53
|
}
|
|
56
|
-
// A MediaRecorder blob
|
|
57
|
-
//
|
|
58
|
-
// 1. stop() flushes whatever the encoder/muxer still had buffered, so the
|
|
59
|
-
// last written packet extends past the stop() call (Chromium's webm muxer
|
|
60
|
-
// and Firefox's are both in the tens-to-low-hundreds of ms here; typical
|
|
61
|
-
// observed flush is well under 500ms).
|
|
62
|
-
// 2. The container duration is the END of the final frame/packet, not the
|
|
63
|
-
// moment capture stopped — one video frame is ~33ms at 30fps and one Opus
|
|
64
|
-
// packet is 20ms, and both readers round that way (the client gate's
|
|
65
|
-
// seek-to-end duration and ffprobe on the server agree with the container).
|
|
66
|
-
// 3. setTimeout fires LATE, never early — a busy main thread encoding video
|
|
67
|
-
// routinely adds tens of ms, more under load or in a hidden tab.
|
|
68
|
-
//
|
|
69
|
-
// Both duration gates are exact — the client upload gate (validateMediaDuration:
|
|
70
|
-
// `duration <= maxDurationSec`) and the server processor
|
|
71
|
-
// (media-processing-core validateDuration: `durationSec <= maxDurationSec`) —
|
|
72
|
-
// with no tolerance. So stopping the recorder exactly AT the cap produced blobs
|
|
73
|
-
// the product then rejected as its own "Media too long. Max N seconds." error.
|
|
74
|
-
// Stopping early by 750ms clears all three effects above with headroom for
|
|
75
|
-
// timer jitter, and costs 1.25% of the smallest real cap (60s).
|
|
54
|
+
// A MediaRecorder blob runs a little longer than start() to stop() (muxer flush, frame-end
|
|
55
|
+
// duration, late timers) and both duration gates are exact, so recording stops before the cap.
|
|
76
56
|
const AUTO_STOP_SAFETY_MARGIN_MS = 750;
|
|
77
|
-
// The auto-stop delay for a cap. The clamp keeps the margin from swallowing a
|
|
78
|
-
// very short cap (a 1s cap would otherwise stop at 250ms); real caps are >= 60s,
|
|
79
|
-
// where the clamp never applies.
|
|
80
57
|
export function autoStopDelayMs(maxRecordDurationSec) {
|
|
81
58
|
const capMs = Math.floor(maxRecordDurationSec * 1000);
|
|
82
59
|
return Math.max(capMs - AUTO_STOP_SAFETY_MARGIN_MS, Math.ceil(capMs / 2));
|
|
@@ -103,8 +80,7 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
103
80
|
const lastRecordUrlRef = useRef(null);
|
|
104
81
|
const livePreviewRef = useRef(null);
|
|
105
82
|
const autoStopTimeoutRef = useRef(null);
|
|
106
|
-
//
|
|
107
|
-
// closed, kind switched, re-record) belongs to nobody and must not be attached.
|
|
83
|
+
// An acquisition that resolves after a teardown belongs to nobody and is never attached.
|
|
108
84
|
const acquireGenRef = useRef(0);
|
|
109
85
|
const previewReqRef = useRef(0);
|
|
110
86
|
const startedAtRef = useRef(0);
|
|
@@ -114,7 +90,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
114
90
|
const audioSourceRef = useRef(null);
|
|
115
91
|
const waveformStopRef = useRef(null);
|
|
116
92
|
const waveformCanvasRef = useRef(null);
|
|
117
|
-
// Reset all transient state whenever the dialog opens.
|
|
118
93
|
useEffect(() => {
|
|
119
94
|
if (open) {
|
|
120
95
|
setRecordKind(initialKind);
|
|
@@ -129,10 +104,7 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
129
104
|
const revokeLastRecordUrl = useCallback(() => {
|
|
130
105
|
if (!lastRecordUrlRef.current)
|
|
131
106
|
return;
|
|
132
|
-
|
|
133
|
-
URL.revokeObjectURL(lastRecordUrlRef.current);
|
|
134
|
-
}
|
|
135
|
-
catch { }
|
|
107
|
+
releaseObjectUrl(lastRecordUrlRef.current);
|
|
136
108
|
lastRecordUrlRef.current = null;
|
|
137
109
|
}, []);
|
|
138
110
|
const makeRecordUrl = useCallback((blob) => {
|
|
@@ -161,19 +133,24 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
161
133
|
try {
|
|
162
134
|
audioSourceRef.current?.disconnect();
|
|
163
135
|
}
|
|
164
|
-
catch {
|
|
136
|
+
catch {
|
|
137
|
+
// A node that is already disconnected throws, and there is nothing left to tear down.
|
|
138
|
+
}
|
|
165
139
|
audioSourceRef.current = null;
|
|
166
140
|
audioAnalyserRef.current = null;
|
|
167
141
|
const ctx = audioContextRef.current;
|
|
168
142
|
audioContextRef.current = null;
|
|
169
143
|
if (ctx && ctx.state !== 'closed') {
|
|
144
|
+
// Closing a context the browser is already closing rejects; the context is released either way.
|
|
170
145
|
ctx.close().catch(() => { });
|
|
171
146
|
}
|
|
172
147
|
}, []);
|
|
173
148
|
const startWaveform = useCallback((stream) => {
|
|
174
149
|
stopWaveform();
|
|
175
150
|
try {
|
|
176
|
-
|
|
151
|
+
// lib.dom does not declare Safari's prefixed webkitAudioContext.
|
|
152
|
+
const prefixed = window;
|
|
153
|
+
const AudioCtx = window.AudioContext || prefixed.webkitAudioContext;
|
|
177
154
|
if (!AudioCtx)
|
|
178
155
|
return;
|
|
179
156
|
const ctx = new AudioCtx();
|
|
@@ -184,9 +161,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
184
161
|
audioContextRef.current = ctx;
|
|
185
162
|
audioSourceRef.current = source;
|
|
186
163
|
audioAnalyserRef.current = analyser;
|
|
187
|
-
// Draw loop is the shared media-viewer engine (the same one the
|
|
188
|
-
// playback AudioPlayer uses) in oscilloscope mode — one waveform
|
|
189
|
-
// implementation across recording and playback.
|
|
190
164
|
waveformStopRef.current = startWaveformLoop({
|
|
191
165
|
analyser,
|
|
192
166
|
getCanvas: () => waveformCanvasRef.current,
|
|
@@ -194,11 +168,9 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
194
168
|
});
|
|
195
169
|
}
|
|
196
170
|
catch {
|
|
197
|
-
//
|
|
171
|
+
// The waveform is decoration: a browser that refuses an audio graph records without one.
|
|
198
172
|
}
|
|
199
173
|
}, [stopWaveform]);
|
|
200
|
-
// Teardown MediaRecorder / MediaStream / auto-stop timeout / timer / waveform.
|
|
201
|
-
// Leaves recorderState alone — callers decide the next state.
|
|
202
174
|
const teardownCapture = useCallback(() => {
|
|
203
175
|
acquireGenRef.current += 1;
|
|
204
176
|
if (autoStopTimeoutRef.current) {
|
|
@@ -218,8 +190,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
218
190
|
stopElapsedTimer();
|
|
219
191
|
stopWaveform();
|
|
220
192
|
}, [stopElapsedTimer, stopWaveform]);
|
|
221
|
-
// Acquire camera/mic and attach the live preview (video) or waveform (audio)
|
|
222
|
-
// WITHOUT starting the recorder. Returns the live stream for reuse on Start.
|
|
223
193
|
const acquireStream = useCallback(async (kind) => {
|
|
224
194
|
const wantVideo = kind === 'video';
|
|
225
195
|
const facingMode = cameraFacingMode ?? 'user';
|
|
@@ -241,15 +211,15 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
241
211
|
try {
|
|
242
212
|
await livePreviewRef.current.play();
|
|
243
213
|
}
|
|
244
|
-
catch {
|
|
214
|
+
catch {
|
|
215
|
+
// A browser that blocks preview playback rejects; recording does not depend on it.
|
|
216
|
+
}
|
|
245
217
|
}
|
|
246
218
|
else if (!wantVideo) {
|
|
247
219
|
startWaveform(stream);
|
|
248
220
|
}
|
|
249
221
|
return stream;
|
|
250
222
|
}, [cameraFacingMode, startWaveform]);
|
|
251
|
-
// Start a live preview for the given kind (used on open and on kind switch),
|
|
252
|
-
// tearing down any existing stream first. Surfaces permission errors inline.
|
|
253
223
|
const startLivePreview = useCallback(async (kind) => {
|
|
254
224
|
teardownCapture();
|
|
255
225
|
const req = ++previewReqRef.current;
|
|
@@ -265,9 +235,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
265
235
|
setAcquiring(false);
|
|
266
236
|
}
|
|
267
237
|
}, [teardownCapture, acquireStream]);
|
|
268
|
-
// Request camera/mic as soon as the dialog opens (parity with PhotoCaptureModal)
|
|
269
|
-
// so the permission prompt fires on open and the user sees a live preview before
|
|
270
|
-
// pressing Start. Release all devices when the dialog closes or unmounts.
|
|
271
238
|
useEffect(() => {
|
|
272
239
|
if (open) {
|
|
273
240
|
void startLivePreview(initialKind);
|
|
@@ -278,14 +245,11 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
278
245
|
};
|
|
279
246
|
}, [open, initialKind, startLivePreview, teardownCapture, revokeLastRecordUrl]);
|
|
280
247
|
const startRecording = useCallback(async (kind) => {
|
|
281
|
-
// Discard any previous preview before starting fresh.
|
|
282
248
|
revokeLastRecordUrl();
|
|
283
249
|
setRecordPreviewUrl(null);
|
|
284
250
|
setRecordedFile(null);
|
|
285
251
|
setRecordKind(kind);
|
|
286
252
|
const wantVideo = kind === 'video';
|
|
287
|
-
// Reuse the live-preview stream when it matches the requested kind;
|
|
288
|
-
// otherwise acquire a fresh one (also (re)triggers the permission prompt).
|
|
289
253
|
let stream = recordStreamRef.current;
|
|
290
254
|
if (!stream || liveStreamKindRef.current !== kind) {
|
|
291
255
|
try {
|
|
@@ -298,9 +262,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
298
262
|
}
|
|
299
263
|
if (!stream)
|
|
300
264
|
return;
|
|
301
|
-
// Request an explicit, supported container so mr.mimeType is
|
|
302
|
-
// deterministic; fall back to the browser default when the explicit
|
|
303
|
-
// request is unavailable or rejected.
|
|
304
265
|
const preferredMimeType = pickRecorderMimeType(kind);
|
|
305
266
|
let mr;
|
|
306
267
|
try {
|
|
@@ -322,32 +283,18 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
322
283
|
type: mr.mimeType || preferredMimeType || (wantVideo ? 'video/webm' : 'audio/webm'),
|
|
323
284
|
});
|
|
324
285
|
const rawFile = new File([blob], recordingFileName(blob.type, kind), { type: blob.type });
|
|
325
|
-
// Normalize to the valid, parameter-less base type
|
|
326
|
-
// ("audio/webm;codecs=opus" → "audio/webm"). The chosen kind is
|
|
327
|
-
// authoritative: an audio recording must never emit a video/* type
|
|
328
|
-
// even if the recorder reports a surprising container.
|
|
329
286
|
const file = ensureFileWithContentType(rawFile, wantVideo ? 'video' : 'audio');
|
|
330
287
|
const url = makeRecordUrl(file);
|
|
331
288
|
setRecordedFile(file);
|
|
332
289
|
setRecordPreviewUrl(url);
|
|
333
290
|
setRecorderState('preview');
|
|
334
|
-
// Release devices but keep the recorded file/url in state.
|
|
335
291
|
teardownCapture();
|
|
336
292
|
};
|
|
337
293
|
mr.start();
|
|
338
294
|
setRecorderState('recording');
|
|
339
295
|
startElapsedTimer();
|
|
340
|
-
// Hard cap. Stops with deliberate headroom (autoStopDelayMs) so the blob
|
|
341
|
-
// this produces lands legally UNDER maxRecordDurationSec at both gates.
|
|
342
296
|
if (maxRecordDurationSec && maxRecordDurationSec > 0) {
|
|
343
|
-
autoStopTimeoutRef.current = setTimeout(() =>
|
|
344
|
-
try {
|
|
345
|
-
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
|
|
346
|
-
mediaRecorderRef.current.stop();
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
catch { }
|
|
350
|
-
}, autoStopDelayMs(maxRecordDurationSec));
|
|
297
|
+
autoStopTimeoutRef.current = setTimeout(() => stopIfRecording(mediaRecorderRef.current), autoStopDelayMs(maxRecordDurationSec));
|
|
351
298
|
}
|
|
352
299
|
}, [
|
|
353
300
|
acquireStream,
|
|
@@ -382,8 +329,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
382
329
|
setRecorderState('idle');
|
|
383
330
|
void startLivePreview(recordKind);
|
|
384
331
|
}, [revokeLastRecordUrl, startLivePreview, recordKind]);
|
|
385
|
-
// Switching kind in idle restarts the live preview for the new kind so the
|
|
386
|
-
// correct device prompt/preview shows before recording.
|
|
387
332
|
const handleSelectKind = useCallback((kind) => {
|
|
388
333
|
setRecordKind(kind);
|
|
389
334
|
setError(null);
|
|
@@ -392,29 +337,17 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
392
337
|
const reportError = useCallback((e) => {
|
|
393
338
|
setError(e instanceof Error ? e.message : 'Something went wrong with the recording.');
|
|
394
339
|
}, []);
|
|
395
|
-
// Start may need to acquire a device first; pending covers that and a repeat click
|
|
396
|
-
// is ignored, so two clicks can never start two recorders.
|
|
397
340
|
const start = useAsyncAction((kind) => startRecording(kind), {
|
|
398
341
|
onError: reportError,
|
|
399
342
|
});
|
|
400
|
-
// Save hands the recording off (the consumer validates and emits it). Pending covers
|
|
401
|
-
// the hand-off and a repeat click is ignored, so a double click can never emit twice.
|
|
402
343
|
const save = useAsyncAction(async () => {
|
|
403
344
|
if (!recordedFile || !recordPreviewUrl)
|
|
404
345
|
return;
|
|
405
346
|
await onRecorded(recordedFile, recordPreviewUrl, recordKind);
|
|
406
347
|
onOpenChange(false);
|
|
407
348
|
}, { onError: reportError });
|
|
408
|
-
// Close the dialog for real (no confirmation). Used for idle/recording
|
|
409
|
-
// states and as the "Discard" action in the confirm AlertDialog.
|
|
410
349
|
const closeDialogImmediately = useCallback(() => {
|
|
411
|
-
|
|
412
|
-
if (mr && mr.state === 'recording') {
|
|
413
|
-
try {
|
|
414
|
-
mr.stop();
|
|
415
|
-
}
|
|
416
|
-
catch { }
|
|
417
|
-
}
|
|
350
|
+
stopIfRecording(mediaRecorderRef.current);
|
|
418
351
|
teardownCapture();
|
|
419
352
|
revokeLastRecordUrl();
|
|
420
353
|
setRecordedFile(null);
|
|
@@ -424,13 +357,11 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
424
357
|
setDiscardConfirmOpen(false);
|
|
425
358
|
onOpenChange(false);
|
|
426
359
|
}, [teardownCapture, revokeLastRecordUrl, onOpenChange]);
|
|
427
|
-
// Dialog close requests. In "preview" state we ask for confirmation
|
|
428
|
-
// (the user has unsaved work). Other states close immediately.
|
|
429
360
|
const handleDialogOpenChange = useCallback((v) => {
|
|
430
361
|
if (v)
|
|
431
|
-
return;
|
|
362
|
+
return;
|
|
432
363
|
if (save.pending)
|
|
433
|
-
return;
|
|
364
|
+
return;
|
|
434
365
|
if (recorderState === 'preview') {
|
|
435
366
|
setDiscardConfirmOpen(true);
|
|
436
367
|
return;
|
|
@@ -440,8 +371,6 @@ export function RecordDialog({ open, onOpenChange, initialKind, canPhoto, canRec
|
|
|
440
371
|
const isRecording = recorderState === 'recording';
|
|
441
372
|
const isPreview = recorderState === 'preview';
|
|
442
373
|
const isIdle = recorderState === 'idle';
|
|
443
|
-
// The label shows the REAL cap ("0:42 / 1:00"), not the earlier auto-stop
|
|
444
|
-
// point — the promise to the user is the cap; the margin is internal.
|
|
445
374
|
const elapsedLabel = maxRecordDurationSec
|
|
446
375
|
? `${formatDuration(elapsedMs)} / ${formatDuration(maxRecordDurationSec * 1000)}`
|
|
447
376
|
: formatDuration(elapsedMs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-dialog.js","sourceRoot":"","sources":["../../../src/react/components/record-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Camera, Video, Mic, X, RotateCcw, Check } from 'lucide-react';\nimport { startWaveformLoop } from '@ttt-productions/media-viewer';\nimport { MediaPreview } from '@ttt-productions/media-viewer/react';\nimport { ensureFileWithContentType } from '../../lib/infer-content-type.js';\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Spinner,\n useAsyncAction,\n} from '@ttt-productions/ui-core/react';\n\ntype RecorderState = 'idle' | 'recording' | 'preview';\n\n// Explicit MediaRecorder container preferences, probed via isTypeSupported.\n// A no-argument MediaRecorder leaves the container to the browser (Chrome\n// reports \"audio/webm;codecs=opus\" for audio); requesting an explicit\n// supported type keeps the recorded container deterministic. Safari has no\n// webm support and falls through to mp4. When isTypeSupported is unavailable\n// or nothing matches (test environments), we fall back to the no-argument\n// constructor and normalize whatever the recorder produced.\nconst RECORDER_MIME_CANDIDATES: Record<'audio' | 'video', readonly string[]> = {\n audio: ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'],\n video: ['video/webm;codecs=vp9,opus', 'video/webm', 'video/mp4'],\n};\n\nfunction pickRecorderMimeType(kind: 'audio' | 'video'): string | undefined {\n if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') {\n return undefined;\n }\n for (const candidate of RECORDER_MIME_CANDIDATES[kind]) {\n try {\n if (MediaRecorder.isTypeSupported(candidate)) return candidate;\n } catch {\n // Treat probe errors as \"unsupported\" and keep trying.\n }\n }\n return undefined;\n}\n\n// Name the recording after its actual container so the filename never\n// contradicts the emitted contentType (a webm-named mp4 would mislead any\n// extension-based fallback downstream). The mp4 container holds either audio\n// (.m4a → audio/mp4) or video (.mp4 → video/mp4), and — unlike webm/ogg — mp4\n// is NOT in the extension-inference ambiguous map (a user-picked .mp4 is trusted\n// as video). Here the RecordDialog's selected `kind` IS authoritative: an audio\n// recording uses an audio-only, `video: false` getUserMedia stream, so no video\n// track can exist. A surprising `video/mp4` mimeType on that audio recording is\n// therefore still named `.m4a`; ensureFileWithContentType distrusts the\n// kind-mismatched video/mp4 type and the `.m4a` extension yields audio/mp4.\nfunction recordingFileName(mime: string, kind: 'audio' | 'video'): string {\n const base = mime.split(';')[0].trim().toLowerCase();\n const ext =\n base === 'audio/mp4' || base === 'video/mp4'\n ? kind === 'audio'\n ? 'm4a'\n : 'mp4'\n : base === 'audio/ogg' || base === 'video/ogg'\n ? 'ogg'\n : 'webm';\n return `recording.${ext}`;\n}\n\n// A MediaRecorder blob is always a little LONGER than the wall-clock window\n// between start() and stop(). Three effects stack, all in the same direction:\n// 1. stop() flushes whatever the encoder/muxer still had buffered, so the\n// last written packet extends past the stop() call (Chromium's webm muxer\n// and Firefox's are both in the tens-to-low-hundreds of ms here; typical\n// observed flush is well under 500ms).\n// 2. The container duration is the END of the final frame/packet, not the\n// moment capture stopped — one video frame is ~33ms at 30fps and one Opus\n// packet is 20ms, and both readers round that way (the client gate's\n// seek-to-end duration and ffprobe on the server agree with the container).\n// 3. setTimeout fires LATE, never early — a busy main thread encoding video\n// routinely adds tens of ms, more under load or in a hidden tab.\n//\n// Both duration gates are exact — the client upload gate (validateMediaDuration:\n// `duration <= maxDurationSec`) and the server processor\n// (media-processing-core validateDuration: `durationSec <= maxDurationSec`) —\n// with no tolerance. So stopping the recorder exactly AT the cap produced blobs\n// the product then rejected as its own \"Media too long. Max N seconds.\" error.\n// Stopping early by 750ms clears all three effects above with headroom for\n// timer jitter, and costs 1.25% of the smallest real cap (60s).\nconst AUTO_STOP_SAFETY_MARGIN_MS = 750;\n\n// The auto-stop delay for a cap. The clamp keeps the margin from swallowing a\n// very short cap (a 1s cap would otherwise stop at 250ms); real caps are >= 60s,\n// where the clamp never applies.\nexport function autoStopDelayMs(maxRecordDurationSec: number): number {\n const capMs = Math.floor(maxRecordDurationSec * 1000);\n return Math.max(capMs - AUTO_STOP_SAFETY_MARGIN_MS, Math.ceil(capMs / 2));\n}\n\ninterface RecordDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n initialKind: 'audio' | 'video';\n canPhoto: boolean;\n canRecVideo: boolean;\n canRecAudio: boolean;\n cameraFacingMode?: 'user' | 'environment';\n maxRecordDurationSec?: number;\n disabled?: boolean;\n isLoading?: boolean;\n /** `recordedKind` is the kind of the ACTUAL recording session (the dialog's\n * own state at Save time, which tracks the requested MediaStream — audio\n * requests `video:false`). It feeds the STRONG `media-recorder` client\n * claim, so it must never be re-derived from the blob's reported MIME. */\n onRecorded: (\n file: File,\n previewUrl: string,\n recordedKind: 'audio' | 'video',\n ) => void | Promise<void>;\n onRequestPhoto?: () => void;\n}\n\nfunction formatDuration(ms: number): string {\n const totalSec = Math.max(0, Math.floor(ms / 1000));\n const m = Math.floor(totalSec / 60);\n const s = totalSec % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n}\n\nexport function RecordDialog({\n open,\n onOpenChange,\n initialKind,\n canPhoto,\n canRecVideo,\n canRecAudio,\n cameraFacingMode,\n maxRecordDurationSec,\n disabled = false,\n isLoading = false,\n onRecorded,\n onRequestPhoto,\n}: RecordDialogProps) {\n const [recordKind, setRecordKind] = useState<'audio' | 'video'>(initialKind);\n const [recorderState, setRecorderState] = useState<RecorderState>('idle');\n const [recordPreviewUrl, setRecordPreviewUrl] = useState<string | null>(null);\n const [recordedFile, setRecordedFile] = useState<File | null>(null);\n const [elapsedMs, setElapsedMs] = useState(0);\n const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [acquiring, setAcquiring] = useState(false);\n\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const recordStreamRef = useRef<MediaStream | null>(null);\n const liveStreamKindRef = useRef<'audio' | 'video' | null>(null);\n const recordChunksRef = useRef<BlobPart[]>([]);\n const lastRecordUrlRef = useRef<string | null>(null);\n const livePreviewRef = useRef<HTMLVideoElement | null>(null);\n const autoStopTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Bumped by every teardown. An acquisition that resolves after a teardown (dialog\n // closed, kind switched, re-record) belongs to nobody and must not be attached.\n const acquireGenRef = useRef(0);\n const previewReqRef = useRef(0);\n\n const startedAtRef = useRef<number>(0);\n const elapsedIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const audioContextRef = useRef<AudioContext | null>(null);\n const audioAnalyserRef = useRef<AnalyserNode | null>(null);\n const audioSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const waveformStopRef = useRef<(() => void) | null>(null);\n const waveformCanvasRef = useRef<HTMLCanvasElement | null>(null);\n\n // Reset all transient state whenever the dialog opens.\n useEffect(() => {\n if (open) {\n setRecordKind(initialKind);\n setRecorderState('idle');\n setRecordedFile(null);\n setRecordPreviewUrl(null);\n setElapsedMs(0);\n setDiscardConfirmOpen(false);\n setError(null);\n }\n }, [open, initialKind]);\n\n const revokeLastRecordUrl = useCallback(() => {\n if (!lastRecordUrlRef.current) return;\n try {\n URL.revokeObjectURL(lastRecordUrlRef.current);\n } catch {}\n lastRecordUrlRef.current = null;\n }, []);\n\n const makeRecordUrl = useCallback(\n (blob: Blob) => {\n revokeLastRecordUrl();\n const url = URL.createObjectURL(blob);\n lastRecordUrlRef.current = url;\n return url;\n },\n [revokeLastRecordUrl],\n );\n\n const stopElapsedTimer = useCallback(() => {\n if (elapsedIntervalRef.current) {\n clearInterval(elapsedIntervalRef.current);\n elapsedIntervalRef.current = null;\n }\n }, []);\n\n const startElapsedTimer = useCallback(() => {\n stopElapsedTimer();\n startedAtRef.current = Date.now();\n setElapsedMs(0);\n elapsedIntervalRef.current = setInterval(() => {\n setElapsedMs(Date.now() - startedAtRef.current);\n }, 100);\n }, [stopElapsedTimer]);\n\n const stopWaveform = useCallback(() => {\n waveformStopRef.current?.();\n waveformStopRef.current = null;\n try {\n audioSourceRef.current?.disconnect();\n } catch {}\n audioSourceRef.current = null;\n audioAnalyserRef.current = null;\n const ctx = audioContextRef.current;\n audioContextRef.current = null;\n if (ctx && ctx.state !== 'closed') {\n ctx.close().catch(() => {});\n }\n }, []);\n\n const startWaveform = useCallback(\n (stream: MediaStream) => {\n stopWaveform();\n try {\n const AudioCtx: typeof AudioContext =\n (window as any).AudioContext || (window as any).webkitAudioContext;\n if (!AudioCtx) return;\n\n const ctx = new AudioCtx();\n const source = ctx.createMediaStreamSource(stream);\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n source.connect(analyser);\n\n audioContextRef.current = ctx;\n audioSourceRef.current = source;\n audioAnalyserRef.current = analyser;\n\n // Draw loop is the shared media-viewer engine (the same one the\n // playback AudioPlayer uses) in oscilloscope mode — one waveform\n // implementation across recording and playback.\n waveformStopRef.current = startWaveformLoop({\n analyser,\n getCanvas: () => waveformCanvasRef.current,\n mode: 'line',\n });\n } catch {\n // Waveform is best-effort; silent failures are acceptable.\n }\n },\n [stopWaveform],\n );\n\n // Teardown MediaRecorder / MediaStream / auto-stop timeout / timer / waveform.\n // Leaves recorderState alone — callers decide the next state.\n const teardownCapture = useCallback(() => {\n acquireGenRef.current += 1;\n if (autoStopTimeoutRef.current) {\n clearTimeout(autoStopTimeoutRef.current);\n autoStopTimeoutRef.current = null;\n }\n mediaRecorderRef.current = null;\n\n const s = recordStreamRef.current;\n recordStreamRef.current = null;\n liveStreamKindRef.current = null;\n if (s) s.getTracks().forEach((t) => t.stop());\n\n if (livePreviewRef.current) {\n livePreviewRef.current.srcObject = null;\n }\n\n recordChunksRef.current = [];\n\n stopElapsedTimer();\n stopWaveform();\n }, [stopElapsedTimer, stopWaveform]);\n\n // Acquire camera/mic and attach the live preview (video) or waveform (audio)\n // WITHOUT starting the recorder. Returns the live stream for reuse on Start.\n const acquireStream = useCallback(\n async (kind: 'audio' | 'video') => {\n const wantVideo = kind === 'video';\n const facingMode = cameraFacingMode ?? 'user';\n const gen = acquireGenRef.current;\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n video: wantVideo ? { facingMode } : false,\n });\n if (gen !== acquireGenRef.current) {\n stream.getTracks().forEach((t) => t.stop());\n return null;\n }\n recordStreamRef.current = stream;\n liveStreamKindRef.current = kind;\n setError(null);\n if (wantVideo && livePreviewRef.current) {\n livePreviewRef.current.srcObject = stream;\n livePreviewRef.current.muted = true;\n try {\n await livePreviewRef.current.play();\n } catch {}\n } else if (!wantVideo) {\n startWaveform(stream);\n }\n return stream;\n },\n [cameraFacingMode, startWaveform],\n );\n\n // Start a live preview for the given kind (used on open and on kind switch),\n // tearing down any existing stream first. Surfaces permission errors inline.\n const startLivePreview = useCallback(\n async (kind: 'audio' | 'video') => {\n teardownCapture();\n const req = ++previewReqRef.current;\n setAcquiring(true);\n try {\n await acquireStream(kind);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Camera/microphone access denied.');\n } finally {\n if (req === previewReqRef.current) setAcquiring(false);\n }\n },\n [teardownCapture, acquireStream],\n );\n\n // Request camera/mic as soon as the dialog opens (parity with PhotoCaptureModal)\n // so the permission prompt fires on open and the user sees a live preview before\n // pressing Start. Release all devices when the dialog closes or unmounts.\n useEffect(() => {\n if (open) {\n void startLivePreview(initialKind);\n }\n return () => {\n teardownCapture();\n revokeLastRecordUrl();\n };\n }, [open, initialKind, startLivePreview, teardownCapture, revokeLastRecordUrl]);\n\n const startRecording = useCallback(\n async (kind: 'audio' | 'video') => {\n // Discard any previous preview before starting fresh.\n revokeLastRecordUrl();\n setRecordPreviewUrl(null);\n setRecordedFile(null);\n setRecordKind(kind);\n\n const wantVideo = kind === 'video';\n\n // Reuse the live-preview stream when it matches the requested kind;\n // otherwise acquire a fresh one (also (re)triggers the permission prompt).\n let stream = recordStreamRef.current;\n if (!stream || liveStreamKindRef.current !== kind) {\n try {\n stream = await acquireStream(kind);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Camera/microphone access denied.');\n return;\n }\n }\n if (!stream) return;\n\n // Request an explicit, supported container so mr.mimeType is\n // deterministic; fall back to the browser default when the explicit\n // request is unavailable or rejected.\n const preferredMimeType = pickRecorderMimeType(kind);\n let mr: MediaRecorder;\n try {\n mr = preferredMimeType\n ? new MediaRecorder(stream, { mimeType: preferredMimeType })\n : new MediaRecorder(stream);\n } catch {\n mr = new MediaRecorder(stream);\n }\n mediaRecorderRef.current = mr;\n recordChunksRef.current = [];\n\n mr.ondataavailable = (e) => {\n if (e.data && e.data.size > 0) recordChunksRef.current.push(e.data);\n };\n\n mr.onstop = () => {\n const blob = new Blob(recordChunksRef.current, {\n type: mr.mimeType || preferredMimeType || (wantVideo ? 'video/webm' : 'audio/webm'),\n });\n\n const rawFile = new File([blob], recordingFileName(blob.type, kind), { type: blob.type });\n // Normalize to the valid, parameter-less base type\n // (\"audio/webm;codecs=opus\" → \"audio/webm\"). The chosen kind is\n // authoritative: an audio recording must never emit a video/* type\n // even if the recorder reports a surprising container.\n const file = ensureFileWithContentType(rawFile, wantVideo ? 'video' : 'audio');\n const url = makeRecordUrl(file);\n\n setRecordedFile(file);\n setRecordPreviewUrl(url);\n setRecorderState('preview');\n\n // Release devices but keep the recorded file/url in state.\n teardownCapture();\n };\n\n mr.start();\n setRecorderState('recording');\n startElapsedTimer();\n\n // Hard cap. Stops with deliberate headroom (autoStopDelayMs) so the blob\n // this produces lands legally UNDER maxRecordDurationSec at both gates.\n if (maxRecordDurationSec && maxRecordDurationSec > 0) {\n autoStopTimeoutRef.current = setTimeout(() => {\n try {\n if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {\n mediaRecorderRef.current.stop();\n }\n } catch {}\n }, autoStopDelayMs(maxRecordDurationSec));\n }\n },\n [\n acquireStream,\n maxRecordDurationSec,\n makeRecordUrl,\n revokeLastRecordUrl,\n teardownCapture,\n startElapsedTimer,\n ],\n );\n\n const stopRecording = useCallback(() => {\n const mr = mediaRecorderRef.current;\n if (mr && mr.state === 'recording') {\n try {\n mr.stop();\n } catch {\n teardownCapture();\n setRecorderState('idle');\n }\n } else {\n teardownCapture();\n setRecorderState('idle');\n }\n }, [teardownCapture]);\n\n const handleReRecord = useCallback(() => {\n revokeLastRecordUrl();\n setRecordPreviewUrl(null);\n setRecordedFile(null);\n setElapsedMs(0);\n setError(null);\n setRecorderState('idle');\n void startLivePreview(recordKind);\n }, [revokeLastRecordUrl, startLivePreview, recordKind]);\n\n // Switching kind in idle restarts the live preview for the new kind so the\n // correct device prompt/preview shows before recording.\n const handleSelectKind = useCallback(\n (kind: 'audio' | 'video') => {\n setRecordKind(kind);\n setError(null);\n void startLivePreview(kind);\n },\n [startLivePreview],\n );\n\n const reportError = useCallback((e: unknown) => {\n setError(e instanceof Error ? e.message : 'Something went wrong with the recording.');\n }, []);\n\n // Start may need to acquire a device first; pending covers that and a repeat click\n // is ignored, so two clicks can never start two recorders.\n const start = useAsyncAction((kind: 'audio' | 'video') => startRecording(kind), {\n onError: reportError,\n });\n\n // Save hands the recording off (the consumer validates and emits it). Pending covers\n // the hand-off and a repeat click is ignored, so a double click can never emit twice.\n const save = useAsyncAction(\n async () => {\n if (!recordedFile || !recordPreviewUrl) return;\n await onRecorded(recordedFile, recordPreviewUrl, recordKind);\n onOpenChange(false);\n },\n { onError: reportError },\n );\n\n // Close the dialog for real (no confirmation). Used for idle/recording\n // states and as the \"Discard\" action in the confirm AlertDialog.\n const closeDialogImmediately = useCallback(() => {\n const mr = mediaRecorderRef.current;\n if (mr && mr.state === 'recording') {\n try {\n mr.stop();\n } catch {}\n }\n teardownCapture();\n revokeLastRecordUrl();\n setRecordedFile(null);\n setRecordPreviewUrl(null);\n setElapsedMs(0);\n setRecorderState('idle');\n setDiscardConfirmOpen(false);\n onOpenChange(false);\n }, [teardownCapture, revokeLastRecordUrl, onOpenChange]);\n\n // Dialog close requests. In \"preview\" state we ask for confirmation\n // (the user has unsaved work). Other states close immediately.\n const handleDialogOpenChange = useCallback(\n (v: boolean) => {\n if (v) return; // opening is parent-driven via the `open` prop\n if (save.pending) return; // never drop a recording mid-hand-off\n if (recorderState === 'preview') {\n setDiscardConfirmOpen(true);\n return;\n }\n closeDialogImmediately();\n },\n [recorderState, save.pending, closeDialogImmediately],\n );\n\n const isRecording = recorderState === 'recording';\n const isPreview = recorderState === 'preview';\n const isIdle = recorderState === 'idle';\n\n // The label shows the REAL cap (\"0:42 / 1:00\"), not the earlier auto-stop\n // point — the promise to the user is the cap; the margin is internal.\n const elapsedLabel = maxRecordDurationSec\n ? `${formatDuration(elapsedMs)} / ${formatDuration(maxRecordDurationSec * 1000)}`\n : formatDuration(elapsedMs);\n\n return (\n <>\n <Dialog open={open} onOpenChange={handleDialogOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Record</DialogTitle>\n <DialogDescription>\n {isIdle && 'Choose audio or video, then press Start.'}\n {isRecording && 'Recording…'}\n {isPreview && 'Review your recording. Save to upload, or Re-record to try again.'}\n </DialogDescription>\n </DialogHeader>\n\n {/* Kind toggle — only active in idle state */}\n <div className=\"flex flex-wrap gap-2\">\n {canRecVideo && (\n <Button\n type=\"button\"\n variant={recordKind === 'video' ? 'default' : 'secondary'}\n aria-pressed={recordKind === 'video'}\n onClick={() => handleSelectKind('video')}\n disabled={!isIdle || acquiring}\n icon={<Video className=\"icon-xs\" />}\n >\n Video\n </Button>\n )}\n {canRecAudio && (\n <Button\n type=\"button\"\n variant={recordKind === 'audio' ? 'default' : 'secondary'}\n aria-pressed={recordKind === 'audio'}\n onClick={() => handleSelectKind('audio')}\n disabled={!isIdle || acquiring}\n icon={<Mic className=\"icon-xs\" />}\n >\n Audio\n </Button>\n )}\n {canPhoto && onRequestPhoto && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={() => {\n onRequestPhoto();\n onOpenChange(false);\n }}\n disabled={!isIdle}\n icon={<Camera className=\"icon-xs\" />}\n >\n Photo\n </Button>\n )}\n </div>\n\n {/* Live video preview — rendered in idle+recording states for video kind */}\n {recordKind === 'video' && !isPreview && (\n <div className=\"relative mt-2 rounded-md overflow-hidden bg-[hsl(var(--neutral-black))] aspect-video\">\n <video\n ref={livePreviewRef}\n className=\"w-full h-full object-contain\"\n playsInline\n muted\n />\n {acquiring && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner size=\"lg\" label=\"Starting camera\" />\n </div>\n )}\n </div>\n )}\n\n {/* Audio waveform — live mic level (idle = ready indicator, recording = active) */}\n {recordKind === 'audio' && !isPreview && (\n <div className=\"relative mt-2 rounded-md bg-muted p-2 text-primary\">\n <canvas\n ref={waveformCanvasRef}\n width={480}\n height={80}\n className=\"w-full h-20\"\n aria-label=\"Audio waveform\"\n />\n {acquiring && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner size=\"lg\" label=\"Starting microphone\" />\n </div>\n )}\n </div>\n )}\n\n {error && (\n <p className=\"text-sm text-destructive\" role=\"alert\">\n {error}\n </p>\n )}\n\n {/* Recording indicator + elapsed timer */}\n {isRecording && (\n <div className=\"flex items-center gap-2 text-sm\" aria-live=\"polite\">\n <span\n className=\"inline-block h-2.5 w-2.5 rounded-full bg-[color:var(--recording-indicator)] animate-pulse\"\n aria-label=\"Recording\"\n />\n <span className=\"tabular-nums\">{elapsedLabel}</span>\n </div>\n )}\n\n {/* Recorded preview — only in preview state */}\n {isPreview && recordPreviewUrl && (\n <div className=\"mt-2\">\n <MediaPreview\n url={recordPreviewUrl}\n type={recordKind}\n controls\n priority\n className=\"w-full rounded-md\"\n />\n </div>\n )}\n\n <DialogFooter className=\"flex-row justify-between gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => handleDialogOpenChange(false)}\n disabled={save.pending}\n icon={<X className=\"icon-xs\" />}\n >\n Close\n </Button>\n\n {isIdle && (\n <Button\n type=\"button\"\n variant=\"default\"\n onClick={() => void start.run(recordKind)}\n disabled={disabled || isLoading || acquiring}\n pending={start.pending}\n icon={\n recordKind === 'video' ? (\n <Video className=\"icon-xs\" />\n ) : (\n <Mic className=\"icon-xs\" />\n )\n }\n >\n Start\n </Button>\n )}\n\n {isRecording && (\n <Button type=\"button\" variant=\"default\" onClick={stopRecording}>\n Stop\n </Button>\n )}\n\n {isPreview && (\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={handleReRecord}\n disabled={save.pending}\n icon={<RotateCcw className=\"icon-xs\" />}\n >\n Re-record\n </Button>\n <Button\n type=\"button\"\n variant=\"default\"\n onClick={() => void save.run()}\n disabled={disabled || isLoading}\n pending={save.pending}\n icon={<Check className=\"icon-xs\" />}\n >\n Save\n </Button>\n </div>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <AlertDialog open={discardConfirmOpen} onOpenChange={setDiscardConfirmOpen}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Discard recording?</AlertDialogTitle>\n <AlertDialogDescription>\n You have a recorded clip that hasn't been saved. Closing now will discard it.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep recording</AlertDialogCancel>\n <AlertDialogAction onClick={closeDialogImmediately}>Discard</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"record-dialog.js","sourceRoot":"","sources":["../../../src/react/components/record-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Camera, Video, Mic, X, RotateCcw, Check } from 'lucide-react';\nimport { startWaveformLoop } from '@ttt-productions/media-viewer';\nimport { MediaPreview } from '@ttt-productions/media-viewer/react';\nimport { ensureFileWithContentType } from '../../lib/infer-content-type.js';\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Spinner,\n useAsyncAction,\n} from '@ttt-productions/ui-core/react';\nimport { releaseObjectUrl } from '../../lib/validate-media-duration.js';\n\ntype RecorderState = 'idle' | 'recording' | 'preview';\n\n// An explicit supported container keeps mr.mimeType deterministic; Safari has no webm and falls\n// through to mp4.\nconst RECORDER_MIME_CANDIDATES: Record<'audio' | 'video', readonly string[]> = {\n audio: ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'],\n video: ['video/webm;codecs=vp9,opus', 'video/webm', 'video/mp4'],\n};\n\nfunction stopIfRecording(recorder: MediaRecorder | null): void {\n if (!recorder || recorder.state !== 'recording') return;\n try {\n recorder.stop();\n } catch {\n // A recorder that stopped between the state check and this call throws; it has stopped.\n }\n}\n\nfunction pickRecorderMimeType(kind: 'audio' | 'video'): string | undefined {\n if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') {\n return undefined;\n }\n for (const candidate of RECORDER_MIME_CANDIDATES[kind]) {\n try {\n if (MediaRecorder.isTypeSupported(candidate)) return candidate;\n } catch {\n // A browser that cannot parse a candidate throws; it counts as unsupported.\n }\n }\n return undefined;\n}\n\n// mp4 is not in the ambiguous-extension map, so an audio recording is named .m4a whatever\n// container type the recorder reports.\nfunction recordingFileName(mime: string, kind: 'audio' | 'video'): string {\n const base = mime.split(';')[0].trim().toLowerCase();\n const ext =\n base === 'audio/mp4' || base === 'video/mp4'\n ? kind === 'audio'\n ? 'm4a'\n : 'mp4'\n : base === 'audio/ogg' || base === 'video/ogg'\n ? 'ogg'\n : 'webm';\n return `recording.${ext}`;\n}\n\n// A MediaRecorder blob runs a little longer than start() to stop() (muxer flush, frame-end\n// duration, late timers) and both duration gates are exact, so recording stops before the cap.\nconst AUTO_STOP_SAFETY_MARGIN_MS = 750;\n\nexport function autoStopDelayMs(maxRecordDurationSec: number): number {\n const capMs = Math.floor(maxRecordDurationSec * 1000);\n return Math.max(capMs - AUTO_STOP_SAFETY_MARGIN_MS, Math.ceil(capMs / 2));\n}\n\ninterface RecordDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n initialKind: 'audio' | 'video';\n canPhoto: boolean;\n canRecVideo: boolean;\n canRecAudio: boolean;\n cameraFacingMode?: 'user' | 'environment';\n maxRecordDurationSec?: number;\n disabled?: boolean;\n isLoading?: boolean;\n onRecorded: (\n file: File,\n previewUrl: string,\n recordedKind: 'audio' | 'video',\n ) => void | Promise<void>;\n onRequestPhoto?: () => void;\n}\n\nfunction formatDuration(ms: number): string {\n const totalSec = Math.max(0, Math.floor(ms / 1000));\n const m = Math.floor(totalSec / 60);\n const s = totalSec % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n}\n\nexport function RecordDialog({\n open,\n onOpenChange,\n initialKind,\n canPhoto,\n canRecVideo,\n canRecAudio,\n cameraFacingMode,\n maxRecordDurationSec,\n disabled = false,\n isLoading = false,\n onRecorded,\n onRequestPhoto,\n}: RecordDialogProps) {\n const [recordKind, setRecordKind] = useState<'audio' | 'video'>(initialKind);\n const [recorderState, setRecorderState] = useState<RecorderState>('idle');\n const [recordPreviewUrl, setRecordPreviewUrl] = useState<string | null>(null);\n const [recordedFile, setRecordedFile] = useState<File | null>(null);\n const [elapsedMs, setElapsedMs] = useState(0);\n const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [acquiring, setAcquiring] = useState(false);\n\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const recordStreamRef = useRef<MediaStream | null>(null);\n const liveStreamKindRef = useRef<'audio' | 'video' | null>(null);\n const recordChunksRef = useRef<BlobPart[]>([]);\n const lastRecordUrlRef = useRef<string | null>(null);\n const livePreviewRef = useRef<HTMLVideoElement | null>(null);\n const autoStopTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // An acquisition that resolves after a teardown belongs to nobody and is never attached.\n const acquireGenRef = useRef(0);\n const previewReqRef = useRef(0);\n\n const startedAtRef = useRef<number>(0);\n const elapsedIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const audioContextRef = useRef<AudioContext | null>(null);\n const audioAnalyserRef = useRef<AnalyserNode | null>(null);\n const audioSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);\n const waveformStopRef = useRef<(() => void) | null>(null);\n const waveformCanvasRef = useRef<HTMLCanvasElement | null>(null);\n\n useEffect(() => {\n if (open) {\n setRecordKind(initialKind);\n setRecorderState('idle');\n setRecordedFile(null);\n setRecordPreviewUrl(null);\n setElapsedMs(0);\n setDiscardConfirmOpen(false);\n setError(null);\n }\n }, [open, initialKind]);\n\n const revokeLastRecordUrl = useCallback(() => {\n if (!lastRecordUrlRef.current) return;\n releaseObjectUrl(lastRecordUrlRef.current);\n lastRecordUrlRef.current = null;\n }, []);\n\n const makeRecordUrl = useCallback(\n (blob: Blob) => {\n revokeLastRecordUrl();\n const url = URL.createObjectURL(blob);\n lastRecordUrlRef.current = url;\n return url;\n },\n [revokeLastRecordUrl],\n );\n\n const stopElapsedTimer = useCallback(() => {\n if (elapsedIntervalRef.current) {\n clearInterval(elapsedIntervalRef.current);\n elapsedIntervalRef.current = null;\n }\n }, []);\n\n const startElapsedTimer = useCallback(() => {\n stopElapsedTimer();\n startedAtRef.current = Date.now();\n setElapsedMs(0);\n elapsedIntervalRef.current = setInterval(() => {\n setElapsedMs(Date.now() - startedAtRef.current);\n }, 100);\n }, [stopElapsedTimer]);\n\n const stopWaveform = useCallback(() => {\n waveformStopRef.current?.();\n waveformStopRef.current = null;\n try {\n audioSourceRef.current?.disconnect();\n } catch {\n // A node that is already disconnected throws, and there is nothing left to tear down.\n }\n audioSourceRef.current = null;\n audioAnalyserRef.current = null;\n const ctx = audioContextRef.current;\n audioContextRef.current = null;\n if (ctx && ctx.state !== 'closed') {\n // Closing a context the browser is already closing rejects; the context is released either way.\n ctx.close().catch(() => {});\n }\n }, []);\n\n const startWaveform = useCallback(\n (stream: MediaStream) => {\n stopWaveform();\n try {\n // lib.dom does not declare Safari's prefixed webkitAudioContext.\n const prefixed = window as Window & { webkitAudioContext?: typeof AudioContext };\n const AudioCtx: typeof AudioContext | undefined =\n window.AudioContext || prefixed.webkitAudioContext;\n if (!AudioCtx) return;\n\n const ctx = new AudioCtx();\n const source = ctx.createMediaStreamSource(stream);\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n source.connect(analyser);\n\n audioContextRef.current = ctx;\n audioSourceRef.current = source;\n audioAnalyserRef.current = analyser;\n\n waveformStopRef.current = startWaveformLoop({\n analyser,\n getCanvas: () => waveformCanvasRef.current,\n mode: 'line',\n });\n } catch {\n // The waveform is decoration: a browser that refuses an audio graph records without one.\n }\n },\n [stopWaveform],\n );\n\n const teardownCapture = useCallback(() => {\n acquireGenRef.current += 1;\n if (autoStopTimeoutRef.current) {\n clearTimeout(autoStopTimeoutRef.current);\n autoStopTimeoutRef.current = null;\n }\n mediaRecorderRef.current = null;\n\n const s = recordStreamRef.current;\n recordStreamRef.current = null;\n liveStreamKindRef.current = null;\n if (s) s.getTracks().forEach((t) => t.stop());\n\n if (livePreviewRef.current) {\n livePreviewRef.current.srcObject = null;\n }\n\n recordChunksRef.current = [];\n\n stopElapsedTimer();\n stopWaveform();\n }, [stopElapsedTimer, stopWaveform]);\n\n const acquireStream = useCallback(\n async (kind: 'audio' | 'video') => {\n const wantVideo = kind === 'video';\n const facingMode = cameraFacingMode ?? 'user';\n const gen = acquireGenRef.current;\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n video: wantVideo ? { facingMode } : false,\n });\n if (gen !== acquireGenRef.current) {\n stream.getTracks().forEach((t) => t.stop());\n return null;\n }\n recordStreamRef.current = stream;\n liveStreamKindRef.current = kind;\n setError(null);\n if (wantVideo && livePreviewRef.current) {\n livePreviewRef.current.srcObject = stream;\n livePreviewRef.current.muted = true;\n try {\n await livePreviewRef.current.play();\n } catch {\n // A browser that blocks preview playback rejects; recording does not depend on it.\n }\n } else if (!wantVideo) {\n startWaveform(stream);\n }\n return stream;\n },\n [cameraFacingMode, startWaveform],\n );\n\n const startLivePreview = useCallback(\n async (kind: 'audio' | 'video') => {\n teardownCapture();\n const req = ++previewReqRef.current;\n setAcquiring(true);\n try {\n await acquireStream(kind);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Camera/microphone access denied.');\n } finally {\n if (req === previewReqRef.current) setAcquiring(false);\n }\n },\n [teardownCapture, acquireStream],\n );\n\n useEffect(() => {\n if (open) {\n void startLivePreview(initialKind);\n }\n return () => {\n teardownCapture();\n revokeLastRecordUrl();\n };\n }, [open, initialKind, startLivePreview, teardownCapture, revokeLastRecordUrl]);\n\n const startRecording = useCallback(\n async (kind: 'audio' | 'video') => {\n revokeLastRecordUrl();\n setRecordPreviewUrl(null);\n setRecordedFile(null);\n setRecordKind(kind);\n\n const wantVideo = kind === 'video';\n\n let stream = recordStreamRef.current;\n if (!stream || liveStreamKindRef.current !== kind) {\n try {\n stream = await acquireStream(kind);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Camera/microphone access denied.');\n return;\n }\n }\n if (!stream) return;\n\n const preferredMimeType = pickRecorderMimeType(kind);\n let mr: MediaRecorder;\n try {\n mr = preferredMimeType\n ? new MediaRecorder(stream, { mimeType: preferredMimeType })\n : new MediaRecorder(stream);\n } catch {\n mr = new MediaRecorder(stream);\n }\n mediaRecorderRef.current = mr;\n recordChunksRef.current = [];\n\n mr.ondataavailable = (e) => {\n if (e.data && e.data.size > 0) recordChunksRef.current.push(e.data);\n };\n\n mr.onstop = () => {\n const blob = new Blob(recordChunksRef.current, {\n type: mr.mimeType || preferredMimeType || (wantVideo ? 'video/webm' : 'audio/webm'),\n });\n\n const rawFile = new File([blob], recordingFileName(blob.type, kind), { type: blob.type });\n const file = ensureFileWithContentType(rawFile, wantVideo ? 'video' : 'audio');\n const url = makeRecordUrl(file);\n\n setRecordedFile(file);\n setRecordPreviewUrl(url);\n setRecorderState('preview');\n\n teardownCapture();\n };\n\n mr.start();\n setRecorderState('recording');\n startElapsedTimer();\n\n if (maxRecordDurationSec && maxRecordDurationSec > 0) {\n autoStopTimeoutRef.current = setTimeout(\n () => stopIfRecording(mediaRecorderRef.current),\n autoStopDelayMs(maxRecordDurationSec),\n );\n }\n },\n [\n acquireStream,\n maxRecordDurationSec,\n makeRecordUrl,\n revokeLastRecordUrl,\n teardownCapture,\n startElapsedTimer,\n ],\n );\n\n const stopRecording = useCallback(() => {\n const mr = mediaRecorderRef.current;\n if (mr && mr.state === 'recording') {\n try {\n mr.stop();\n } catch {\n teardownCapture();\n setRecorderState('idle');\n }\n } else {\n teardownCapture();\n setRecorderState('idle');\n }\n }, [teardownCapture]);\n\n const handleReRecord = useCallback(() => {\n revokeLastRecordUrl();\n setRecordPreviewUrl(null);\n setRecordedFile(null);\n setElapsedMs(0);\n setError(null);\n setRecorderState('idle');\n void startLivePreview(recordKind);\n }, [revokeLastRecordUrl, startLivePreview, recordKind]);\n\n const handleSelectKind = useCallback(\n (kind: 'audio' | 'video') => {\n setRecordKind(kind);\n setError(null);\n void startLivePreview(kind);\n },\n [startLivePreview],\n );\n\n const reportError = useCallback((e: unknown) => {\n setError(e instanceof Error ? e.message : 'Something went wrong with the recording.');\n }, []);\n\n const start = useAsyncAction((kind: 'audio' | 'video') => startRecording(kind), {\n onError: reportError,\n });\n\n const save = useAsyncAction(\n async () => {\n if (!recordedFile || !recordPreviewUrl) return;\n await onRecorded(recordedFile, recordPreviewUrl, recordKind);\n onOpenChange(false);\n },\n { onError: reportError },\n );\n\n const closeDialogImmediately = useCallback(() => {\n stopIfRecording(mediaRecorderRef.current);\n teardownCapture();\n revokeLastRecordUrl();\n setRecordedFile(null);\n setRecordPreviewUrl(null);\n setElapsedMs(0);\n setRecorderState('idle');\n setDiscardConfirmOpen(false);\n onOpenChange(false);\n }, [teardownCapture, revokeLastRecordUrl, onOpenChange]);\n\n const handleDialogOpenChange = useCallback(\n (v: boolean) => {\n if (v) return;\n if (save.pending) return;\n if (recorderState === 'preview') {\n setDiscardConfirmOpen(true);\n return;\n }\n closeDialogImmediately();\n },\n [recorderState, save.pending, closeDialogImmediately],\n );\n\n const isRecording = recorderState === 'recording';\n const isPreview = recorderState === 'preview';\n const isIdle = recorderState === 'idle';\n\n const elapsedLabel = maxRecordDurationSec\n ? `${formatDuration(elapsedMs)} / ${formatDuration(maxRecordDurationSec * 1000)}`\n : formatDuration(elapsedMs);\n\n return (\n <>\n <Dialog open={open} onOpenChange={handleDialogOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Record</DialogTitle>\n <DialogDescription>\n {isIdle && 'Choose audio or video, then press Start.'}\n {isRecording && 'Recording…'}\n {isPreview && 'Review your recording. Save to upload, or Re-record to try again.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"flex flex-wrap gap-2\">\n {canRecVideo && (\n <Button\n type=\"button\"\n variant={recordKind === 'video' ? 'default' : 'secondary'}\n aria-pressed={recordKind === 'video'}\n onClick={() => handleSelectKind('video')}\n disabled={!isIdle || acquiring}\n icon={<Video className=\"icon-xs\" />}\n >\n Video\n </Button>\n )}\n {canRecAudio && (\n <Button\n type=\"button\"\n variant={recordKind === 'audio' ? 'default' : 'secondary'}\n aria-pressed={recordKind === 'audio'}\n onClick={() => handleSelectKind('audio')}\n disabled={!isIdle || acquiring}\n icon={<Mic className=\"icon-xs\" />}\n >\n Audio\n </Button>\n )}\n {canPhoto && onRequestPhoto && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={() => {\n onRequestPhoto();\n onOpenChange(false);\n }}\n disabled={!isIdle}\n icon={<Camera className=\"icon-xs\" />}\n >\n Photo\n </Button>\n )}\n </div>\n\n {recordKind === 'video' && !isPreview && (\n <div className=\"relative mt-2 rounded-md overflow-hidden bg-[hsl(var(--neutral-black))] aspect-video\">\n <video\n ref={livePreviewRef}\n className=\"w-full h-full object-contain\"\n playsInline\n muted\n />\n {acquiring && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner size=\"lg\" label=\"Starting camera\" />\n </div>\n )}\n </div>\n )}\n\n {recordKind === 'audio' && !isPreview && (\n <div className=\"relative mt-2 rounded-md bg-muted p-2 text-primary\">\n <canvas\n ref={waveformCanvasRef}\n width={480}\n height={80}\n className=\"w-full h-20\"\n aria-label=\"Audio waveform\"\n />\n {acquiring && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Spinner size=\"lg\" label=\"Starting microphone\" />\n </div>\n )}\n </div>\n )}\n\n {error && (\n <p className=\"text-sm text-destructive\" role=\"alert\">\n {error}\n </p>\n )}\n\n {isRecording && (\n <div className=\"flex items-center gap-2 text-sm\" aria-live=\"polite\">\n <span\n className=\"inline-block h-2.5 w-2.5 rounded-full bg-[color:var(--recording-indicator)] animate-pulse\"\n aria-label=\"Recording\"\n />\n <span className=\"tabular-nums\">{elapsedLabel}</span>\n </div>\n )}\n\n {isPreview && recordPreviewUrl && (\n <div className=\"mt-2\">\n <MediaPreview\n url={recordPreviewUrl}\n type={recordKind}\n controls\n priority\n className=\"w-full rounded-md\"\n />\n </div>\n )}\n\n <DialogFooter className=\"flex-row justify-between gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => handleDialogOpenChange(false)}\n disabled={save.pending}\n icon={<X className=\"icon-xs\" />}\n >\n Close\n </Button>\n\n {isIdle && (\n <Button\n type=\"button\"\n variant=\"default\"\n onClick={() => void start.run(recordKind)}\n disabled={disabled || isLoading || acquiring}\n pending={start.pending}\n icon={\n recordKind === 'video' ? (\n <Video className=\"icon-xs\" />\n ) : (\n <Mic className=\"icon-xs\" />\n )\n }\n >\n Start\n </Button>\n )}\n\n {isRecording && (\n <Button type=\"button\" variant=\"default\" onClick={stopRecording}>\n Stop\n </Button>\n )}\n\n {isPreview && (\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={handleReRecord}\n disabled={save.pending}\n icon={<RotateCcw className=\"icon-xs\" />}\n >\n Re-record\n </Button>\n <Button\n type=\"button\"\n variant=\"default\"\n onClick={() => void save.run()}\n disabled={disabled || isLoading}\n pending={save.pending}\n icon={<Check className=\"icon-xs\" />}\n >\n Save\n </Button>\n </div>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <AlertDialog open={discardConfirmOpen} onOpenChange={setDiscardConfirmOpen}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Discard recording?</AlertDialogTitle>\n <AlertDialogDescription>\n You have a recorded clip that hasn't been saved. Closing now will discard it.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep recording</AlertDialogCancel>\n <AlertDialogAction onClick={closeDialogImmediately}>Discard</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/video-input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"video-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/video-input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,wBAAgB,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,aAAa,CAAC,+BAEpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-input.js","sourceRoot":"","sources":["../../../src/react/components/video-input.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,UAAU,UAAU,CAAC,KAA0C;IACnE,OAAO,KAAC,SAAS,OAAK,KAAK,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,GAAI,CAAC;AAC1D,CAAC","sourcesContent":["import type { FileInputProps } from '
|
|
1
|
+
{"version":3,"file":"video-input.js","sourceRoot":"","sources":["../../../src/react/components/video-input.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,UAAU,UAAU,CAAC,KAA0C;IACnE,OAAO,KAAC,SAAS,OAAK,KAAK,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,GAAI,CAAC;AAC1D,CAAC","sourcesContent":["import type { FileInputProps } from './file-input.js';\nimport { FileInput } from './file-input.js';\n\nexport function VideoInput(props: Omit<FileInputProps, 'acceptTypes'>) {\n return <FileInput {...props} acceptTypes={['video']} />;\n}\n"]}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { FileInput } from './components/file-input.js';
|
|
2
|
+
export type { FileInputProps } from './components/file-input.js';
|
|
2
3
|
export { ImageInput } from './components/image-input.js';
|
|
3
4
|
export { VideoInput } from './components/video-input.js';
|
|
4
5
|
export { AudioInput } from './components/audio-input.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC","sourcesContent":["'use client';\n\nexport { FileInput } from './components/file-input.js';\nexport type { FileInputProps } from './components/file-input.js';\nexport { ImageInput } from './components/image-input.js';\nexport { VideoInput } from './components/video-input.js';\nexport { AudioInput } from './components/audio-input.js';\nexport { MediaInput } from './components/media-input.js';\nexport type { PhotoCaptureModalProps } from './components/photo-capture-modal.js';\nexport { PhotoCaptureModal } from './components/photo-capture-modal.js';\nexport { RecordDialog } from './components/record-dialog.js';\n"]}
|