expo-pro-video-editor 0.2.13 → 0.2.14

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.
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceRecorder.d.ts","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AA0CtD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC,CAAC;AAUH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAC9B;;4DAEwD;IACxD,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB;;4EAEoE;QACpE,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC;KACnE,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,+BAuLA"}
1
+ {"version":3,"file":"VoiceRecorder.d.ts","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AA0CtD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC,CAAC;AAUH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAC9B;;4DAEwD;IACxD,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB;;4EAEoE;QACpE,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC;KACnE,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,+BA6MA"}
@@ -56,6 +56,13 @@ function DefaultContainer(props) {
56
56
  export function VoiceRecorder(props) {
57
57
  const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false, } = props;
58
58
  const [permissionError, setPermissionError] = useState(null);
59
+ /**
60
+ * Distinct from `permissionError`: the mic being held by another app/call
61
+ * is transient (the record button should stay tappable so the user can
62
+ * retry once it's free), unlike an OS-level permission denial which
63
+ * nothing in this sheet can resolve without the user leaving it.
64
+ */
65
+ const [micBusyError, setMicBusyError] = useState(null);
59
66
  const [recordedUri, setRecordedUri] = useState(null);
60
67
  const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);
61
68
  /**
@@ -87,7 +94,16 @@ export function VoiceRecorder(props) {
87
94
  * any background playback and the mic recording both alive together
88
95
  * rather than one silencing the other.
89
96
  */
90
- await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
97
+ try {
98
+ await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
99
+ }
100
+ catch {
101
+ // The mic is likely held elsewhere (an active call, another app's
102
+ // AVAudioSession/AudioRecord) — surface it instead of leaving the
103
+ // record button silently non-functional.
104
+ if (!cancelled)
105
+ setMicBusyError('Your microphone is being used elsewhere. Please try again once it’s free.');
106
+ }
91
107
  });
92
108
  return () => {
93
109
  cancelled = true;
@@ -105,6 +121,7 @@ export function VoiceRecorder(props) {
105
121
  setRecordedUri(null);
106
122
  setRecordedDurationSeconds(0);
107
123
  setFrozenTime(null);
124
+ setMicBusyError(null);
108
125
  }
109
126
  }
110
127
  // Don't start playing anything until the user actually presses record.
@@ -145,8 +162,16 @@ export function VoiceRecorder(props) {
145
162
  * a take had already been stopped once, re-preparing that same instance
146
163
  * produced a file with no audio data on the second take.
147
164
  */
148
- await recorder.prepareToRecordAsync(RECORDING_OPTIONS);
149
- recorder.record();
165
+ try {
166
+ await recorder.prepareToRecordAsync(RECORDING_OPTIONS);
167
+ recorder.record();
168
+ setMicBusyError(null);
169
+ }
170
+ catch {
171
+ // Mic likely held elsewhere (active call, another app's audio session).
172
+ setMicBusyError('Your microphone is being used elsewhere. Please try again once it’s free.');
173
+ return;
174
+ }
150
175
  progress?.onPlaybackRequest('play');
151
176
  }
