expo-pro-video-editor 0.2.6 → 0.2.8

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.
@@ -23,6 +23,26 @@ declare class ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorMo
23
23
  * `WAVEFORM_ERROR` if the file has no audio track or cannot be read.
24
24
  */
25
25
  extractWaveform(inputPath: string, bucketCount: number): Promise<number[]>;
26
+ /**
27
+ * Exports a photo-library video asset to a real, playable local file at
28
+ * `destinationPath`, using `PHImageManager.requestExportSession` — the API
29
+ * Apple documents for producing a genuinely exportable representation of a
30
+ * video asset. A `ph://` URI (or a resolved `file://` path under the
31
+ * Photos app's own protected storage) cannot be opened directly by
32
+ * AVPlayer/expo-video, and copying via `PHAssetResourceManager` has been
33
+ * observed failing unpredictably on some devices/OS versions even for
34
+ * locally-available assets — this is the more robust alternative.
35
+ *
36
+ * @param localIdentifier A `PHAsset.localIdentifier` — the part of a
37
+ * `ph://<localIdentifier>/L0/001` URI before the `/L0/001` suffix.
38
+ * @param destinationPath A `file://` path in the caller's own sandbox
39
+ * (e.g. its cache directory) to write the exported `.mov` file to. Any
40
+ * existing file at this path is overwritten.
41
+ * @returns The same `destinationPath`, once the file has been written.
42
+ *
43
+ * @platform ios
44
+ */
45
+ exportPhotoLibraryVideo(localIdentifier: string, destinationPath: string): Promise<string>;
26
46
  }
27
47
  declare const _default: ExpoProVideoEditorModule;
28
48
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoProVideoEditorModule.d.ts","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,KAAK,EAAE,8BAA8B,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/F,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,8BAA8B,CAAC;IACzF;;;;;;;;;OASG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAEpE,+EAA+E;IAC/E,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvC;;;;;;;OAOG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAC3E;;AAED,wBAAmF"}
1
+ {"version":3,"file":"ExpoProVideoEditorModule.d.ts","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,KAAK,EAAE,8BAA8B,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/F,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,8BAA8B,CAAC;IACzF;;;;;;;;;OASG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAEpE,+EAA+E;IAC/E,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvC;;;;;;;OAOG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAE1E;;;;;;;;;;;;;;;;;;OAkBG;IACH,uBAAuB,CAAC,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAC3F;;AAED,wBAAmF"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoProVideoEditorModule.js","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+BzD,eAAe,mBAAmB,CAA2B,oBAAoB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport type { ExpoProVideoEditorModuleEvents, RenderConfig } from './ExpoProVideoEditor.types';\n\ndeclare class ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorModuleEvents> {\n /**\n * Renders `config` to a single video file, reporting progress via the\n * `onRenderProgress` event for the same `id`. Resolves with the output\n * bytes when `config.outputPath` is omitted, or `null` when it wrote\n * directly to that path.\n *\n * Rejects with a `CANCELED` error if `cancelRender(id)` is called (or the\n * pipeline cancels itself, e.g. a stalled-export watchdog) before it\n * finishes, or a `RENDER_ERROR` on any other failure.\n */\n render(config: RenderConfig, id: string): Promise<Uint8Array | null>;\n\n /** Cancels the render job started with `render(..., id)`, if still running. */\n cancelRender(id: string): Promise<null>;\n\n /**\n * Extracts a waveform from the first audio track of `inputPath` (video or\n * audio file), downsampled to `bucketCount` peak-amplitude values in the\n * `0...1` range, evenly spanning the file's full duration.\n *\n * Rejects with `INVALID_ARGUMENTS` if `bucketCount` is not positive, or\n * `WAVEFORM_ERROR` if the file has no audio track or cannot be read.\n */\n extractWaveform(inputPath: string, bucketCount: number): Promise<number[]>;\n}\n\nexport default requireNativeModule<ExpoProVideoEditorModule>('ExpoProVideoEditor');\n"]}
1
+ {"version":3,"file":"ExpoProVideoEditorModule.js","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAoDzD,eAAe,mBAAmB,CAA2B,oBAAoB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport type { ExpoProVideoEditorModuleEvents, RenderConfig } from './ExpoProVideoEditor.types';\n\ndeclare class ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorModuleEvents> {\n /**\n * Renders `config` to a single video file, reporting progress via the\n * `onRenderProgress` event for the same `id`. Resolves with the output\n * bytes when `config.outputPath` is omitted, or `null` when it wrote\n * directly to that path.\n *\n * Rejects with a `CANCELED` error if `cancelRender(id)` is called (or the\n * pipeline cancels itself, e.g. a stalled-export watchdog) before it\n * finishes, or a `RENDER_ERROR` on any other failure.\n */\n render(config: RenderConfig, id: string): Promise<Uint8Array | null>;\n\n /** Cancels the render job started with `render(..., id)`, if still running. */\n cancelRender(id: string): Promise<null>;\n\n /**\n * Extracts a waveform from the first audio track of `inputPath` (video or\n * audio file), downsampled to `bucketCount` peak-amplitude values in the\n * `0...1` range, evenly spanning the file's full duration.\n *\n * Rejects with `INVALID_ARGUMENTS` if `bucketCount` is not positive, or\n * `WAVEFORM_ERROR` if the file has no audio track or cannot be read.\n */\n extractWaveform(inputPath: string, bucketCount: number): Promise<number[]>;\n\n /**\n * Exports a photo-library video asset to a real, playable local file at\n * `destinationPath`, using `PHImageManager.requestExportSession` — the API\n * Apple documents for producing a genuinely exportable representation of a\n * video asset. A `ph://` URI (or a resolved `file://` path under the\n * Photos app's own protected storage) cannot be opened directly by\n * AVPlayer/expo-video, and copying via `PHAssetResourceManager` has been\n * observed failing unpredictably on some devices/OS versions even for\n * locally-available assets — this is the more robust alternative.\n *\n * @param localIdentifier A `PHAsset.localIdentifier` — the part of a\n * `ph://<localIdentifier>/L0/001` URI before the `/L0/001` suffix.\n * @param destinationPath A `file://` path in the caller's own sandbox\n * (e.g. its cache directory) to write the exported `.mov` file to. Any\n * existing file at this path is overwritten.\n * @returns The same `destinationPath`, once the file has been written.\n *\n * @platform ios\n */\n exportPhotoLibraryVideo(localIdentifier: string, destinationPath: string): Promise<string>;\n}\n\nexport default requireNativeModule<ExpoProVideoEditorModule>('ExpoProVideoEditor');\n"]}
@@ -9,6 +9,7 @@ declare class ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorMo
9
9
  render(_config: RenderConfig, _id: string): Promise<Uint8Array | null>;
