expo-pro-video-editor 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,5 +40,19 @@ export declare function AudioTrimScrubber(props: {
40
40
  * how aggressively the loudest moment is pushed to full height.
41
41
  */
42
42
  contrastExponent?: number;
43
+ /**
44
+ * Already-extracted, already-normalized peaks for this exact inputPath —
45
+ * pass the result of a prior extractWaveform(inputPath,
46
+ * computeAudioTrimBarCount(width, durationSeconds, !!onDelete)) call (run
47
+ * normalizeWaveformPeaks(peaks, contrastExponent) yourself first, since
48
+ * this skips that step too) to skip this component's own extraction
49
+ * entirely, so pre-warming elsewhere actually avoids a second visible
50
+ * loading state here rather than merely priming a cache this component
51
+ * doesn't share. Ignored (falls back to extracting normally) if its
52
+ * length doesn't match the barCount this component computes for the
53
+ * given width/durationSeconds/onDelete — that mismatch means it was
54
+ * computed differently and can't be trusted as-is.
55
+ */
56
+ initialPeaks?: number[];
43
57
  }): import("react").JSX.Element;
44
58
  //# sourceMappingURL=AudioTrimScrubber.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AudioTrimScrubber.d.ts","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"AA2BA;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,GAAG,MAAM,CAMjH;AAgDD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;gBAGY;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,+BA0LA"}
1
+ {"version":3,"file":"AudioTrimScrubber.d.ts","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"AA2BA;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,GAAG,MAAM,CAMjH;AAgDD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;gBAGY;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,+BAmMA"}
@@ -91,18 +91,24 @@ const styles = {
91
91
  * before placing the handles; short tracks fit without scrolling.
92
92
  */
93
93
  export function AudioTrimScrubber(props) {
94
- const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent } = props;
94
+ const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent, initialPeaks } = props;
95
95
  const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;
96
96
  const scrubberWidth = width - deleteButtonSpace;
97
- /**
98
- * Keyed by the request that produced it, so a stale result is simply
99
- * never rendered — no separate reset-to-loading write is needed.
100
- */
101
- const [result, setResult] = useState(null);
102
97
  const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);
103
98
  const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;
104
99
  const barCount = computeAudioTrimBarCount(width, durationSeconds, !!onDelete);