152
177
  /**
@@ -178,7 +203,7 @@ export function VoiceRecorder(props) {
178
203
  <View className={styles.sheet({ bare })}>
179
204
  <Text className={styles.title()}>{title}</Text>
180
205
 
181
- {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}
206
+ {permissionError || micBusyError ? <Text className={styles.errorText()}>{permissionError ?? micBusyError}</Text> : null}
182
207
 
183
208
  {progress ? (<RecordingIndicator mode="filmstrip" uri={progress.videoUri} durationSeconds={progress.totalDuration} currentTime={frozenTime ?? progress.currentTime} width={width}/>) : (<RecordingIndicator mode="waveform" metering={recorderState.metering} isRecording={recorderState.isRecording} width={width}/>)}
184
209
 
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceRecorder.js","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync, useAudioRecorder, useAudioRecorderState } from 'expo-audio';\nimport { Mic, Square } from 'lucide-react-native';\nimport type { ComponentType, ReactNode } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Modal, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport { RecordingIndicator } from './RecordingIndicator';\n\nconst RECORDING_OPTIONS = { ...RecordingPresets.HIGH_QUALITY, isMeteringEnabled: true };\n\nconst styles = {\n backdrop: tv({ base: 'flex-1 justify-end bg-black/45' }),\n sheet: tv({\n base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',\n variants: {\n bare: { true: 'gap-4 bg-transparent px-5 py-0' },\n },\n }),\n title: tv({ base: 'text-base font-semibold text-video-editor-text' }),\n errorText: tv({ base: 'text-center text-[13px] text-video-editor-text-muted' }),\n controlsRow: tv({ base: 'flex-row items-center justify-center gap-6 py-2' }),\n controlSpacer: tv({ base: 'w-10' }),\n recordButtonRing: tv({\n base: 'h-[76px] w-[76px] items-center justify-center rounded-full border-4 border-video-editor-border',\n variants: { disabled: { true: 'opacity-50' } },\n }),\n recordCircle: tv({\n base: 'h-[60px] w-[60px] items-center justify-center rounded-full bg-video-editor-accent',\n variants: { disabled: { true: 'bg-video-editor-bar-unplayed' } },\n }),\n discardButton: tv({\n base: 'h-10 w-10 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-video-editor-surface',\n }),\n discardButtonText: tv({ base: 'text-base text-video-editor-text' }),\n bottomBar: tv({ base: 'flex-row items-center justify-between' }),\n cancelText: tv({ base: 'text-sm text-video-editor-text' }),\n doneButton: tv({\n base: 'h-[38px] items-center justify-center rounded-full bg-video-editor-accent px-5',\n variants: { disabled: { true: 'opacity-40' } },\n }),\n doneButtonText: tv({ base: 'text-sm font-semibold text-white' }),\n};\n\nexport type VoiceRecorderResult = {\n uri: string;\n durationSeconds: number;\n};\n\n/**\n * The container a `VoiceRecorder` presents in — defaults to a plain React\n * Native `Modal` rendered as a bottom sheet. Pass a real bottom-sheet\n * component (e.g. `@gorhom/bottom-sheet`'s `BottomSheetModal`, or a wrapper\n * like `@niibase/bottom-sheet-manager`'s `BottomSheet`) to get native sheet\n * behavior (drag-to-dismiss, snap points) instead — this package has no\n * opinion on which sheet library you use, and doesn't depend on one.\n */\nexport type VoiceRecorderContainer = ComponentType<{\n visible: boolean;\n onRequestClose: () => void;\n children: ReactNode;\n}>;\n\nfunction DefaultContainer(props: { visible: boolean; onRequestClose: () => void; children: ReactNode }) {\n return (\n <Modal visible={props.visible} transparent animationType=\"slide\" onRequestClose={props.onRequestClose}>\n <View className={styles.backdrop()}>{props.children}</View>\n </Modal>\n );\n}\n\n/**\n * A record/stop/discard/done voice recorder, presented as a bottom sheet.\n * Renders on top of whatever the caller passes as `backgroundContent` (e.g.\n * an editor's own video preview, kept alive and playing behind the sheet) —\n * or nothing, for a recorder with no visual context (e.g. a chat app).\n *\n * `progress`, if provided, drives a `RecordingIndicator` in `'filmstrip'`\n * mode (a video's own frames with a moving playhead) so the user can see a\n * fixed-length recording's end coming. Without it, the indicator falls back\n * to a live mic-amplitude waveform — no video required.\n */\nexport function VoiceRecorder(props: {\n visible: boolean;\n onCancel: () => void;\n onConfirm: (result: VoiceRecorderResult) => void;\n title?: string;\n width: number;\n backgroundContent?: ReactNode;\n /** Ties the recording indicator to a video's own playhead (filmstrip\n * mode) and auto-stops the take once the video reaches its end. Omit for\n * a plain live-amplitude waveform with no fixed end. */\n progress?: {\n videoUri: string | null;\n currentTime: number;\n totalDuration: number;\n /** Called every render while recording so the caller can drive its own\n * video player in lockstep (play on record, pause/rewind on\n * stop/discard) — this component has no video player of its own. */\n onPlaybackRequest: (action: 'play' | 'pause' | 'restart') => void;\n };\n /** Sheet container to present in — defaults to a plain `Modal`. */\n Container?: VoiceRecorderContainer;\n /**\n * Set when embedding this inside a caller-provided sheet component that\n * already supplies its own background, padding, and rounded top corners\n * (e.g. via a custom `Container`) — drops this component's own matching\n * styling so the two don't stack into two slightly different, visibly\n * seamed surfaces. Leave unset for the default `Modal` presentation.\n */\n bare?: boolean;\n}) {\n const {\n visible,\n onCancel,\n onConfirm,\n title = 'Voice-over',\n width,\n backgroundContent,\n progress,\n Container = DefaultContainer,\n bare = false,\n } = props;\n const [permissionError, setPermissionError] = useState<string | null>(null);\n const [recordedUri, setRecordedUri] = useState<string | null>(null);\n const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);\n /**\n * Where the filmstrip's played-overlay should freeze once a take is\n * stopped. The live `progress.currentTime` keeps moving after that — a\n * caller's own trim-preview effect may rewind its player once it reaches\n * the end — so the overlay needs its own frozen snapshot instead of\n * reading the live position, to stay put where the recording actually\n * ended.\n */\n const [frozenTime, setFrozenTime] = useState<number | null>(null);\n\n const recorder = useAudioRecorder(RECORDING_OPTIONS);\n const recorderState = useAudioRecorderState(recorder, 100);\n\n useEffect(() => {\n if (!visible) return;\n let cancelled = false;\n\n requestRecordingPermissionsAsync().then(async ({ granted }) => {\n if (cancelled) return;\n if (!granted) {\n setPermissionError('Microphone access is required to record.');\n return;\n }\n /**\n * The audio session defaults to `allowsRecording: false` — record()\n * rejects with RecordingDisabledException until this is set, even\n * with the OS permission already granted. `playsInSilentMode` keeps\n * any background playback and the mic recording both alive together\n * rather than one silencing the other.\n */\n await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });\n });\n\n return () => {\n cancelled = true;\n };\n }, [visible]);\n\n /**\n * Reset to a clean slate every time the sheet opens, computed during\n * render (not an effect) since this component stays mounted across\n * open/close rather than remounting.\n */\n const [wasVisible, setWasVisible] = useState(visible);\n if (visible !== wasVisible) {\n setWasVisible(visible);\n if (visible) {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n }\n }\n\n // Don't start playing anything until the user actually presses record.\n useEffect(() => {\n if (!visible) return;\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n }, [visible]);\n\n async function stopRecording() {\n setFrozenTime(progress?.currentTime ?? 0);\n await recorder.stop();\n progress?.onPlaybackRequest('pause');\n setRecordedUri(recorder.uri);\n setRecordedDurationSeconds(recorderState.durationMillis / 1000);\n }\n\n async function handleToggleRecording() {\n if (recorderState.isRecording) {\n await stopRecording();\n return;\n }\n\n setRecordedUri(null);\n setFrozenTime(null);\n /**\n * Fully pause/rewind before asking AVAudioRecorder to prepare — starting\n * playback and preparing the recorder in the same tick can leave the two\n * contending for the audio session on-device (\"Failed to prepare\n * recorder\"). Only once the recorder is confirmed ready do both\n * playback and recording start together.\n */\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n /**\n * Re-passing the preset (instead of calling prepareToRecordAsync() bare)\n * forces the native side to build a brand-new AVAudioRecorder at a fresh\n * file path every take. With no options, prepare() reuses the same\n * recorder instance from the previous take at its original file — after\n * a take had already been stopped once, re-preparing that same instance\n * produced a file with no audio data on the second take.\n */\n await recorder.prepareToRecordAsync(RECORDING_OPTIONS);\n recorder.record();\n progress?.onPlaybackRequest('play');\n }\n\n /**\n * A fixed-length recording context (e.g. narrating over a video) auto-stops\n * the take once that context reaches its end, capping it at exactly one\n * pass instead of freezing progress without ever stopping the recording.\n */\n useEffect(() => {\n if (!visible || !progress || !recorderState.isRecording) return;\n if (progress.totalDuration > 0 && progress.currentTime >= progress.totalDuration - 0.05) {\n queueMicrotask(stopRecording);\n }\n }, [visible, recorderState.isRecording, progress?.currentTime, progress?.totalDuration]);\n\n function handleDiscard() {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n progress?.onPlaybackRequest('restart');\n progress?.onPlaybackRequest('play');\n }\n\n function handleDone() {\n if (!recordedUri) return;\n onConfirm({ uri: recordedUri, durationSeconds: recordedDurationSeconds });\n }\n\n return (\n <Container visible={visible} onRequestClose={onCancel}>\n {backgroundContent}\n <View className={styles.sheet({ bare })}>\n <Text className={styles.title()}>{title}</Text>\n\n {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}\n\n {progress ? (\n <RecordingIndicator\n mode=\"filmstrip\"\n uri={progress.videoUri}\n durationSeconds={progress.totalDuration}\n currentTime={frozenTime ?? progress.currentTime}\n width={width}\n />\n ) : (\n <RecordingIndicator mode=\"waveform\" metering={recorderState.metering} isRecording={recorderState.isRecording} width={width} />\n )}\n\n <View className={styles.controlsRow()}>\n {recordedUri ? (\n <>\n <View className={styles.controlSpacer()} />\n <RecordButton isRecording={false} disabled onPress={handleToggleRecording} />\n <TouchableOpacity className={styles.discardButton()} onPress={handleDiscard}>\n <Text className={styles.discardButtonText()}>✕</Text>\n </TouchableOpacity>\n </>\n ) : (\n <RecordButton isRecording={recorderState.isRecording} disabled={!!permissionError} onPress={handleToggleRecording} />\n )}\n </View>\n\n <View className={styles.bottomBar()}>\n <TouchableOpacity onPress={onCancel}>\n <Text className={styles.cancelText()}>Cancel</Text>\n </TouchableOpacity>\n <TouchableOpacity className={styles.doneButton({ disabled: !recordedUri })} onPress={handleDone} disabled={!recordedUri}>\n <Text className={styles.doneButtonText()}>Done</Text>\n </TouchableOpacity>\n </View>\n </View>\n </Container>\n );\n}\n\nconst ICON_COLOR = '#ffffff';\n\nfunction RecordButton(props: { isRecording: boolean; disabled: boolean; onPress: () => void }) {\n const { isRecording, disabled, onPress } = props;\n return (\n <TouchableOpacity className={styles.recordButtonRing({ disabled })} onPress={onPress} disabled={disabled}>\n {isRecording ? (\n <View className={styles.recordCircle()}>\n <Square size={20} color={ICON_COLOR} fill={ICON_COLOR} />\n </View>\n ) : (\n <View className={styles.recordCircle({ disabled })}>\n <Mic size={26} color={ICON_COLOR} />\n </View>\n )}\n </TouchableOpacity>\n );\n}\n"]}
1
+ {"version":3,"file":"VoiceRecorder.js","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync, useAudioRecorder, useAudioRecorderState } from 'expo-audio';\nimport { Mic, Square } from 'lucide-react-native';\nimport type { ComponentType, ReactNode } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Modal, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport { RecordingIndicator } from './RecordingIndicator';\n\nconst RECORDING_OPTIONS = { ...RecordingPresets.HIGH_QUALITY, isMeteringEnabled: true };\n\nconst styles = {\n backdrop: tv({ base: 'flex-1 justify-end bg-black/45' }),\n sheet: tv({\n base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',\n variants: {\n bare: { true: 'gap-4 bg-transparent px-5 py-0' },\n },\n }),\n title: tv({ base: 'text-base font-semibold text-video-editor-text' }),\n errorText: tv({ base: 'text-center text-[13px] text-video-editor-text-muted' }),\n controlsRow: tv({ base: 'flex-row items-center justify-center gap-6 py-2' }),\n controlSpacer: tv({ base: 'w-10' }),\n recordButtonRing: tv({\n base: 'h-[76px] w-[76px] items-center justify-center rounded-full border-4 border-video-editor-border',\n variants: { disabled: { true: 'opacity-50' } },\n }),\n recordCircle: tv({\n base: 'h-[60px] w-[60px] items-center justify-center rounded-full bg-video-editor-accent',\n variants: { disabled: { true: 'bg-video-editor-bar-unplayed' } },\n }),\n discardButton: tv({\n base: 'h-10 w-10 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-video-editor-surface',\n }),\n discardButtonText: tv({ base: 'text-base text-video-editor-text' }),\n bottomBar: tv({ base: 'flex-row items-center justify-between' }),\n cancelText: tv({ base: 'text-sm text-video-editor-text' }),\n doneButton: tv({\n base: 'h-[38px] items-center justify-center rounded-full bg-video-editor-accent px-5',\n variants: { disabled: { true: 'opacity-40' } },\n }),\n doneButtonText: tv({ base: 'text-sm font-semibold text-white' }),\n};\n\nexport type VoiceRecorderResult = {\n uri: string;\n durationSeconds: number;\n};\n\n/**\n * The container a `VoiceRecorder` presents in — defaults to a plain React\n * Native `Modal` rendered as a bottom sheet. Pass a real bottom-sheet\n * component (e.g. `@gorhom/bottom-sheet`'s `BottomSheetModal`, or a wrapper\n * like `@niibase/bottom-sheet-manager`'s `BottomSheet`) to get native sheet\n * behavior (drag-to-dismiss, snap points) instead — this package has no\n * opinion on which sheet library you use, and doesn't depend on one.\n */\nexport type VoiceRecorderContainer = ComponentType<{\n visible: boolean;\n onRequestClose: () => void;\n children: ReactNode;\n}>;\n\nfunction DefaultContainer(props: { visible: boolean; onRequestClose: () => void; children: ReactNode }) {\n return (\n <Modal visible={props.visible} transparent animationType=\"slide\" onRequestClose={props.onRequestClose}>\n <View className={styles.backdrop()}>{props.children}</View>\n </Modal>\n );\n}\n\n/**\n * A record/stop/discard/done voice recorder, presented as a bottom sheet.\n * Renders on top of whatever the caller passes as `backgroundContent` (e.g.\n * an editor's own video preview, kept alive and playing behind the sheet) —\n * or nothing, for a recorder with no visual context (e.g. a chat app).\n *\n * `progress`, if provided, drives a `RecordingIndicator` in `'filmstrip'`\n * mode (a video's own frames with a moving playhead) so the user can see a\n * fixed-length recording's end coming. Without it, the indicator falls back\n * to a live mic-amplitude waveform — no video required.\n */\nexport function VoiceRecorder(props: {\n visible: boolean;\n onCancel: () => void;\n onConfirm: (result: VoiceRecorderResult) => void;\n title?: string;\n width: number;\n backgroundContent?: ReactNode;\n /** Ties the recording indicator to a video's own playhead (filmstrip\n * mode) and auto-stops the take once the video reaches its end. Omit for\n * a plain live-amplitude waveform with no fixed end. */\n progress?: {\n videoUri: string | null;\n currentTime: number;\n totalDuration: number;\n /** Called every render while recording so the caller can drive its own\n * video player in lockstep (play on record, pause/rewind on\n * stop/discard) — this component has no video player of its own. */\n onPlaybackRequest: (action: 'play' | 'pause' | 'restart') => void;\n };\n /** Sheet container to present in — defaults to a plain `Modal`. */\n Container?: VoiceRecorderContainer;\n /**\n * Set when embedding this inside a caller-provided sheet component that\n * already supplies its own background, padding, and rounded top corners\n * (e.g. via a custom `Container`) — drops this component's own matching\n * styling so the two don't stack into two slightly different, visibly\n * seamed surfaces. Leave unset for the default `Modal` presentation.\n */\n bare?: boolean;\n}) {\n const {\n visible,\n onCancel,\n onConfirm,\n title = 'Voice-over',\n width,\n backgroundContent,\n progress,\n Container = DefaultContainer,\n bare = false,\n } = props;\n const [permissionError, setPermissionError] = useState<string | null>(null);\n /**\n * Distinct from `permissionError`: the mic being held by another app/call\n * is transient (the record button should stay tappable so the user can\n * retry once it's free), unlike an OS-level permission denial which\n * nothing in this sheet can resolve without the user leaving it.\n */\n const [micBusyError, setMicBusyError] = useState<string | null>(null);\n const [recordedUri, setRecordedUri] = useState<string | null>(null);\n const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);\n /**\n * Where the filmstrip's played-overlay should freeze once a take is\n * stopped. The live `progress.currentTime` keeps moving after that — a\n * caller's own trim-preview effect may rewind its player once it reaches\n * the end — so the overlay needs its own frozen snapshot instead of\n * reading the live position, to stay put where the recording actually\n * ended.\n */\n const [frozenTime, setFrozenTime] = useState<number | null>(null);\n\n const recorder = useAudioRecorder(RECORDING_OPTIONS);\n const recorderState = useAudioRecorderState(recorder, 100);\n\n useEffect(() => {\n if (!visible) return;\n let cancelled = false;\n\n requestRecordingPermissionsAsync().then(async ({ granted }) => {\n if (cancelled) return;\n if (!granted) {\n setPermissionError('Microphone access is required to record.');\n return;\n }\n /**\n * The audio session defaults to `allowsRecording: false` — record()\n * rejects with RecordingDisabledException until this is set, even\n * with the OS permission already granted. `playsInSilentMode` keeps\n * any background playback and the mic recording both alive together\n * rather than one silencing the other.\n */\n try {\n await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });\n } catch {\n // The mic is likely held elsewhere (an active call, another app's\n // AVAudioSession/AudioRecord) — surface it instead of leaving the\n // record button silently non-functional.\n if (!cancelled) setMicBusyError('Your microphone is being used elsewhere. Please try again once it’s free.');\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [visible]);\n\n /**\n * Reset to a clean slate every time the sheet opens, computed during\n * render (not an effect) since this component stays mounted across\n * open/close rather than remounting.\n */\n const [wasVisible, setWasVisible] = useState(visible);\n if (visible !== wasVisible) {\n setWasVisible(visible);\n if (visible) {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n setMicBusyError(null);\n }\n }\n\n // Don't start playing anything until the user actually presses record.\n useEffect(() => {\n if (!visible) return;\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n }, [visible]);\n\n async function stopRecording() {\n setFrozenTime(progress?.currentTime ?? 0);\n await recorder.stop();\n progress?.onPlaybackRequest('pause');\n setRecordedUri(recorder.uri);\n setRecordedDurationSeconds(recorderState.durationMillis / 1000);\n }\n\n async function handleToggleRecording() {\n if (recorderState.isRecording) {\n await stopRecording();\n return;\n }\n\n setRecordedUri(null);\n setFrozenTime(null);\n /**\n * Fully pause/rewind before asking AVAudioRecorder to prepare — starting\n * playback and preparing the recorder in the same tick can leave the two\n * contending for the audio session on-device (\"Failed to prepare\n * recorder\"). Only once the recorder is confirmed ready do both\n * playback and recording start together.\n */\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n /**\n * Re-passing the preset (instead of calling prepareToRecordAsync() bare)\n * forces the native side to build a brand-new AVAudioRecorder at a fresh\n * file path every take. With no options, prepare() reuses the same\n * recorder instance from the previous take at its original file — after\n * a take had already been stopped once, re-preparing that same instance\n * produced a file with no audio data on the second take.\n */\n try {\n await recorder.prepareToRecordAsync(RECORDING_OPTIONS);\n recorder.record();\n setMicBusyError(null);\n } catch {\n // Mic likely held elsewhere (active call, another app's audio session).\n setMicBusyError('Your microphone is being used elsewhere. Please try again once it’s free.');\n return;\n }\n progress?.onPlaybackRequest('play');\n }\n\n /**\n * A fixed-length recording context (e.g. narrating over a video) auto-stops\n * the take once that context reaches its end, capping it at exactly one\n * pass instead of freezing progress without ever stopping the recording.\n */\n useEffect(() => {\n if (!visible || !progress || !recorderState.isRecording) return;\n if (progress.totalDuration > 0 && progress.currentTime >= progress.totalDuration - 0.05) {\n queueMicrotask(stopRecording);\n }\n }, [visible, recorderState.isRecording, progress?.currentTime, progress?.totalDuration]);\n\n function handleDiscard() {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n progress?.onPlaybackRequest('restart');\n progress?.onPlaybackRequest('play');\n }\n\n function handleDone() {\n if (!recordedUri) return;\n onConfirm({ uri: recordedUri, durationSeconds: recordedDurationSeconds });\n }\n\n return (\n <Container visible={visible} onRequestClose={onCancel}>\n {backgroundContent}\n <View className={styles.sheet({ bare })}>\n <Text className={styles.title()}>{title}</Text>\n\n {permissionError || micBusyError ? <Text className={styles.errorText()}>{permissionError ?? micBusyError}</Text> : null}\n\n {progress ? (\n <RecordingIndicator\n mode=\"filmstrip\"\n uri={progress.videoUri}\n durationSeconds={progress.totalDuration}\n currentTime={frozenTime ?? progress.currentTime}\n width={width}\n />\n ) : (\n <RecordingIndicator mode=\"waveform\" metering={recorderState.metering} isRecording={recorderState.isRecording} width={width} />\n )}\n\n <View className={styles.controlsRow()}>\n {recordedUri ? (\n <>\n <View className={styles.controlSpacer()} />\n <RecordButton isRecording={false} disabled onPress={handleToggleRecording} />\n <TouchableOpacity className={styles.discardButton()} onPress={handleDiscard}>\n <Text className={styles.discardButtonText()}>✕</Text>\n </TouchableOpacity>\n </>\n ) : (\n <RecordButton isRecording={recorderState.isRecording} disabled={!!permissionError} onPress={handleToggleRecording} />\n )}\n </View>\n\n <View className={styles.bottomBar()}>\n <TouchableOpacity onPress={onCancel}>\n <Text className={styles.cancelText()}>Cancel</Text>\n </TouchableOpacity>\n <TouchableOpacity className={styles.doneButton({ disabled: !recordedUri })} onPress={handleDone} disabled={!recordedUri}>\n <Text className={styles.doneButtonText()}>Done</Text>\n </TouchableOpacity>\n </View>\n </View>\n </Container>\n );\n}\n\nconst ICON_COLOR = '#ffffff';\n\nfunction RecordButton(props: { isRecording: boolean; disabled: boolean; onPress: () => void }) {\n const { isRecording, disabled, onPress } = props;\n return (\n <TouchableOpacity className={styles.recordButtonRing({ disabled })} onPress={onPress} disabled={disabled}>\n {isRecording ? (\n <View className={styles.recordCircle()}>\n <Square size={20} color={ICON_COLOR} fill={ICON_COLOR} />\n </View>\n ) : (\n <View className={styles.recordCircle({ disabled })}>\n <Mic size={26} color={ICON_COLOR} />\n </View>\n )}\n </TouchableOpacity>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-pro-video-editor",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Native video composition (trim, filters, text/image overlays, audio mixing) for Expo/React Native, adapted from pro_video_editor's AVFoundation and Media3 Transformer implementation.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -122,6 +122,13 @@ export function VoiceRecorder(props: {
122
122
  bare = false,
123
123
  } = props;