10
10
  cancelRender(_id: string): Promise<null>;
11
11
  extractWaveform(_inputPath: string, _bucketCount: number): Promise<number[]>;
12
+ exportPhotoLibraryVideo(_localIdentifier: string, _destinationPath: string): Promise<string>;
12
13
  }
13
14
  declare const _default: typeof ExpoProVideoEditorModule;
14
15
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoProVideoEditorModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,KAAK,EAAE,8BAA8B,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAM/F;;;;GAIG;AACH,cAAM,wBAAyB,SAAQ,YAAY,CAAC,8BAA8B,CAAC;IACjF,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAItE,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAG7E;;AAED,wBAAuF"}
1
+ {"version":3,"file":"ExpoProVideoEditorModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,KAAK,EAAE,8BAA8B,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAM/F;;;;GAIG;AACH,cAAM,wBAAyB,SAAQ,YAAY,CAAC,8BAA8B,CAAC;IACjF,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAItE,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAI5E,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAG7F;;AAED,wBAAuF"}
@@ -16,6 +16,9 @@ class ExpoProVideoEditorModule extends NativeModule {
16
16
  extractWaveform(_inputPath, _bucketCount) {
17
17
  throw new Error(UNSUPPORTED_MESSAGE);
18
18
  }
19
+ exportPhotoLibraryVideo(_localIdentifier, _destinationPath) {
20
+ throw new Error(UNSUPPORTED_MESSAGE);
21
+ }
19
22
  }
20
23
  export default registerWebModule(ExpoProVideoEditorModule, 'ExpoProVideoEditorModule');
