expo-image-to-video 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.imagetovideo'
4
- version = '0.1.3'
4
+ version = '0.1.4'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -35,7 +35,7 @@ android {
35
35
  namespace "expo.modules.imagetovideo"
36
36
  defaultConfig {
37
37
  versionCode 1
38
- versionName "0.1.3"
38
+ versionName "0.1.4"
39
39
  }
40
40
  lintOptions {
41
41
  abortOnError false
@@ -2,6 +2,8 @@ package expo.modules.imagetovideo
2
2
 
3
3
  import expo.modules.kotlin.modules.Module
4
4
  import expo.modules.kotlin.modules.ModuleDefinition
5
+ import expo.modules.kotlin.records.Field
6
+ import expo.modules.kotlin.records.Record
5
7
  import kotlinx.coroutines.CoroutineScope
6
8
  import kotlinx.coroutines.Dispatchers
7
9
  import kotlinx.coroutines.launch
@@ -40,35 +42,42 @@ class ExpoImageToVideoModule : Module() {
40
42
  ))
41
43
  }
42
44
 
43
- AsyncFunction("generateVideo") { options: VideoOptions, promise: expo.modules.kotlin.Promise ->
45
+ // The 'options' argument will now automatically map to the VideoOptions class below
46
+ AsyncFunction("generateVideo") { options: VideoOptions, promise: expo.modules.kotlin.Promise ->
44
47
  val context = appContext.reactContext ?: throw Exception("Context not found")
45
48
 
46
- // Run in background thread to keep UI and JS thread responsive
47
49
  CoroutineScope(Dispatchers.IO).launch {
48
50
  try {
49
51
  val outputFile = if (options.outputPath != null) {
50
- File(options.outputPath)
52
+ File(options.outputPath!!)
51
53
  } else {
52
54
  File(context.cacheDir, "video_${System.currentTimeMillis()}.mp4")
53
55
  }
54
56
 
57
+ // Ensure defaults are handled if 0 or null
58
+ val finalBitrate = options.bitrate ?: 2500000
59
+
55
60
  val encoder = VideoEncoder(
56
61
  outputFile,
57
62
  options.width,
58
63
  options.height,
59
64
  options.fps,
60
- options.bitrate ?: 2500000
65
+ finalBitrate
61
66
  )
62
67
 
63
68
  encoder.start()
64
69
 
65
70
  options.images.forEach { uri ->
66
- // Resolve Expo/Content URIs to Bitmaps
71
+ // Load bitmap with the fixed width/height
67
72
  val bitmap = ImageUtils.loadBitmap(context, uri, options.width, options.height)
68
- ?: throw Exception("Failed to load image: $uri")
69
73
 
70
- encoder.encodeFrame(bitmap)
71
- bitmap.recycle() // Critical for memory management
74
+ if (bitmap != null) {
75
+ encoder.encodeFrame(bitmap)
76
+ bitmap.recycle()
77
+ } else {
78
+ // Optional: Log warning for failed image load
79
+ println("ExpoImageToVideo: Failed to load $uri")
80
+ }
72
81
  }
73
82
 
74
83
  encoder.stop()
@@ -93,11 +102,25 @@ class ExpoImageToVideoModule : Module() {
93
102
  }
94
103
 
95
104
  // Data class for Expo Module auto-serialization
96
- data class VideoOptions(
97
- val images: List<String>,
98
- val fps: Int,
99
- val width: Int,
100
- val height: Int,
101
- val bitrate: Int?,
102
- val outputPath: String?
103
- ) : expo.modules.kotlin.types.Enumerable
105
+ // 1. Must implement 'Record'
106
+ // 2. Must use 'var' (mutable) properties
107
+ // 3. Must use '@Field' annotation
108
+ class VideoOptions : Record {
109
+ @Field
110
+ var images: List<String> = emptyList()
111
+
112
+ @Field
113
+ var fps: Int = 30
114
+
115
+ @Field
116
+ var width: Int = 1280
117
+
118
+ @Field
119
+ var height: Int = 720
120
+
121
+ @Field
122
+ var bitrate: Int? = null
123
+
124
+ @Field
125
+ var outputPath: String? = null
126
+ }
@@ -16,17 +16,36 @@ export type ExpoImageToVideoViewProps = {
16
16
  style?: StyleProp<ViewStyle>;
17
17
  };
18
18
  export interface VideoOptions {
19
- /** Array of local file URIs (e.g., from expo-image-picker or expo-file-system) */
19
+ /**
20
+ * An array of local file URIs (file://...) to images.
21
+ * At least one image is required.
22
+ */
20
23
  images: string[];
21
- /** Frames per second (e.g., 30 or 60) */
24
+ /**
25
+ * Frames per second for the output video.
26
+ * Recommended: 30 or 60.
27
+ */
22
28
  fps: number;
23
- /** Final video width (pixels) */
29
+ /**
30
+ * Width of the output video in pixels.
31
+ * Example: 1920 (for 1080p), 1280 (for 720p).
32
+ */
24
33
  width: number;
25
- /** Final video height (pixels) */
34
+ /**
35
+ * Height of the output video in pixels.
36
+ * Example: 1080 (for 1080p), 720 (for 720p).
37
+ */
26
38
  height: number;
27
- /** Target bitrate in bits per second. Default is 2.5Mbps (2,500,000) */
39
+ /**
40
+ * Optional bitrate in bits per second.
41
+ * Higher values = better quality but larger file size.
42
+ * Default: 2,500,000 (2.5 Mbps).
43
+ */
28
44
  bitrate?: number;
29
- /** Optional custom output path. If not provided, a temp file is created */
45
+ /**
46
+ * Optional full path to the output file.
47
+ * If not provided, a temporary file is created in the cache directory.
48
+ */
30
49
  outputPath?: string;
31
50
  }
32
51
  export type VideoModuleEvents = {};
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImageToVideo.types.d.ts","sourceRoot":"","sources":["../src/ExpoImageToVideo.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,yCAAyC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GAAG,EAE/B,CAAC"}
1
+ {"version":3,"file":"ExpoImageToVideo.types.d.ts","sourceRoot":"","sources":["../src/ExpoImageToVideo.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GAAG,EAE/B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImageToVideo.types.js","sourceRoot":"","sources":["../src/ExpoImageToVideo.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\nexport type OnLoadEventPayload = {\n url: string;\n};\n\nexport type ExpoImageToVideoModuleEvents = {\n onChange: (params: ChangeEventPayload) => void;\n};\n\nexport type ChangeEventPayload = {\n value: string;\n};\n\nexport type ExpoImageToVideoViewProps = {\n url: string;\n onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;\n style?: StyleProp<ViewStyle>;\n};\n\nexport interface VideoOptions {\n /** Array of local file URIs (e.g., from expo-image-picker or expo-file-system) */\n images: string[];\n /** Frames per second (e.g., 30 or 60) */\n fps: number;\n /** Final video width (pixels) */\n width: number;\n /** Final video height (pixels) */\n height: number;\n /** Target bitrate in bits per second. Default is 2.5Mbps (2,500,000) */\n bitrate?: number;\n /** Optional custom output path. If not provided, a temp file is created */\n outputPath?: string;\n}\n\nexport type VideoModuleEvents = {\n // You could add onProgress here later if needed\n};"]}
1
+ {"version":3,"file":"ExpoImageToVideo.types.js","sourceRoot":"","sources":["../src/ExpoImageToVideo.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\nexport type OnLoadEventPayload = {\n url: string;\n};\n\nexport type ExpoImageToVideoModuleEvents = {\n onChange: (params: ChangeEventPayload) => void;\n};\n\nexport type ChangeEventPayload = {\n value: string;\n};\n\nexport type ExpoImageToVideoViewProps = {\n url: string;\n onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;\n style?: StyleProp<ViewStyle>;\n};\n\nexport interface VideoOptions {\n /**\n * An array of local file URIs (file://...) to images.\n * At least one image is required.\n */\n images: string[];\n\n /**\n * Frames per second for the output video.\n * Recommended: 30 or 60.\n */\n fps: number;\n\n /**\n * Width of the output video in pixels.\n * Example: 1920 (for 1080p), 1280 (for 720p).\n */\n width: number;\n\n /**\n * Height of the output video in pixels.\n * Example: 1080 (for 1080p), 720 (for 720p).\n */\n height: number;\n\n /**\n * Optional bitrate in bits per second.\n * Higher values = better quality but larger file size.\n * Default: 2,500,000 (2.5 Mbps).\n */\n bitrate?: number;\n\n /**\n * Optional full path to the output file.\n * If not provided, a temporary file is created in the cache directory.\n */\n outputPath?: string;\n}\n\nexport type VideoModuleEvents = {\n // You could add onProgress here later if needed\n};"]}
package/build/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
- export { default as ExpoImageToVideoView } from './ExpoImageToVideoView';
2
1
  import { VideoOptions } from './ExpoImageToVideo.types';
3
2
  /**
4
- * Converts a list of images to an MP4 video file using native hardware encoders.
5
- * @param options Configuration for the video encoding process.
6
- * @returns A promise that resolves to the local URI of the generated .mp4 file.
3
+ * Converts a sequence of images into an MP4 video.
4
+ * * @param options Configuration object for video generation.
5
+ * @returns A Promise that resolves to the file URI of the generated video.
7
6
  */
8
7
  export declare function generateVideo(options: VideoOptions): Promise<string>;
9
- export * from './ExpoImageToVideo.types';
8
+ export { VideoOptions };
10
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAIzE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAO1E;AAED,cAAc,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB1E;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
package/build/index.js CHANGED
@@ -1,20 +1,23 @@
1
- // Reexport the native module. On web, it will be resolved to ExpoImageToVideoModule.web.ts
2
- // and on native platforms to ExpoImageToVideoModule.ts
3
- // export { default } from './ExpoImageToVideoModule';
4
- export { default as ExpoImageToVideoView } from './ExpoImageToVideoView';
5
- // export * from './ExpoImageToVideo.types';
6
1
  import ExpoImageToVideoModule from './ExpoImageToVideoModule';
7
2
  /**
8
- * Converts a list of images to an MP4 video file using native hardware encoders.
9
- * @param options Configuration for the video encoding process.
10
- * @returns A promise that resolves to the local URI of the generated .mp4 file.
3
+ * Converts a sequence of images into an MP4 video.
4
+ * * @param options Configuration object for video generation.
5
+ * @returns A Promise that resolves to the file URI of the generated video.
11
6
  */
12
7
  export async function generateVideo(options) {
13
- // Validate basic requirements before hitting native code
14
- if (options.images.length === 0) {
15
- throw new Error("At least one image is required to generate a video.");
8
+ // 1. Validate Image List
9
+ if (!options.images || options.images.length === 0) {
10
+ throw new Error("[ExpoImageToVideo] The 'images' array cannot be empty. Please provide at least one image URI.");
16
11
  }
12
+ // 2. Validate Dimensions
13
+ if (options.width <= 0 || options.height <= 0) {
14
+ throw new Error(`[ExpoImageToVideo] Invalid dimensions: ${options.width}x${options.height}. Width and height must be positive integers.`);
15
+ }
16
+ // 3. Validate FPS
17
+ if (options.fps <= 0) {
18
+ throw new Error(`[ExpoImageToVideo] Invalid FPS: ${options.fps}. Frames per second must be greater than 0.`);
19
+ }
20
+ // 4. Call Native Module
17
21
  return await ExpoImageToVideoModule.generateVideo(options);
18
22
  }
19
- export * from './ExpoImageToVideo.types';
20
23
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,uDAAuD;AACvD,sDAAsD;AACtD,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACzE,6CAA6C;AAE7C,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAI9D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAqB;IACvD,yDAAyD;IACzD,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,MAAM,sBAAsB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,cAAc,0BAA0B,CAAC","sourcesContent":["// Reexport the native module. On web, it will be resolved to ExpoImageToVideoModule.web.ts\n// and on native platforms to ExpoImageToVideoModule.ts\n// export { default } from './ExpoImageToVideoModule';\nexport { default as ExpoImageToVideoView } from './ExpoImageToVideoView';\n// export * from './ExpoImageToVideo.types';\n\nimport ExpoImageToVideoModule from './ExpoImageToVideoModule';\nimport { VideoOptions } from './ExpoImageToVideo.types';\n\n\n/**\n * Converts a list of images to an MP4 video file using native hardware encoders.\n * @param options Configuration for the video encoding process.\n * @returns A promise that resolves to the local URI of the generated .mp4 file.\n */\nexport async function generateVideo(options: VideoOptions): Promise<string> {\n // Validate basic requirements before hitting native code\n if (options.images.length === 0) {\n throw new Error(\"At least one image is required to generate a video.\");\n }\n \n return await ExpoImageToVideoModule.generateVideo(options);\n}\n\nexport * from './ExpoImageToVideo.types';"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAG9D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAqB;IACvD,yBAAyB;IACzB,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,+FAA+F,CAChG,CAAC;IACJ,CAAC;IAED,yBAAyB;IACzB,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,0CAA0C,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,+CAA+C,CACzH,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,IAAI,OAAO,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,mCAAmC,OAAO,CAAC,GAAG,6CAA6C,CAC5F,CAAC;IACJ,CAAC;IAED,wBAAwB;IACxB,OAAO,MAAM,sBAAsB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC7D,CAAC","sourcesContent":["import ExpoImageToVideoModule from './ExpoImageToVideoModule';\nimport { VideoOptions } from './ExpoImageToVideo.types';\n\n/**\n * Converts a sequence of images into an MP4 video.\n * * @param options Configuration object for video generation.\n * @returns A Promise that resolves to the file URI of the generated video.\n */\nexport async function generateVideo(options: VideoOptions): Promise<string> {\n // 1. Validate Image List\n if (!options.images || options.images.length === 0) {\n throw new Error(\n \"[ExpoImageToVideo] The 'images' array cannot be empty. Please provide at least one image URI.\"\n );\n }\n\n // 2. Validate Dimensions\n if (options.width <= 0 || options.height <= 0) {\n throw new Error(\n `[ExpoImageToVideo] Invalid dimensions: ${options.width}x${options.height}. Width and height must be positive integers.`\n );\n }\n\n // 3. Validate FPS\n if (options.fps <= 0) {\n throw new Error(\n `[ExpoImageToVideo] Invalid FPS: ${options.fps}. Frames per second must be greater than 0.`\n );\n }\n\n // 4. Call Native Module\n return await ExpoImageToVideoModule.generateVideo(options);\n}\n\nexport { VideoOptions };"]}
@@ -1,5 +1,6 @@
1
1
  import ExpoModulesCore
2
2
  import AVFoundation
3
+ import UIKit
3
4
 
4
5
  public class ExpoImageToVideoModule: Module {
5
6
  // Each module class must implement the definition function. The definition consists of components
@@ -33,19 +34,22 @@ public class ExpoImageToVideoModule: Module {
33
34
  ])
34
35
  }
35
36
 
37
+ // The 'options' argument automatically maps to the VideoOptions struct below
36
38
  AsyncFunction("generateVideo") { (options: VideoOptions, promise: Promise) in
37
- // Move to background thread to avoid blocking the JS/UI threads
39
+
40
+ // Run on global background queue to avoid blocking UI
38
41
  DispatchQueue.global(qos: .userInitiated).async {
39
42
  do {
43
+ // 1. Output Path Setup
40
44
  let outputURL = options.outputPath != nil
41
45
  ? URL(fileURLWithPath: options.outputPath!)
42
46
  : URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("video_\(UUID().uuidString).mp4")
43
47
 
44
- // Delete existing file if it exists (AVAssetWriter will fail otherwise)
45
48
  if FileManager.default.fileExists(atPath: outputURL.path) {
46
49
  try FileManager.default.removeItem(at: outputURL)
47
50
  }
48
51
 
52
+ // 2. Initialize Encoder
49
53
  let encoder = try VideoEncoder(
50
54
  outputURL: outputURL,
51
55
  width: options.width,
@@ -54,13 +58,18 @@ public class ExpoImageToVideoModule: Module {
54
58
  bitrate: options.bitrate ?? 2_500_000
55
59
  )
56
60
 
61
+ // 3. Loop through images
57
62
  for (index, imageUri) in options.images.enumerated() {
58
- // Use autoreleasepool to prevent memory spikes during high-res processing
63
+ // Autoreleasepool is critical for memory dumping between frames
59
64
  try autoreleasepool {
60
- guard let url = URL(string: imageUri),
61
- let data = try? Data(contentsOf: url),
62
- let image = UIImage(data: data) else {
63
- throw NSError(domain: "ExpoImageToVideo", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to load image at \(imageUri)"])
65
+ guard let url = URL(string: imageUri) else { return }
66
+
67
+ // IMPROVEMENT: Downsample image on load to save RAM (like Android's inSampleSize)
68
+ let downsampledImage = self.loadDownsampledImage(at: url, for: CGSize(width: options.width, height: options.height))
69
+
70
+ guard let image = downsampledImage else {
71
+ print("ExpoImageToVideo: Failed to load \(imageUri)")
72
+ return
64
73
  }
65
74
 
66
75
  let frameTime = CMTime(value: Int64(index), timescale: Int32(options.fps))
@@ -68,6 +77,7 @@ public class ExpoImageToVideoModule: Module {
68
77
  }
69
78
  }
70
79
 
80
+ // 4. Finish
71
81
  encoder.finish { success in
72
82
  if success {
73
83
  promise.resolve(outputURL.path)
@@ -81,6 +91,23 @@ public class ExpoImageToVideoModule: Module {
81
91
  }
82
92
  }
83
93
 
94
+ // Helper: Efficiently loads and downsamples image without decoding full resolution first
95
+ private func loadDownsampledImage(at url: URL, for size: CGSize) -> UIImage? {
96
+ let options: [CFString: Any] = [
97
+ kCGImageSourceCreateThumbnailFromImageAlways: true,
98
+ kCGImageSourceCreateThumbnailWithTransform: true, // Respects EXIF orientation
99
+ kCGImageSourceShouldCacheImmediately: true,
100
+ kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
101
+ ]
102
+
103
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
104
+ let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
105
+ return nil
106
+ }
107
+ return UIImage(cgImage: cgImage)
108
+ }
109
+
110
+
84
111
  // Enables the module to be used as a native view. Definition components that are accepted as part of the
85
112
  // view definition: Prop, Events.
86
113
  View(ExpoImageToVideoView.self) {
@@ -98,11 +125,23 @@ public class ExpoImageToVideoModule: Module {
98
125
 
99
126
 
100
127
  // Structure to map the JS options object
128
+ // Must inherit from 'Record' and use '@Field'
101
129
  struct VideoOptions: Record {
102
- @Field var images: [String] = []
103
- @Field var fps: Int = 30
104
- @Field var width: Int = 1280
105
- @Field var height: Int = 720
106
- @Field var bitrate: Int? = nil
107
- @Field var outputPath: String? = nil
130
+ @Field
131
+ var images: [String] = []
132
+
133
+ @Field
134
+ var fps: Int = 30
135
+
136
+ @Field
137
+ var width: Int = 1280
138
+
139
+ @Field
140
+ var height: Int = 720
141
+
142
+ @Field
143
+ var bitrate: Int? = nil
144
+
145
+ @Field
146
+ var outputPath: String? = nil
108
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-image-to-video",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "High-performance native image-to-video conversion for Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",