diff --git a/README.md b/README.md index 49b76df..1d784dc 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. 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)** ## Audio diff --git a/__tests__/compressor.test.ts b/__tests__/compressor.test.ts index 9d9a4dd..d48c7c4 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 db13093..a285cf8 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 489a08d..d007a6e 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) @@ -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 7a20b15..a7b38d6 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 269fb28..856029d 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 d4c4629..06ef3d2 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 0b8b385..9a29afe 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") { @@ -268,6 +269,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 +279,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 +293,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 +303,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 +312,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 +353,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 563a015..d91a68e 100644 --- a/src/Video/index.tsx +++ b/src/Video/index.tsx @@ -18,6 +18,13 @@ 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. + * When false, a video at or under minimumFileSizeForCompress is still compressed. + * Default: true + */ + preserveMetadata?: boolean; }; export type VideoCompressorType = { @@ -56,6 +63,9 @@ const Video: VideoCompressorType = { if (options?.stripAudio) { modifiedOptions.stripAudio = options.stripAudio; } + if (options?.preserveMetadata === false) { + modifiedOptions.preserveMetadata = false; + } if (options?.getCancellationId) { options?.getCancellationId(uuid); }