21
24
  //# sourceMappingURL=ExpoProVideoEditorModule.web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoProVideoEditorModule.web.js","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAIvD,MAAM,mBAAmB,GACvB,kFAAkF;IAClF,iFAAiF,CAAC;AAEpF;;;;GAIG;AACH,MAAM,wBAAyB,SAAQ,YAA4C;IACjF,MAAM,CAAC,OAAqB,EAAE,GAAW;QACvC,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IAED,YAAY,CAAC,GAAW;QACtB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IAED,eAAe,CAAC,UAAkB,EAAE,YAAoB;QACtD,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,wBAAwB,EAAE,0BAA0B,CAAC,CAAC","sourcesContent":["import { registerWebModule, NativeModule } from 'expo';\n\nimport type { ExpoProVideoEditorModuleEvents, RenderConfig } from './ExpoProVideoEditor.types';\n\nconst UNSUPPORTED_MESSAGE =\n 'expo-pro-video-editor has no web implementation — its native composition engine ' +\n '(AVFoundation on iOS, Media3 Transformer on Android) has no browser equivalent.';\n\n/**\n * ExpoProVideoEditorModule is not available on the web platform. Every method\n * throws explicitly rather than being silently undefined, so a web build\n * fails loudly at the call site instead of crashing on a missing function.\n */\nclass ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorModuleEvents> {\n render(_config: RenderConfig, _id: string): Promise<Uint8Array | null> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n\n cancelRender(_id: string): Promise<null> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n\n extractWaveform(_inputPath: string, _bucketCount: number): Promise<number[]> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n}\n\nexport default registerWebModule(ExpoProVideoEditorModule, 'ExpoProVideoEditorModule');\n"]}
