diff --git a/lib/media/video-utils.js b/lib/media/video-utils.js --- a/lib/media/video-utils.js +++ b/lib/media/video-utils.js @@ -97,14 +97,15 @@ ); const outputPath = `${outputDirectory}${outputFilename}`; - let quality, speed, scale; - if (outputCodec === 'h264') { - const { floor, min, max, log2 } = Math; - const crf = floor(min(5, max(0, log2(inputDuration / 5)))) + 23; - quality = `-crf ${crf}`; - speed = '-preset ultrafast'; + let hardwareAcceleration, quality, speed, scale, pixelFormat; + if (outputCodec === 'h264_mediacodec') { + hardwareAcceleration = 'mediacodec'; + quality = ''; + speed = ''; scale = `-vf scale=${maxWidth}:${maxHeight}:force_original_aspect_ratio=decrease`; + pixelFormat = ''; } else if (outputCodec === 'h264_videotoolbox') { + hardwareAcceleration = 'videotoolbox'; quality = '-profile:v baseline'; speed = '-realtime 1'; const { width, height } = inputDimensions; @@ -115,19 +116,21 @@ } else if (exceedsDimensions) { scale = `-vf scale=-1:${maxHeight}`; } + pixelFormat = '-pix_fmt yuv420p'; } else { invariant(false, `unrecognized outputCodec ${outputCodec}`); } const ffmpegCommand = + `-hwaccel ${hardwareAcceleration} ` + `-i ${inputPath} ` + `-c:a copy -c:v ${outputCodec} ` + `${quality} ` + - '-vsync 2 -r 30 ' + + '-fps_mode cfr -r 30 ' + `${scale} ` + `${speed} ` + '-movflags +faststart ' + - '-pix_fmt yuv420p ' + + `${pixelFormat} ` + '-v quiet ' + outputPath; diff --git a/native/android/build.gradle b/native/android/build.gradle --- a/native/android/build.gradle +++ b/native/android/build.gradle @@ -31,7 +31,7 @@ } ext { - reactNativeFFmpegPackage = "min-gpl-lts" + ffmpegKitPackage = "min-lts" } allprojects { diff --git a/native/ios/Comm.xcodeproj/project.pbxproj b/native/ios/Comm.xcodeproj/project.pbxproj --- a/native/ios/Comm.xcodeproj/project.pbxproj +++ b/native/ios/Comm.xcodeproj/project.pbxproj @@ -1171,11 +1171,27 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Comm/Pods-Comm-frameworks.sh", "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/ffmpegkit.framework/ffmpegkit", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libavcodec.framework/libavcodec", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libavdevice.framework/libavdevice", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libavfilter.framework/libavfilter", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libavformat.framework/libavformat", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libavutil.framework/libavutil", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libswresample.framework/libswresample", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-min/libswscale.framework/libswscale", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ffmpegkit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavcodec.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavdevice.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavfilter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavformat.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavutil.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswresample.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswscale.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -1413,7 +1429,6 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/lottie-react-native\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-camera\"", - "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-ffmpeg\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-in-app-message\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-netinfo\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-notifications\"", @@ -1555,7 +1570,6 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/lottie-react-native\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-camera\"", - "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-ffmpeg\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-in-app-message\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-netinfo\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-notifications\"", diff --git a/native/ios/Podfile b/native/ios/Podfile --- a/native/ios/Podfile +++ b/native/ios/Podfile @@ -10,7 +10,7 @@ def common_comm_target_pods pod 'SQLCipher-Amalgamation', :path => '../../node_modules/@commapp/sqlcipher-amalgamation' pod 'react-native-video/VideoCaching', :podspec => '../node_modules/react-native-video' - pod 'react-native-ffmpeg/min-lts', :podspec => '../node_modules/react-native-ffmpeg' + pod 'ffmpeg-kit-react-native', :subspecs => ['min'], :podspec => '../node_modules/ffmpeg-kit-react-native' end post_integrate do |installer| diff --git a/native/ios/Podfile.lock b/native/ios/Podfile.lock --- a/native/ios/Podfile.lock +++ b/native/ios/Podfile.lock @@ -132,6 +132,10 @@ - React-Core (= 0.70.9) - React-jsi (= 0.70.9) - ReactCommon/turbomodule/core (= 0.70.9) + - ffmpeg-kit-ios-min (6.0) + - ffmpeg-kit-react-native/min (6.0.2): + - ffmpeg-kit-ios-min (= 6.0) + - React-Core - fmt (6.2.1) - glog (0.3.5) - hermes-engine (0.70.9) @@ -163,7 +167,6 @@ - MMKVAppExtension (1.3.5): - MMKVCore (~> 1.3.5) - MMKVCore (1.3.5) - - mobile-ffmpeg-min (4.3.1.LTS) - OLMKit (3.2.14): - OLMKit/olmc (= 3.2.14) - OLMKit/olmcpp (= 3.2.14) @@ -404,9 +407,6 @@ - React - react-native-camera/RN (3.31.0): - React - - react-native-ffmpeg/min-lts (0.4.4): - - mobile-ffmpeg-min (= 4.3.1.LTS) - - React - react-native-in-app-message (1.0.2): - React - react-native-netinfo (9.3.7): @@ -605,6 +605,7 @@ - EXUpdatesInterface (from `../../node_modules/expo-updates-interface/ios`) - FBLazyVector (from `../../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../../node_modules/react-native/React/FBReactNativeSpec`) + - ffmpeg-kit-react-native/min (from `../node_modules/ffmpeg-kit-react-native`) - glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../../node_modules/react-native/sdks/hermes/hermes-engine.podspec`) - libevent (~> 2.1.12) @@ -629,7 +630,6 @@ - React-jsinspector (from `../../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../../node_modules/react-native/ReactCommon/logger`) - react-native-camera (from `../node_modules/react-native-camera`) - - react-native-ffmpeg/min-lts (from `../node_modules/react-native-ffmpeg`) - react-native-in-app-message (from `../node_modules/react-native-in-app-message`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`) @@ -668,6 +668,7 @@ SPEC REPOS: trunk: - DVAssetLoaderDelegate + - ffmpeg-kit-ios-min - fmt - libaom - libavif @@ -678,7 +679,6 @@ - MMKV - MMKVAppExtension - MMKVCore - - mobile-ffmpeg-min - OpenSSL-Universal - SDWebImage - SDWebImageAVIFCoder @@ -744,6 +744,8 @@ :path: "../../node_modules/react-native/Libraries/FBLazyVector" FBReactNativeSpec: :path: "../../node_modules/react-native/React/FBReactNativeSpec" + ffmpeg-kit-react-native: + :podspec: "../node_modules/ffmpeg-kit-react-native" glog: :podspec: "../../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: @@ -784,8 +786,6 @@ :path: "../../node_modules/react-native/ReactCommon/logger" react-native-camera: :path: "../node_modules/react-native-camera" - react-native-ffmpeg: - :podspec: "../node_modules/react-native-ffmpeg" react-native-in-app-message: :path: "../node_modules/react-native-in-app-message" react-native-netinfo: @@ -885,6 +885,8 @@ EXUpdatesInterface: bffd1ead18f0bab04fa784ca159c115607b8a23c FBLazyVector: bc76253beb7463b688aa6af913b822ed631de31a FBReactNativeSpec: 2dfdfdc025c136effd657e62879c66122718030b + ffmpeg-kit-ios-min: 4e9a088f4ee9629435960b9d68e54848975f1931 + ffmpeg-kit-react-native: 3cea88c9c5cfad62e1465279ea7d800dfbba3b00 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 918ec5addfbc430c9f443376d739f088b6dc96c3 @@ -898,7 +900,6 @@ MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb MMKVAppExtension: 0cb4ebe918cb739fea57f9ed892c050d7c4d2cf6 MMKVCore: 9e2e5fd529b64a9fe15f1a7afb3d73b2e27b4db9 - mobile-ffmpeg-min: d5d22dcef5c8ec56f771258f1f5be245d914f193 OLMKit: a13e1a20579e88d03971c5821360be24949a1a09 OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346 RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda @@ -917,7 +918,6 @@ React-jsinspector: d76d32327f21d4f40dcf696231fdbbca60de4711 React-logger: 1b3522f1e05c6360e93df3607a016c39e30a7351 react-native-camera: b5c8c7a71feecfdd5b39f0dbbf6b64b957ed55f2 - react-native-ffmpeg: f9a60452aaa5d478aac205b248224994f3bde416 react-native-in-app-message: f91de5009620af01456531118264c93e249b83ec react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-orientation-locker: 851f6510d8046ea2f14aa169b1e01fcd309a94ba @@ -959,6 +959,6 @@ Yoga: dc109b79db907f0f589fc423e991b09ec42d2295 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 3ad7489a9ca814690867cca40f302b9432cebe02 +PODFILE CHECKSUM: 08ac12954526f5d7b062d36d800e1b628afe9040 COCOAPODS: 1.14.3 diff --git a/native/media/ffmpeg.js b/native/media/ffmpeg.js --- a/native/media/ffmpeg.js +++ b/native/media/ffmpeg.js @@ -1,6 +1,10 @@ // @flow -import { RNFFmpeg, RNFFprobe, RNFFmpegConfig } from 'react-native-ffmpeg'; +import { + FFmpegKit, + FFprobeKit, + FFmpegKitConfig, +} from 'ffmpeg-kit-react-native'; import { getHasMultipleFramesProbeCommand } from 'lib/media/video-utils.js'; import type { @@ -85,19 +89,26 @@ ): Promise<{ rc: number, lastStats: ?FFmpegStatistics }> { const duration = inputVideoDuration > 0 ? inputVideoDuration : 0.001; const wrappedCommand = async () => { - RNFFmpegConfig.resetStatistics(); let lastStats; if (onTranscodingProgress) { - RNFFmpegConfig.enableStatisticsCallback( - (statisticsData: FFmpegStatistics) => { - lastStats = statisticsData; - const { time } = statisticsData; - onTranscodingProgress(time / 1000 / duration); - }, - ); + FFmpegKitConfig.enableStatisticsCallback(statisticsObject => { + const time = statisticsObject.getTime(); + onTranscodingProgress(time / 1000 / duration); + lastStats = { + speed: statisticsObject.getSpeed(), + time, + size: statisticsObject.getSize(), + videoQuality: statisticsObject.getVideoQuality(), + videoFrameNumber: statisticsObject.getVideoFrameNumber(), + videoFps: statisticsObject.getVideoFps(), + bitrate: statisticsObject.getBitrate(), + }; + }); } - const ffmpegResult = await RNFFmpeg.execute(ffmpegCommand); - return { ...ffmpegResult, lastStats }; + const session = await FFmpegKit.execute(ffmpegCommand); + const returnCode = await session.getReturnCode(); + const rc = returnCode.getValue(); + return { rc, lastStats }; }; return this.queueCommand('process', wrappedCommand); } @@ -112,9 +123,10 @@ videoPath: string, outputPath: string, ): Promise { - const thumbnailCommand = `-i ${videoPath} -frames 1 -f singlejpeg ${outputPath}`; - const { rc } = await RNFFmpeg.execute(thumbnailCommand); - return rc; + const thumbnailCommand = `-i ${videoPath} -frames 1 -f mjpeg ${outputPath}`; + const session = await FFmpegKit.execute(thumbnailCommand); + const returnCode = await session.getReturnCode(); + return returnCode.getValue(); } getVideoInfo(path: string): Promise { @@ -123,26 +135,28 @@ } static async innerGetVideoInfo(path: string): Promise { - const info = await RNFFprobe.getMediaInformation(path); + const session = await FFprobeKit.getMediaInformation(path); + const info = await session.getMediaInformation(); const videoStreamInfo = FFmpeg.getVideoStreamInfo(info); const codec = videoStreamInfo?.codec; const dimensions = videoStreamInfo && videoStreamInfo.dimensions; - const format = info.format.split(','); - const duration = info.duration / 1000; + const format = info.getFormat().split(','); + const duration = info.getDuration(); return { codec, format, dimensions, duration }; } static getVideoStreamInfo( info: Object, ): ?{ +codec: string, +dimensions: Dimensions } { - if (!info.streams) { + const streams = info.getStreams(); + if (!streams) { return null; } - for (const stream of info.streams) { - if (stream.type === 'video') { - const codec: string = stream.codec; - const width: number = stream.width; - const height: number = stream.height; + for (const stream of streams) { + if (stream.getType() === 'video') { + const codec: string = stream.getCodec(); + const width: number = stream.getWidth(); + const height: number = stream.getHeight(); return { codec, dimensions: { width, height } }; } } @@ -155,9 +169,11 @@ } static async innerHasMultipleFrames(path: string): Promise { - await RNFFprobe.execute(getHasMultipleFramesProbeCommand(path)); - const probeOutput = await RNFFmpegConfig.getLastCommandOutput(); - const numFrames = parseInt(probeOutput.lastCommandOutput); + const session = await FFprobeKit.execute( + getHasMultipleFramesProbeCommand(path), + ); + const probeOutput = await session.getOutput(); + const numFrames = parseInt(probeOutput); return numFrames > 1; } } diff --git a/native/media/video-utils.js b/native/media/video-utils.js --- a/native/media/video-utils.js +++ b/native/media/video-utils.js @@ -79,12 +79,9 @@ inputDuration: duration, inputDimensions: input.dimensions, outputDirectory: temporaryDirectoryPath, - // We want ffmpeg to use hardware-accelerated encoders. On iOS we can do - // this using VideoToolbox, but ffmpeg on Android is still missing - // MediaCodec encoding support: https://trac.ffmpeg.org/ticket/6407 outputCodec: Platform.select({ ios: 'h264_videotoolbox', - //android: 'h264_mediacodec', + android: 'h264_mediacodec', default: 'h264', }), clientConnectionInfo: { diff --git a/native/package.json b/native/package.json --- a/native/package.json +++ b/native/package.json @@ -84,6 +84,7 @@ "expo-media-library": "~15.0.0", "expo-secure-store": "~12.0.0", "expo-splash-screen": "~0.17.4", + "ffmpeg-kit-react-native": "^6.0.2", "find-root": "^1.1.0", "invariant": "^2.2.4", "lib": "0.0.1", @@ -95,7 +96,6 @@ "react-native": "0.70.9", "react-native-camera": "^3.31.0", "react-native-device-info": "^10.3.0", - "react-native-ffmpeg": "^0.4.4", "react-native-figma-squircle": "^0.1.2", "react-native-floating-action": "^1.22.0", "react-native-fs": "^2.20.0", diff --git a/patches/react-native-ffmpeg+0.4.4.patch b/patches/react-native-ffmpeg+0.4.4.patch deleted file mode 100644 --- a/patches/react-native-ffmpeg+0.4.4.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-ffmpeg/android/build.gradle b/node_modules/react-native-ffmpeg/android/build.gradle -index 32923e4..283b63c 100644 ---- a/node_modules/react-native-ffmpeg/android/build.gradle -+++ b/node_modules/react-native-ffmpeg/android/build.gradle -@@ -14,6 +14,7 @@ apply plugin: 'com.android.library' - - buildscript { - repositories { -+ gradlePluginPortal() - jcenter() - maven { url 'https://maven.google.com' } - } diff --git a/yarn.lock b/yarn.lock --- a/yarn.lock +++ b/yarn.lock @@ -11836,6 +11836,11 @@ biskviit "1.0.1" encoding "0.1.12" +ffmpeg-kit-react-native@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ffmpeg-kit-react-native/-/ffmpeg-kit-react-native-6.0.2.tgz#9eeac96ad89367c99480bd90431391405d4eb73e" + integrity sha512-r9uSmahq8TeyIb7fXf3ft+uUXyoeWRFa99+khjo0TAzWO9y0z9wU7eGnab9JLw1MmCB9v64o4yojNluJhVm9nQ== + figma-squircle@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/figma-squircle/-/figma-squircle-0.1.2.tgz#fa97de644131d99f2c42a2d8b70d56a36783c234" @@ -20253,11 +20258,6 @@ resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.3.0.tgz#6bab64d84d3415dd00cc446c73ec5e2e61fddbe7" integrity sha512-/ziZN1sA1REbJTv5mQZ4tXggcTvSbct+u5kCaze8BmN//lbxcTvWsU6NQd4IihLt89VkbX+14IGc9sVApSxd/w== -react-native-ffmpeg@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/react-native-ffmpeg/-/react-native-ffmpeg-0.4.4.tgz#9f4dbda53c96078cecbbe83a866d4b535b957131" - integrity sha512-MUBV3Xvto1Hl049Y9EaOZdjazkK1ixQzCzPEt1o7V2duSOrM2kJ2o/RiC4rSRgapU2uqYRHMZ6X4JJI7y40qXw== - react-native-figma-squircle@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/react-native-figma-squircle/-/react-native-figma-squircle-0.1.2.tgz#64973afcfb42a53cc662ac2ccfba73ced7297124"