From 0f49f28be561e0c455ae2c072e0140bf4b32522f Mon Sep 17 00:00:00 2001 From: "Simon G. Tsegay" Date: Tue, 27 Oct 2026 09:48:55 -0400 Subject: [PATCH 1/2] feat(video): add a preserveMetadata option to drop source metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Video.compress copies the source video's metadata into the output on both platforms: Android writes the location as a udta/©xyz box, and iOS forwards every source metadata item to the writer. An app that uploads the compressed video has no way to leave the location out. preserveMetadata defaults to true, so the output is unchanged unless the caller opts out. With false, Android skips the location lookup and writes no ©xyz box, and iOS forwards no source metadata. The option takes the same path as stripAudio. --- README.md | 4 ++++ __tests__/compressor.test.ts | 10 ++++++++++ .../com/reactnativecompressor/Utils/Utils.kt | 4 ++-- .../Video/AutoVideoCompression.kt | 1 + .../VideoCompressor/VideoCompressorClass.kt | 6 ++++++ .../VideoCompressor/compressor/Compressor.kt | 13 ++++++++---- .../Video/VideoCompressorHelper.kt | 3 +++ ios/Video/VideoMain.swift | 20 +++++++++++-------- src/Video/index.tsx | 9 +++++++++ 9 files changed, 56 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 49b76df1..3205751e 100644 --- a/README.md +++ b/README.md @@ -483,6 +483,10 @@ await clearCache(); // this will clear cache of thumbnails cache directory previously default was 16 but now it is 0 by default. 0 mean 0mb. This is an offset, which you can set for minimumFileSizeForCompress will allow this package to dont compress less than or equal to `minimumFileSizeForCompress` ref [#26](https://github.com/numandev1/react-native-compressor/issues/26) +- ###### `preserveMetadata: boolean` (default: true) + + By default the output keeps the source video's metadata, such as its GPS location and creation date. Pass `false` to leave it out, for example when the compressed video leaves the device and the location should not go with it. + **if you wanna get video metadata then [read this](#get-metadata-of-video)** ## Audio diff --git a/__tests__/compressor.test.ts b/__tests__/compressor.test.ts index 9d9a4ddf..d48c7c40 100644 --- a/__tests__/compressor.test.ts +++ b/__tests__/compressor.test.ts @@ -165,6 +165,16 @@ describe('react-native-compressor JS wrapper API', () => { ); }); + it('forwards preserveMetadata only when the caller opts out of source metadata', async () => { + mockCompressor.compress.mockResolvedValue('file://no-metadata.mp4'); + + await api.Video.compress(localVideoUri, { compressionMethod: 'manual', preserveMetadata: false }); + await api.Video.compress(localVideoUri, { compressionMethod: 'manual', preserveMetadata: true }); + + expect(mockCompressor.compress.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ preserveMetadata: false })); + expect(mockCompressor.compress.mock.calls[1]?.[1]).not.toHaveProperty('preserveMetadata'); + }); + it('proxies video cancellation and background task lifecycle', async () => { mockCompressor.activateBackgroundTask.mockImplementation(async (_options, onExpired) => { onExpired?.(); diff --git a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt index db130931..a285cf8f 100644 --- a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +++ b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt @@ -29,12 +29,12 @@ object Utils { } @JvmStatic - fun compressVideo(srcPath: String, destinationPath: String, resultWidth: Int, resultHeight: Int, videoBitRate: Float, frameRate: Int, uuid: String,progressDivider:Int, stripAudio: Boolean = false, promise: Promise, reactContext: ReactApplicationContext) { + fun compressVideo(srcPath: String, destinationPath: String, resultWidth: Int, resultHeight: Int, videoBitRate: Float, frameRate: Int, uuid: String,progressDivider:Int, stripAudio: Boolean = false, preserveMetadata: Boolean = true, promise: Promise, reactContext: ReactApplicationContext) { val currentVideoCompression = intArrayOf(0) val videoCompressorClass: VideoCompressorClass? = VideoCompressorClass(reactContext); compressorExports[uuid] = videoCompressorClass videoCompressorClass?.start( - srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(), frameRate, stripAudio, + srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(), frameRate, stripAudio, preserveMetadata, listener = object : CompressionListener { override fun onProgress(index: Int, percent: Float) { if (percent <= 100) { diff --git a/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt b/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt index 489a08d6..5db502a7 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt @@ -46,6 +46,7 @@ object AutoVideoCompression { options.uuid!!, options.progressDivider!!, options.stripAudio, + options.preserveMetadata, promise, reactContext, ) diff --git a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt index 7a20b153..a7b38d62 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt @@ -26,6 +26,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) { bitrate: Int, frameRate: Int, stripAudio: Boolean = false, + preserveMetadata: Boolean = true, listener: CompressionListener, ) { val uris = mutableListOf() @@ -40,6 +41,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) { bitrate, frameRate, stripAudio, + preserveMetadata, listener, destPath ) @@ -58,6 +60,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) { bitrate: Int, frameRate: Int, stripAudio: Boolean, + preserveMetadata: Boolean, listener: CompressionListener, destPath: String ) { @@ -82,6 +85,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) { bitrate, frameRate, stripAudio, + preserveMetadata, listener, ) @@ -106,6 +110,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) { bitrate: Int, frameRate: Int, disableAudio: Boolean = false, + preserveMetadata: Boolean = true, listener: CompressionListener, ): Result = withContext(Dispatchers.Default) { return@withContext compressVideo( @@ -119,6 +124,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) { bitrate, frameRate, disableAudio, + preserveMetadata, object : CompressionProgressListener { override fun onProgressChanged(index: Int, percent: Float) { listener.onProgress(index, percent) diff --git a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt index 269fb28e..856029dc 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt @@ -71,6 +71,7 @@ object Compressor { outputBitrate: Int, outputFrameRate: Int, disableAudio: Boolean = false, + preserveMetadata: Boolean = true, listener: CompressionProgressListener, ): Result = withContext(Dispatchers.Default) { @@ -109,16 +110,20 @@ object Compressor { // MediaMetadataRetriever only reads moov/udta/©xyz — returning null // (or empty) and dropping GPS. Fall back to a raw MP4 walker that // scans the whole file for every known location encoding. - val retrievedLocation = + val retrievedLocation = if (preserveMetadata) { mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION) - val locationData = if (!retrievedLocation.isNullOrEmpty()) { - retrievedLocation } else { - LocationExtractor.extract(context, srcUri) + null + } + val locationData = when { + !preserveMetadata -> null + !retrievedLocation.isNullOrEmpty() -> retrievedLocation + else -> LocationExtractor.extract(context, srcUri) } // Never log the resolved ISO 6709 string — it is the user's exact GPS // coordinates. Log only presence and which mechanism resolved it. val locationSource = when { + !preserveMetadata -> "not preserved" !retrievedLocation.isNullOrEmpty() -> "retriever" !locationData.isNullOrEmpty() -> "extractor" else -> "none" diff --git a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt index d4c46299..06ef3d21 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt @@ -29,6 +29,7 @@ class VideoCompressorHelper { var progressDivider: Int? = 0 var minimumFileSizeForCompress = 0.0f var stripAudio = false + var preserveMetadata = true companion object { private var _reactContext: ReactApplicationContext? = null @@ -89,6 +90,7 @@ class VideoCompressorHelper { "bitrate" -> options.bitrate = map.getDouble(key).toFloat() "progressDivider" -> options.progressDivider = map.getInt(key) "stripAudio" -> options.stripAudio = map.getBoolean(key) + "preserveMetadata" -> options.preserveMetadata = map.getBoolean(key) } } return options @@ -159,6 +161,7 @@ class VideoCompressorHelper { options.uuid!!, options.progressDivider!!, options.stripAudio, + options.preserveMetadata, promise, reactContext, ) diff --git a/ios/Video/VideoMain.swift b/ios/Video/VideoMain.swift index 0b8b3859..ca85b672 100644 --- a/ios/Video/VideoMain.swift +++ b/ios/Video/VideoMain.swift @@ -268,6 +268,7 @@ class VideoCompressor { let uuid:String = options["uuid"] as! String let progressDivider=(options["progressDivider"] as? NSNumber)?.intValue ?? 0 let stripAudio=options["stripAudio"] as? Bool ?? false + let preserveMetadata=options["preserveMetadata"] as? Bool ?? true let asset = AVAsset(url: url) guard let track = getVideoTrack(asset: asset) else { @@ -277,7 +278,7 @@ class VideoCompressor { } let profile = createCompressionProfile(track: track, maxSize: CGFloat(maxSize), requestedBitrate: nil) - exportVideoHelper(url: url, asset: asset, bitRate: profile.bitrate, frameRate: profile.frameRate, resultWidth: profile.width, resultHeight: profile.height,uuid: uuid,progressDivider: progressDivider, stripAudio: stripAudio) { progress in + exportVideoHelper(url: url, asset: asset, bitRate: profile.bitrate, frameRate: profile.frameRate, resultWidth: profile.width, resultHeight: profile.height,uuid: uuid,progressDivider: progressDivider, stripAudio: stripAudio, preserveMetadata: preserveMetadata) { progress in onProgress(progress) } onCompletion: { outputURL in onCompletion(outputURL) @@ -291,6 +292,7 @@ class VideoCompressor { let bitRate = (options["bitrate"] as? NSNumber)?.intValue let progressDivider=(options["progressDivider"] as? NSNumber)?.intValue ?? 0 let stripAudio=options["stripAudio"] as? Bool ?? false + let preserveMetadata=options["preserveMetadata"] as? Bool ?? true let asset = AVAsset(url: url) guard let track = getVideoTrack(asset: asset) else { let error = CompressionError(message: "Invalid video URL, no track found") @@ -300,7 +302,7 @@ class VideoCompressor { let maxSize = (options["maxSize"] as? NSNumber)?.floatValue ?? Float(1920) let profile = createCompressionProfile(track: track, maxSize: CGFloat(maxSize), requestedBitrate: bitRate) - exportVideoHelper(url: url, asset: asset, bitRate: profile.bitrate, frameRate: profile.frameRate, resultWidth: profile.width, resultHeight: profile.height,uuid: uuid,progressDivider: progressDivider, stripAudio: stripAudio) { progress in + exportVideoHelper(url: url, asset: asset, bitRate: profile.bitrate, frameRate: profile.frameRate, resultWidth: profile.width, resultHeight: profile.height,uuid: uuid,progressDivider: progressDivider, stripAudio: stripAudio, preserveMetadata: preserveMetadata) { progress in onProgress(progress) } onCompletion: { outputURL in onCompletion(outputURL) @@ -309,7 +311,7 @@ class VideoCompressor { } } - func exportVideoHelper(url: URL,asset: AVAsset, bitRate: Int, frameRate: Int, resultWidth:Int,resultHeight:Int,uuid:String,progressDivider: Int, stripAudio: Bool = false, onProgress: @escaping (Float) -> Void, onCompletion: @escaping (URL) -> Void, onFailure: @escaping (Error) -> Void){ + func exportVideoHelper(url: URL,asset: AVAsset, bitRate: Int, frameRate: Int, resultWidth:Int,resultHeight:Int,uuid:String,progressDivider: Int, stripAudio: Bool = false, preserveMetadata: Bool = true, onProgress: @escaping (Float) -> Void, onCompletion: @escaping (URL) -> Void, onFailure: @escaping (Error) -> Void){ var currentVideoCompression:Int=0 var tmpURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) @@ -350,12 +352,14 @@ class VideoCompressor { } // Preserve source metadata (location, creation date, etc.) by forwarding - // every available metadata format to the writer. - var preservedMetadata: [AVMetadataItem] = asset.metadata - for format in asset.availableMetadataFormats { - preservedMetadata.append(contentsOf: asset.metadata(forFormat: format)) + // every available metadata format to the writer, unless the caller opted out. + if preserveMetadata { + var preservedMetadata: [AVMetadataItem] = asset.metadata + for format in asset.availableMetadataFormats { + preservedMetadata.append(contentsOf: asset.metadata(forFormat: format)) + } + exporter.metadata = preservedMetadata } - exporter.metadata = preservedMetadata compressorExports[uuid] = exporter exporter.export(progressHandler: { (progress) in diff --git a/src/Video/index.tsx b/src/Video/index.tsx index 563a0157..a0ee9971 100644 --- a/src/Video/index.tsx +++ b/src/Video/index.tsx @@ -18,6 +18,12 @@ type videoCompresssionType = { * Default: false */ stripAudio?: boolean; + /*** + * When false, the source video's metadata (its GPS location, creation date, etc.) is not copied + * into the output video. + * Default: true + */ + preserveMetadata?: boolean; }; export type VideoCompressorType = { @@ -56,6 +62,9 @@ const Video: VideoCompressorType = { if (options?.stripAudio) { modifiedOptions.stripAudio = options.stripAudio; } + if (options?.preserveMetadata === false) { + modifiedOptions.preserveMetadata = false; + } if (options?.getCancellationId) { options?.getCancellationId(uuid); } From cb08b06922261a5d2537213bb493637d4c76b01f Mon Sep 17 00:00:00 2001 From: "Simon G. Tsegay" Date: Mon, 2 Nov 2026 13:13:21 -0500 Subject: [PATCH 2/2] fix(video): apply preserveMetadata below the size threshold A video at or under minimumFileSizeForCompress skipped compression and came back as the source file, with its location still in it. That happened on iOS in both modes and on Android in auto mode, so preserveMetadata: false did not hold when a caller set a threshold. With preserveMetadata false, the size check no longer short-circuits, and the export runs without the source metadata. The option is new and defaults to true, so no existing caller changes. --- README.md | 2 +- .../com/reactnativecompressor/Video/AutoVideoCompression.kt | 2 +- ios/Video/VideoMain.swift | 3 ++- src/Video/index.tsx | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3205751e..1d784dcf 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,7 @@ await clearCache(); // this will clear cache of thumbnails cache directory - ###### `preserveMetadata: boolean` (default: true) - By default the output keeps the source video's metadata, such as its GPS location and creation date. Pass `false` to leave it out, for example when the compressed video leaves the device and the location should not go with it. + By default the output keeps the source video's metadata, such as its GPS location and creation date. Pass `false` to leave it out, for example when the compressed video leaves the device and the location should not go with it. With `false`, a video at or under `minimumFileSizeForCompress` is still compressed, because returning the source file would keep its metadata. **if you wanna get video metadata then [read this](#get-metadata-of-video)** diff --git a/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt b/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt index 5db502a7..d007a6e7 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt @@ -19,7 +19,7 @@ object AutoVideoCompression { val file = File(srcPath) val sizeInBytes = file.length().toFloat() val sizeInMb = sizeInBytes / (1024 * 1024) - if (sizeInMb > minimumFileSizeForCompress) { + if (sizeInMb > minimumFileSizeForCompress || !options.preserveMetadata) { val destinationPath = generateCacheFilePath("mp4", reactContext!!) val actualHeight = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) val actualWidth = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) diff --git a/ios/Video/VideoMain.swift b/ios/Video/VideoMain.swift index ca85b672..9a29afef 100644 --- a/ios/Video/VideoMain.swift +++ b/ios/Video/VideoMain.swift @@ -111,7 +111,8 @@ class VideoCompressor { { minimumFileSizeForCompress=(options["minimumFileSizeForCompress"] as? NSNumber)?.doubleValue ?? 0; } - if(fileSize>minimumFileSizeForCompress) + let preserveMetadata=options["preserveMetadata"] as? Bool ?? true + if(fileSize>minimumFileSizeForCompress || !preserveMetadata) { if(options["compressionMethod"] as! String=="auto") { diff --git a/src/Video/index.tsx b/src/Video/index.tsx index a0ee9971..d91a68ed 100644 --- a/src/Video/index.tsx +++ b/src/Video/index.tsx @@ -21,6 +21,7 @@ type videoCompresssionType = { /*** * When false, the source video's metadata (its GPS location, creation date, etc.) is not copied * into the output video. + * When false, a video at or under minimumFileSizeForCompress is still compressed. * Default: true */ preserveMetadata?: boolean;