1
+ {"version":3,"file":"ExpoProVideoEditorModule.web.js","sourceRoot":"","sources":["../src/ExpoProVideoEditorModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAIvD,MAAM,mBAAmB,GACvB,kFAAkF;IAClF,iFAAiF,CAAC;AAEpF;;;;GAIG;AACH,MAAM,wBAAyB,SAAQ,YAA4C;IACjF,MAAM,CAAC,OAAqB,EAAE,GAAW;QACvC,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IAED,YAAY,CAAC,GAAW;QACtB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IAED,eAAe,CAAC,UAAkB,EAAE,YAAoB;QACtD,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IAED,uBAAuB,CAAC,gBAAwB,EAAE,gBAAwB;QACxE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,wBAAwB,EAAE,0BAA0B,CAAC,CAAC","sourcesContent":["import { registerWebModule, NativeModule } from 'expo';\n\nimport type { ExpoProVideoEditorModuleEvents, RenderConfig } from './ExpoProVideoEditor.types';\n\nconst UNSUPPORTED_MESSAGE =\n 'expo-pro-video-editor has no web implementation — its native composition engine ' +\n '(AVFoundation on iOS, Media3 Transformer on Android) has no browser equivalent.';\n\n/**\n * ExpoProVideoEditorModule is not available on the web platform. Every method\n * throws explicitly rather than being silently undefined, so a web build\n * fails loudly at the call site instead of crashing on a missing function.\n */\nclass ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorModuleEvents> {\n render(_config: RenderConfig, _id: string): Promise<Uint8Array | null> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n\n cancelRender(_id: string): Promise<null> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n\n extractWaveform(_inputPath: string, _bucketCount: number): Promise<number[]> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n\n exportPhotoLibraryVideo(_localIdentifier: string, _destinationPath: string): Promise<string> {\n throw new Error(UNSUPPORTED_MESSAGE);\n }\n}\n\nexport default registerWebModule(ExpoProVideoEditorModule, 'ExpoProVideoEditorModule');\n"]}
package/build/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { normalizeWaveformPeaks } from './waveform';
13
13
  export declare const render: typeof ExpoProVideoEditorModule.render;
14
14
  export declare const cancelRender: typeof ExpoProVideoEditorModule.cancelRender;
15
15
  export declare const extractWaveform: typeof ExpoProVideoEditorModule.extractWaveform;
16
+ export declare const exportPhotoLibraryVideo: typeof ExpoProVideoEditorModule.exportPhotoLibraryVideo;
16
17
  export { AudioWaveform } from './ui/AudioWaveform';
17
18
  export { AudioTrimScrubber, computeAudioTrimBarCount } from './ui/AudioTrimScrubber';
18
19
  export { TrimScrubber, type TimelineClip } from './ui/TrimScrubber';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,wBAAwB,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,OAAO,wBAAwB,CAAC,MAA0F,CAAC;AAEhJ,eAAO,MAAM,YAAY,EAAE,OAAO,wBAAwB,CAAC,YAAwE,CAAC;AAEpI,eAAO,MAAM,eAAe,EAAE,OAAO,wBAAwB,CAAC,eACI,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,wBAAwB,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,OAAO,wBAAwB,CAAC,MAA0F,CAAC;AAEhJ,eAAO,MAAM,YAAY,EAAE,OAAO,wBAAwB,CAAC,YAAwE,CAAC;AAEpI,eAAO,MAAM,eAAe,EAAE,OAAO,wBAAwB,CAAC,eACI,CAAC;AAEnE,eAAO,MAAM,uBAAuB,EAAE,OAAO,wBAAwB,CAAC,uBACc,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC"}
package/build/index.js CHANGED
@@ -13,6 +13,7 @@ export { normalizeWaveformPeaks } from './waveform';
13
13
  export const render = (config, id) => ExpoProVideoEditorModule.render(config, id);
14
14
  export const cancelRender = (id) => ExpoProVideoEditorModule.cancelRender(id);
15
15
  export const extractWaveform = (inputPath, bucketCount) => ExpoProVideoEditorModule.extractWaveform(inputPath, bucketCount);
16
+ export const exportPhotoLibraryVideo = (localIdentifier, destinationPath) => ExpoProVideoEditorModule.exportPhotoLibraryVideo(localIdentifier, destinationPath);
16
17
  export { AudioWaveform } from './ui/AudioWaveform';
17
18
  export { AudioTrimScrubber, computeAudioTrimBarCount } from './ui/AudioTrimScrubber';
18
19
  export { TrimScrubber } from './ui/TrimScrubber';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,wBAAwB,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,MAAM,GAA2C,CAAC,MAAoB,EAAE,EAAU,EAAE,EAAE,CAAC,wBAAwB,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAEhJ,MAAM,CAAC,MAAM,YAAY,GAAiD,CAAC,EAAU,EAAE,EAAE,CAAC,wBAAwB,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAEpI,MAAM,CAAC,MAAM,eAAe,GAAoD,CAAC,SAAiB,EAAE,WAAmB,EAAE,EAAE,CACzH,wBAAwB,CAAC,eAAe,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAyD,MAAM,oBAAoB,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["/**\n * Reexport the native module. On web, it will be resolved to ExpoProVideoEditorModule.web.ts\n * and on native platforms to ExpoProVideoEditorModule.ts\n */\nimport type { RenderConfig } from './ExpoProVideoEditor.types';\nimport ExpoProVideoEditorModule from './ExpoProVideoEditorModule';\n\nexport { default } from './ExpoProVideoEditorModule';\nexport * from './ExpoProVideoEditor.types';\nexport { normalizeWaveformPeaks } from './waveform';\n\n/**\n * Named re-exports of the native module's own methods, so callers can\n * `import { render, extractWaveform } from 'expo-pro-video-editor'` instead\n * of going through the default-exported module object — wrapped (rather\n * than destructured directly off the module instance) so `this` still\n * resolves to the native module regardless of how its methods are bound\n * internally.\n */\nexport const render: typeof ExpoProVideoEditorModule.render = (config: RenderConfig, id: string) => ExpoProVideoEditorModule.render(config, id);\n\nexport const cancelRender: typeof ExpoProVideoEditorModule.cancelRender = (id: string) => ExpoProVideoEditorModule.cancelRender(id);\n\nexport const extractWaveform: typeof ExpoProVideoEditorModule.extractWaveform = (inputPath: string, bucketCount: number) =>\n ExpoProVideoEditorModule.extractWaveform(inputPath, bucketCount);\nexport { AudioWaveform } from './ui/AudioWaveform';\nexport { AudioTrimScrubber, computeAudioTrimBarCount } from './ui/AudioTrimScrubber';\nexport { TrimScrubber, type TimelineClip } from './ui/TrimScrubber';\nexport { RecordingIndicator } from './ui/RecordingIndicator';\nexport { VoiceRecorder, type VoiceRecorderResult, type VoiceRecorderContainer } from './ui/VoiceRecorder';\nexport { useTimelineClips } from './hooks/useTimelineClips';\nexport { waitForSourceLoad } from './utils/waitForSourceLoad';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,wBAAwB,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,MAAM,GAA2C,CAAC,MAAoB,EAAE,EAAU,EAAE,EAAE,CAAC,wBAAwB,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAEhJ,MAAM,CAAC,MAAM,YAAY,GAAiD,CAAC,EAAU,EAAE,EAAE,CAAC,wBAAwB,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAEpI,MAAM,CAAC,MAAM,eAAe,GAAoD,CAAC,SAAiB,EAAE,WAAmB,EAAE,EAAE,CACzH,wBAAwB,CAAC,eAAe,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAEnE,MAAM,CAAC,MAAM,uBAAuB,GAA4D,CAAC,eAAuB,EAAE,eAAuB,EAAE,EAAE,CACnJ,wBAAwB,CAAC,uBAAuB,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAyD,MAAM,oBAAoB,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["/**\n * Reexport the native module. On web, it will be resolved to ExpoProVideoEditorModule.web.ts\n * and on native platforms to ExpoProVideoEditorModule.ts\n */\nimport type { RenderConfig } from './ExpoProVideoEditor.types';\nimport ExpoProVideoEditorModule from './ExpoProVideoEditorModule';\n\nexport { default } from './ExpoProVideoEditorModule';\nexport * from './ExpoProVideoEditor.types';\nexport { normalizeWaveformPeaks } from './waveform';\n\n/**\n * Named re-exports of the native module's own methods, so callers can\n * `import { render, extractWaveform } from 'expo-pro-video-editor'` instead\n * of going through the default-exported module object — wrapped (rather\n * than destructured directly off the module instance) so `this` still\n * resolves to the native module regardless of how its methods are bound\n * internally.\n */\nexport const render: typeof ExpoProVideoEditorModule.render = (config: RenderConfig, id: string) => ExpoProVideoEditorModule.render(config, id);\n\nexport const cancelRender: typeof ExpoProVideoEditorModule.cancelRender = (id: string) => ExpoProVideoEditorModule.cancelRender(id);\n\nexport const extractWaveform: typeof ExpoProVideoEditorModule.extractWaveform = (inputPath: string, bucketCount: number) =>\n ExpoProVideoEditorModule.extractWaveform(inputPath, bucketCount);\n\nexport const exportPhotoLibraryVideo: typeof ExpoProVideoEditorModule.exportPhotoLibraryVideo = (localIdentifier: string, destinationPath: string) =>\n ExpoProVideoEditorModule.exportPhotoLibraryVideo(localIdentifier, destinationPath);\nexport { AudioWaveform } from './ui/AudioWaveform';\nexport { AudioTrimScrubber, computeAudioTrimBarCount } from './ui/AudioTrimScrubber';\nexport { TrimScrubber, type TimelineClip } from './ui/TrimScrubber';\nexport { RecordingIndicator } from './ui/RecordingIndicator';\nexport { VoiceRecorder, type VoiceRecorderResult, type VoiceRecorderContainer } from './ui/VoiceRecorder';\nexport { useTimelineClips } from './hooks/useTimelineClips';\nexport { waitForSourceLoad } from './utils/waitForSourceLoad';\n"]}
@@ -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;IAC1B;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,+BAmMA"}
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,+BAkMA"}
@@ -122,7 +122,6 @@ export function AudioTrimScrubber(props) {
122
122
  return () => {
123
123
  cancelled = true;
124
124
  };
125
- // eslint-disable-next-line react-hooks/exhaustive-deps
126
125
  }, [inputPath, barCount, contrastExponent, !!usableInitialPeaks]);
127
126
  const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;
128
127
  /**
@@ -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 * 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"]}
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 }, [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"]}
@@ -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;AAgDtD,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,+BA6KA"}
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;AAqDtD,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"}
@@ -11,12 +11,17 @@ const styles = {
11
11
  base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',
12
12
  variants: {
13
13
  // For a caller embedding this inside their own sheet component (which
14
- // already provides its own background/padding/rounded-top-corners)
15
- // painting both stacks two slightly different surfaces with a visible
16
- // seam between them (this component's own white/dark box on top of
17
- // the outer sheet's own themed background). `bare` drops all of that,
18
- // leaving just this component's content.
19
- bare: { true: 'gap-4 bg-transparent p-0' },
14
+ // already provides its own background, rounded top corners, and
15
+ // vertical spacing) — painting both stacks two slightly different
16
+ // surfaces with a visible seam between them (this component's own
17
+ // white/dark box on top of the outer sheet's own themed background).
18
+ // `bare` drops the background/rounding/vertical padding this
19
+ // component would otherwise add on top of the outer sheet's own, but
20
+ // keeps horizontal padding — the outer sheet only supplies its own
21
+ // top/bottom inset (e.g. handle spacing, safe-area), not left/right,
22
+ // so this content still needs its own side margins from the sheet's
23
+ // edges.
24
+ bare: { true: 'gap-4 bg-transparent px-5 py-0' },
20
25
  },
21
26
  }),
22
27
  title: tv({ base: 'text-base font-semibold text-video-editor-text' }),
@@ -60,7 +65,7 @@ function DefaultContainer(props) {
60
65
  * to a live mic-amplitude waveform — no video required.
61
66
  */
62
67
  export function VoiceRecorder(props) {
63
- const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false } = props;
68
+ const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false, } = props;
64
69
  const [permissionError, setPermissionError] = useState(null);