100
+ const usableInitialPeaks = initialPeaks && initialPeaks.length === barCount ? initialPeaks : null;
101
+ /**
102
+ * Keyed by the request that produced it, so a stale result is simply
103
+ * never rendered — no separate reset-to-loading write is needed. Seeded
104
+ * from usableInitialPeaks so a caller that already extracted this exact
105
+ * (inputPath, barCount) pair never sees this component's own loading
106
+ * state at all.
107
+ */
108
+ const [result, setResult] = useState(usableInitialPeaks ? { inputPath, barCount, peaks: usableInitialPeaks } : null);
105
109
  useEffect(() => {
110
+ if (usableInitialPeaks)
111
+ return;
106
112
  let cancelled = false;
107
113
  ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)
108
114
  .then(peaks => {
@@ -116,7 +122,8 @@ export function AudioTrimScrubber(props) {
116
122
  return () => {
117
123
  cancelled = true;
118
124
  };
119
- }, [inputPath, barCount, contrastExponent]);
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [inputPath, barCount, contrastExponent, !!usableInitialPeaks]);
120
127
  const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;
121
128
  /**
122
129
  * Mirrored into refs (in an effect, not during render) so the
@@ -1 +1 @@
1
- {"version":3,"file":"AudioTrimScrubber.js","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, PanResponder, ScrollView, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport ExpoProVideoEditorModule from '../ExpoProVideoEditorModule';\nimport { normalizeWaveformPeaks } from '../waveform';\n\nconst HANDLE_WIDTH = 18;\nconst STRIP_HEIGHT = 51;\nconst MIN_TRIM_SECONDS = 0.5;\nconst MIN_BAR_COUNT = 90;\n// Caps extraction/render cost on a very long track — beyond this, bars\n// widen again rather than growing the native extraction and view count\n// without bound.\nconst MAX_BAR_COUNT = 2000;\nconst BAR_GAP = 1;\n// Target width (px) of a single bar+gap — barCount is derived from the\n// scrollable content's actual pixel width divided by this, not from\n// duration alone, so a bar stays this thin regardless of how long\n// (and therefore how wide, since this content scrolls) the track is.\nconst TARGET_PX_PER_BAR = 4;\nconst MIN_BAR_HEIGHT = 3;\nconst MAX_BAR_HEIGHT = STRIP_HEIGHT - 10;\nconst DELETE_BUTTON_SIZE = 24;\n// Width per second of audio, before scrolling kicks in.\nconst PIXELS_PER_SECOND = 40;\n\n/**\n * The exact bar-count math AudioTrimScrubber uses internally, exported so a\n * caller that wants to pre-warm extractWaveform (matching barCount so the\n * pre-warmed result is actually reused instead of the component silently\n * re-extracting with a different count) can compute the same value instead\n * of duplicating — and risking drifting out of sync with — this formula.\n *\n * `hasDeleteButton` should match whether the AudioTrimScrubber call passes\n * `onDelete` (it changes the usable width, and therefore the count).\n */\nexport function computeAudioTrimBarCount(width: number, durationSeconds: number, hasDeleteButton: boolean): number {\n const deleteButtonSpace = hasDeleteButton ? DELETE_BUTTON_SIZE + 8 : 0;\n const scrubberWidth = width - deleteButtonSpace;\n const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);\n const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;\n return Math.min(MAX_BAR_COUNT, Math.max(MIN_BAR_COUNT, Math.round(usableContentWidth / TARGET_PX_PER_BAR)));\n}\n\nconst styles = {\n row: tv({ base: 'h-[51px] flex-row items-center gap-2' }),\n scrollContainer: tv({ base: 'h-[51px]' }),\n container: tv({ base: 'h-[51px] justify-center overflow-hidden rounded-[10px]' }),\n waveform: tv({\n base: 'h-[51px] flex-row items-center justify-center gap-px overflow-hidden bg-video-editor-surface',\n }),\n bar: tv({\n base: 'rounded-full',\n variants: {\n played: {\n true: 'bg-video-editor-accent-light',\n false: 'bg-video-editor-bar-unplayed',\n },\n },\n }),\n selectionBorder: tv({\n base: 'absolute top-0 h-[51px] border-y-4 border-video-editor-accent-light',\n }),\n dimOverlay: tv({\n base: 'absolute bottom-0 top-0 bg-black/55',\n variants: {\n rounded: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n none: '',\n },\n },\n }),\n loadingOverlay: tv({ base: 'absolute inset-0 items-center justify-center' }),\n handle: tv({\n base: 'absolute top-0 h-[51px] w-[18px] items-center justify-center bg-video-editor-accent-light',\n variants: {\n side: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n },\n },\n }),\n handleGrip: tv({ base: 'h-[29px] w-0.5 rounded-full bg-white' }),\n deleteButton: tv({\n base: 'h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-white',\n }),\n deleteButtonText: tv({ base: 'text-xs text-video-editor-text' }),\n};\n\n/**\n * Independent trim UI for a standalone audio file (music / voice / a picked\n * file) — a waveform with drag handles marking the selected window. Long\n * tracks scroll horizontally so the user can pan to the part they want\n * before placing the handles; short tracks fit without scrolling.\n */\nexport function AudioTrimScrubber(props: {\n inputPath: string;\n durationSeconds: number;\n startSeconds: number;\n endSeconds: number;\n /** The audio player's own current position, in this file's own time\n * base — used to color the played portion of the waveform. `null` while\n * not actively playing this track, so bars fall back to the unplayed\n * shade. */\n currentTime: number | null;\n onChange: (range: { startSeconds: number; endSeconds: number }) => void;\n onDelete?: () => void;\n width: number;\n /**\n * Passed through to normalizeWaveformPeaks — lower it for a track whose\n * loudness is consistently close to its own peak (a synth/chiptune track\n * normalizes to near-max-height bars throughout at the default 1.6,\n * looking \"filled\" compared to a quieter, more dynamic recording). Has no\n * effect on relative bar-to-bar proportions within a single file, only on\n * how aggressively the loudest moment is pushed to full height.\n */\n contrastExponent?: number;\n}) {\n const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent } = props;\n const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;\n const scrubberWidth = width - deleteButtonSpace;\n /**\n * Keyed by the request that produced it, so a stale result is simply\n * never rendered — no separate reset-to-loading write is needed.\n */\n const [result, setResult] = useState<{ inputPath: string; barCount: number; peaks: number[] } | null>(null);\n\n const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);\n const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;\n const barCount = computeAudioTrimBarCount(width, durationSeconds, !!onDelete);\n\n useEffect(() => {\n let cancelled = false;\n\n ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)\n .then(peaks => {\n if (cancelled) return;\n setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent) });\n })\n .catch(error => {\n console.warn(`Waveform extraction failed for ${inputPath}:`, error);\n });\n\n return () => {\n cancelled = true;\n };\n }, [inputPath, barCount, contrastExponent]);\n\n const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;\n\n /**\n * Mirrored into refs (in an effect, not during render) so the\n * PanResponder instances below — created once, in a later effect — always\n * read current values instead of whatever was in scope when built.\n */\n const startXRef = useRef(startSeconds);\n const endXRef = useRef(endSeconds);\n const durationSecondsRef = useRef(durationSeconds);\n const usableContentWidthRef = useRef(usableContentWidth);\n useEffect(() => {\n startXRef.current = startSeconds;\n endXRef.current = endSeconds;\n durationSecondsRef.current = durationSeconds;\n usableContentWidthRef.current = usableContentWidth;\n });\n\n const startGestureAnchorRef = useRef(0);\n const endGestureAnchorRef = useRef(0);\n /**\n * The trim handles sit inside the horizontal ScrollView, so a drag on a\n * handle is ambiguous with the ScrollView's own pan-to-scroll recognizer —\n * without this, dragging a handle also scrolls the content underneath it.\n * Disabling scroll for the duration of a handle drag (grant → release)\n * resolves the conflict outright instead of fighting gesture priority.\n */\n const [scrollEnabled, setScrollEnabled] = useState(true);\n\n function secondsToX(seconds: number) {\n if (durationSeconds <= 0) return 0;\n return (seconds / durationSeconds) * usableContentWidth;\n }\n\n /**\n * Built once, after mount, so PanResponder.create() never runs during\n * render — its callbacks read the refs above for live values.\n */\n const [panResponders, setPanResponders] = useState<{\n start: ReturnType<typeof PanResponder.create>;\n end: ReturnType<typeof PanResponder.create>;\n } | null>(null);\n\n useEffect(() => {\n function secondsToXCurrent(seconds: number) {\n if (durationSecondsRef.current <= 0) return 0;\n return (seconds / durationSecondsRef.current) * usableContentWidthRef.current;\n }\n\n function xToSecondsCurrent(x: number) {\n if (usableContentWidthRef.current <= 0) return 0;\n return (x / usableContentWidthRef.current) * durationSecondsRef.current;\n }\n\n setPanResponders({\n start: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n startGestureAnchorRef.current = secondsToXCurrent(startXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(startGestureAnchorRef.current + gesture.dx);\n const clamped = Math.max(0, Math.min(nextSeconds, endXRef.current - MIN_TRIM_SECONDS));\n onChange({ startSeconds: clamped, endSeconds: endXRef.current });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n end: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n endGestureAnchorRef.current = secondsToXCurrent(endXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(endGestureAnchorRef.current + gesture.dx);\n const clamped = Math.min(durationSecondsRef.current, Math.max(nextSeconds, startXRef.current + MIN_TRIM_SECONDS));\n onChange({ startSeconds: startXRef.current, endSeconds: clamped });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n });\n }, []);\n\n const startX = secondsToX(startSeconds);\n const endX = secondsToX(endSeconds);\n const barWidth = peaks && peaks.length > 0 ? usableContentWidth / peaks.length - BAR_GAP : 0;\n\n const isStartAtEdge = startX <= 0;\n const isEndAtEdge = endX >= usableContentWidth;\n\n const trimDuration = Math.max(0, endSeconds - startSeconds);\n const playedFraction = currentTime !== null && trimDuration > 0 ? Math.min(1, Math.max(0, (currentTime - startSeconds) / trimDuration)) : 0;\n const playedBarCount = currentTime !== null && peaks ? Math.round(playedFraction * peaks.length) : 0;\n\n return (\n <View className={styles.row()} style={{ width }}>\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n scrollEnabled={scrollEnabled}\n className={styles.scrollContainer()}\n style={{ width: scrubberWidth }}\n contentContainerStyle={{ width: contentWidth }}>\n <View className={styles.container()} style={{ width: contentWidth }}>\n <View className={styles.waveform()} style={{ width: usableContentWidth, marginHorizontal: HANDLE_WIDTH }}>\n {peaks ? (\n peaks.map((peak, index) => (\n <View\n key={index}\n className={styles.bar({ played: index < playedBarCount })}\n style={{\n width: Math.max(1, barWidth),\n height: Math.min(MAX_BAR_HEIGHT, Math.max(MIN_BAR_HEIGHT, peak * STRIP_HEIGHT)),\n }}\n />\n ))\n ) : (\n <View className={styles.loadingOverlay()}>\n <ActivityIndicator size=\"small\" />\n </View>\n )}\n <View\n pointerEvents=\"none\"\n className={styles.dimOverlay({ rounded: isStartAtEdge ? 'none' : 'start' })}\n style={{ left: 0, width: startX }}\n />\n <View pointerEvents=\"none\" className={styles.dimOverlay({ rounded: isEndAtEdge ? 'none' : 'end' })} style={{ left: endX, right: 0 }} />\n <View pointerEvents=\"none\" className={styles.selectionBorder()} style={{ left: startX, width: endX - startX }} />\n </View>\n <View\n className={styles.handle({ side: 'start' })}\n style={{ left: startX }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.start.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n <View\n className={styles.handle({ side: 'end' })}\n style={{ left: endX + HANDLE_WIDTH }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.end.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n </View>\n </ScrollView>\n {onDelete ? (\n <TouchableOpacity className={styles.deleteButton()} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} onPress={onDelete}>\n <Text className={styles.deleteButtonText()}>✕</Text>\n </TouchableOpacity>\n ) : null}\n </View>\n );\n}\n"]}
1
+ {"version":3,"file":"AudioTrimScrubber.js","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, PanResponder, ScrollView, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport ExpoProVideoEditorModule from '../ExpoProVideoEditorModule';\nimport { normalizeWaveformPeaks } from '../waveform';\n\nconst HANDLE_WIDTH = 18;\nconst STRIP_HEIGHT = 51;\nconst MIN_TRIM_SECONDS = 0.5;\nconst MIN_BAR_COUNT = 90;\n// Caps extraction/render cost on a very long track — beyond this, bars\n// widen again rather than growing the native extraction and view count\n// without bound.\nconst MAX_BAR_COUNT = 2000;\nconst BAR_GAP = 1;\n// Target width (px) of a single bar+gap — barCount is derived from the\n// scrollable content's actual pixel width divided by this, not from\n// duration alone, so a bar stays this thin regardless of how long\n// (and therefore how wide, since this content scrolls) the track is.\nconst TARGET_PX_PER_BAR = 4;\nconst MIN_BAR_HEIGHT = 3;\nconst MAX_BAR_HEIGHT = STRIP_HEIGHT - 10;\nconst DELETE_BUTTON_SIZE = 24;\n// Width per second of audio, before scrolling kicks in.\nconst PIXELS_PER_SECOND = 40;\n\n/**\n * The exact bar-count math AudioTrimScrubber uses internally, exported so a\n * caller that wants to pre-warm extractWaveform (matching barCount so the\n * pre-warmed result is actually reused instead of the component silently\n * re-extracting with a different count) can compute the same value instead\n * of duplicating — and risking drifting out of sync with — this formula.\n *\n * `hasDeleteButton` should match whether the AudioTrimScrubber call passes\n * `onDelete` (it changes the usable width, and therefore the count).\n */\nexport function computeAudioTrimBarCount(width: number, durationSeconds: number, hasDeleteButton: boolean): number {\n const deleteButtonSpace = hasDeleteButton ? DELETE_BUTTON_SIZE + 8 : 0;\n const scrubberWidth = width - deleteButtonSpace;\n const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);\n const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;\n return Math.min(MAX_BAR_COUNT, Math.max(MIN_BAR_COUNT, Math.round(usableContentWidth / TARGET_PX_PER_BAR)));\n}\n\nconst styles = {\n row: tv({ base: 'h-[51px] flex-row items-center gap-2' }),\n scrollContainer: tv({ base: 'h-[51px]' }),\n container: tv({ base: 'h-[51px] justify-center overflow-hidden rounded-[10px]' }),\n waveform: tv({\n base: 'h-[51px] flex-row items-center justify-center gap-px overflow-hidden bg-video-editor-surface',\n }),\n bar: tv({\n base: 'rounded-full',\n variants: {\n played: {\n true: 'bg-video-editor-accent-light',\n false: 'bg-video-editor-bar-unplayed',\n },\n },\n }),\n selectionBorder: tv({\n base: 'absolute top-0 h-[51px] border-y-4 border-video-editor-accent-light',\n }),\n dimOverlay: tv({\n base: 'absolute bottom-0 top-0 bg-black/55',\n variants: {\n rounded: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n none: '',\n },\n },\n }),\n loadingOverlay: tv({ base: 'absolute inset-0 items-center justify-center' }),\n handle: tv({\n base: 'absolute top-0 h-[51px] w-[18px] items-center justify-center bg-video-editor-accent-light',\n variants: {\n side: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n },\n },\n }),\n handleGrip: tv({ base: 'h-[29px] w-0.5 rounded-full bg-white' }),\n deleteButton: tv({\n base: 'h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-white',\n }),\n deleteButtonText: tv({ base: 'text-xs text-video-editor-text' }),\n};\n\n/**\n * Independent trim UI for a standalone audio file (music / voice / a picked\n * file) — a waveform with drag handles marking the selected window. Long\n * tracks scroll horizontally so the user can pan to the part they want\n * before placing the handles; short tracks fit without scrolling.\n */\nexport function AudioTrimScrubber(props: {\n inputPath: string;\n durationSeconds: number;\n startSeconds: number;\n endSeconds: number;\n /** The audio player's own current position, in this file's own time\n * base — used to color the played portion of the waveform. `null` while\n * not actively playing this track, so bars fall back to the unplayed\n * shade. */\n currentTime: number | null;\n onChange: (range: { startSeconds: number; endSeconds: number }) => void;\n onDelete?: () => void;\n width: number;\n /**\n * Passed through to normalizeWaveformPeaks — lower it for a track whose\n * loudness is consistently close to its own peak (a synth/chiptune track\n * normalizes to near-max-height bars throughout at the default 1.6,\n * looking \"filled\" compared to a quieter, more dynamic recording). Has no\n * effect on relative bar-to-bar proportions within a single file, only on\n * how aggressively the loudest moment is pushed to full height.\n */\n contrastExponent?: number;\n /**\n * Already-extracted, already-normalized peaks for this exact inputPath —\n * pass the result of a prior extractWaveform(inputPath,\n * computeAudioTrimBarCount(width, durationSeconds, !!onDelete)) call (run\n * normalizeWaveformPeaks(peaks, contrastExponent) yourself first, since\n * this skips that step too) to skip this component's own extraction\n * entirely, so pre-warming elsewhere actually avoids a second visible\n * loading state here rather than merely priming a cache this component\n * doesn't share. Ignored (falls back to extracting normally) if its\n * length doesn't match the barCount this component computes for the\n * given width/durationSeconds/onDelete — that mismatch means it was\n * computed differently and can't be trusted as-is.\n */\n initialPeaks?: number[];\n}) {\n const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent, initialPeaks } = props;\n const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;\n const scrubberWidth = width - deleteButtonSpace;\n const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);\n const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;\n const barCount = computeAudioTrimBarCount(width, durationSeconds, !!onDelete);\n\n const usableInitialPeaks = initialPeaks && initialPeaks.length === barCount ? initialPeaks : null;\n\n /**\n * Keyed by the request that produced it, so a stale result is simply\n * never rendered — no separate reset-to-loading write is needed. Seeded\n * from usableInitialPeaks so a caller that already extracted this exact\n * (inputPath, barCount) pair never sees this component's own loading\n * state at all.\n */\n const [result, setResult] = useState<{ inputPath: string; barCount: number; peaks: number[] } | null>(\n usableInitialPeaks ? { inputPath, barCount, peaks: usableInitialPeaks } : null,\n );\n\n useEffect(() => {\n if (usableInitialPeaks) return;\n let cancelled = false;\n\n ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)\n .then(peaks => {\n if (cancelled) return;\n setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent) });\n })\n .catch(error => {\n console.warn(`Waveform extraction failed for ${inputPath}:`, error);\n });\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [inputPath, barCount, contrastExponent, !!usableInitialPeaks]);\n\n const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;\n\n /**\n * Mirrored into refs (in an effect, not during render) so the\n * PanResponder instances below — created once, in a later effect — always\n * read current values instead of whatever was in scope when built.\n */\n const startXRef = useRef(startSeconds);\n const endXRef = useRef(endSeconds);\n const durationSecondsRef = useRef(durationSeconds);\n const usableContentWidthRef = useRef(usableContentWidth);\n useEffect(() => {\n startXRef.current = startSeconds;\n endXRef.current = endSeconds;\n durationSecondsRef.current = durationSeconds;\n usableContentWidthRef.current = usableContentWidth;\n });\n\n const startGestureAnchorRef = useRef(0);\n const endGestureAnchorRef = useRef(0);\n /**\n * The trim handles sit inside the horizontal ScrollView, so a drag on a\n * handle is ambiguous with the ScrollView's own pan-to-scroll recognizer —\n * without this, dragging a handle also scrolls the content underneath it.\n * Disabling scroll for the duration of a handle drag (grant → release)\n * resolves the conflict outright instead of fighting gesture priority.\n */\n const [scrollEnabled, setScrollEnabled] = useState(true);\n\n function secondsToX(seconds: number) {\n if (durationSeconds <= 0) return 0;\n return (seconds / durationSeconds) * usableContentWidth;\n }\n\n /**\n * Built once, after mount, so PanResponder.create() never runs during\n * render — its callbacks read the refs above for live values.\n */\n const [panResponders, setPanResponders] = useState<{\n start: ReturnType<typeof PanResponder.create>;\n end: ReturnType<typeof PanResponder.create>;\n } | null>(null);\n\n useEffect(() => {\n function secondsToXCurrent(seconds: number) {\n if (durationSecondsRef.current <= 0) return 0;\n return (seconds / durationSecondsRef.current) * usableContentWidthRef.current;\n }\n\n function xToSecondsCurrent(x: number) {\n if (usableContentWidthRef.current <= 0) return 0;\n return (x / usableContentWidthRef.current) * durationSecondsRef.current;\n }\n\n setPanResponders({\n start: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n startGestureAnchorRef.current = secondsToXCurrent(startXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(startGestureAnchorRef.current + gesture.dx);\n const clamped = Math.max(0, Math.min(nextSeconds, endXRef.current - MIN_TRIM_SECONDS));\n onChange({ startSeconds: clamped, endSeconds: endXRef.current });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n end: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n endGestureAnchorRef.current = secondsToXCurrent(endXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(endGestureAnchorRef.current + gesture.dx);\n const clamped = Math.min(durationSecondsRef.current, Math.max(nextSeconds, startXRef.current + MIN_TRIM_SECONDS));\n onChange({ startSeconds: startXRef.current, endSeconds: clamped });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n });\n }, []);\n\n const startX = secondsToX(startSeconds);\n const endX = secondsToX(endSeconds);\n const barWidth = peaks && peaks.length > 0 ? usableContentWidth / peaks.length - BAR_GAP : 0;\n\n const isStartAtEdge = startX <= 0;\n const isEndAtEdge = endX >= usableContentWidth;\n\n const trimDuration = Math.max(0, endSeconds - startSeconds);\n const playedFraction = currentTime !== null && trimDuration > 0 ? Math.min(1, Math.max(0, (currentTime - startSeconds) / trimDuration)) : 0;\n const playedBarCount = currentTime !== null && peaks ? Math.round(playedFraction * peaks.length) : 0;\n\n return (\n <View className={styles.row()} style={{ width }}>\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n scrollEnabled={scrollEnabled}\n className={styles.scrollContainer()}\n style={{ width: scrubberWidth }}\n contentContainerStyle={{ width: contentWidth }}>\n <View className={styles.container()} style={{ width: contentWidth }}>\n <View className={styles.waveform()} style={{ width: usableContentWidth, marginHorizontal: HANDLE_WIDTH }}>\n {peaks ? (\n peaks.map((peak, index) => (\n <View\n key={index}\n className={styles.bar({ played: index < playedBarCount })}\n style={{\n width: Math.max(1, barWidth),\n height: Math.min(MAX_BAR_HEIGHT, Math.max(MIN_BAR_HEIGHT, peak * STRIP_HEIGHT)),\n }}\n />\n ))\n ) : (\n <View className={styles.loadingOverlay()}>\n <ActivityIndicator size=\"small\" />\n </View>\n )}\n <View\n pointerEvents=\"none\"\n className={styles.dimOverlay({ rounded: isStartAtEdge ? 'none' : 'start' })}\n style={{ left: 0, width: startX }}\n />\n <View pointerEvents=\"none\" className={styles.dimOverlay({ rounded: isEndAtEdge ? 'none' : 'end' })} style={{ left: endX, right: 0 }} />\n <View pointerEvents=\"none\" className={styles.selectionBorder()} style={{ left: startX, width: endX - startX }} />\n </View>\n <View\n className={styles.handle({ side: 'start' })}\n style={{ left: startX }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.start.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n <View\n className={styles.handle({ side: 'end' })}\n style={{ left: endX + HANDLE_WIDTH }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.end.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n </View>\n </ScrollView>\n {onDelete ? (\n <TouchableOpacity className={styles.deleteButton()} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} onPress={onDelete}>\n <Text className={styles.deleteButtonText()}>✕</Text>\n </TouchableOpacity>\n ) : null}\n </View>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-pro-video-editor",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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",
@@ -117,21 +117,43 @@ export function AudioTrimScrubber(props: {
117
117
  * how aggressively the loudest moment is pushed to full height.
118
118
  */
119
119
  contrastExponent?: number;
120
+ /**
121
+ * Already-extracted, already-normalized peaks for this exact inputPath —
122
+ * pass the result of a prior extractWaveform(inputPath,
123
+ * computeAudioTrimBarCount(width, durationSeconds, !!onDelete)) call (run
124
+ * normalizeWaveformPeaks(peaks, contrastExponent) yourself first, since
125
+ * this skips that step too) to skip this component's own extraction
126
+ * entirely, so pre-warming elsewhere actually avoids a second visible
127
+ * loading state here rather than merely priming a cache this component
128
+ * doesn't share. Ignored (falls back to extracting normally) if its
129
+ * length doesn't match the barCount this component computes for the
130
+ * given width/durationSeconds/onDelete — that mismatch means it was
131
+ * computed differently and can't be trusted as-is.
132
+ */
133
+ initialPeaks?: number[];
120
134
  }) {
121
- const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent } = props;
135
+ const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent, initialPeaks } = props;
122
136
  const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;
123
137
  const scrubberWidth = width - deleteButtonSpace;
124
- /**
125
- * Keyed by the request that produced it, so a stale result is simply
126
- * never rendered — no separate reset-to-loading write is needed.
127
- */
128
- const [result, setResult] = useState<{ inputPath: string; barCount: number; peaks: number[] } | null>(null);
129
-
130
138
  const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);
131
139
  const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;
132
140
  const barCount = computeAudioTrimBarCount(width, durationSeconds, !!onDelete);
133
141
 
142
+ const usableInitialPeaks = initialPeaks && initialPeaks.length === barCount ? initialPeaks : null;
143
+
144
+ /**
145
+ * Keyed by the request that produced it, so a stale result is simply
146
+ * never rendered — no separate reset-to-loading write is needed. Seeded
147
+ * from usableInitialPeaks so a caller that already extracted this exact
148
+ * (inputPath, barCount) pair never sees this component's own loading
149
+ * state at all.
150
+ */
151
+ const [result, setResult] = useState<{ inputPath: string; barCount: number; peaks: number[] } | null>(
152
+ usableInitialPeaks ? { inputPath, barCount, peaks: usableInitialPeaks } : null,
153
+ );
154
+
134
155
  useEffect(() => {
156
+ if (usableInitialPeaks) return;
135
157
  let cancelled = false;
136
158
 
137
159
  ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)
@@ -146,7 +168,8 @@ export function AudioTrimScrubber(props: {
146
168
  return () => {
147
169
  cancelled = true;
148
170
  };
149
- }, [inputPath, barCount, contrastExponent]);
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [inputPath, barCount, contrastExponent, !!usableInitialPeaks]);
150
173
 
151
174
  const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;
152
175