124
124
  const [permissionError, setPermissionError] = useState<string | null>(null);
125
+ /**
126
+ * Distinct from `permissionError`: the mic being held by another app/call
127
+ * is transient (the record button should stay tappable so the user can
128
+ * retry once it's free), unlike an OS-level permission denial which
129
+ * nothing in this sheet can resolve without the user leaving it.
130
+ */
131
+ const [micBusyError, setMicBusyError] = useState<string | null>(null);
125
132
  const [recordedUri, setRecordedUri] = useState<string | null>(null);
126
133
  const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);
127
134
  /**
@@ -154,7 +161,14 @@ export function VoiceRecorder(props: {
154
161
  * any background playback and the mic recording both alive together
155
162
  * rather than one silencing the other.
156
163
  */
157
- await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
164
+ try {
165
+ await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
166
+ } catch {
167
+ // The mic is likely held elsewhere (an active call, another app's
168
+ // AVAudioSession/AudioRecord) — surface it instead of leaving the
169
+ // record button silently non-functional.
170
+ if (!cancelled) setMicBusyError('Your microphone is being used elsewhere. Please try again once it’s free.');
171
+ }
158
172
  });
159
173
 
160
174
  return () => {
@@ -174,6 +188,7 @@ export function VoiceRecorder(props: {
174
188
  setRecordedUri(null);
175
189
  setRecordedDurationSeconds(0);
176
190
  setFrozenTime(null);
191
+ setMicBusyError(null);
177
192
  }
178
193
  }
179
194
 
@@ -217,8 +232,15 @@ export function VoiceRecorder(props: {
217
232
  * a take had already been stopped once, re-preparing that same instance
218
233
  * produced a file with no audio data on the second take.
219
234
  */
220
- await recorder.prepareToRecordAsync(RECORDING_OPTIONS);
221
- recorder.record();
235
+ try {
236
+ await recorder.prepareToRecordAsync(RECORDING_OPTIONS);
237
+ recorder.record();
238
+ setMicBusyError(null);
239
+ } catch {
240
+ // Mic likely held elsewhere (active call, another app's audio session).
241
+ setMicBusyError('Your microphone is being used elsewhere. Please try again once it’s free.');
242
+ return;
243
+ }
222
244
  progress?.onPlaybackRequest('play');
223
245
  }
224
246
 
@@ -253,7 +275,7 @@ export function VoiceRecorder(props: {
253
275
  <View className={styles.sheet({ bare })}>
254
276
  <Text className={styles.title()}>{title}</Text>
255
277
 
256
- {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}
278
+ {permissionError || micBusyError ? <Text className={styles.errorText()}>{permissionError ?? micBusyError}</Text> : null}
257
279
 
258
280
  {progress ? (
259
281
  <RecordingIndicator