65
70
  const [recordedUri, setRecordedUri] = useState(null);
66
71
  const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);
@@ -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 // For a caller embedding this inside their own sheet component (which\n // already provides its own background/padding/rounded-top-corners) —\n // painting both stacks two slightly different surfaces with a visible\n // seam between them (this component's own white/dark box on top of\n // the outer sheet's own themed background). `bare` drops all of that,\n // leaving just this component's content.\n bare: { true: 'gap-4 bg-transparent p-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 { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false } = 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 // For a caller embedding this inside their own sheet component (which\n // already provides its own background, rounded top corners, and\n // vertical spacing) — painting both stacks two slightly different\n // surfaces with a visible seam between them (this component's own\n // white/dark box on top of the outer sheet's own themed background).\n // `bare` drops the background/rounding/vertical padding this\n // component would otherwise add on top of the outer sheet's own, but\n // keeps horizontal padding — the outer sheet only supplies its own\n // top/bottom inset (e.g. handle spacing, safe-area), not left/right,\n // so this content still needs its own side margins from the sheet's\n // edges.\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"]}
@@ -97,6 +97,27 @@ public class ExpoProVideoEditorModule: Module {
97
97
  }
98
98
  }
99
99
  }
100
+
101
+ AsyncFunction("exportPhotoLibraryVideo") { (localIdentifier: String, destinationPath: String, promise: Promise) in
102
+ guard !localIdentifier.isEmpty else {
103
+ promise.reject(ExpoProVideoEditorError.invalidArguments("Missing localIdentifier"))
104
+ return
105
+ }
106
+ guard !destinationPath.isEmpty else {
107
+ promise.reject(ExpoProVideoEditorError.invalidArguments("Missing destinationPath"))
108
+ return
109
+ }
110
+
111
+ Task {
112
+ do {
113
+ try await PhotoLibraryVideoExporter.export(
114
+ localIdentifier: localIdentifier, to: URL(fileURLWithPath: destinationPath))
115
+ promise.resolve(destinationPath)
116
+ } catch {
117
+ promise.reject(ExpoProVideoEditorError.photoLibraryExportFailed(error))
118
+ }
119
+ }
120
+ }
100
121
  }
101
122
  }
102
123
 
@@ -150,4 +171,9 @@ private enum ExpoProVideoEditorError {
150
171
  ExpoProVideoEditorException(
151
172
  name: "WAVEFORM_ERROR", reason: error.localizedDescription, code: "WAVEFORM_ERROR")
152
173
  }
174
+
175
+ static func photoLibraryExportFailed(_ error: Error) -> Exception {
176
+ let code = error is CancellationError ? "CANCELED" : "PHOTO_LIBRARY_EXPORT_ERROR"
177
+ return ExpoProVideoEditorException(name: code, reason: error.localizedDescription, code: code)
178
+ }
153
179
  }
@@ -0,0 +1,88 @@
1
+ import Photos
2
+ import Foundation
3
+
4
+ /// Exports a photo-library video PHAsset to a real, playable local file.
5
+ ///
6
+ /// A `ph://<localIdentifier>` URI cannot be opened directly by AVPlayer/expo-video, nor
7
+ /// copied via `expo-file-system`'s `PHAssetResourceManager.writeData` path on some
8
+ /// devices/OS versions — that call has been observed failing with an opaque
9
+ /// `PHPhotosErrorDomain error -1` even for locally-available, non-iCloud assets. This
10
+ /// uses `PHImageManager.requestExportSession`, the API Apple documents specifically for
11
+ /// producing an exportable/playable representation of a video asset (distinct from
12
+ /// `requestAVAsset`, which only yields a playback-only `AVAsset`, and from
13
+ /// `PHAssetResourceManager`, which operates on the asset's raw resource data).
14
+ enum PhotoLibraryVideoExporter {
15
+ enum ExportError: Error, CustomStringConvertible {
16
+ case assetNotFound(String)
17
+ case notAVideoAsset(String)
18
+ case exportSessionUnavailable(String)
19
+ case exportFailed(String)
20
+
21
+ var description: String {
22
+ switch self {
23
+ case .assetNotFound(let id): return "No photo library asset found for id \(id)"
24
+ case .notAVideoAsset(let id): return "Asset \(id) is not a video"
25
+ case .exportSessionUnavailable(let id): return "Could not create an export session for asset \(id)"
26
+ case .exportFailed(let message): return message
27
+ }
28
+ }
29
+ }
30
+
31
+ /// - Parameters:
32
+ /// - localIdentifier: A `PHAsset.localIdentifier` (the part of a `ph://` URI before
33
+ /// the `/L0/001` suffix this app's JS side already strips before calling in).
34
+ /// - destinationURL: Where to write the exported `.mov`/`.mp4` file — the caller owns
35
+ /// picking a path in its own sandboxed cache directory.
36
+ static func export(localIdentifier: String, to destinationURL: URL) async throws {
37
+ guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil).firstObject else {
38
+ throw ExportError.assetNotFound(localIdentifier)
39
+ }
40
+ guard asset.mediaType == .video else {
41
+ throw ExportError.notAVideoAsset(localIdentifier)
42
+ }
43
+
44
+ if FileManager.default.fileExists(atPath: destinationURL.path) {
45
+ try FileManager.default.removeItem(at: destinationURL)
46
+ }
47
+
48
+ let options = PHVideoRequestOptions()
49
+ options.isNetworkAccessAllowed = true
50
+ options.deliveryMode = .highQualityFormat
51
+ options.version = .current
52
+
53
+ let exportSession: AVAssetExportSession? = await withCheckedContinuation { continuation in
54
+ PHImageManager.default().requestExportSession(
55
+ forVideo: asset,
56
+ options: options,
57
+ exportPreset: AVAssetExportPresetHighestQuality
58
+ ) { session, _ in
59
+ continuation.resume(returning: session)
60
+ }
61
+ }
62
+
63
+ guard let exportSession else {
64
+ throw ExportError.exportSessionUnavailable(localIdentifier)
65
+ }
66
+
67
+ exportSession.outputURL = destinationURL
68
+ exportSession.outputFileType = .mov
69
+
70
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
71
+ exportSession.exportAsynchronously {
72
+ switch exportSession.status {
73
+ case .completed:
74
+ continuation.resume(returning: ())
75
+ case .failed:
76
+ continuation.resume(
77
+ throwing: ExportError.exportFailed(
78
+ exportSession.error?.localizedDescription ?? "Export failed for asset \(localIdentifier)"))
79
+ case .cancelled:
80
+ continuation.resume(throwing: CancellationError())
81
+ default:
82
+ continuation.resume(
83
+ throwing: ExportError.exportFailed("Unexpected export status \(exportSession.status.rawValue) for asset \(localIdentifier)"))
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-pro-video-editor",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",
@@ -27,6 +27,27 @@ declare class ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorMo
27
27
  * `WAVEFORM_ERROR` if the file has no audio track or cannot be read.
28
28
  */
29
29
  extractWaveform(inputPath: string, bucketCount: number): Promise<number[]>;
30
+
31
+ /**
32
+ * Exports a photo-library video asset to a real, playable local file at
33
+ * `destinationPath`, using `PHImageManager.requestExportSession` — the API
34
+ * Apple documents for producing a genuinely exportable representation of a
35
+ * video asset. A `ph://` URI (or a resolved `file://` path under the
36
+ * Photos app's own protected storage) cannot be opened directly by
37
+ * AVPlayer/expo-video, and copying via `PHAssetResourceManager` has been
38
+ * observed failing unpredictably on some devices/OS versions even for
39
+ * locally-available assets — this is the more robust alternative.
40
+ *
41
+ * @param localIdentifier A `PHAsset.localIdentifier` — the part of a
42
+ * `ph://<localIdentifier>/L0/001` URI before the `/L0/001` suffix.
43
+ * @param destinationPath A `file://` path in the caller's own sandbox
44
+ * (e.g. its cache directory) to write the exported `.mov` file to. Any
45
+ * existing file at this path is overwritten.
46
+ * @returns The same `destinationPath`, once the file has been written.
47
+ *
48
+ * @platform ios
49
+ */
50
+ exportPhotoLibraryVideo(localIdentifier: string, destinationPath: string): Promise<string>;
30
51
  }
31
52
 
32
53
  export default requireNativeModule<ExpoProVideoEditorModule>('ExpoProVideoEditor');
@@ -23,6 +23,10 @@ class ExpoProVideoEditorModule extends NativeModule<ExpoProVideoEditorModuleEven
23
23
  extractWaveform(_inputPath: string, _bucketCount: number): Promise<number[]> {
24
24
  throw new Error(UNSUPPORTED_MESSAGE);
25
25
  }
26
+
27
+ exportPhotoLibraryVideo(_localIdentifier: string, _destinationPath: string): Promise<string> {
28
+ throw new Error(UNSUPPORTED_MESSAGE);
29
+ }
26
30
  }
27
31
 
28
32
  export default registerWebModule(ExpoProVideoEditorModule, 'ExpoProVideoEditorModule');
package/src/index.ts CHANGED
@@ -23,6 +23,9 @@ export const cancelRender: typeof ExpoProVideoEditorModule.cancelRender = (id: s
23
23
 
24
24
  export const extractWaveform: typeof ExpoProVideoEditorModule.extractWaveform = (inputPath: string, bucketCount: number) =>
25
25
  ExpoProVideoEditorModule.extractWaveform(inputPath, bucketCount);
26
+
27
+ export const exportPhotoLibraryVideo: typeof ExpoProVideoEditorModule.exportPhotoLibraryVideo = (localIdentifier: string, destinationPath: string) =>
28
+ ExpoProVideoEditorModule.exportPhotoLibraryVideo(localIdentifier, destinationPath);
26
29
  export { AudioWaveform } from './ui/AudioWaveform';
27
30
  export { AudioTrimScrubber, computeAudioTrimBarCount } from './ui/AudioTrimScrubber';
28
31
  export { TrimScrubber, type TimelineClip } from './ui/TrimScrubber';
@@ -168,7 +168,6 @@ export function AudioTrimScrubber(props: {
168
168
  return () => {
169
169
  cancelled = true;
170
170
  };
171
- // eslint-disable-next-line react-hooks/exhaustive-deps
172
171
  }, [inputPath, barCount, contrastExponent, !!usableInitialPeaks]);
173
172
 
174
173
  const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;
@@ -15,12 +15,17 @@ const styles = {
15
15
  base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',
16
16
  variants: {
17
17
  // For a caller embedding this inside their own sheet component (which
18
- // already provides its own background/padding/rounded-top-corners)
19
- // painting both stacks two slightly different surfaces with a visible
20
- // seam between them (this component's own white/dark box on top of
21
- // the outer sheet's own themed background). `bare` drops all of that,
22
- // leaving just this component's content.
23
- bare: { true: 'gap-4 bg-transparent p-0' },
18
+ // already provides its own background, rounded top corners, and
19
+ // vertical spacing) — painting both stacks two slightly different
20
+ // surfaces with a visible seam between them (this component's own
21
+ // white/dark box on top of the outer sheet's own themed background).
22
+ // `bare` drops the background/rounding/vertical padding this
23
+ // component would otherwise add on top of the outer sheet's own, but
24
+ // keeps horizontal padding — the outer sheet only supplies its own
25
+ // top/bottom inset (e.g. handle spacing, safe-area), not left/right,
26
+ // so this content still needs its own side margins from the sheet's
27
+ // edges.
28
+ bare: { true: 'gap-4 bg-transparent px-5 py-0' },
24
29
  },
25
30
  }),
26
31
  title: tv({ base: 'text-base font-semibold text-video-editor-text' }),
@@ -116,7 +121,17 @@ export function VoiceRecorder(props: {
116
121
  */
117
122
  bare?: boolean;
118
123
  }) {
119
- const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false } = props;
124
+ const {
125
+ visible,
126
+ onCancel,
127
+ onConfirm,
128
+ title = 'Voice-over',
129
+ width,
130
+ backgroundContent,
131
+ progress,
132
+ Container = DefaultContainer,
133
+ bare = false,
134
+ } = props;
120
135
  const [permissionError, setPermissionError] = useState<string | null>(null);
121
136
  const [recordedUri, setRecordedUri] = useState<string | null>(null);
122
137
  const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);