From b99e712931fa2af3f0b21e7d32d65db2bc71c1ae Mon Sep 17 00:00:00 2001 From: Taner Sener Date: Sat, 27 Mar 2021 00:11:45 +0000 Subject: [PATCH] implement react-native plugin --- README.md | 67 +- docs/index.md | 59 +- react-native/.editorconfig | 15 + react-native/.gitattributes | 3 + react-native/.gitignore | 60 + react-native/README.md | 303 ++ react-native/android/build.gradle | 126 + react-native/android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + react-native/android/gradlew | 185 ++ react-native/android/gradlew.bat | 89 + .../android/src/main/AndroidManifest.xml | 3 + .../reactnative/AsyncWriteToPipeTask.java | 66 + .../FFmpegKitReactNativeModule.java | 1159 +++++++ .../FFmpegKitReactNativePackage.java | 46 + react-native/babel.config.js | 3 + react-native/ffmpeg-kit-react-native.podspec | 135 + react-native/ios/FFmpegKitReactNativeModule.h | 26 + react-native/ios/FFmpegKitReactNativeModule.m | 779 +++++ .../project.pbxproj | 293 ++ react-native/package.json | 118 + react-native/src/index.d.ts | 538 ++++ react-native/src/index.js | 2750 +++++++++++++++++ 24 files changed, 6775 insertions(+), 56 deletions(-) create mode 100644 react-native/.editorconfig create mode 100644 react-native/.gitattributes create mode 100644 react-native/.gitignore create mode 100644 react-native/README.md create mode 100644 react-native/android/build.gradle create mode 100644 react-native/android/gradle.properties create mode 100644 react-native/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 react-native/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 react-native/android/gradlew create mode 100644 react-native/android/gradlew.bat create mode 100644 react-native/android/src/main/AndroidManifest.xml create mode 100644 react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/AsyncWriteToPipeTask.java create mode 100644 react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativeModule.java create mode 100644 react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativePackage.java create mode 100644 react-native/babel.config.js create mode 100644 react-native/ffmpeg-kit-react-native.podspec create mode 100644 react-native/ios/FFmpegKitReactNativeModule.h create mode 100644 react-native/ios/FFmpegKitReactNativeModule.m create mode 100644 react-native/ios/FFmpegKitReactNativeModule.xcodeproj/project.pbxproj create mode 100644 react-native/package.json create mode 100644 react-native/src/index.d.ts create mode 100644 react-native/src/index.js diff --git a/README.md b/README.md index a46a9122..6048253a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FFmpegKit ![GitHub release](https://img.shields.io/badge/release-v4.5-blue.svg) ![Maven Central](https://img.shields.io/maven-central/v/com.arthenica/ffmpeg-kit-min) ![CocoaPods](https://img.shields.io/cocoapods/v/ffmpeg-kit-ios-min) +# FFmpegKit ![GitHub release](https://img.shields.io/badge/release-v4.5-blue.svg) ![Maven Central](https://img.shields.io/maven-central/v/com.arthenica/ffmpeg-kit-min) ![CocoaPods](https://img.shields.io/cocoapods/v/ffmpeg-kit-ios-min) [![npm](https://img.shields.io/npm/v/ffmpeg-kit-react-native.svg)](ffmpeg-kit-react-native) FFmpeg Kit for applications. @@ -7,42 +7,48 @@ FFmpeg Kit for applications. ### 1. Features - Scripts to build FFmpeg libraries - `FFmpegKit` wrapper library to run `FFmpeg`/`FFprobe` commands in applications -- Supports Android, iOS, macOS and tvOS +- Supports native platforms: Android, iOS, macOS and tvOS +- Supports hybrid platforms: React Native - Based on FFmpeg `v4.5-dev` with optional system and external libraries -- 8 prebuilt binary packages available at [Github](https://github.com/tanersener/ffmpeg-kit/releases), [Maven Central](https://search.maven.org) and [CocoaPods](https://cocoapods.org). +- 8 prebuilt binary packages available at [Github](https://github.com/tanersener/ffmpeg-kit/releases), [Maven Central](https://search.maven.org), [CocoaPods](https://cocoapods.org) and [npm](https://www.npmjs.com) - Licensed under `LGPL 3.0`, or `GPL v3.0` if GPL licensed libraries are enabled ### 2. Android -See [Android](https://github.com/tanersener/ffmpeg-kit/tree/development/android) to learn more about `FFmpegKit` for +See [Android](https://github.com/tanersener/ffmpeg-kit/tree/main/android) to learn more about `FFmpegKit` for `Android`. ### 3. iOS, macOS, tvOS -See [Apple](https://github.com/tanersener/ffmpeg-kit/tree/development/apple) to use `FFmpegKit` on `Apple` platforms +See [Apple](https://github.com/tanersener/ffmpeg-kit/tree/main/apple) to use `FFmpegKit` on `Apple` platforms (`iOS`, `macOS`, `tvOS`). -### 4. Build Scripts +### 4. React Native -Use `android.sh`, `ios.sh`, `macos.sh` and `tvos.sh` to build `FFmpegKit` for each platform. +See [React Native](https://github.com/tanersener/ffmpeg-kit/tree/main/react-native) to learn more about `FFmpegKit` for +`React Native`. + +### 5. Build Scripts + +Use `android.sh`, `ios.sh`, `macos.sh` and `tvos.sh` to build `FFmpegKit` for each native platform. All scripts support additional options to enable optional libraries and disable platform architectures. -### 5. FFmpegKit Library +### 6. FFmpegKit Library `FFmpegKit` is a wrapper library that allows you to easily run `FFmpeg`/`FFprobe` commands in applications. It provides additional features on top of `FFmpeg` to enable platform specific resources, control how commands are executed and how the results are handled. -`Android` library has a `Java` API and `Apple` libraries (`iOS`, `macOS`, `tvOS`) have an `Objective-C` API, -which are identical in terms of features and capabilities. +`Android` library of `FFmpegKit` has a `Java` API, `Apple` libraries (`iOS`, `macOS`, `tvOS`) have an `Objective-C` + API and `React Native` library provides a `JavaScript` API, which are identical in terms of features and capabilities. -### 6. Binary Packages +### 7. Packages There are eight different `ffmpeg-kit` packages distributed on [Github](https://github.com/tanersener/ffmpeg-kit/releases), -[Maven Central](https://search.maven.org) and [CocoaPods](https://cocoapods.org). -Below you can see which system libraries and external libraries are enabled in each one of them. +[Maven Central](https://search.maven.org), [CocoaPods](https://cocoapods.org) and [npm](https://www.npmjs.com). +Below you can see which system libraries and external libraries are enabled in each one of them. Please remember that some parts of `FFmpeg` are licensed under the `GPL` and only `GPL` licensed `ffmpeg-kit` packages include them. @@ -96,21 +102,22 @@ include them. - `AVFoundation` is not available on `tvOS` - `VideoToolbox` is not available on LTS releases of `iOS` and `tvOS` -### 7. Versions +### 8. Versions `FFmpegKit` binaries generated use the same major and minor version numbers as the upstream `FFmpeg` project. The exact version number of `FFmpeg` is obtained using `git describe --tags`. `dev` part in the version string indicates that `FFmpeg` source code is cloned from the `FFmpeg` `master` branch. -| FFmpegKit Version | FFmpeg Version | Release Date | -| :----: | :----: |:----: | -| [4.5](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5) | 4.5-dev-2008 | Sep 18, 2021 | -| [4.5.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5.LTS) | 4.5-dev-2008 | Sep 18, 2021 | -| [4.4](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4) | 4.4-dev-3015 | Mar 03, 2021 | -| [4.4.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4.LTS) | 4.4-dev-3015 | Mar 03, 2021 | +| Platforms | FFmpegKit Version | FFmpeg Version | Release Date | +| :----: | :----: | :----: | :----: | +| React Native | [4.5.0](https://github.com/tanersener/ffmpeg-kit/releases/tag/react.native.v4.5.0) | 4.5-dev-2008 | Oct 01, 2021 | +| Android
Apple | [4.5](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5) | 4.5-dev-2008 | Sep 18, 2021 | +| Android
Apple | [4.5.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5.LTS) | 4.5-dev-2008 | Sep 18, 2021 | +| Android
Apple | [4.4](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4) | 4.4-dev-3015 | Mar 03, 2021 | +| Android
Apple | [4.4.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4.LTS) | 4.4-dev-3015 | Mar 03, 2021 | -### 8. LTS Releases +### 9. LTS Releases `FFmpegKit` binaries are published in two release variants: `Main Release` and `LTS Release`. @@ -139,11 +146,11 @@ This table shows the differences between two variants. | tvOS Architectures | arm64
x86-64
arm64-simulator | arm64
x86-64 | | tvOS Bundle Format | XCFrameworks | Frameworks | -### 9. Documentation +### 10. Documentation A more detailed documentation is available under [Wiki](https://github.com/tanersener/ffmpeg-kit/wiki). -### 10. Test Applications +### 11. Test Applications You can see how `FFmpegKit` is used inside an application by running test applications created under [FFmpegKit Test](https://github.com/tanersener/ffmpeg-kit-test) project. @@ -151,12 +158,12 @@ You can see how `FFmpegKit` is used inside an application by running test applic All applications are identical and supports command execution, video encoding, accessing https urls, encoding audio, burning subtitles, video stabilisation, pipe operations and concurrent command execution. -### 11. License +### 12. License `FFmpegKit` is licensed under the `LGPL v3.0`. However, if source code is built using the optional `--enable-gpl` flag or prebuilt binaries with `-gpl` postfix are used, then `FFmpegKit` is subject to the `GPL v3.0` license. -### 12. Patents +### 13. Patents It is not clearly explained in their documentation, but it is believed that `FFmpeg`, `kvazaar`, `x264` and `x265` include algorithms which are subject to software patents. If you live in a country where software algorithms are @@ -167,15 +174,15 @@ that you seek legal advice first. See [FFmpeg Patent Mini-FAQ](https://ffmpeg.or distribute that library, then you are subject to pay MPEG LA licensing fees. Refer to [OpenH264 FAQ](https://www.openh264.org/faq.html) page for the details. -### 13. Contributing +### 14. Contributing Feel free to submit issues or pull requests. -Please note that `main` includes only the latest released source code. Changes planned for the next release are -developed under the `development` branch. Therefore, if you want to create a pull request, please open it against -the `development`. +Please note that `main` includes only the latest released source code. Changes planned for the next release are +developed under the `development` branches (`development` for native platforms, `development-react-native` for +`react-native`). Therefore, if you want to create a pull request, please open it against them. -### 14. See Also +### 15. See Also - [FFmpeg API Documentation](https://ffmpeg.org/doxygen/4.0/index.html) - [FFmpeg Wiki](https://trac.ffmpeg.org/wiki/WikiStart) diff --git a/docs/index.md b/docs/index.md index 98bf3fda..76150c93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,9 +7,10 @@ FFmpeg Kit for applications. ### 1. Features - Scripts to build FFmpeg libraries - `FFmpegKit` wrapper library to run `FFmpeg`/`FFprobe` commands in applications -- Supports Android, iOS, macOS and tvOS +- Supports native platforms: Android, iOS, macOS and tvOS +- Supports hybrid platforms: React Native - Based on FFmpeg `v4.5-dev` with optional system and external libraries -- 8 prebuilt binary packages available at [Github](https://github.com/tanersener/ffmpeg-kit/releases), [Maven Central](https://search.maven.org) and [CocoaPods](https://cocoapods.org). +- 8 prebuilt binary packages available at [Github](https://github.com/tanersener/ffmpeg-kit/releases), [Maven Central](https://search.maven.org), [CocoaPods](https://cocoapods.org) and [npm](https://www.npmjs.com) - Licensed under `LGPL 3.0`, or `GPL v3.0` if GPL licensed libraries are enabled ### 2. Android @@ -22,27 +23,32 @@ See [Android](https://github.com/tanersener/ffmpeg-kit/tree/development/android) See [Apple](https://github.com/tanersener/ffmpeg-kit/tree/development/apple) to use `FFmpegKit` on `Apple` platforms (`iOS`, `macOS`, `tvOS`). -### 4. Build Scripts +### 4. React Native -Use `android.sh`, `ios.sh`, `macos.sh` and `tvos.sh` to build `FFmpegKit` for each platform. +See [React Native](https://github.com/tanersener/ffmpeg-kit/tree/main/react-native) to learn more about `FFmpegKit` for +`React Native`. + +### 5. Build Scripts + +Use `android.sh`, `ios.sh`, `macos.sh` and `tvos.sh` to build `FFmpegKit` for each native platform. All scripts support additional options to enable optional libraries and disable platform architectures. -### 5. FFmpegKit Library +### 6. FFmpegKit Library `FFmpegKit` is a wrapper library that allows you to easily run `FFmpeg`/`FFprobe` commands in applications. It provides additional features on top of `FFmpeg` to enable platform specific resources, control how commands are executed and how the results are handled. -`Android` library has a `Java` API and `Apple` libraries (`iOS`, `macOS`, `tvOS`) have an `Objective-C` API, -which are identical in terms of features and capabilities. +`Android` library of `FFmpegKit` has a `Java` API, `Apple` libraries (`iOS`, `macOS`, `tvOS`) have an `Objective-C` +API and `React Native` library provides a `JavaScript` API, which are identical in terms of features and capabilities. -### 6. Binary Packages +### 7. Packages There are eight different `ffmpeg-kit` packages distributed on [Github](https://github.com/tanersener/ffmpeg-kit/releases), -[Maven Central](https://search.maven.org) and [CocoaPods](https://cocoapods.org). -Below you can see which external libraries are enabled in each one of them. +[Maven Central](https://search.maven.org), [CocoaPods](https://cocoapods.org) and [npm](https://www.npmjs.com). +Below you can see which system libraries and external libraries are enabled in each one of them. Please remember that some parts of `FFmpeg` are licensed under the `GPL` and only `GPL` licensed `ffmpeg-kit` packages include them. @@ -79,21 +85,22 @@ include them. - `AVFoundation` is not available on `tvOS` - `VideoToolbox` is not available on LTS releases of `iOS` and `tvOS` -### 7. Versions +### 8. Versions `FFmpegKit` binaries generated use the same major and minor version numbers as the upstream `FFmpeg` project. The exact version number of `FFmpeg` is obtained using `git describe --tags`. `dev` part in the version string indicates that `FFmpeg` source code is cloned from the `FFmpeg` `master` branch. -| FFmpegKit Version | FFmpeg Version | Release Date | -| :----: | :----: |:----: | -| [4.5](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5) | 4.5-dev-2008 | Sep 18, 2021 | -| [4.5.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5.LTS) | 4.5-dev-2008 | Sep 18, 2021 | -| [4.4](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4) | 4.4-dev-3015 | Mar 03, 2021 | -| [4.4.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4.LTS) | 4.4-dev-3015 | Mar 03, 2021 | +| Platforms | FFmpegKit Version | FFmpeg Version | Release Date | +| :----: | :----: | :----: | :----: | +| React Native | [4.5.0](https://github.com/tanersener/ffmpeg-kit/releases/tag/react.native.v4.5.0) | 4.5-dev-2008 | Oct 01, 2021 | +| Android
Apple | [4.5](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5) | 4.5-dev-2008 | Sep 18, 2021 | +| Android
Apple | [4.5.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.5.LTS) | 4.5-dev-2008 | Sep 18, 2021 | +| Android
Apple | [4.4](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4) | 4.4-dev-3015 | Mar 03, 2021 | +| Android
Apple | [4.4.LTS](https://github.com/tanersener/ffmpeg-kit/releases/tag/v4.4.LTS) | 4.4-dev-3015 | Mar 03, 2021 | -### 8. LTS Releases +### 9. LTS Releases `FFmpegKit` binaries are published in two release variants: `Main Release` and `LTS Release`. @@ -122,11 +129,11 @@ This table shows the differences between two variants. | tvOS Architectures | arm64
x86-64
arm64-simulator | arm64
x86-64 | | tvOS Bundle Format | XCFrameworks | Frameworks | -### 9. Documentation +### 10. Documentation A more detailed documentation is available under [Wiki](https://github.com/tanersener/ffmpeg-kit/wiki). -### 10. Test Applications +### 11. Test Applications You can see how `FFmpegKit` is used inside an application by running test applications created under [FFmpegKit Test](https://github.com/tanersener/ffmpeg-kit-test) project. @@ -134,12 +141,12 @@ You can see how `FFmpegKit` is used inside an application by running test applic All applications are identical and supports command execution, video encoding, accessing https urls, encoding audio, burning subtitles, video stabilisation, pipe operations, concurrent command execution. -### 11. License +### 12. License `FFmpegKit` is licensed under the `LGPL v3.0`. However, if source code is built using the optional `--enable-gpl` flag or prebuilt binaries with `-gpl` postfix are used, then `FFmpegKit` is subject to the `GPL v3.0` license. -### 12. Patents +### 13. Patents It is not clearly explained in their documentation, but it is believed that `FFmpeg`, `kvazaar`, `x264` and `x265` include algorithms which are subject to software patents. If you live in a country where software algorithms are @@ -150,15 +157,15 @@ that you seek legal advice first. See [FFmpeg Patent Mini-FAQ](https://ffmpeg.or distribute that library, then you are subject to pay MPEG LA licensing fees. Refer to [OpenH264 FAQ](https://www.openh264.org/faq.html) page for the details. -### 13. Contributing +### 14. Contributing Feel free to submit issues or pull requests. Please note that `main` includes only the latest released source code. Changes planned for the next release are -developed under the `development` branch. Therefore, if you want to create a pull request, please open it against -the `development`. +developed under the `development` branches (`development` for native platforms, `development-react-native` for +`react-native`). Therefore, if you want to create a pull request, please open it against them. -### 14. See Also +### 15. See Also - [FFmpeg API Documentation](https://ffmpeg.org/doxygen/4.0/index.html) - [FFmpeg Wiki](https://trac.ffmpeg.org/wiki/WikiStart) diff --git a/react-native/.editorconfig b/react-native/.editorconfig new file mode 100644 index 00000000..65365be6 --- /dev/null +++ b/react-native/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/react-native/.gitattributes b/react-native/.gitattributes new file mode 100644 index 00000000..030ef144 --- /dev/null +++ b/react-native/.gitattributes @@ -0,0 +1,3 @@ +*.pbxproj -text +# specific for windows script files +*.bat text eol=crlf \ No newline at end of file diff --git a/react-native/.gitignore b/react-native/.gitignore new file mode 100644 index 00000000..4f1c43c5 --- /dev/null +++ b/react-native/.gitignore @@ -0,0 +1,60 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.idea +.gradle +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Expo +.expo/* + +# generated by bob +lib/ diff --git a/react-native/README.md b/react-native/README.md new file mode 100644 index 00000000..08e53572 --- /dev/null +++ b/react-native/README.md @@ -0,0 +1,303 @@ +# FFmpegKit for React Native + +### 1. Features +- Includes both `FFmpeg` and `FFprobe` +- Supports + - Both `Android` and `iOS` + - FFmpeg `v4.5-dev` releases + - `arm-v7a`, `arm-v7a-neon`, `arm64-v8a`, `x86` and `x86_64` architectures on Android + - `Android API Level 16` or later + - `armv7`, `armv7s`, `arm64`, `arm64-simulator`, `i386`, `x86_64`, `x86_64-mac-catalyst` and `arm64-mac-catalyst` architectures on iOS + - `iOS SDK 9.3` or later + - Can process Storage Access Framework (SAF) Uris on Android + - 24 external libraries + + `dav1d`, `fontconfig`, `freetype`, `fribidi`, `gmp`, `gnutls`, `kvazaar`, `lame`, `libass`, `libiconv`, `libilbc`, `libtheora`, `libvorbis`, `libvpx`, `libwebp`, `libxml2`, `opencore-amr`, `opus`, `shine`, `snappy`, `soxr`, `speex`, `twolame`, `vo-amrwbenc` + + - 4 external libraries with GPL license + + `vid.stab`, `x264`, `x265`, `xvidcore` + + - `zlib` and `MediaCodec` Android system libraries + - `bzip2`, `iconv`, `libuuid`, `zlib` system libraries and `AudioToolbox`, `VideoToolbox`, `AVFoundation` system frameworks on iOS + +- Includes Typescript definitions +- Licensed under LGPL 3.0, can be customized to support GPL v3.0 + +### 2. Installation + +```sh +yarn add ffmpeg-kit-react-native +``` + +#### 2.1 Packages + +`ffmpeg` includes built-in encoders for some popular formats. However, there are certain external libraries that needs +to be enabled in order to encode specific formats/codecs. For example, to encode an `mp3` file you need `lame` or +`shine` library enabled. You have to install a `ffmpeg-kit-react-native` package that has at least one of them inside. +To encode an `h264` video, you need to install a package with `x264` inside. To encode `vp8` or `vp9` videos, you need +a `ffmpeg-kit-react-native` package with `libvpx` inside. + +`ffmpeg-kit` provides eight packages that include different sets of external libraries. These packages are +named according to the external libraries included in them. Refer to +[Packages](https://github.com/tanersener/ffmpeg-kit#7-packages) section of the project README to see the names +of those packages and external libraries included in each of them. + +##### 2.1.1 Package Names + +The following table shows all package names defined for `ffmpeg-kit-react-native`. + +| Package | Main Release | LTS Release | +| :----: | :----: | :----: | +| min | min | min-lts | +| min-gpl | min-gpl | min-gpl-lts | +| https | https | https-lts | +| https-gpl | https-gpl | https-gpl-lts | +| audio | audio | audio-lts | +| video | video | video-lts | +| full | full | full-lts | +| full-gpl | full-gpl | full-gpl-lts | + +#### 2.2 Enabling Packages + +Installing `ffmpeg-kit-react-native` enables the `https` package by default. It is possible to enable other +packages using the instructions below. + +##### 2.2.1 Enabling a Package on Android + +- Edit `android/build.gradle` file and add the package name in `ext.ffmpegKitPackage` variable. + + ``` + ext { + ffmpegKitPackage = "" + } + + ``` + +##### 2.2.2 Enabling a Package on iOS + +- Edit `ios/Podfile` file and add the package name as `subspec`. After that run `pod install` again. + + ``` + pod 'ffmpeg-kit-react-native', :subspecs => [''], :podspec => '../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec' + ``` + +- Note that if you have `use_native_modules!` in your `Podfile`, specifying a `subspec` may cause the following error. + You can fix it by defining `ffmpeg-kit-react-native` dependency before `use_native_modules!` in your `Podfile`. + + ``` + [!] There are multiple dependencies with different sources for `ffmpeg-kit-react-native` in `Podfile`: + + - ffmpeg-kit-react-native (from `../node_modules/ffmpeg-kit-react-native`) + - ffmpeg-kit-react-native/video (from `../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec`) + ``` + +#### 2.3 Enabling LTS Releases + +In order to install the `LTS` variant, install the `https-lts` package using instructions in `2.2` or append `-lts` to +the package name you are using. + +#### 2.4 LTS Releases + +`ffmpeg-kit-react-native` is published in two different variants: `Main Release` and `LTS Release`. Both releases +share the same source code but is built with different settings (Architectures, API Level, iOS Min SDK, etc.). Refer to +[LTS Releases](https://github.com/tanersener/ffmpeg-kit#9-lts-releases) section of the project README to see how they +compare to each other. + +### 3. Using + +1. Execute FFmpeg commands. + + ```js + import { FFmpegKit } from 'ffmpeg-kit-react-native'; + + FFmpegKit.executeAsync('-i file1.mp4 -c:v mpeg4 file2.mp4', async (session) => { + const returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + + // SUCCESS + + } else if (ReturnCode.isCancel(returnCode)) { + + // CANCEL + + } else { + + // ERROR + + } + }); + ``` + +2. Each `execute` call creates a new session. Access every detail about your execution from the + session created. + + ```js + FFmpegKit.executeAsync('-i file1.mp4 -c:v mpeg4 file2.mp4').then(async (session) => { + + // Unique session id created for this execution + const sessionId = session.getSessionId(); + + // Command arguments as a single string + const command = session.getCommand(); + + // Command arguments + const commandArguments = session.getArguments(); + + // State of the execution. Shows whether it is still running or completed + const state = await session.getState(); + + // Return code for completed sessions. Will be undefined if session is still running or FFmpegKit fails to run it + const returnCode = await session.getReturnCode() + + const startTime = session.getStartTime(); + const endTime = await session.getEndTime(); + const duration = await session.getDuration(); + + // Console output generated for this execution + const output = await session.getOutput(); + + // The stack trace if FFmpegKit fails to run a command + const failStackTrace = await session.getFailStackTrace() + + // The list of logs generated for this execution + const logs = await session.getLogs(); + + // The list of statistics generated for this execution (only available on FFmpegSession) + const statistics = await session.getStatistics(); + + }); + ``` + +3. Execute `FFmpeg` commands by providing session specific `execute`/`log`/`session` callbacks. + + ```js + FFmpegKit.executeAsync('-i file1.mp4 -c:v mpeg4 file2.mp4', session => { + + // CALLED WHEN SESSION IS EXECUTED + + }, log => { + + // CALLED WHEN SESSION PRINTS LOGS + + }, statistics => { + + // CALLED WHEN SESSION GENERATES STATISTICS + + }); + ``` + +4. Execute `FFprobe` commands. + + ```js + FFprobeKit.executeAsync(ffprobeCommand, session => { + + // CALLED WHEN SESSION IS EXECUTED + + }); + ``` + +5. Get media information for a file/url. + + ```js + FFprobeKit.getMediaInformationAsync('', async (session) => { + const information = await session.getMediaInformation(); + }); + ``` + +6. Stop ongoing FFmpeg operations. + + - Stop all sessions + ```js + FFmpegKit.cancel(); + ``` + - Stop a specific session + ```js + FFmpegKit.cancel(sessionId); + ``` + +7. (Android) Convert Storage Access Framework (SAF) Uris into paths that can be read or written by +`FFmpegKit` and `FFprobeKit`. + + - Reading a file: + ```js + FFmpegKitConfig.selectDocumentForRead('*/*').then(uri => { + FFmpegKitConfig.getSafParameterForRead(uri).then(safUrl => { + FFmpegKit.executeAsync(`-i ${safUrl} -c:v mpeg4 file2.mp4`); + }); + }); + ``` + + - Writing to a file: + ```js + FFmpegKitConfig.selectDocumentForWrite('video.mp4', 'video/*').then(uri => { + FFmpegKitConfig.getSafParameterForWrite(uri).then(safUrl => { + FFmpegKit.executeAsync(`-i file1.mp4 -c:v mpeg4 ${safUrl}`); + }); + }); + ``` + +8. Get previous `FFmpeg` and `FFprobe` sessions from the session history. + + ```js + FFmpegKit.listSessions().then(sessionList => { + sessionList.forEach(async session => { + const sessionId = session.getSessionId(); + }); + }); + + FFprobeKit.listSessions().then(sessionList => { + sessionList.forEach(async session => { + const sessionId = session.getSessionId(); + }); + }); + ``` + +9. Enable global callbacks. + - Execute Callback, called when an async execution is ended + + ```js + FFmpegKitConfig.enableExecuteCallback(session => { + const sessionId = session.getSessionId(); + }); + ``` + + - Log Callback, called when a session generates logs + + ```js + FFmpegKitConfig.enableLogCallback(log => { + const message = log.getMessage(); + }); + ``` + + - Statistics Callback, called when a session generates statistics + + ```js + FFmpegKitConfig.enableStatisticsCallback(statistics => { + const size = statistics.getSize(); + }); + ``` + +10. Register system fonts and custom font directories. + + ```js + FFmpegKitConfig.setFontDirectoryList(["/system/fonts", "/System/Library/Fonts", ""]); + ``` + +### 4. Test Application + +You can see how `FFmpegKit` is used inside an application by running `react-native` test applications developed under +the [FFmpegKit Test](https://github.com/tanersener/ffmpeg-kit-test) project. + +### 5. Tips + +See [Tips](https://github.com/tanersener/ffmpeg-kit/wiki/Tips) wiki page. + +### 6. License + +See [License](https://github.com/tanersener/ffmpeg-kit/wiki/License) wiki page. + +### 7. Patents + +See [Patents](https://github.com/tanersener/ffmpeg-kit/wiki/Patents) wiki page. diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle new file mode 100644 index 00000000..e51ec382 --- /dev/null +++ b/react-native/android/build.gradle @@ -0,0 +1,126 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.2' + } +} + +apply plugin: 'com.android.library' + +static def safePackageName(String packageName) { + packageName.replace("-lts", "") +} + +def safePackageVersion(String packageName) { + def version = project.properties['ffmpegKit.android.main.version'] + def ltsVersion = project.properties['ffmpegKit.android.lts.version'] + packageName.contains("-lts") ? ltsVersion + ".LTS" : version +} + +def safeExtGet(String prop, String fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + +android { + compileSdkVersion 30 + + defaultConfig { + minSdkVersion safeExtGet('ffmpegKitPackage', 'https').contains("-lts") ? 16 : 24 + targetSdkVersion 30 + versionCode 450 + versionName "4.5.0" + } + + buildTypes { + release { + minifyEnabled false + } + } + lintOptions { + disable 'GradleCompatible' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +repositories { + mavenCentral() + jcenter() + google() + + def found = false + def defaultDir = null + def androidSourcesName = 'React Native sources' + + if (rootProject.ext.has('reactNativeAndroidRoot')) { + defaultDir = rootProject.ext.get('reactNativeAndroidRoot') + } else { + defaultDir = new File( + projectDir, + 'node_modules/react-native/android' + ) + } + + if (defaultDir.exists()) { + maven { + url defaultDir.toString() + name androidSourcesName + } + + logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}") + found = true + } else { + def parentDir = rootProject.projectDir + + 1.upto(5, { + if (found) return true + parentDir = parentDir.parentFile + + def androidSourcesDir = new File( + parentDir, + 'node_modules/react-native' + ) + + def androidPrebuiltBinaryDir = new File( + parentDir, + 'node_modules/react-native/android' + ) + + if (androidPrebuiltBinaryDir.exists()) { + maven { + url androidPrebuiltBinaryDir.toString() + name androidSourcesName + } + + logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}") + found = true + } else if (androidSourcesDir.exists()) { + maven { + url androidSourcesDir.toString() + name androidSourcesName + } + + logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}") + found = true + } + }) + } + + if (!found) { + throw new GradleException( + "${project.name}: unable to locate React Native android sources. " + + "Ensure you have you installed React Native as a dependency in your project and try again." + ) + } +} + +dependencies { + api 'com.facebook.react:react-native:+' + implementation 'com.arthenica:ffmpeg-kit-' + safePackageName(safeExtGet('ffmpegKitPackage', 'https')) + ':' + safePackageVersion(safeExtGet('ffmpegKitPackage', 'https')) +} diff --git a/react-native/android/gradle.properties b/react-native/android/gradle.properties new file mode 100644 index 00000000..48aa8f1f --- /dev/null +++ b/react-native/android/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +ffmpegKit.android.main.version=4.5 +ffmpegKit.android.lts.version=4.5 diff --git a/react-native/android/gradle/wrapper/gradle-wrapper.jar b/react-native/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/react-native/android/gradle/wrapper/gradle-wrapper.properties b/react-native/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..28ff446a --- /dev/null +++ b/react-native/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/react-native/android/gradlew b/react-native/android/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/react-native/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/react-native/android/gradlew.bat b/react-native/android/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/react-native/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/react-native/android/src/main/AndroidManifest.xml b/react-native/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b75bc365 --- /dev/null +++ b/react-native/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/AsyncWriteToPipeTask.java b/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/AsyncWriteToPipeTask.java new file mode 100644 index 00000000..a2d10794 --- /dev/null +++ b/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/AsyncWriteToPipeTask.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit.reactnative; + +import static com.arthenica.ffmpegkit.reactnative.FFmpegKitReactNativeModule.LIBRARY_NAME; + +import android.util.Log; + +import com.facebook.react.bridge.Promise; + +import java.io.IOException; + +public class AsyncWriteToPipeTask implements Runnable { + private final String inputPath; + private final String namedPipePath; + private final Promise promise; + + public AsyncWriteToPipeTask(final String inputPath, final String namedPipePath, final Promise promise) { + this.inputPath = inputPath; + this.namedPipePath = namedPipePath; + this.promise = promise; + } + + @Override + public void run() { + int rc; + + try { + final String asyncCommand = "cat " + inputPath + " > " + namedPipePath; + Log.d(LIBRARY_NAME, String.format("Starting copy %s to pipe %s operation.", inputPath, namedPipePath)); + + final long startTime = System.currentTimeMillis(); + + final Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", asyncCommand}); + rc = process.waitFor(); + + final long endTime = System.currentTimeMillis(); + + Log.d(LIBRARY_NAME, String.format("Copying %s to pipe %s operation completed with rc %d in %d seconds.", inputPath, namedPipePath, rc, (endTime - startTime) / 1000)); + + promise.resolve(rc); + + } catch (final IOException | InterruptedException e) { + Log.e(LIBRARY_NAME, String.format("Copy %s to pipe %s failed with error.", inputPath, namedPipePath), e); + promise.reject("Copy failed", String.format("Copy %s to pipe %s failed with error.", inputPath, namedPipePath), e); + } + } + +} diff --git a/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativeModule.java b/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativeModule.java new file mode 100644 index 00000000..e8c702a3 --- /dev/null +++ b/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativeModule.java @@ -0,0 +1,1159 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit.reactnative; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.arthenica.ffmpegkit.AbiDetect; +import com.arthenica.ffmpegkit.AbstractSession; +import com.arthenica.ffmpegkit.FFmpegKit; +import com.arthenica.ffmpegkit.FFmpegKitConfig; +import com.arthenica.ffmpegkit.FFmpegSession; +import com.arthenica.ffmpegkit.FFprobeKit; +import com.arthenica.ffmpegkit.FFprobeSession; +import com.arthenica.ffmpegkit.Level; +import com.arthenica.ffmpegkit.LogRedirectionStrategy; +import com.arthenica.ffmpegkit.MediaInformation; +import com.arthenica.ffmpegkit.MediaInformationJsonParser; +import com.arthenica.ffmpegkit.MediaInformationSession; +import com.arthenica.ffmpegkit.Packages; +import com.arthenica.ffmpegkit.ReturnCode; +import com.arthenica.ffmpegkit.Session; +import com.arthenica.ffmpegkit.SessionState; +import com.arthenica.ffmpegkit.Signal; +import com.arthenica.ffmpegkit.Statistics; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.BaseActivityEventListener; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +public class FFmpegKitReactNativeModule extends ReactContextBaseJavaModule implements LifecycleEventListener { + + public static final String LIBRARY_NAME = "ffmpeg-kit-react-native"; + public static final String PLATFORM_NAME = "android"; + + // LOG CLASS + public static final String KEY_LOG_SESSION_ID = "sessionId"; + public static final String KEY_LOG_LEVEL = "level"; + public static final String KEY_LOG_MESSAGE = "message"; + + // STATISTICS CLASS + public static final String KEY_STATISTICS_SESSION_ID = "sessionId"; + public static final String KEY_STATISTICS_VIDEO_FRAME_NUMBER = "videoFrameNumber"; + public static final String KEY_STATISTICS_VIDEO_FPS = "videoFps"; + public static final String KEY_STATISTICS_VIDEO_QUALITY = "videoQuality"; + public static final String KEY_STATISTICS_SIZE = "size"; + public static final String KEY_STATISTICS_TIME = "time"; + public static final String KEY_STATISTICS_BITRATE = "bitrate"; + public static final String KEY_STATISTICS_SPEED = "speed"; + + // SESSION CLASS + public static final String KEY_SESSION_ID = "sessionId"; + public static final String KEY_SESSION_CREATE_TIME = "createTime"; + public static final String KEY_SESSION_START_TIME = "startTime"; + public static final String KEY_SESSION_COMMAND = "command"; + public static final String KEY_SESSION_TYPE = "type"; + public static final String KEY_SESSION_MEDIA_INFORMATION = "mediaInformation"; + + // SESSION TYPE + public static final int SESSION_TYPE_FFMPEG = 1; + public static final int SESSION_TYPE_FFPROBE = 2; + public static final int SESSION_TYPE_MEDIA_INFORMATION = 3; + + // EVENTS + public static final String EVENT_LOG_CALLBACK_EVENT = "FFmpegKitLogCallbackEvent"; + public static final String EVENT_STATISTICS_CALLBACK_EVENT = "FFmpegKitStatisticsCallbackEvent"; + public static final String EVENT_EXECUTE_CALLBACK_EVENT = "FFmpegKitExecuteCallbackEvent"; + + // REQUEST CODES + public static final int READABLE_REQUEST_CODE = 10000; + public static final int WRITABLE_REQUEST_CODE = 20000; + + private static final int asyncWriteToPipeConcurrencyLimit = 10; + + private final AtomicBoolean logsEnabled; + private final AtomicBoolean statisticsEnabled; + private final ExecutorService asyncWriteToPipeExecutorService; + + public FFmpegKitReactNativeModule(@Nullable ReactApplicationContext reactContext) { + super(reactContext); + + this.logsEnabled = new AtomicBoolean(false); + this.statisticsEnabled = new AtomicBoolean(false); + this.asyncWriteToPipeExecutorService = Executors.newFixedThreadPool(asyncWriteToPipeConcurrencyLimit); + + if (reactContext != null) { + reactContext.addLifecycleEventListener(this); + registerGlobalCallbacks(reactContext); + } + } + + @ReactMethod + public void addListener(final String eventName) { + Log.i(LIBRARY_NAME, String.format("Listener added for %s event.", eventName)); + } + + @ReactMethod + public void removeListeners(Integer count) { + } + + @NonNull + @Override + public String getName() { + return "FFmpegKitReactNativeModule"; + } + + @Override + public void onHostResume() { + } + + @Override + public void onHostPause() { + } + + @Override + public void onHostDestroy() { + this.asyncWriteToPipeExecutorService.shutdown(); + } + + protected void registerGlobalCallbacks(final ReactApplicationContext reactContext) { + FFmpegKitConfig.enableExecuteCallback(session -> { + final DeviceEventManagerModule.RCTDeviceEventEmitter jsModule = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + jsModule.emit(EVENT_EXECUTE_CALLBACK_EVENT, toMap(session)); + }); + + FFmpegKitConfig.enableLogCallback(log -> { + if (logsEnabled.get()) { + final DeviceEventManagerModule.RCTDeviceEventEmitter jsModule = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + jsModule.emit(EVENT_LOG_CALLBACK_EVENT, toMap(log)); + } + }); + + FFmpegKitConfig.enableStatisticsCallback(statistics -> { + if (statisticsEnabled.get()) { + final DeviceEventManagerModule.RCTDeviceEventEmitter jsModule = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + jsModule.emit(EVENT_STATISTICS_CALLBACK_EVENT, toMap(statistics)); + } + }); + } + + // AbstractSession + + @ReactMethod + public void abstractSessionGetEndTime(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + final Date endTime = session.getEndTime(); + if (endTime == null) { + promise.resolve(null); + } else { + promise.resolve(endTime.getTime()); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetDuration(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + promise.resolve((double) session.getDuration()); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetAllLogs(final Double sessionId, final Double waitTimeout, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout.intValue(); + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final List allLogs = session.getAllLogs(timeout); + promise.resolve(toArray(allLogs.stream().map(FFmpegKitReactNativeModule::toMap))); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetLogs(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + final List allLogs = session.getLogs(); + promise.resolve(toArray(allLogs.stream().map(FFmpegKitReactNativeModule::toMap))); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetAllLogsAsString(final Double sessionId, final Double waitTimeout, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout.intValue(); + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final String allLogsAsString = session.getAllLogsAsString(timeout); + promise.resolve(allLogsAsString); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetState(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + promise.resolve(session.getState().ordinal()); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetReturnCode(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + final ReturnCode returnCode = session.getReturnCode(); + if (returnCode == null) { + promise.resolve(null); + } else { + promise.resolve(returnCode.getValue()); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void abstractSessionGetFailStackTrace(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + promise.resolve(session.getFailStackTrace()); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void thereAreAsynchronousMessagesInTransmit(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + promise.resolve(session.thereAreAsynchronousMessagesInTransmit()); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + // ArchDetect + + @ReactMethod + public void getArch(final Promise promise) { + promise.resolve(AbiDetect.getAbi()); + } + + // FFmpegSession + + @ReactMethod + public void ffmpegSession(final ReadableArray readableArray, final Promise promise) { + promise.resolve(toMap(new FFmpegSession(toArgumentsArray(readableArray), null, null, null, LogRedirectionStrategy.NEVER_PRINT_LOGS))); + } + + @ReactMethod + public void ffmpegSessionGetAllStatistics(final Double sessionId, final Double waitTimeout, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + if (session instanceof FFmpegSession) { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout.intValue(); + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + final List allStatistics = ((FFmpegSession) session).getAllStatistics(timeout); + promise.resolve(toArray(allStatistics.stream().map(FFmpegKitReactNativeModule::toMap))); + } else { + promise.reject("NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void ffmpegSessionGetStatistics(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + if (session instanceof FFmpegSession) { + final List statistics = ((FFmpegSession) session).getStatistics(); + promise.resolve(toArray(statistics.stream().map(FFmpegKitReactNativeModule::toMap))); + } else { + promise.reject("NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + // FFprobeSession + + @ReactMethod + public void ffprobeSession(final ReadableArray readableArray, final Promise promise) { + promise.resolve(toMap(new FFprobeSession(toArgumentsArray(readableArray), null, null, LogRedirectionStrategy.NEVER_PRINT_LOGS))); + } + + // MediaInformationSession + + @ReactMethod + public void mediaInformationSession(final ReadableArray readableArray, final Promise promise) { + promise.resolve(toMap(new MediaInformationSession(toArgumentsArray(readableArray), null, null))); + } + + // MediaInformationJsonParser + + @ReactMethod + public void mediaInformationJsonParserFrom(final String ffprobeJsonOutput, final Promise promise) { + try { + final MediaInformation mediaInformation = MediaInformationJsonParser.fromWithError(ffprobeJsonOutput); + promise.resolve(toMap(mediaInformation)); + } catch (JSONException e) { + Log.i(LIBRARY_NAME, "Parsing MediaInformation failed.", e); + promise.resolve(null); + } + } + + @ReactMethod + public void mediaInformationJsonParserFromWithError(final String ffprobeJsonOutput, final Promise promise) { + try { + final MediaInformation mediaInformation = MediaInformationJsonParser.fromWithError(ffprobeJsonOutput); + promise.resolve(toMap(mediaInformation)); + } catch (JSONException e) { + Log.i(LIBRARY_NAME, "Parsing MediaInformation failed.", e); + promise.reject("PARSE_FAILED", "Parsing MediaInformation failed with JSON error."); + } + } + + // FFmpegKitConfig + + @ReactMethod + public void enableRedirection(final Promise promise) { + enableLogs(); + enableStatistics(); + FFmpegKitConfig.enableRedirection(); + + promise.resolve(null); + } + + @ReactMethod + public void disableRedirection(final Promise promise) { + FFmpegKitConfig.disableRedirection(); + + promise.resolve(null); + } + + @ReactMethod + public void enableLogs(final Promise promise) { + enableLogs(); + + promise.resolve(null); + } + + @ReactMethod + public void disableLogs(final Promise promise) { + disableLogs(); + + promise.resolve(null); + } + + @ReactMethod + public void enableStatistics(final Promise promise) { + enableStatistics(); + + promise.resolve(null); + } + + @ReactMethod + public void disableStatistics(final Promise promise) { + disableStatistics(); + + promise.resolve(null); + } + + @ReactMethod + public void setFontconfigConfigurationPath(final String path, final Promise promise) { + FFmpegKitConfig.setFontconfigConfigurationPath(path); + + promise.resolve(null); + } + + @ReactMethod + public void setFontDirectory(final String fontDirectoryPath, final ReadableMap fontNameMap, final Promise promise) { + final ReactApplicationContext reactContext = getReactApplicationContext(); + if (reactContext != null) { + FFmpegKitConfig.setFontDirectory(reactContext, fontDirectoryPath, toMap(fontNameMap)); + promise.resolve(null); + } else { + promise.reject("INVALID_CONTEXT", "React context is not initialized."); + } + } + + @ReactMethod + public void setFontDirectoryList(final ReadableArray readableArray, final ReadableMap fontNameMap, final Promise promise) { + final ReactApplicationContext reactContext = getReactApplicationContext(); + if (reactContext != null) { + FFmpegKitConfig.setFontDirectoryList(reactContext, Arrays.asList(toArgumentsArray(readableArray)), toMap(fontNameMap)); + promise.resolve(null); + } else { + promise.reject("INVALID_CONTEXT", "React context is not initialized."); + } + } + + @ReactMethod + public void registerNewFFmpegPipe(final Promise promise) { + final ReactApplicationContext reactContext = getReactApplicationContext(); + if (reactContext != null) { + promise.resolve(FFmpegKitConfig.registerNewFFmpegPipe(reactContext)); + } else { + promise.reject("INVALID_CONTEXT", "React context is not initialized."); + } + } + + @ReactMethod + public void closeFFmpegPipe(final String ffmpegPipePath, final Promise promise) { + FFmpegKitConfig.closeFFmpegPipe(ffmpegPipePath); + + promise.resolve(null); + } + + @ReactMethod + public void getFFmpegVersion(final Promise promise) { + promise.resolve(FFmpegKitConfig.getFFmpegVersion()); + } + + @ReactMethod + public void isLTSBuild(final Promise promise) { + promise.resolve(FFmpegKitConfig.isLTSBuild()); + } + + @ReactMethod + public void getBuildDate(final Promise promise) { + promise.resolve(FFmpegKitConfig.getBuildDate()); + } + + @ReactMethod + public void setEnvironmentVariable(final String variableName, final String variableValue, final Promise promise) { + FFmpegKitConfig.setEnvironmentVariable(variableName, variableValue); + + promise.resolve(null); + } + + @ReactMethod + public void ignoreSignal(final Double signalValue, final Promise promise) { + Signal signal = null; + + if (signalValue.intValue() == Signal.SIGINT.getValue()) { + signal = Signal.SIGINT; + } else if (signalValue.intValue() == Signal.SIGQUIT.getValue()) { + signal = Signal.SIGQUIT; + } else if (signalValue.intValue() == Signal.SIGPIPE.getValue()) { + signal = Signal.SIGPIPE; + } else if (signalValue.intValue() == Signal.SIGTERM.getValue()) { + signal = Signal.SIGTERM; + } else if (signalValue.intValue() == Signal.SIGXCPU.getValue()) { + signal = Signal.SIGXCPU; + } + + if (signal != null) { + FFmpegKitConfig.ignoreSignal(signal); + + promise.resolve(null); + } else { + promise.reject("INVALID_SIGNAL", "Signal value not supported."); + } + } + + @ReactMethod + public void asyncFFmpegSessionExecute(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + if (session instanceof FFmpegSession) { + FFmpegKitConfig.asyncFFmpegExecute((FFmpegSession) session); + promise.resolve(null); + } else { + promise.reject("NOT_FFMPEG_SESSION", "A session is found but it does not have the correct type."); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void asyncFFprobeSessionExecute(final Double sessionId, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + if (session instanceof FFprobeSession) { + FFmpegKitConfig.asyncFFprobeExecute((FFprobeSession) session); + promise.resolve(null); + } else { + promise.reject("NOT_FFPROBE_SESSION", "A session is found but it does not have the correct type."); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void asyncMediaInformationSessionExecute(final Double sessionId, final Double waitTimeout, final Promise promise) { + if (sessionId != null) { + Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + if (session instanceof MediaInformationSession) { + final int timeout; + if (isValidPositiveNumber(waitTimeout)) { + timeout = waitTimeout.intValue(); + } else { + timeout = AbstractSession.DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT; + } + FFmpegKitConfig.asyncGetMediaInformationExecute((MediaInformationSession) session, timeout); + promise.resolve(null); + } else { + promise.reject("NOT_MEDIA_INFORMATION_SESSION", "A session is found but it does not have the correct type."); + } + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void getLogLevel(final Promise promise) { + promise.resolve(toInt(FFmpegKitConfig.getLogLevel())); + } + + @ReactMethod + public void setLogLevel(final Double level, final Promise promise) { + if (level != null) { + FFmpegKitConfig.setLogLevel(Level.from(level.intValue())); + promise.resolve(null); + } else { + promise.reject("INVALID_LEVEL", "Invalid level value."); + } + } + + @ReactMethod + public void getSessionHistorySize(final Promise promise) { + promise.resolve(FFmpegKitConfig.getSessionHistorySize()); + } + + @ReactMethod + public void setSessionHistorySize(final Double sessionHistorySize, final Promise promise) { + if (sessionHistorySize != null) { + FFmpegKitConfig.setSessionHistorySize(sessionHistorySize.intValue()); + promise.resolve(null); + } else { + promise.reject("INVALID_SIZE", "Invalid session history size value."); + } + } + + @ReactMethod + public void getSession(final Double sessionId, final Promise promise) { + if (sessionId != null) { + final Session session = FFmpegKitConfig.getSession(sessionId.longValue()); + if (session == null) { + promise.reject("SESSION_NOT_FOUND", "Session not found."); + } else { + promise.resolve(toMap(session)); + } + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void getLastSession(final Promise promise) { + final Session session = FFmpegKitConfig.getLastSession(); + promise.resolve(toMap(session)); + } + + @ReactMethod + public void getLastCompletedSession(final Promise promise) { + final Session session = FFmpegKitConfig.getLastCompletedSession(); + promise.resolve(toMap(session)); + } + + @ReactMethod + public void getSessions(final Promise promise) { + promise.resolve(toSessionArray(FFmpegKitConfig.getSessions())); + } + + @ReactMethod + public void clearSessions(final Promise promise) { + FFmpegKitConfig.clearSessions(); + promise.resolve(null); + } + + @ReactMethod + public void getSessionsByState(final Double sessionState, final Promise promise) { + if (sessionState != null) { + promise.resolve(toSessionArray(FFmpegKitConfig.getSessionsByState(toSessionState(sessionState.intValue())))); + } else { + promise.reject("INVALID_SESSION_STATE", "Invalid session state value."); + } + } + + @ReactMethod + public void getLogRedirectionStrategy(final Promise promise) { + promise.resolve(toInt(FFmpegKitConfig.getLogRedirectionStrategy())); + } + + @ReactMethod + public void setLogRedirectionStrategy(final Double logRedirectionStrategy, final Promise promise) { + if (logRedirectionStrategy != null) { + FFmpegKitConfig.setLogRedirectionStrategy(toLogRedirectionStrategy(logRedirectionStrategy.intValue())); + promise.resolve(null); + } else { + promise.reject("INVALID_LOG_REDIRECTION_STRATEGY", "Invalid log redirection strategy value."); + } + } + + @ReactMethod + public void messagesInTransmit(final Double sessionId, final Promise promise) { + if (sessionId != null) { + promise.resolve(FFmpegKitConfig.messagesInTransmit(sessionId.longValue())); + } else { + promise.reject("INVALID_SESSION", "Invalid session id."); + } + } + + @ReactMethod + public void getPlatform(final Promise promise) { + promise.resolve(PLATFORM_NAME); + } + + @ReactMethod + public void writeToPipe(final String inputPath, final String namedPipePath, final Promise promise) { + final AsyncWriteToPipeTask asyncTask = new AsyncWriteToPipeTask(inputPath, namedPipePath, promise); + asyncWriteToPipeExecutorService.submit(asyncTask); + } + + @ReactMethod + public void selectDocument(final Boolean writable, final String title, final String type, final ReadableArray extraTypes, final Promise promise) { + final ReactApplicationContext reactContext = getReactApplicationContext(); + + final Intent intent; + if (writable) { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } else { + intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + if (type != null) { + intent.setType(type); + } else { + intent.setType("*/*"); + } + + if (title != null) { + intent.putExtra(Intent.EXTRA_TITLE, title); + } + + if (extraTypes != null) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, toArgumentsArray(extraTypes)); + } + + if (reactContext != null) { + final Activity currentActivity = reactContext.getCurrentActivity(); + + if (currentActivity != null) { + reactContext.addActivityEventListener(new BaseActivityEventListener() { + + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + reactContext.removeActivityEventListener(this); + + Log.d(LIBRARY_NAME, String.format("selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s completed with requestCode: %d, resultCode: %d, data: %s.", writable, type, title, extraTypes == null ? null : Arrays.toString(toArgumentsArray(extraTypes)), requestCode, resultCode, data == null ? null : data.toString())); + + if (requestCode == READABLE_REQUEST_CODE || requestCode == WRITABLE_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + if (data == null) { + promise.resolve(null); + } else { + final Uri uri = data.getData(); + promise.resolve(uri == null ? null : uri.toString()); + } + } else { + promise.reject("SELECT_CANCELLED", String.valueOf(resultCode)); + } + } else { + super.onActivityResult(activity, requestCode, resultCode, data); + } + } + }); + + try { + currentActivity.startActivityForResult(intent, writable ? WRITABLE_REQUEST_CODE : READABLE_REQUEST_CODE); + } catch (final Exception e) { + Log.i(LIBRARY_NAME, String.format("Failed to selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s!", writable, type, title, extraTypes == null ? null : Arrays.toString(toArgumentsArray(extraTypes))), e); + promise.reject("SELECT_FAILED", e.getMessage()); + } + } else { + Log.w(LIBRARY_NAME, String.format("Cannot selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s. Current activity is null.", writable, type, title, extraTypes == null ? null : Arrays.toString(toArgumentsArray(extraTypes)))); + promise.reject("INVALID_ACTIVITY", "Activity is null."); + } + } else { + Log.w(LIBRARY_NAME, String.format("Cannot selectDocument using parameters writable: %s, type: %s, title: %s and extra types: %s. React context is null.", writable, type, title, extraTypes == null ? null : Arrays.toString(toArgumentsArray(extraTypes)))); + promise.reject("INVALID_CONTEXT", "Context is null."); + } + } + + @ReactMethod + public void getSafParameter(final Boolean writable, final String uriString, final Promise promise) { + final ReactApplicationContext reactContext = getReactApplicationContext(); + + final Uri uri = Uri.parse(uriString); + if (uri == null) { + Log.w(LIBRARY_NAME, String.format("Cannot getSafParameter using parameters writable: %s, uriString: %s. Uri string cannot be parsed.", writable, uriString)); + promise.reject("GET_SAF_PARAMETER_FAILED", "Uri string cannot be parsed."); + } else { + final String safParameter; + if (writable) { + safParameter = FFmpegKitConfig.getSafParameterForWrite(reactContext, uri); + } else { + safParameter = FFmpegKitConfig.getSafParameterForRead(reactContext, uri); + } + + Log.d(LIBRARY_NAME, String.format("getSafParameter using parameters writable: %s, uriString: %s completed with saf parameter: %s.", writable, uriString, safParameter)); + + promise.resolve(safParameter); + } + } + + // FFmpegKit + + @ReactMethod + public void cancel(final Promise promise) { + FFmpegKit.cancel(); + promise.resolve(null); + } + + @ReactMethod + public void cancelSession(final Double sessionId, final Promise promise) { + if (sessionId != null) { + FFmpegKit.cancel(sessionId.longValue()); + } else { + FFmpegKit.cancel(); + } + promise.resolve(null); + } + + @ReactMethod + public void getFFmpegSessions(final Promise promise) { + promise.resolve(toSessionArray(FFmpegKit.listSessions())); + } + + // FFprobeKit + + @ReactMethod + public void getFFprobeSessions(final Promise promise) { + promise.resolve(toSessionArray(FFprobeKit.listSessions())); + } + + // Packages + + @ReactMethod + public void getPackageName(final Promise promise) { + promise.resolve(Packages.getPackageName()); + } + + @ReactMethod + public void getExternalLibraries(final Promise promise) { + promise.resolve(toStringArray(Packages.getExternalLibraries())); + } + + protected void enableLogs() { + logsEnabled.compareAndSet(false, true); + } + + protected void disableLogs() { + logsEnabled.compareAndSet(true, false); + } + + protected void enableStatistics() { + statisticsEnabled.compareAndSet(false, true); + } + + protected void disableStatistics() { + statisticsEnabled.compareAndSet(true, false); + } + + protected static int toInt(final Level level) { + return (level == null) ? Level.AV_LOG_TRACE.getValue() : level.getValue(); + } + + protected static WritableMap toMap(final Session session) { + if (session == null) { + return null; + } + + final WritableMap sessionMap = Arguments.createMap(); + + sessionMap.putDouble(KEY_SESSION_ID, session.getSessionId()); + sessionMap.putDouble(KEY_SESSION_CREATE_TIME, toLong(session.getCreateTime())); + sessionMap.putDouble(KEY_SESSION_START_TIME, toLong(session.getStartTime())); + sessionMap.putString(KEY_SESSION_COMMAND, session.getCommand()); + + if (session.isFFprobe()) { + if (session instanceof MediaInformationSession) { + final MediaInformationSession mediaInformationSession = (MediaInformationSession) session; + final MediaInformation mediaInformation = mediaInformationSession.getMediaInformation(); + if (mediaInformation != null) { + sessionMap.putMap(KEY_SESSION_MEDIA_INFORMATION, toMap(mediaInformation)); + } + sessionMap.putDouble(KEY_SESSION_TYPE, SESSION_TYPE_MEDIA_INFORMATION); + } else { + sessionMap.putDouble(KEY_SESSION_TYPE, SESSION_TYPE_FFPROBE); + } + } else { + sessionMap.putDouble(KEY_SESSION_TYPE, SESSION_TYPE_FFMPEG); + } + + return sessionMap; + } + + protected static long toLong(final Date date) { + if (date != null) { + return date.getTime(); + } else { + return 0; + } + } + + protected static int toInt(final LogRedirectionStrategy logRedirectionStrategy) { + switch (logRedirectionStrategy) { + case ALWAYS_PRINT_LOGS: + return 0; + case PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED: + return 1; + case PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED: + return 2; + case PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED: + return 3; + case NEVER_PRINT_LOGS: + default: + return 4; + } + } + + protected static LogRedirectionStrategy toLogRedirectionStrategy(final int value) { + switch (value) { + case 0: + return LogRedirectionStrategy.ALWAYS_PRINT_LOGS; + case 1: + return LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED; + case 2: + return LogRedirectionStrategy.PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED; + case 3: + return LogRedirectionStrategy.PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED; + case 4: + default: + return LogRedirectionStrategy.NEVER_PRINT_LOGS; + } + } + + protected static SessionState toSessionState(final int value) { + switch (value) { + case 0: + return SessionState.CREATED; + case 1: + return SessionState.RUNNING; + case 2: + return SessionState.FAILED; + case 3: + default: + return SessionState.COMPLETED; + } + } + + protected static WritableArray toStringArray(final List list) { + final WritableArray array = Arguments.createArray(); + + if (list != null) { + for (String item : list) { + array.pushString(item); + } + } + + return array; + } + + protected static Map toMap(final ReadableMap readableMap) { + final Map map = new HashMap<>(); + + if (readableMap != null) { + final ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while (iterator.hasNextKey()) { + final String key = iterator.nextKey(); + final ReadableType type = readableMap.getType(key); + + if (type == ReadableType.String) { + map.put(key, readableMap.getString(key)); + } + } + } + + return map; + } + + protected static WritableMap toMap(final com.arthenica.ffmpegkit.Log log) { + final WritableMap logMap = Arguments.createMap(); + + logMap.putDouble(KEY_LOG_SESSION_ID, log.getSessionId()); + logMap.putDouble(KEY_LOG_LEVEL, toInt(log.getLevel())); + logMap.putString(KEY_LOG_MESSAGE, log.getMessage()); + + return logMap; + } + + protected static WritableMap toMap(final Statistics statistics) { + final WritableMap statisticsMap = Arguments.createMap(); + + if (statistics != null) { + statisticsMap.putDouble(KEY_STATISTICS_SESSION_ID, statistics.getSessionId()); + statisticsMap.putDouble(KEY_STATISTICS_VIDEO_FRAME_NUMBER, statistics.getVideoFrameNumber()); + statisticsMap.putDouble(KEY_STATISTICS_VIDEO_FPS, statistics.getVideoFps()); + statisticsMap.putDouble(KEY_STATISTICS_VIDEO_QUALITY, statistics.getVideoQuality()); + statisticsMap.putDouble(KEY_STATISTICS_SIZE, statistics.getSize()); + statisticsMap.putDouble(KEY_STATISTICS_TIME, statistics.getTime()); + statisticsMap.putDouble(KEY_STATISTICS_BITRATE, statistics.getBitrate()); + statisticsMap.putDouble(KEY_STATISTICS_SPEED, statistics.getSpeed()); + } + + return statisticsMap; + } + + protected static WritableArray toSessionArray(final List sessions) { + final WritableArray sessionArray = Arguments.createArray(); + + for (int i = 0; i < sessions.size(); i++) { + sessionArray.pushMap(toMap(sessions.get(i))); + } + + return sessionArray; + } + + protected static WritableMap toMap(final MediaInformation mediaInformation) { + WritableMap map = Arguments.createMap(); + + if (mediaInformation != null) { + JSONObject allProperties = mediaInformation.getAllProperties(); + if (allProperties != null) { + map = toMap(allProperties); + } + } + + return map; + } + + protected static WritableMap toMap(final JSONObject jsonObject) { + final WritableMap map = Arguments.createMap(); + + if (jsonObject != null) { + Iterator keys = jsonObject.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object value = jsonObject.opt(key); + if (value != null) { + if (value instanceof JSONArray) { + map.putArray(key, toList((JSONArray) value)); + } else if (value instanceof JSONObject) { + map.putMap(key, toMap((JSONObject) value)); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else if (value instanceof Number) { + if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else { + map.putDouble(key, ((Number) value).doubleValue()); + } + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else { + Log.i(LIBRARY_NAME, String.format("Cannot map json key %s using value %s:%s", key, value.toString(), value.getClass().toString())); + } + } + } + } + + return map; + } + + protected static WritableArray toArray(final Stream stream) { + final WritableArray list = Arguments.createArray(); + + stream.forEachOrdered(list::pushMap); + + return list; + } + + protected static WritableArray toList(final JSONArray array) { + final WritableArray list = Arguments.createArray(); + + for (int i = 0; i < array.length(); i++) { + Object value = array.opt(i); + if (value != null) { + if (value instanceof JSONArray) { + list.pushArray(toList((JSONArray) value)); + } else if (value instanceof JSONObject) { + list.pushMap(toMap((JSONObject) value)); + } else if (value instanceof String) { + list.pushString((String) value); + } else if (value instanceof Number) { + if (value instanceof Integer) { + list.pushInt((Integer) value); + } else { + list.pushDouble(((Number) value).doubleValue()); + } + } else if (value instanceof Boolean) { + list.pushBoolean((Boolean) value); + } else { + Log.i(LIBRARY_NAME, String.format("Cannot map json value %s:%s", value.toString(), value.getClass().toString())); + } + } + } + + return list; + } + + protected static String[] toArgumentsArray(final ReadableArray readableArray) { + final List arguments = new ArrayList<>(); + for (int i = 0; i < readableArray.size(); i++) { + final ReadableType type = readableArray.getType(i); + + if (type == ReadableType.String) { + arguments.add(readableArray.getString(i)); + } + } + + return arguments.toArray(new String[0]); + } + + protected static boolean isValidPositiveNumber(final Double value) { + return (value != null) && (value.intValue() >= 0); + } + +} diff --git a/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativePackage.java b/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativePackage.java new file mode 100644 index 00000000..f409ad70 --- /dev/null +++ b/react-native/android/src/main/java/com/arthenica/ffmpegkit/reactnative/FFmpegKitReactNativePackage.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit.reactnative; + +import androidx.annotation.NonNull; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.Collections; +import java.util.List; + +public class FFmpegKitReactNativePackage implements ReactPackage { + + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { + return Collections.singletonList(new FFmpegKitReactNativeModule(reactContext)); + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + +} diff --git a/react-native/babel.config.js b/react-native/babel.config.js new file mode 100644 index 00000000..f842b77f --- /dev/null +++ b/react-native/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/react-native/ffmpeg-kit-react-native.podspec b/react-native/ffmpeg-kit-react-native.podspec new file mode 100644 index 00000000..e56aada1 --- /dev/null +++ b/react-native/ffmpeg-kit-react-native.podspec @@ -0,0 +1,135 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = package["name"] + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platform = :ios + s.requires_arc = true + s.static_framework = true + + s.source = { :git => "https://github.com/tanersener/ffmpeg-kit.git", :tag => "react.native.v#{s.version}" } + + s.default_subspec = 'https' + + s.dependency "React-Core" + + s.subspec 'min' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-min', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'min-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-min', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'min-gpl' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-min-gpl', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'min-gpl-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-min-gpl', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'https' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-https', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'https-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-https', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'https-gpl' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-https-gpl', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'https-gpl-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-https-gpl', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'audio' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-audio', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'audio-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-audio', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'video' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-video', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'video-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-video', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'full' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-full', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'full-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-full', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + + s.subspec 'full-gpl' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-full-gpl', "4.5" + ss.ios.deployment_target = '12.1' + end + + s.subspec 'full-gpl-lts' do |ss| + ss.source_files = '**/FFmpegKitReactNativeModule.m', + '**/FFmpegKitReactNativeModule.h' + ss.dependency 'ffmpeg-kit-ios-full-gpl', "4.5.LTS" + ss.ios.deployment_target = '9.3' + end + +end diff --git a/react-native/ios/FFmpegKitReactNativeModule.h b/react-native/ios/FFmpegKitReactNativeModule.h new file mode 100644 index 00000000..51458f98 --- /dev/null +++ b/react-native/ios/FFmpegKitReactNativeModule.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +#import +#import +#import +#import + +@interface FFmpegKitReactNativeModule : RCTEventEmitter +@end diff --git a/react-native/ios/FFmpegKitReactNativeModule.m b/react-native/ios/FFmpegKitReactNativeModule.m new file mode 100644 index 00000000..7861877c --- /dev/null +++ b/react-native/ios/FFmpegKitReactNativeModule.m @@ -0,0 +1,779 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +#import "FFmpegKitReactNativeModule.h" +#import +#import +#import + +#import +#import +#import +#import +#import + +static NSString *const PLATFORM_NAME = @"ios"; + +// LOG CLASS +static NSString *const KEY_LOG_SESSION_ID = @"sessionId"; +static NSString *const KEY_LOG_LEVEL = @"level"; +static NSString *const KEY_LOG_MESSAGE = @"message"; + +// STATISTICS CLASS +static NSString *const KEY_STATISTICS_SESSION_ID = @"sessionId"; +static NSString *const KEY_STATISTICS_VIDEO_FRAME_NUMBER = @"videoFrameNumber"; +static NSString *const KEY_STATISTICS_VIDEO_FPS = @"videoFps"; +static NSString *const KEY_STATISTICS_VIDEO_QUALITY = @"videoQuality"; +static NSString *const KEY_STATISTICS_SIZE = @"size"; +static NSString *const KEY_STATISTICS_TIME = @"time"; +static NSString *const KEY_STATISTICS_BITRATE = @"bitrate"; +static NSString *const KEY_STATISTICS_SPEED = @"speed"; + +// SESSION CLASS +static NSString *const KEY_SESSION_ID = @"sessionId"; +static NSString *const KEY_SESSION_CREATE_TIME = @"createTime"; +static NSString *const KEY_SESSION_START_TIME = @"startTime"; +static NSString *const KEY_SESSION_COMMAND = @"command"; +static NSString *const KEY_SESSION_TYPE = @"type"; +static NSString *const KEY_SESSION_MEDIA_INFORMATION = @"mediaInformation"; + +// SESSION TYPE +static int const SESSION_TYPE_FFMPEG = 1; +static int const SESSION_TYPE_FFPROBE = 2; +static int const SESSION_TYPE_MEDIA_INFORMATION = 3; + +// EVENTS +static NSString *const EVENT_LOG_CALLBACK_EVENT = @"FFmpegKitLogCallbackEvent"; +static NSString *const EVENT_STATISTICS_CALLBACK_EVENT = @"FFmpegKitStatisticsCallbackEvent"; +static NSString *const EVENT_EXECUTE_CALLBACK_EVENT = @"FFmpegKitExecuteCallbackEvent"; + +extern int const AbstractSessionDefaultTimeoutForAsynchronousMessagesInTransmit; + +@implementation FFmpegKitReactNativeModule { + BOOL logsEnabled; + BOOL statisticsEnabled; + dispatch_queue_t asyncWriteToPipeDispatchQueue; +} + +RCT_EXPORT_MODULE(FFmpegKitReactNativeModule); + +- (instancetype)init { + self = [super init]; + if (self) { + logsEnabled = false; + statisticsEnabled = false; + asyncWriteToPipeDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + [self registerGlobalCallbacks]; + } + + return self; +} + +- (NSArray*)supportedEvents { + NSMutableArray *array = [NSMutableArray array]; + + [array addObject:EVENT_LOG_CALLBACK_EVENT]; + [array addObject:EVENT_STATISTICS_CALLBACK_EVENT]; + [array addObject:EVENT_EXECUTE_CALLBACK_EVENT]; + + return array; +} + +- (void)registerGlobalCallbacks { + [FFmpegKitConfig enableExecuteCallback:^(id session){ + NSDictionary *dictionary = [FFmpegKitReactNativeModule toSessionDictionary:session]; + [self sendEventWithName:EVENT_EXECUTE_CALLBACK_EVENT body:dictionary]; + }]; + + [FFmpegKitConfig enableLogCallback: ^(Log* log){ + if (self->logsEnabled) { + NSDictionary *dictionary = [FFmpegKitReactNativeModule toLogDictionary:log]; + [self sendEventWithName:EVENT_LOG_CALLBACK_EVENT body:dictionary]; + } + }]; + + [FFmpegKitConfig enableStatisticsCallback:^(Statistics* statistics){ + if (self->statisticsEnabled) { + NSDictionary *dictionary = [FFmpegKitReactNativeModule toStatisticsDictionary:statistics]; + [self sendEventWithName:EVENT_STATISTICS_CALLBACK_EVENT body:dictionary]; + } + }]; +} + +// AbstractSession + +RCT_EXPORT_METHOD(abstractSessionGetEndTime:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + NSDate* endTime = [session getEndTime]; + if (endTime == nil) { + resolve(nil); + } else { + resolve([NSNumber numberWithDouble:[endTime timeIntervalSince1970]*1000]); + } + } +} + +RCT_EXPORT_METHOD(abstractSessionGetDuration:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + resolve([NSNumber numberWithLong:[session getDuration]]); + } +} + +RCT_EXPORT_METHOD(abstractSessionGetAllLogs:(int)sessionId withTimeout:(int)waitTimeout resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + int timeout; + if ([FFmpegKitReactNativeModule isValidPositiveNumber:waitTimeout]) { + timeout = waitTimeout; + } else { + timeout = AbstractSessionDefaultTimeoutForAsynchronousMessagesInTransmit; + } + NSArray* allLogs = [session getAllLogsWithTimeout:timeout]; + resolve([FFmpegKitReactNativeModule toLogArray:allLogs]); + } +} + +RCT_EXPORT_METHOD(abstractSessionGetLogs:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + NSArray* logs = [session getLogs]; + resolve([FFmpegKitReactNativeModule toLogArray:logs]); + } +} + +RCT_EXPORT_METHOD(abstractSessionGetAllLogsAsString:(int)sessionId withTimeout:(int)waitTimeout resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + int timeout; + if ([FFmpegKitReactNativeModule isValidPositiveNumber:waitTimeout]) { + timeout = waitTimeout; + } else { + timeout = AbstractSessionDefaultTimeoutForAsynchronousMessagesInTransmit; + } + NSString* allLogsAsString = [session getAllLogsAsStringWithTimeout:timeout]; + resolve(allLogsAsString); + } +} + +RCT_EXPORT_METHOD(abstractSessionGetState:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + resolve([FFmpegKitReactNativeModule sessionStateToNumber:[session getState]]); + } +} + +RCT_EXPORT_METHOD(abstractSessionGetReturnCode:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + ReturnCode* returnCode = [session getReturnCode]; + if (returnCode == nil) { + resolve(nil); + } else { + resolve([NSNumber numberWithInt:[returnCode getValue]]); + } + } +} + +RCT_EXPORT_METHOD(abstractSessionGetFailStackTrace:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + resolve([session getFailStackTrace]); + } +} + +RCT_EXPORT_METHOD(thereAreAsynchronousMessagesInTransmit:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + resolve([NSNumber numberWithBool:[session thereAreAsynchronousMessagesInTransmit]]); + } +} + +// ArchDetect + +RCT_EXPORT_METHOD(getArch:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([ArchDetect getArch]); +} + +// FFmpegSession + +RCT_EXPORT_METHOD(ffmpegSession:(NSArray*)arguments resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + FFmpegSession* session = [[FFmpegSession alloc] init:arguments withExecuteCallback:nil withLogCallback:nil withStatisticsCallback:nil withLogRedirectionStrategy:LogRedirectionStrategyNeverPrintLogs]; + resolve([FFmpegKitReactNativeModule toSessionDictionary:session]); +} + +RCT_EXPORT_METHOD(ffmpegSessionGetAllStatistics:(int)sessionId withTimeout:(int)waitTimeout resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + if ([session isMemberOfClass:[FFmpegSession class]]) { + int timeout; + if ([FFmpegKitReactNativeModule isValidPositiveNumber:waitTimeout]) { + timeout = waitTimeout; + } else { + timeout = AbstractSessionDefaultTimeoutForAsynchronousMessagesInTransmit; + } + NSArray* allStatistics = [(FFmpegSession*)session getAllStatisticsWithTimeout:timeout]; + resolve([FFmpegKitReactNativeModule toStatisticsArray:allStatistics]); + } else { + reject(@"NOT_FFMPEG_SESSION", @"A session is found but it does not have the correct type.", nil); + } + } +} + +RCT_EXPORT_METHOD(ffmpegSessionGetStatistics:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + if ([session isMemberOfClass:[FFmpegSession class]]) { + NSArray* statistics = [(FFmpegSession*)session getStatistics]; + resolve([FFmpegKitReactNativeModule toStatisticsArray:statistics]); + } else { + reject(@"NOT_FFMPEG_SESSION", @"A session is found but it does not have the correct type.", nil); + } + } +} + +// FFprobeSession + +RCT_EXPORT_METHOD(ffprobeSession:(NSArray*)arguments resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + FFprobeSession* session = [[FFprobeSession alloc] init:arguments withExecuteCallback:nil withLogCallback:nil withLogRedirectionStrategy:LogRedirectionStrategyNeverPrintLogs]; + resolve([FFmpegKitReactNativeModule toSessionDictionary:session]); +} + +// MediaInformationSession + +RCT_EXPORT_METHOD(mediaInformationSession:(NSArray*)arguments resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + MediaInformationSession* session = [[MediaInformationSession alloc] init:arguments withExecuteCallback:nil withLogCallback:nil]; + resolve([FFmpegKitReactNativeModule toSessionDictionary:session]); +} + +// MediaInformationJsonParser + +RCT_EXPORT_METHOD(mediaInformationJsonParserFrom:(NSString*)ffprobeJsonOutput resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + NSError *error; + MediaInformation* mediaInformation = [MediaInformationJsonParser from:ffprobeJsonOutput with:error]; + if (error == nil) { + resolve([FFmpegKitReactNativeModule toMediaInformationDictionary:mediaInformation]); + } else { + NSLog(@"MediaInformation parsing failed: %@.\n", error); + resolve(nil); + } +} + +RCT_EXPORT_METHOD(mediaInformationJsonParserFromWithError:(NSString*)ffprobeJsonOutput resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + NSError *error; + MediaInformation* mediaInformation = [MediaInformationJsonParser from:ffprobeJsonOutput with:error]; + if (error == nil) { + resolve([FFmpegKitReactNativeModule toMediaInformationDictionary:mediaInformation]); + } else { + NSLog(@"MediaInformation parsing failed: %@.\n", error); + reject(@"PARSE_FAILED", @"Parsing MediaInformation failed with JSON error.", nil); + } +} + +// FFmpegKitConfig + +RCT_EXPORT_METHOD(enableRedirection:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self enableLogs]; + [self enableStatistics]; + [FFmpegKitConfig enableRedirection]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(disableRedirection:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig disableRedirection]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(enableLogs:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self enableLogs]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(disableLogs:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self disableLogs]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(enableStatistics:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self enableStatistics]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(disableStatistics:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self disableStatistics]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(setFontconfigConfigurationPath:(NSString*)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setFontconfigConfigurationPath:path]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(setFontDirectory:(NSString*)fontDirectoryPath with:(NSDictionary*)fontNameMap resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setFontDirectory:fontDirectoryPath with:fontNameMap]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(setFontDirectoryList:(NSArray*)fontDirectoryList with:(NSDictionary*)fontNameMap resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setFontDirectoryList:fontDirectoryList with:fontNameMap]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(registerNewFFmpegPipe:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitConfig registerNewFFmpegPipe]); +} + +RCT_EXPORT_METHOD(closeFFmpegPipe:(NSString*)ffmpegPipePath resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig closeFFmpegPipe:ffmpegPipePath]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(getFFmpegVersion:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitConfig getFFmpegVersion]); +} + +RCT_EXPORT_METHOD(isLTSBuild:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([NSNumber numberWithInt:[FFmpegKitConfig isLTSBuild]]); +} + +RCT_EXPORT_METHOD(getBuildDate:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitConfig getBuildDate]); +} + +RCT_EXPORT_METHOD(setEnvironmentVariable:(NSString*)variableName with:(NSString*)variableValue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setEnvironmentVariable:variableName value:variableValue]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(ignoreSignal:(int)signalValue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + if ((signalValue == SignalInt) || (signalValue == SignalQuit) || (signalValue == SignalPipe) || (signalValue == SignalTerm) || (signalValue == SignalXcpu)) { + resolve(nil); + } else { + reject(@"INVALID_SIGNAL", @"Signal value not supported.", nil); + } +} + +RCT_EXPORT_METHOD(asyncFFmpegSessionExecute:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + if ([session isMemberOfClass:[FFmpegSession class]]) { + [FFmpegKitConfig asyncFFmpegExecute:(FFmpegSession*)session]; + resolve(nil); + } else { + reject(@"NOT_FFMPEG_SESSION", @"A session is found but it does not have the correct type.", nil); + } + } +} + +RCT_EXPORT_METHOD(asyncFFprobeSessionExecute:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + if ([session isMemberOfClass:[FFprobeSession class]]) { + [FFmpegKitConfig asyncFFprobeExecute:(FFprobeSession*)session]; + resolve(nil); + } else { + reject(@"NOT_FFPROBE_SESSION", @"A session is found but it does not have the correct type.", nil); + } + } +} + +RCT_EXPORT_METHOD(asyncMediaInformationSessionExecute:(int)sessionId withTimeout:(int)waitTimeout resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + if ([session isMemberOfClass:[MediaInformationSession class]]) { + int timeout; + if ([FFmpegKitReactNativeModule isValidPositiveNumber:waitTimeout]) { + timeout = waitTimeout; + } else { + timeout = AbstractSessionDefaultTimeoutForAsynchronousMessagesInTransmit; + } + [FFmpegKitConfig asyncGetMediaInformationExecute:(MediaInformationSession*)session withTimeout:timeout]; + resolve(nil); + } else { + reject(@"NOT_MEDIA_INFORMATION_SESSION", @"A session is found but it does not have the correct type.", nil); + } + } +} + +RCT_EXPORT_METHOD(getLogLevel:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([NSNumber numberWithInt:[FFmpegKitConfig getLogLevel]]); +} + +RCT_EXPORT_METHOD(setLogLevel:(int)level resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setLogLevel:level]; + resolve(nil); +} + +RCT_EXPORT_METHOD(getSessionHistorySize:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([NSNumber numberWithInt:[FFmpegKitConfig getSessionHistorySize]]); +} + +RCT_EXPORT_METHOD(setSessionHistorySize:(int)sessionHistorySize resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setSessionHistorySize:sessionHistorySize]; + resolve(nil); +} + +RCT_EXPORT_METHOD(getSession:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + AbstractSession* session = (AbstractSession*)[FFmpegKitConfig getSession:sessionId]; + if (session == nil) { + reject(@"SESSION_NOT_FOUND", @"Session not found.", nil); + } else { + resolve([FFmpegKitReactNativeModule toSessionDictionary:session]); + } +} + +RCT_EXPORT_METHOD(getLastSession:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule toSessionDictionary:[FFmpegKitConfig getLastSession]]); +} + +RCT_EXPORT_METHOD(getLastCompletedSession:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule toSessionDictionary:[FFmpegKitConfig getLastCompletedSession]]); +} + +RCT_EXPORT_METHOD(getSessions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule toSessionArray:[FFmpegKitConfig getSessions]]); +} + +RCT_EXPORT_METHOD(clearSessions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig clearSessions]; + resolve(nil); +} + +RCT_EXPORT_METHOD(getSessionsByState:(int)sessionState resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule toSessionArray:[FFmpegKitConfig getSessionsByState:sessionState]]); +} + +RCT_EXPORT_METHOD(getLogRedirectionStrategy:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule logRedirectionStrategyToNumber:[FFmpegKitConfig getLogRedirectionStrategy]]); +} + +RCT_EXPORT_METHOD(setLogRedirectionStrategy:(int)logRedirectionStrategy resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKitConfig setLogRedirectionStrategy:logRedirectionStrategy]; + resolve(nil); +} + +RCT_EXPORT_METHOD(messagesInTransmit:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([NSNumber numberWithInt:[FFmpegKitConfig messagesInTransmit:sessionId]]); +} + +RCT_EXPORT_METHOD(getPlatform:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve(PLATFORM_NAME); +} + +RCT_EXPORT_METHOD(writeToPipe:(NSString*)inputPath onPipe:(NSString*)namedPipePath resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(asyncWriteToPipeDispatchQueue, ^{ + + NSLog(@"Starting copy %@ to pipe %@ operation.\n", inputPath, namedPipePath); + + NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath: inputPath]; + if (fileHandle == nil) { + NSLog(@"Failed to open file %@.\n", inputPath); + reject(@"Copy failed", [NSString stringWithFormat:@"Failed to open file %@.", inputPath], nil); + return; + } + + NSFileHandle *pipeHandle = [NSFileHandle fileHandleForWritingAtPath: namedPipePath]; + if (pipeHandle == nil) { + NSLog(@"Failed to open pipe %@.\n", namedPipePath); + reject(@"Copy failed", [NSString stringWithFormat:@"Failed to open pipe %@.", namedPipePath], nil); + [fileHandle closeFile]; + return; + } + + int BUFFER_SIZE = 4096; + unsigned long readBytes = 0; + unsigned long totalBytes = 0; + double startTime = CACurrentMediaTime(); + + @try { + [fileHandle seekToFileOffset: 0]; + + do { + NSData *data = [fileHandle readDataOfLength:BUFFER_SIZE]; + readBytes = [data length]; + if (readBytes > 0) { + totalBytes += readBytes; + [pipeHandle writeData:data]; + } + } while (readBytes > 0); + + double endTime = CACurrentMediaTime(); + + NSLog(@"Copying %@ to pipe %@ operation completed successfully. %lu bytes copied in %f seconds.\n", inputPath, namedPipePath, totalBytes, (endTime - startTime)/1000); + + resolve(0); + + } @catch (NSException *e) { + NSLog(@"Copy failed %@.\n", [e reason]); + reject(@"Copy failed", [NSString stringWithFormat:@"Copy %@ to %@ failed with error %@.", inputPath, namedPipePath, [e reason]], nil); + } @finally { + [fileHandle closeFile]; + [pipeHandle closeFile]; + } + }); +} + +RCT_EXPORT_METHOD(selectDocument:(BOOL)writable title:(NSString*)title type:(NSString*)type array:(NSArray*)extraTypes resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + reject(@"Not Supported", @"Not supported on iOS platform.", nil); +} + +RCT_EXPORT_METHOD(getSafParameter:(BOOL)writable uri:(NSString*)uriString resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + reject(@"Not Supported", @"Not supported on iOS platform.", nil); +} + +// FFmpegKit + +RCT_EXPORT_METHOD(cancel:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKit cancel]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(cancelSession:(int)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [FFmpegKit cancel:sessionId]; + + resolve(nil); +} + +RCT_EXPORT_METHOD(getFFmpegSessions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule toSessionArray:[FFmpegKit listSessions]]); +} + +// FFprobeKit + +RCT_EXPORT_METHOD(getFFprobeSessions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([FFmpegKitReactNativeModule toSessionArray:[FFprobeKit listSessions]]); +} + +// Packages + +RCT_EXPORT_METHOD(getPackageName:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([Packages getPackageName]); +} + +RCT_EXPORT_METHOD(getExternalLibraries:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([Packages getExternalLibraries]); +} + +- (void)enableLogs { + logsEnabled = true; +} + +- (void)disableLogs { + logsEnabled = false; +} + +- (void)enableStatistics { + statisticsEnabled = true; +} + +- (void)disableStatistics { + statisticsEnabled = false; +} + ++ (NSDictionary*)toSessionDictionary:(id) session { + if (session != nil) { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + dictionary[KEY_SESSION_ID] = [NSNumber numberWithLong: [session getSessionId]]; + dictionary[KEY_SESSION_CREATE_TIME] = [NSNumber numberWithDouble:[[session getCreateTime] timeIntervalSince1970]*1000]; + dictionary[KEY_SESSION_START_TIME] = [NSNumber numberWithDouble:[[session getStartTime] timeIntervalSince1970]*1000]; + dictionary[KEY_SESSION_COMMAND] = [session getCommand]; + + if ([session isFFprobe]) { + if ([(AbstractSession*)session isMemberOfClass:[MediaInformationSession class]]) { + MediaInformationSession *mediaInformationSession = (MediaInformationSession*)session; + dictionary[KEY_SESSION_MEDIA_INFORMATION] = [FFmpegKitReactNativeModule toMediaInformationDictionary:[mediaInformationSession getMediaInformation]]; + dictionary[KEY_SESSION_TYPE] = [NSNumber numberWithInt:SESSION_TYPE_MEDIA_INFORMATION]; + } else { + dictionary[KEY_SESSION_TYPE] = [NSNumber numberWithInt:SESSION_TYPE_FFPROBE]; + } + } else { + dictionary[KEY_SESSION_TYPE] = [NSNumber numberWithInt:SESSION_TYPE_FFMPEG]; + } + + return dictionary; + } else { + return nil; + } +} + ++ (NSDictionary*)toLogDictionary:(Log*)log { + if (log != nil) { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + dictionary[KEY_LOG_SESSION_ID] = [NSNumber numberWithLong: [log getSessionId]]; + dictionary[KEY_LOG_LEVEL] = [NSNumber numberWithInt: [log getLevel]]; + dictionary[KEY_LOG_MESSAGE] = [log getMessage]; + + return dictionary; + } else { + return nil; + } +} + ++ (NSDictionary*)toStatisticsDictionary:(Statistics*)statistics { + if (statistics != nil) { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + dictionary[KEY_STATISTICS_SESSION_ID] = [NSNumber numberWithLong: [statistics getSessionId]]; + dictionary[KEY_STATISTICS_VIDEO_FRAME_NUMBER] = [NSNumber numberWithInt: [statistics getVideoFrameNumber]]; + dictionary[KEY_STATISTICS_VIDEO_FPS] = [NSNumber numberWithFloat: [statistics getVideoFps]]; + dictionary[KEY_STATISTICS_VIDEO_QUALITY] = [NSNumber numberWithFloat: [statistics getVideoQuality]]; + dictionary[KEY_STATISTICS_SIZE] = [NSNumber numberWithLong: [statistics getSize]]; + dictionary[KEY_STATISTICS_TIME] = [NSNumber numberWithInt: [statistics getTime]]; + dictionary[KEY_STATISTICS_BITRATE] = [NSNumber numberWithDouble: [statistics getBitrate]]; + dictionary[KEY_STATISTICS_SPEED] = [NSNumber numberWithDouble: [statistics getSpeed]]; + + return dictionary; + } else { + return nil; + } +} + ++ (NSDictionary*)toMediaInformationDictionary:(MediaInformation*)mediaInformation { + if (mediaInformation != nil) { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + NSDictionary* allProperties = [mediaInformation getAllProperties]; + if (allProperties != nil) { + for(NSString *key in [allProperties allKeys]) { + dictionary[key] = [allProperties objectForKey:key]; + } + } + + return dictionary; + } else { + return nil; + } +} + ++ (NSArray*)toLogArray:(NSArray*)logs { + NSMutableArray *array = [[NSMutableArray alloc] init]; + + for (int i = 0; i < [logs count]; i++) { + Log* log = [logs objectAtIndex:i]; + [array addObject: [FFmpegKitReactNativeModule toLogDictionary:log]]; + } + + return array; +} + ++ (NSArray*)toStatisticsArray:(NSArray*)statisticsArray { + NSMutableArray *array = [[NSMutableArray alloc] init]; + + for (int i = 0; i < [statisticsArray count]; i++) { + Statistics* statistics = [statisticsArray objectAtIndex:i]; + [array addObject: [FFmpegKitReactNativeModule toStatisticsDictionary:statistics]]; + } + + return array; +} + ++ (NSArray*)toSessionArray:(NSArray*)sessions { + NSMutableArray *array = [[NSMutableArray alloc] init]; + + for (int i = 0; i < [sessions count]; i++) { + AbstractSession* session = (AbstractSession*)[sessions objectAtIndex:i]; + [array addObject: [FFmpegKitReactNativeModule toSessionDictionary:session]]; + } + + return array; +} + ++ (NSNumber*)sessionStateToNumber:(SessionState)sessionState { + switch (sessionState) { + case SessionStateCreated: + return [NSNumber numberWithInt:0]; + case SessionStateRunning: + return [NSNumber numberWithInt:1]; + case SessionStateFailed: + return [NSNumber numberWithInt:2]; + case SessionStateCompleted: + default: + return [NSNumber numberWithInt:3]; + } +} + ++ (NSNumber*)logRedirectionStrategyToNumber:(LogRedirectionStrategy)logRedirectionStrategy { + switch (logRedirectionStrategy) { + case LogRedirectionStrategyAlwaysPrintLogs: + return [NSNumber numberWithInt:0]; + case LogRedirectionStrategyPrintLogsWhenNoCallbacksDefined: + return [NSNumber numberWithInt:1]; + case LogRedirectionStrategyPrintLogsWhenGlobalCallbackNotDefined: + return [NSNumber numberWithInt:2]; + case LogRedirectionStrategyPrintLogsWhenSessionCallbackNotDefined: + return [NSNumber numberWithInt:3]; + case LogRedirectionStrategyNeverPrintLogs: + default: + return [NSNumber numberWithInt:4]; + } +} + ++ (BOOL)isValidPositiveNumber:(int)value { + if (value >= 0) { + return true; + } else { + return false; + } +} + +@end diff --git a/react-native/ios/FFmpegKitReactNativeModule.xcodeproj/project.pbxproj b/react-native/ios/FFmpegKitReactNativeModule.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3d92181f --- /dev/null +++ b/react-native/ios/FFmpegKitReactNativeModule.xcodeproj/project.pbxproj @@ -0,0 +1,293 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + + 5E555C0D2413F4C50049A1A2 /* FFmpegKitReactNativeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* FFmpegKitReactNativeModule.m */; }; + F4FF95D7245B92E800C19C63 /* FFmpegKitReactNativeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D6245B92E800C19C63 /* FFmpegKitReactNativeModule.swift */; }; + +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libFFmpegKitReactNativeModule.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libFFmpegKitReactNativeModule.a; sourceTree = BUILT_PRODUCTS_DIR; }; + + B3E7B5891CC2AC0600A0062D /* FFmpegKitReactNativeModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFmpegKitReactNativeModule.m; sourceTree = ""; }; + F4FF95D5245B92E700C19C63 /* FFmpegKitReactNativeModule-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FFmpegKitReactNativeModule-Bridging-Header.h"; sourceTree = ""; }; + F4FF95D6245B92E800C19C63 /* FFmpegKitReactNativeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FFmpegKitReactNativeModule.swift; sourceTree = ""; }; + +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libFFmpegKitReactNativeModule.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + + F4FF95D6245B92E800C19C63 /* FFmpegKitReactNativeModule.swift */, + B3E7B5891CC2AC0600A0062D /* FFmpegKitReactNativeModule.m */, + F4FF95D5245B92E700C19C63 /* FFmpegKitReactNativeModule-Bridging-Header.h */, + + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* FFmpegKitReactNativeModule */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "FFmpegKitReactNativeModule" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FFmpegKitReactNativeModule; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libFFmpegKitReactNativeModule.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "FFmpegKitReactNativeModule" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* FFmpegKitReactNativeModule */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + + F4FF95D7245B92E800C19C63 /* FFmpegKitReactNativeModule.swift in Sources */, + B3E7B58A1CC2AC0600A0062D /* FFmpegKitReactNativeModule.m in Sources */, + + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = FFmpegKitReactNativeModule; + SKIP_INSTALL = YES; + + SWIFT_OBJC_BRIDGING_HEADER = "FFmpegKitReactNativeModule-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = FFmpegKitReactNativeModule; + SKIP_INSTALL = YES; + + SWIFT_OBJC_BRIDGING_HEADER = "FFmpegKitReactNativeModule-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "FFmpegKitReactNativeModule" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "FFmpegKitReactNativeModule" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/react-native/package.json b/react-native/package.json new file mode 100644 index 00000000..0d463201 --- /dev/null +++ b/react-native/package.json @@ -0,0 +1,118 @@ +{ + "name": "ffmpeg-kit-react-native", + "version": "4.5.0", + "description": "FFmpeg Kit for React Native", + "main": "src/index", + "types": "src/index.d.ts", + "react-native": "src/index", + "source": "src/index", + "files": [ + "android", + "ffmpeg-kit-react-native.podspec", + "ios", + "src", + "!android/build", + "!ios/build", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__" + ], + "scripts": { + "test": "jest", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "release": "release-it" + }, + "keywords": [ + "react-native", + "android", + "ffmpeg", + "ffmpeg-kit", + "ios" + ], + "repository": "https://github.com/tanersener/ffmpeg-kit", + "author": "Taner Sener (https://github.com/tanersener)", + "license": "LGPL-3.0", + "bugs": { + "url": "https://github.com/tanersener/ffmpeg-kit/issues" + }, + "homepage": "https://github.com/tanersener/ffmpeg-kit", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^11.0.0", + "@react-native-community/eslint-config": "^2.0.0", + "@release-it/conventional-changelog": "^2.0.0", + "@types/jest": "^26.0.0", + "@types/react": "^16.9.19", + "@types/react-native": "^0.62.13", + "commitlint": "^11.0.0", + "eslint": "^7.2.0", + "eslint-config-prettier": "^7.0.0", + "eslint-plugin-prettier": "^3.1.3", + "jest": "^26.0.1", + "pod-install": "^0.1.0", + "prettier": "^2.0.5", + "react": "^16.13.1", + "react-native": "^0.63.4", + "release-it": "^14.2.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "jest": { + "preset": "react-native" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": "angular" + } + } + }, + "eslintConfig": { + "root": true, + "extends": [ + "@react-native-community", + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } + ] + } + }, + "eslintIgnore": [ + "node_modules/" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } +} diff --git a/react-native/src/index.d.ts b/react-native/src/index.d.ts new file mode 100644 index 00000000..348362e5 --- /dev/null +++ b/react-native/src/index.d.ts @@ -0,0 +1,538 @@ +declare module 'ffmpeg-kit-react-native' { + + export abstract class AbstractSession implements Session { + + protected constructor(); + + static createFFmpegSession(argumentsArray: Array, logRedirectionStrategy?: LogRedirectionStrategy): Promise; + + static createFFmpegSessionFromMap(sessionMap: { [key: string]: any }): FFmpegSession; + + static createFFprobeSession(argumentsArray: Array, logRedirectionStrategy?: LogRedirectionStrategy): Promise; + + static createFFprobeSessionFromMap(sessionMap: { [key: string]: any }): FFprobeSession; + + static createMediaInformationSession(argumentsArray: Array): Promise; + + static createMediaInformationSessionFromMap(sessionMap: { [key: string]: any }): MediaInformationSession; + + getExecuteCallback(): ExecuteCallback; + + getLogCallback(): LogCallback; + + getSessionId(): number; + + getCreateTime(): Date; + + getStartTime(): Date; + + getEndTime(): Promise; + + getDuration(): Promise; + + getArguments(): Array; + + getCommand(): string; + + getAllLogs(waitTimeout ?: number): Promise>; + + getLogs(): Promise>; + + getAllLogsAsString(waitTimeout?: number): Promise; + + getLogsAsString(): Promise; + + getOutput(): Promise; + + getState(): Promise; + + getReturnCode(): Promise; + + getFailStackTrace(): Promise; + + getLogRedirectionStrategy(): LogRedirectionStrategy; + + thereAreAsynchronousMessagesInTransmit(): Promise; + + isFFmpeg(): boolean; + + isFFprobe(): boolean; + + cancel(): Promise; + + } + + export class ArchDetect { + + static getArch(): Promise; + + } + + export type ExecuteCallback = (session: Session) => void; + + export class FFmpegKit { + + static executeAsync(command: string, executeCallback?: ExecuteCallback, logCallback?: LogCallback, statisticsCallback?: StatisticsCallback): Promise; + + static executeWithArgumentsAsync(commandArguments: string[], executeCallback?: ExecuteCallback, logCallback?: LogCallback, statisticsCallback?: StatisticsCallback): Promise; + + static cancel(sessionId?: number): Promise; + + static listSessions(): Promise; + + } + + export class FFmpegKitConfig { + + static init(): Promise; + + static enableRedirection(): Promise; + + static disableRedirection(): Promise; + + static setFontconfigConfigurationPath(path: string): Promise; + + static setFontDirectory(path: string, mapping?: { [key: string]: string }): Promise; + + static setFontDirectoryList(fontDirectoryList: string[], mapping?: { [key: string]: string }): Promise; + + static registerNewFFmpegPipe(): Promise; + + static closeFFmpegPipe(ffmpegPipePath: string): Promise; + + static getFFmpegVersion(): Promise; + + static getVersion(): Promise; + + static isLTSBuild(): Promise; + + static getBuildDate(): Promise; + + static setEnvironmentVariable(name: string, value: string): Promise; + + static ignoreSignal(signal: Signal): Promise; + + static asyncFFmpegExecute(session: FFmpegSession): Promise; + + static asyncFFprobeExecute(session: FFprobeSession): Promise; + + static asyncGetMediaInformationExecute(session: MediaInformationSession, waitTimeout?: number): Promise; + + static enableLogCallback(logCallback: LogCallback): void; + + static enableStatisticsCallback(statisticsCallback: StatisticsCallback): void; + + static enableExecuteCallback(executeCallback: ExecuteCallback): void; + + static getLogLevel(): Level; + + static setLogLevel(level: Level): Promise; + + static getSessionHistorySize(): Promise; + + static setSessionHistorySize(sessionHistorySize: number): Promise; + + static getSession(sessionId: number): Promise; + + static getLastSession(): Promise; + + static getLastCompletedSession(): Promise; + + static getSessions(): Promise; + + static clearSessions(): Promise; + + static getSessionsByState(state): Promise; + + static getLogRedirectionStrategy(): LogRedirectionStrategy; + + static setLogRedirectionStrategy(logRedirectionStrategy: LogRedirectionStrategy); + + static messagesInTransmit(sessionId: number): Promise; + + static sessionStateToString(state): string; + + static parseArguments(command: string): string[]; + + static argumentsToString(commandArguments: string[]): string; + + static enableLogs(): Promise; + + static disableLogs(): Promise; + + static enableStatistics(): Promise; + + static disableStatistics(): Promise; + + static getPlatform(): Promise; + + static writeToPipe(inputPath: string, pipePath: string): Promise; + + static selectDocumentForRead(type?: string, extraTypes?: string[]): Promise; + + static selectDocumentForWrite(title?: string, type?: string, extraTypes?: string[]): Promise; + + static getSafParameterForRead(uriString): Promise; + + static getSafParameterForWrite(uriString): Promise; + + } + + export class FFmpegSession extends AbstractSession implements Session { + + constructor(); + + static create(argumentsArray: Array, executeCallback?: ExecuteCallback, logCallback?: LogCallback, statisticsCallback?: StatisticsCallback, logRedirectionStrategy?: LogRedirectionStrategy): Promise; + + static fromMap(sessionMap: { [key: string]: any }): FFmpegSession; + + getStatisticsCallback(): StatisticsCallback; + + getAllStatistics(waitTimeout?: number): Promise>; + + getStatistics(): Promise>; + + getLastReceivedStatistics(): Promise; + + isFFmpeg(): boolean; + + isFFprobe(): boolean; + + } + + export class FFprobeKit { + + static executeAsync(command: string, executeCallback?: ExecuteCallback, logCallback?: LogCallback): Promise; + + static executeWithArgumentsAsync(commandArguments: string[], executeCallback?: ExecuteCallback, logCallback?: LogCallback): Promise; + + static getMediaInformationAsync(path: string, executeCallback?: ExecuteCallback, logCallback?: LogCallback, waitTimeout?: number): Promise; + + static getMediaInformationFromCommandAsync(command: string, executeCallback?: ExecuteCallback, logCallback?: LogCallback, waitTimeout?: number): Promise; + + static getMediaInformationFromCommandArgumentsAsync(commandArguments: string[], executeCallback?: ExecuteCallback, logCallback?: LogCallback, waitTimeout?: number): Promise; + + static listSessions(): Promise; + + } + + export class FFprobeSession extends AbstractSession implements Session { + + constructor(); + + static create(argumentsArray: Array, executeCallback?: ExecuteCallback, logCallback?: LogCallback, logRedirectionStrategy?: LogRedirectionStrategy): Promise; + + static fromMap(sessionMap: { [key: string]: any }): FFprobeSession; + + isFFmpeg(): boolean; + + isFFprobe(): boolean; + + } + + export class Level { + static readonly AV_LOG_STDERR: number; + static readonly AV_LOG_QUIET: number; + static readonly AV_LOG_PANIC: number; + static readonly AV_LOG_FATAL: number; + static readonly AV_LOG_ERROR: number; + static readonly AV_LOG_WARNING: number; + static readonly AV_LOG_INFO: number; + static readonly AV_LOG_VERBOSE: number; + static readonly AV_LOG_DEBUG: number; + static readonly AV_LOG_TRACE: number; + + static levelToString(number: number): string; + } + + export class Log { + + constructor(sessionId: number, level: number, message: String); + + getSessionId(): number; + + getLevel(): number; + + getMessage(): String; + + } + + export type LogCallback = (log: Log) => void; + + export enum LogRedirectionStrategy { + ALWAYS_PRINT_LOGS = 0, + PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED = 1, + PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED = 2, + PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED = 3, + NEVER_PRINT_LOGS = 4 + } + + export class MediaInformation { + + static readonly KEY_MEDIA_PROPERTIES: string; + static readonly KEY_FILENAME: string; + static readonly KEY_FORMAT: string; + static readonly KEY_FORMAT_LONG: string; + static readonly KEY_START_TIME: string; + static readonly KEY_DURATION: string; + static readonly KEY_SIZE: string; + static readonly KEY_BIT_RATE: string; + static readonly KEY_TAGS: string; + + constructor(properties: Record); + + getFilename(): string; + + getFormat(): string; + + getLongFormat(): string; + + getDuration(): number; + + getStartTime(): string; + + getSize(): string; + + getBitrate(): string; + + getTags(): Record; + + getStreams(): Array; + + getStringProperty(key: string): string; + + getNumberProperty(key: string): number; + + getProperties(key: string): Record; + + getMediaProperties(): Record; + + getAllProperties(): Record; + + } + + export class MediaInformationJsonParser { + + static from(ffprobeJsonOutput: string): Promise; + + static fromWithError(ffprobeJsonOutput: string): Promise; + + } + + export class MediaInformationSession extends FFprobeSession { + + constructor(); + + static create(argumentsArray: Array, executeCallback?: ExecuteCallback, logCallback?: LogCallback): Promise; + + static fromMap(sessionMap: { [key: string]: any }): MediaInformationSession; + + getMediaInformation(): MediaInformation; + + setMediaInformation(mediaInformation: MediaInformation): void; + + } + + export class Packages { + + static getPackageName(): Promise; + + static getExternalLibraries(): Promise; + + } + + export class ReturnCode { + + static readonly SUCCESS: number; + + static readonly CANCEL: number; + + constructor(value: number); + + static isSuccess(returnCode: ReturnCode): boolean; + + static isCancel(returnCode: ReturnCode): boolean; + + getValue(): number; + + isValueSuccess(): boolean; + + isValueError(): boolean; + + isValueCancel(): boolean; + + } + + export interface Session { + + getExecuteCallback(): ExecuteCallback; + + getLogCallback(): LogCallback; + + getSessionId(): number; + + getCreateTime(): Date; + + getStartTime(): Date; + + getEndTime(): Promise; + + getDuration(): Promise; + + getArguments(): Array; + + getCommand(): String; + + getAllLogs(waitTimeout ?: number): Promise>; + + getLogs(): Promise>; + + getAllLogsAsString(waitTimeout?: number): Promise; + + getLogsAsString(): Promise; + + getOutput(): Promise; + + getState(): Promise; + + getReturnCode(): Promise; + + getFailStackTrace(): Promise; + + getLogRedirectionStrategy(): LogRedirectionStrategy; + + thereAreAsynchronousMessagesInTransmit(): Promise; + + isFFmpeg(): boolean; + + isFFprobe(): boolean; + + cancel(): Promise; + + } + + export enum SessionState { + CREATED = 0, + RUNNING = 1, + FAILED = 2, + COMPLETED = 3 + } + + export enum Signal { + SIGINT = 2, + SIGQUIT = 3, + SIGPIPE = 13, + SIGTERM = 15, + SIGXCPU = 24 + } + + export class Statistics { + + constructor(sessionId: number, videoFrameNumber: number, videoFps: number, videoQuality: number, size: number, time: number, bitrate: number, speed: number); + + getSessionId(): number; + + setSessionId(sessionId: number): void; + + getVideoFrameNumber(): number; + + setVideoFrameNumber(videoFrameNumber: number): void; + + getVideoFps(): number; + + setVideoFps(videoFps: number): void; + + getVideoQuality(): number; + + setVideoQuality(videoQuality: number): void; + + getSize(): number; + + setSize(size: number): void; + + getTime(): number; + + setTime(time: number): void; + + getBitrate(): number; + + setBitrate(bitrate: number): void; + + getSpeed(): number; + + setSpeed(speed: number): void; + + } + + export type StatisticsCallback = (statistics: Statistics) => void; + + export class StreamInformation { + + static readonly KEY_INDEX: string; + static readonly KEY_TYPE: string; + static readonly KEY_CODEC: string; + static readonly KEY_CODEC_LONG: string; + static readonly KEY_FORMAT: string; + static readonly KEY_WIDTH: string; + static readonly KEY_HEIGHT: string; + static readonly KEY_BIT_RATE: string; + static readonly KEY_SAMPLE_RATE: string; + static readonly KEY_SAMPLE_FORMAT: string; + static readonly KEY_CHANNEL_LAYOUT: string; + static readonly KEY_SAMPLE_ASPECT_RATIO: string; + static readonly KEY_DISPLAY_ASPECT_RATIO: string; + static readonly KEY_AVERAGE_FRAME_RATE: string; + static readonly KEY_REAL_FRAME_RATE: string; + static readonly KEY_TIME_BASE: string; + static readonly KEY_CODEC_TIME_BASE: string; + static readonly KEY_TAGS: string; + + constructor(properties: Record); + + getIndex(): number; + + getType(): string; + + getCodec(): string; + + getCodecLong(): string; + + getFormat(): string; + + getWidth(): number; + + getHeight(): number; + + getBitrate(): string; + + getSampleRate(): string; + + getSampleFormat(): string; + + getChannelLayout(): string; + + getSampleAspectRatio(): string; + + getDisplayAspectRatio(): string; + + getAverageFrameRate(): string; + + getRealFrameRate(): string; + + getTimeBase(): string; + + getCodecTimeBase(): string; + + getTags(): Record; + + getStringProperty(key): string; + + getNumberProperty(key): number; + + getProperties(key): Record; + + getAllProperties(): Record; + + } + +} diff --git a/react-native/src/index.js b/react-native/src/index.js new file mode 100644 index 00000000..467608a4 --- /dev/null +++ b/react-native/src/index.js @@ -0,0 +1,2750 @@ +import {NativeEventEmitter, NativeModules} from 'react-native'; + +const {FFmpegKitReactNativeModule} = NativeModules; + +const executeCallbackMap = new Map() +const logCallbackMap = new Map() +const statisticsCallbackMap = new Map() +const logRedirectionStrategyMap = new Map() + +const eventLogCallbackEvent = "FFmpegKitLogCallbackEvent"; +const eventStatisticsCallbackEvent = "FFmpegKitStatisticsCallbackEvent"; +const eventExecuteCallbackEvent = "FFmpegKitExecuteCallbackEvent"; + +export const LogRedirectionStrategy = { + ALWAYS_PRINT_LOGS: 0, + PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED: 1, + PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED: 2, + PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED: 3, + NEVER_PRINT_LOGS: 4 +} + +export const SessionState = { + CREATED: 0, + RUNNING: 1, + FAILED: 2, + COMPLETED: 3 +} + +export const Signal = { + SIGINT: 2, + SIGQUIT: 3, + SIGPIPE: 13, + SIGTERM: 15, + SIGXCPU: 24 +} + +class FFmpegKitReactNativeEventEmitter extends NativeEventEmitter { + constructor() { + super(FFmpegKitReactNativeModule); + } + + addListener(eventType, listener, context) { + let subscription = super.addListener(eventType, listener, context); + subscription.eventType = eventType; + let subscriptionRemove = subscription.remove; + subscription.remove = () => { + if (super.removeSubscription != null) { + super.removeSubscription(subscription); + } else if (subscriptionRemove != null) { + subscriptionRemove(); + } + }; + return subscription; + } + + removeSubscription(subscription) { + if (super.removeSubscription) { + super.removeSubscription(subscription); + } + } +} + +/** + *

Common interface for all FFmpegKit sessions. + */ +export class Session { + + /** + * Returns the session specific execute callback function. + * + * @return session specific execute callback function + */ + getExecuteCallback() { + } + + /** + * Returns the session specific log callback function. + * + * @return session specific log callback function + */ + getLogCallback() { + } + + /** + * Returns the session identifier. + * + * @return session identifier + */ + getSessionId() { + } + + /** + * Returns session create time. + * + * @return session create time + */ + getCreateTime() { + } + + /** + * Returns session start time. + * + * @return session start time + */ + getStartTime() { + } + + /** + * Returns session end time. + * + * @return session end time + */ + getEndTime() { + } + + /** + * Returns the time taken to execute this session. + * + * @return time taken to execute this session in milliseconds or zero (0) if the session is + * not over yet + */ + getDuration() { + } + + /** + * Returns command arguments as an array. + * + * @return command arguments as an array + */ + getArguments() { + } + + /** + * Returns command arguments as a concatenated string. + * + * @return command arguments as a concatenated string + */ + getCommand() { + } + + /** + * Returns all log entries generated for this session. If there are asynchronous + * messages that are not delivered yet, this method waits for them until the given timeout. + * + * @param waitTimeout wait timeout for asynchronous messages in milliseconds + * @return list of log entries generated for this session + */ + getAllLogs(waitTimeout) { + } + + /** + * Returns all log entries delivered for this session. Note that if there are asynchronous log + * messages that are not delivered yet, this method will not wait for them and will return + * immediately. + * + * @return list of log entries received for this session + */ + getLogs() { + } + + /** + * Returns all log entries generated for this session as a concatenated string. If there are + * asynchronous messages that are not delivered yet, this method waits for them until + * the given timeout. + * + * @param waitTimeout wait timeout for asynchronous messages in milliseconds + * @return all log entries generated for this session as a concatenated string + */ + getAllLogsAsString(waitTimeout) { + } + + /** + * Returns all log entries delivered for this session as a concatenated string. Note that if + * there are asynchronous log messages that are not delivered yet, this method will not wait + * for them and will return immediately. + * + * @return list of log entries received for this session + */ + getLogsAsString() { + } + + /** + * Returns the log output generated while running the session. + * + * @return log output generated + */ + getOutput() { + } + + /** + * Returns the state of the session. + * + * @return state of the session + */ + getState() { + } + + /** + * Returns the return code for this session. Note that return code is only set for sessions + * that end with COMPLETED state. If a session is not started, still running or failed then + * this method returns undefined. + * + * @return the return code for this session if the session is COMPLETED, undefined if session is + * not started, still running or failed + */ + getReturnCode() { + } + + /** + * Returns the stack trace of the exception received while executing this session. + *

+ * The stack trace is only set for sessions that end with FAILED state. For sessions that has + * COMPLETED state this method returns undefined. + * + * @return stack trace of the exception received while executing this session, undefined if session + * is not started, still running or completed + */ + getFailStackTrace() { + } + + /** + * Returns session specific log redirection strategy. + * + * @return session specific log redirection strategy + */ + getLogRedirectionStrategy() { + } + + /** + * Returns whether there are still asynchronous messages being transmitted for this + * session or not. + * + * @return true if there are still asynchronous messages being transmitted, false + * otherwise + */ + thereAreAsynchronousMessagesInTransmit() { + } + + /** + * Returns whether it is an FFmpeg session or not. + * + * @return true if it is an FFmpeg session, false otherwise + */ + isFFmpeg() { + } + + /** + * Returns whether it is an FFprobe session or not. + * + * @return true if it is an FFprobe session, false otherwise + */ + isFFprobe() { + } + + /** + * Cancels running the session. + */ + cancel() { + } + +} + +/** + * Abstract session implementation which includes common features shared by FFmpeg + * and FFprobe sessions. + */ +export class AbstractSession extends Session { + + /** + * Defines how long default "getAll" methods wait, in milliseconds. + */ + static DEFAULT_TIMEOUT_FOR_ASYNCHRONOUS_MESSAGES_IN_TRANSMIT = 5000; + + /** + * Session identifier. + */ + #sessionId; + + /** + * Date and time the session was created. + */ + #createTime; + + /** + * Date and time the session was started. + */ + #startTime; + + /** + * Command string. + */ + #command; + + /** + * Command arguments as an array. + */ + #argumentsArray; + + /** + * Session specific log redirection strategy. + */ + #logRedirectionStrategy; + + /** + * Creates a new abstract session. + */ + constructor() { + super(); + } + + /** + * Creates a new FFmpeg session. + * + * @param argumentsArray FFmpeg command arguments + * @param logRedirectionStrategy defines how logs will be redirected + * @returns FFmpeg session created + */ + static async createFFmpegSession(argumentsArray, logRedirectionStrategy) { + await FFmpegKitConfig.init(); + + if (logRedirectionStrategy === undefined) { + logRedirectionStrategy = FFmpegKitConfig.getLogRedirectionStrategy(); + } + + let nativeSession = await FFmpegKitReactNativeModule.ffmpegSession(argumentsArray); + let session = new FFmpegSession(); + + session.#sessionId = nativeSession.sessionId; + session.#createTime = FFmpegKitFactory.validDate(nativeSession.createTime); + session.#startTime = FFmpegKitFactory.validDate(nativeSession.startTime); + session.#command = nativeSession.command; + session.#argumentsArray = argumentsArray; + session.#logRedirectionStrategy = logRedirectionStrategy; + + FFmpegKitFactory.setLogRedirectionStrategy(session.#sessionId, logRedirectionStrategy); + + return session; + } + + /** + * Creates a new FFmpeg session from the given map. + * + * @param sessionMap map that includes session fields as map keys + * @returns FFmpeg session created + */ + static createFFmpegSessionFromMap(sessionMap) { + let session = new FFmpegSession(); + + session.#sessionId = sessionMap.sessionId; + session.#createTime = FFmpegKitFactory.validDate(sessionMap.createTime); + session.#startTime = FFmpegKitFactory.validDate(sessionMap.startTime); + session.#command = sessionMap.command; + session.#argumentsArray = FFmpegKitConfig.parseArguments(sessionMap.command); + session.#logRedirectionStrategy = FFmpegKitFactory.getLogRedirectionStrategy(session.#sessionId); + + return session; + } + + /** + * Creates a new FFprobe session. + * + * @param argumentsArray FFprobe command arguments + * @param logRedirectionStrategy defines how logs will be redirected + * @returns FFprobe session created + */ + static async createFFprobeSession(argumentsArray, logRedirectionStrategy) { + await FFmpegKitConfig.init(); + + if (logRedirectionStrategy === undefined) { + logRedirectionStrategy = FFmpegKitConfig.getLogRedirectionStrategy(); + } + + let nativeSession = await FFmpegKitReactNativeModule.ffprobeSession(argumentsArray); + let session = new FFprobeSession(); + + session.#sessionId = nativeSession.sessionId; + session.#createTime = FFmpegKitFactory.validDate(nativeSession.createTime); + session.#startTime = FFmpegKitFactory.validDate(nativeSession.startTime); + session.#command = nativeSession.command; + session.#argumentsArray = argumentsArray; + session.#logRedirectionStrategy = logRedirectionStrategy; + + FFmpegKitFactory.setLogRedirectionStrategy(session.#sessionId, logRedirectionStrategy); + + return session; + } + + /** + * Creates a new FFprobe session from the given map. + * + * @param sessionMap map that includes session fields as map keys + * @returns FFprobe session created + */ + static createFFprobeSessionFromMap(sessionMap) { + let session = new FFprobeSession(); + + session.#sessionId = sessionMap.sessionId; + session.#createTime = FFmpegKitFactory.validDate(sessionMap.createTime); + session.#startTime = FFmpegKitFactory.validDate(sessionMap.startTime); + session.#command = sessionMap.command; + session.#argumentsArray = FFmpegKitConfig.parseArguments(sessionMap.command); + session.#logRedirectionStrategy = FFmpegKitFactory.getLogRedirectionStrategy(session.#sessionId); + + return session; + } + + /** + * Creates a new MediaInformationSession session. + * + * @param argumentsArray FFprobe command arguments + * @returns MediaInformationSession session created + */ + static async createMediaInformationSession(argumentsArray) { + await FFmpegKitConfig.init(); + + let nativeSession = await FFmpegKitReactNativeModule.mediaInformationSession(argumentsArray); + let session = new MediaInformationSession(); + + session.#sessionId = nativeSession.sessionId; + session.#createTime = FFmpegKitFactory.validDate(nativeSession.createTime); + session.#startTime = FFmpegKitFactory.validDate(nativeSession.startTime); + session.#command = nativeSession.command; + session.#argumentsArray = argumentsArray; + session.#logRedirectionStrategy = LogRedirectionStrategy.NEVER_PRINT_LOGS; + + FFmpegKitFactory.setLogRedirectionStrategy(session.#sessionId, LogRedirectionStrategy.NEVER_PRINT_LOGS); + + return session; + } + + /** + * Creates a new MediaInformationSession from the given map. + * + * @param sessionMap map that includes session fields as map keys + * @returns MediaInformationSession created + */ + static createMediaInformationSessionFromMap(sessionMap) { + let session = new MediaInformationSession(); + + session.#sessionId = sessionMap.sessionId; + session.#createTime = FFmpegKitFactory.validDate(sessionMap.createTime); + session.#startTime = FFmpegKitFactory.validDate(sessionMap.startTime); + session.#command = sessionMap.command; + session.#argumentsArray = FFmpegKitConfig.parseArguments(sessionMap.command); + session.#logRedirectionStrategy = LogRedirectionStrategy.NEVER_PRINT_LOGS; + + if (sessionMap.mediaInformation !== undefined && sessionMap.mediaInformation !== null) { + session.setMediaInformation(new MediaInformation(sessionMap.mediaInformation)); + } + + return session; + } + + /** + * Returns the session specific execute callback function. + * + * @return session specific execute callback function + */ + getExecuteCallback() { + return FFmpegKitFactory.getExecuteCallback(this.getSessionId()) + } + + /** + * Returns the session specific log callback function. + * + * @return session specific log callback function + */ + getLogCallback() { + return FFmpegKitFactory.getLogCallback(this.getSessionId()) + } + + /** + * Returns the session identifier. + * + * @return session identifier + */ + getSessionId() { + return this.#sessionId; + } + + /** + * Returns session create time. + * + * @return session create time + */ + getCreateTime() { + return this.#createTime; + } + + /** + * Returns session start time. + * + * @return session start time + */ + getStartTime() { + return this.#startTime; + } + + /** + * Returns session end time. + * + * @return session end time + */ + async getEndTime() { + const endTime = FFmpegKitReactNativeModule.abstractSessionGetEndTime(this.getSessionId()); + return FFmpegKitFactory.validDate(endTime); + } + + /** + * Returns the time taken to execute this session. + * + * @return time taken to execute this session in milliseconds or zero (0) if the session is + * not over yet + */ + getDuration() { + return FFmpegKitReactNativeModule.abstractSessionGetDuration(this.getSessionId()); + } + + /** + * Returns command arguments as an array. + * + * @return command arguments as an array + */ + getArguments() { + return this.#argumentsArray; + } + + /** + * Returns command arguments as a concatenated string. + * + * @return command arguments as a concatenated string + */ + getCommand() { + return this.#command; + } + + /** + * Returns all log entries generated for this session. If there are asynchronous + * messages that are not delivered yet, this method waits for them until the given timeout. + * + * @param waitTimeout wait timeout for asynchronous messages in milliseconds + * @return list of log entries generated for this session + */ + async getAllLogs(waitTimeout) { + const allLogs = await FFmpegKitReactNativeModule.abstractSessionGetAllLogs(this.getSessionId(), FFmpegKitFactory.optionalNumericParameter(waitTimeout)); + return allLogs.map(FFmpegKitFactory.mapToLog); + } + + /** + * Returns all log entries delivered for this session. Note that if there are asynchronous log + * messages that are not delivered yet, this method will not wait for them and will return + * immediately. + * + * @return list of log entries received for this session + */ + async getLogs() { + const logs = await FFmpegKitReactNativeModule.abstractSessionGetLogs(this.getSessionId()); + return logs.map(FFmpegKitFactory.mapToLog); + } + + /** + * Returns all log entries generated for this session as a concatenated string. If there are + * asynchronous messages that are not delivered yet, this method waits for them until + * the given timeout. + * + * @param waitTimeout wait timeout for asynchronous messages in milliseconds + * @return all log entries generated for this session as a concatenated string + */ + async getAllLogsAsString(waitTimeout) { + return FFmpegKitReactNativeModule.abstractSessionGetAllLogsAsString(this.getSessionId(), FFmpegKitFactory.optionalNumericParameter(waitTimeout)); + } + + /** + * Returns all log entries delivered for this session as a concatenated string. Note that if + * there are asynchronous log messages that are not delivered yet, this method will not wait + * for them and will return immediately. + * + * @return list of log entries received for this session + */ + async getLogsAsString() { + let logs = await this.getLogs(); + + let concatenatedString = ''; + + logs.forEach(log => concatenatedString += log.getMessage()); + + return concatenatedString; + } + + /** + * Returns the log output generated while running the session. + * + * @return log output generated + */ + async getOutput() { + return this.getAllLogsAsString(); + } + + /** + * Returns the state of the session. + * + * @return state of the session + */ + async getState() { + return FFmpegKitReactNativeModule.abstractSessionGetState(this.getSessionId()); + } + + /** + * Returns the return code for this session. Note that return code is only set for sessions + * that end with COMPLETED state. If a session is not started, still running or failed then + * this method returns undefined. + * + * @return the return code for this session if the session is COMPLETED, undefined if session is + * not started, still running or failed + */ + async getReturnCode() { + const returnCodeValue = await FFmpegKitReactNativeModule.abstractSessionGetReturnCode(this.getSessionId()); + if (returnCodeValue === undefined) { + return undefined; + } else { + return new ReturnCode(returnCodeValue); + } + } + + /** + * Returns the stack trace of the exception received while executing this session. + *

+ * The stack trace is only set for sessions that end with FAILED state. For sessions that has + * COMPLETED state this method returns undefined. + * + * @return stack trace of the exception received while executing this session, undefined if session + * is not started, still running or completed + */ + getFailStackTrace() { + return FFmpegKitReactNativeModule.abstractSessionGetFailStackTrace(this.getSessionId()); + } + + /** + * Returns session specific log redirection strategy. + * + * @return session specific log redirection strategy + */ + getLogRedirectionStrategy() { + return this.#logRedirectionStrategy; + } + + /** + * Returns whether there are still asynchronous messages being transmitted for this + * session or not. + * + * @return true if there are still asynchronous messages being transmitted, false + * otherwise + */ + thereAreAsynchronousMessagesInTransmit() { + return FFmpegKitReactNativeModule.abstractSessionThereAreAsynchronousMessagesInTransmit(this.getSessionId()); + } + + /** + * Returns whether it is an FFmpeg session or not. + * + * @return true if it is an FFmpeg session, false otherwise + */ + isFFmpeg() { + return false; + } + + /** + * Returns whether it is an FFprobe session or not. + * + * @return true if it is an FFprobe session, false otherwise + */ + isFFprobe() { + return false; + } + + /** + * Cancels running the session. + */ + cancel() { + if (sessionId === undefined) { + return FFmpegKitReactNativeModule.cancel(); + } else { + return FFmpegKitReactNativeModule.cancelSession(sessionId); + } + } + +} + +/** + * Detects the running architecture. + */ +export class ArchDetect { + + /** + * Returns architecture name loaded. + * + * @return architecture name loaded + */ + static async getArch() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getArch(); + } + +} + +/** + *

Main class to run FFmpeg commands. + */ +export class FFmpegKit { + + /** + *

Asynchronously executes FFmpeg with command provided. + * + * @param command FFmpeg command + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @param statisticsCallback callback that will receive statistics + * @return FFmpeg session created for this execution + */ + static async executeAsync(command, executeCallback, logCallback, statisticsCallback) { + return FFmpegKit.executeWithArgumentsAsync(FFmpegKitConfig.parseArguments(command), executeCallback, logCallback, statisticsCallback); + } + + /** + *

Asynchronously executes FFmpeg with arguments provided. + * + * @param commandArguments FFmpeg command options/arguments as string array + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @param statisticsCallback callback that will receive statistics + * @return FFmpeg session created for this execution + */ + static async executeWithArgumentsAsync(commandArguments, executeCallback, logCallback, statisticsCallback) { + let session = await FFmpegSession.create(commandArguments, executeCallback, logCallback, statisticsCallback); + + await FFmpegKitConfig.asyncFFmpegExecute(session); + + return session; + } + + /** + *

Cancels the session specified with sessionId. + * + *

This function does not wait for termination to complete and returns immediately. + * + * @param sessionId id of the session that will be cancelled + */ + static async cancel(sessionId) { + await FFmpegKitConfig.init(); + + if (sessionId === undefined) { + return FFmpegKitReactNativeModule.cancel(); + } else { + return FFmpegKitReactNativeModule.cancelSession(sessionId); + } + } + + /** + *

Lists all FFmpeg sessions in the session history. + * + * @return all FFmpeg sessions in the session history + */ + static async listSessions() { + await FFmpegKitConfig.init(); + + const sessionArray = await FFmpegKitReactNativeModule.getFFmpegSessions(); + return sessionArray.map(FFmpegKitFactory.mapToSession); + } + +} + +/** + *

Configuration class of FFmpegKit library. + */ +export class FFmpegKitConfig { + + static #globalLogRedirectionStrategy = LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED; + + /** + * Initializes the library asynchronously. + */ + static async init() { + await FFmpegKitInitializer.initialize(); + } + + /** + *

Enables log and statistics redirection. + * + *

When redirection is enabled FFmpeg/FFprobe logs are redirected to console and sessions + * collect log and statistics entries for the executions. It is possible to define global or + * session specific log/statistics callbacks as well. + * + *

Note that redirection is enabled by default. If you do not want to use its functionality + * please use {@link #disableRedirection()} to disable it. + */ + static async enableRedirection() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.enableRedirection(); + } + + /** + *

Disables log and statistics redirection. + * + *

When redirection is disabled logs are printed to stderr, all logs and statistics + * callbacks are disabled and FFprobe's getMediaInformation methods + * do not work. + */ + static async disableRedirection() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.disableRedirection(); + } + + /** + *

Sets and overrides fontconfig configuration directory. + * + * @param path directory that contains fontconfig configuration (fonts.conf) + * @return zero on success, non-zero on error + */ + static async setFontconfigConfigurationPath(path) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.setFontconfigConfigurationPath(path); + } + + /** + *

Registers the fonts inside the given path, so they become available to use in FFmpeg + * filters. + * + *

Note that you need to use a package with fontconfig inside to be + * able to use fonts in FFmpeg. + * + * @param fontDirectoryPath directory that contains fonts (.ttf and .otf files) + * @param fontNameMapping custom font name mappings, useful to access your fonts with more + * friendly names + */ + static async setFontDirectory(fontDirectoryPath, fontNameMapping) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.setFontDirectory(fontDirectoryPath, fontNameMapping); + } + + /** + *

Registers the fonts inside the given list of font directories, so they become available + * to use in FFmpeg filters. + * + *

Note that you need to use a package with fontconfig inside to be + * able to use fonts in FFmpeg. + * + * @param fontDirectoryList list of directories that contain fonts (.ttf and .otf files) + * @param fontNameMapping custom font name mappings, useful to access your fonts with more + * friendly names + */ + static async setFontDirectoryList(fontDirectoryList, fontNameMapping) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.setFontDirectoryList(fontDirectoryList, fontNameMapping); + } + + /** + *

Creates a new named pipe to use in FFmpeg operations. + * + *

Please note that creator is responsible of closing created pipes. + * + * @return the full path of the named pipe + */ + static async registerNewFFmpegPipe() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.registerNewFFmpegPipe(); + } + + /** + *

Closes a previously created FFmpeg pipe. + * + * @param ffmpegPipePath full path of the FFmpeg pipe + */ + static async closeFFmpegPipe(ffmpegPipePath) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.closeFFmpegPipe(ffmpegPipePath); + } + + /** + *

Returns the version of FFmpeg bundled within FFmpegKit library. + * + * @return the version of FFmpeg + */ + static async getFFmpegVersion() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getFFmpegVersion(); + } + + /** + *

Returns FFmpegKit ReactNative library version. + * + * @return FFmpegKit version + */ + static async getVersion() { + return new Promise((resolve) => resolve(FFmpegKitFactory.getVersion())); + } + + /** + *

Returns whether FFmpegKit release is a Long Term Release or not. + * + * @return true/yes or false/no + */ + static async isLTSBuild() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.isLTSBuild(); + } + + /** + *

Returns FFmpegKit native library build date. + * + * @return FFmpegKit native library build date + */ + static async getBuildDate() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getBuildDate(); + } + + /** + *

Sets an environment variable. + * + * @param variableName environment variable name + * @param variableValue environment variable value + * @return zero on success, non-zero on error + */ + static async setEnvironmentVariable(variableName, variableValue) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.setEnvironmentVariable(variableName, variableValue); + } + + /** + *

Registers a new ignored signal. Ignored signals are not handled by FFmpegKit + * library. + * + * @param signal signal to be ignored + */ + static async ignoreSignal(signal) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.ignoreSignal(signal); + } + + /** + *

Asynchronously executes the FFmpeg session provided. + * + * @param ffmpegSession FFmpeg session which includes command options/arguments + */ + static async asyncFFmpegExecute(ffmpegSession) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.asyncFFmpegSessionExecute(ffmpegSession.getSessionId()); + } + + /** + *

Asynchronously executes the FFprobe session provided. + * + * @param ffprobeSession FFprobe session which includes command options/arguments + */ + static async asyncFFprobeExecute(ffprobeSession) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.asyncFFprobeSessionExecute(ffprobeSession.getSessionId()); + } + + /** + *

Asynchronously executes the media information session provided. + * + * @param mediaInformationSession media information session which includes command options/arguments + * @param waitTimeout max time to wait until media information is transmitted + */ + static async asyncGetMediaInformationExecute(mediaInformationSession, waitTimeout) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.asyncMediaInformationSessionExecute(mediaInformationSession.getSessionId(), FFmpegKitFactory.optionalNumericParameter(waitTimeout)); + } + + /** + *

Sets a global callback function to redirect FFmpeg/FFprobe logs. + * + * @param logCallback log callback function or undefined to disable a previously defined + * callback + */ + static enableLogCallback(logCallback) { + FFmpegKitFactory.setGlobalLogCallback(logCallback); + } + + /** + *

Sets a global callback function to redirect FFmpeg statistics. + * + * @param statisticsCallback statistics callback function or undefined to disable a previously + * defined callback + */ + static enableStatisticsCallback(statisticsCallback) { + FFmpegKitFactory.setGlobalStatisticsCallback(statisticsCallback); + } + + /** + *

Sets a global callback function to receive execution results. + * + * @param executeCallback execute callback function or undefined to disable a previously + * defined callback + */ + static enableExecuteCallback(executeCallback) { + FFmpegKitFactory.setGlobalExecuteCallback(executeCallback); + } + + /** + * Returns the current log level. + * + * @return current log level + */ + static getLogLevel() { + return FFmpegKitFactory.getLogLevel(); + } + + /** + * Sets the log level. + * + * @param level new log level + */ + static async setLogLevel(level) { + await FFmpegKitConfig.init(); + + FFmpegKitFactory.setLogLevel(level); + return FFmpegKitReactNativeModule.setLogLevel(level); + } + + /** + * Returns the session history size. + * + * @return session history size + */ + static async getSessionHistorySize() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getSessionHistorySize(); + } + + /** + * Sets the session history size. + * + * @param sessionHistorySize session history size, should be smaller than 1000 + */ + static async setSessionHistorySize(sessionHistorySize) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.setSessionHistorySize(sessionHistorySize); + } + + /** + * Returns the session specified with sessionId from the session history. + * + * @param sessionId session identifier + * @return session specified with sessionId or undefined if it is not found in the history + */ + static async getSession(sessionId) { + await FFmpegKitConfig.init(); + + if (sessionId === undefined) { + return undefined; + } else { + const sessionMap = await FFmpegKitReactNativeModule.getSession(sessionId); + return FFmpegKitFactory.mapToSession(sessionMap); + } + } + + /** + * Returns the last session created from the session history. + * + * @return the last session created or undefined if session history is empty + */ + static async getLastSession() { + await FFmpegKitConfig.init(); + + const sessionMap = await FFmpegKitReactNativeModule.getLastSession(); + return FFmpegKitFactory.mapToSession(sessionMap); + } + + /** + * Returns the last session completed from the session history. + * + * @return the last session completed. If there are no completed sessions in the history this + * method will return undefined + */ + static async getLastCompletedSession() { + await FFmpegKitConfig.init(); + + const sessionMap = await FFmpegKitReactNativeModule.getLastCompletedSession(); + return FFmpegKitFactory.mapToSession(sessionMap); + } + + /** + *

Returns all sessions in the session history. + * + * @return all sessions in the session history + */ + static async getSessions() { + await FFmpegKitConfig.init(); + + const sessionArray = await FFmpegKitReactNativeModule.getSessions(); + return sessionArray.map(FFmpegKitFactory.mapToSession); + } + + /** + *

Clears all, including ongoing, sessions in the session history. + *

Note that callbacks cannot be triggered for deleted sessions. + */ + static async clearSessions() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.clearSessions(); + } + + /** + *

Returns sessions that have the given state. + * + * @param state session state + * @return sessions that have the given state from the session history + */ + static async getSessionsByState(state) { + await FFmpegKitConfig.init(); + + const sessionArray = await FFmpegKitReactNativeModule.getSessionsByState(state); + return sessionArray.map(FFmpegKitFactory.mapToSession); + } + + /** + * Returns the active log redirection strategy. + * + * @return log redirection strategy + */ + static getLogRedirectionStrategy() { + return this.#globalLogRedirectionStrategy; + } + + /** + *

Sets the log redirection strategy. + * + * @param logRedirectionStrategy log redirection strategy + */ + static setLogRedirectionStrategy(logRedirectionStrategy) { + this.#globalLogRedirectionStrategy = logRedirectionStrategy; + } + + /** + *

Returns the number of messages that are not transmitted to the ReactNative callbacks yet for + * this session. + * + * @param sessionId id of the session + * @return number of messages that are not transmitted to the ReactNative callbacks yet for + * this session + */ + static async messagesInTransmit(sessionId) { + await FFmpegKitConfig.init(); + + const sessionMap = await FFmpegKitReactNativeModule.messagesInTransmit(sessionId); + return FFmpegKitFactory.mapToSession(sessionMap); + } + + /** + * Returns the string representation of the SessionState provided. + * + * @param state session state instance + * @returns string representation of the SessionState provided + */ + static sessionStateToString(state) { + switch (state) { + case SessionState.CREATED: + return "CREATED"; + case SessionState.RUNNING: + return "RUNNING"; + case SessionState.FAILED: + return "FAILED"; + case SessionState.COMPLETED: + return "COMPLETED"; + default: + return ""; + } + } + + /** + *

Parses the given command into arguments. Uses space character to split the arguments. + * Supports single and double quote characters. + * + * @param command string command + * @return array of arguments + */ + static parseArguments(command) { + let argumentList = []; + let currentArgument = ""; + + let singleQuoteStarted = 0; + let doubleQuoteStarted = 0; + + for (let i = 0; i < command.length; i++) { + let previousChar; + if (i > 0) { + previousChar = command.charAt(i - 1); + } else { + previousChar = null; + } + let currentChar = command.charAt(i); + + if (currentChar === ' ') { + if (singleQuoteStarted === 1 || doubleQuoteStarted === 1) { + currentArgument += currentChar; + } else if (currentArgument.length > 0) { + argumentList.push(currentArgument); + currentArgument = ""; + } + } else if (currentChar === '\'' && (previousChar == null || previousChar !== '\\')) { + if (singleQuoteStarted === 1) { + singleQuoteStarted = 0; + } else if (doubleQuoteStarted === 1) { + currentArgument += currentChar; + } else { + singleQuoteStarted = 1; + } + } else if (currentChar === '\"' && (previousChar == null || previousChar !== '\\')) { + if (doubleQuoteStarted === 1) { + doubleQuoteStarted = 0; + } else if (singleQuoteStarted === 1) { + currentArgument += currentChar; + } else { + doubleQuoteStarted = 1; + } + } else { + currentArgument += currentChar; + } + } + + if (currentArgument.length > 0) { + argumentList.push(currentArgument); + } + + return argumentList; + } + + /** + *

Concatenates arguments into a string adding a space character between two arguments. + * + * @param commandArguments arguments + * @return concatenated string containing all arguments + */ + static argumentsToString(commandArguments) { + if (commandArguments === undefined) { + return 'undefined'; + } + + let command = ''; + + function appendArgument(value, index) { + if (index > 0) { + command += ' '; + } + command += value; + } + + commandArguments.forEach(appendArgument); + return command; + } + + // THE FOLLOWING TWO METHODS ARE REACT-NATIVE SPECIFIC + + /** + * Enables logs. + */ + static async enableLogs() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.enableLogs(); + } + + /** + * Disable logs. + */ + static async disableLogs() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.disableLogs(); + } + + /** + * Enables statistics. + */ + static async enableStatistics() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.enableStatistics(); + } + + /** + * Disables statistics. + */ + static async disableStatistics() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.disableStatistics(); + } + + /** + * Returns the platform name the library is loaded on. + */ + static async getPlatform() { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getPlatform(); + } + + /** + * Writes the given file to a pipe. + * + * @param inputPath input file path + * @param pipePath pipe path + * @returns zero on success, non-zero on error + */ + static async writeToPipe(inputPath, pipePath) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.writeToPipe(inputPath, pipePath); + } + + /** + *

Displays the native file dialog to select a file in read mode. If a file is selected then this + * method returns the Structured Access Framework Uri for that file. + * + *

Note that this method is Android only. It will fail if called on other platforms. + * + * @param type specifies a mime type for the file dialog + * @param extraTypes additional mime types + * @returns Structured Access Framework Uri ("content:…") of the file selected or undefined + * if no files are selected + */ + static async selectDocumentForRead(type, extraTypes) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.selectDocument(false, undefined, type, extraTypes); + } + + /** + *

Displays the native file dialog to select a file in write mode. If a file is selected then this + * method returns the Structured Access Framework Uri for that file. + * + *

Note that this method is Android only. It will fail if called on other platforms. + * + * @param title file name + * @param type specifies a mime type for the file dialog + * @param extraTypes additional mime types + * @returns Structured Access Framework Uri ("content:…") of the file selected or undefined + * if no files are selected + */ + static async selectDocumentForWrite(title, type, extraTypes) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.selectDocument(true, title, type, extraTypes); + } + + /** + *

Converts the given Structured Access Framework Uri into an input url that can be used in FFmpeg + * and FFprobe commands. + * + *

Note that this method is Android only. It will fail if called on other platforms. It also requires + * API Level ≥ 19. On older API levels it returns an empty url. + * + * @param uriString SAF uri ("content:…") + * @return input url that can be passed to FFmpegKit or FFprobeKit + */ + static async getSafParameterForRead(uriString) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getSafParameter(false, uriString); + } + + /** + *

Converts the given Structured Access Framework Uri into an output url that can be used in FFmpeg + * and FFprobe commands. + * + *

Note that this method is Android only. It will fail if called on other platforms. It also requires + * API Level ≥ 19. On older API levels it returns an empty url. + * + * @param uriString SAF uri ("content:…") + * @return output url that can be passed to FFmpegKit or FFprobeKit + */ + static async getSafParameterForWrite(uriString) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.getSafParameter(true, uriString); + } + +} + +class FFmpegKitFactory { + + static #logCallback = undefined; + static #statisticsCallback = undefined; + static #executeCallback = undefined; + static #activeLogLevel = undefined; + + static mapToStatistics(statisticsMap) { + if (statisticsMap !== undefined) { + return new Statistics( + statisticsMap.sessionId, + statisticsMap.videoFrameNumber, + statisticsMap.videoFps, + statisticsMap.videoQuality, + statisticsMap.size, + statisticsMap.time, + statisticsMap.bitrate, + statisticsMap.speed + ); + } else { + return undefined; + } + } + + static mapToLog(logMap) { + if (logMap !== undefined) { + return new Log(logMap.sessionId, logMap.level, logMap.message) + } else { + return undefined; + } + } + + static mapToSession(sessionMap) { + if (sessionMap !== undefined) { + switch (sessionMap.type) { + case 2: + return FFprobeSession.fromMap(sessionMap); + case 3: + return MediaInformationSession.fromMap(sessionMap); + case 1: + default: + return FFmpegSession.fromMap(sessionMap); + } + } else { + return undefined; + } + } + + static getVersion() { + return "4.5"; + } + + static getLogRedirectionStrategy(sessionId) { + return logRedirectionStrategyMap.get(sessionId); + } + + static setLogRedirectionStrategy(sessionId, logRedirectionStrategy) { + logRedirectionStrategyMap.set(sessionId, logRedirectionStrategy); + } + + static getLogCallback(sessionId) { + return logCallbackMap.get(sessionId); + } + + static setLogCallback(sessionId, logCallback) { + if (logCallback !== undefined) { + logCallbackMap.set(sessionId, logCallback); + } + } + + static getGlobalLogCallback() { + return this.#logCallback; + } + + static setGlobalLogCallback(logCallback) { + this.#logCallback = logCallback; + } + + static getStatisticsCallback(sessionId) { + return statisticsCallbackMap.get(sessionId); + } + + static setStatisticsCallback(sessionId, statisticsCallback) { + if (statisticsCallback !== undefined) { + statisticsCallbackMap.set(sessionId, statisticsCallback); + } + } + + static getGlobalStatisticsCallback() { + return this.#statisticsCallback; + } + + static setGlobalStatisticsCallback(statisticsCallback) { + this.#statisticsCallback = statisticsCallback; + } + + static getExecuteCallback(sessionId) { + return executeCallbackMap.get(sessionId); + } + + static setExecuteCallback(sessionId, executeCallback) { + if (executeCallback !== undefined) { + executeCallbackMap.set(sessionId, executeCallback); + } + } + + static getGlobalExecuteCallback() { + return this.#executeCallback; + } + + static setGlobalExecuteCallback(executeCallback) { + this.#executeCallback = executeCallback; + } + + static setLogLevel(logLevel) { + this.#activeLogLevel = logLevel; + } + + static getLogLevel() { + return this.#activeLogLevel; + } + + static optionalNumericParameter(value) { + return value ?? -1; + } + + static validDate(time) { + if (time === undefined || time === null || time <= 0) { + return undefined; + } else { + return new Date(time); + } + } + +} + +class FFmpegKitInitializer { + static #initialized = false; + static #eventEmitter = new FFmpegKitReactNativeEventEmitter(); + + static processLogCallbackEvent(event) { + const log = FFmpegKitFactory.mapToLog(event) + const sessionId = event.sessionId; + const level = event.level; + const text = event.message; + const activeLogLevel = FFmpegKitConfig.getLogLevel(); + let globalCallbackDefined = false; + let sessionCallbackDefined = false; + let activeLogRedirectionStrategy = FFmpegKitConfig.getLogRedirectionStrategy(); + + // AV_LOG_STDERR logs are always redirected + if ((activeLogLevel === Level.AV_LOG_QUIET && level !== Level.AV_LOG_STDERR) || level > activeLogLevel) { + // LOG NEITHER PRINTED NOR FORWARDED + return; + } + + FFmpegKitConfig.getSession(sessionId).then(session => { + activeLogRedirectionStrategy = session.getLogRedirectionStrategy(); + + if (session.getLogCallback() !== undefined) { + sessionCallbackDefined = true; + + try { + // NOTIFY SESSION CALLBACK DEFINED + session.getLogCallback()(log); + } catch (err) { + console.log("Exception thrown inside session LogCallback block.", err.stack); + } + } + + let globalLogCallbackFunction = FFmpegKitFactory.getGlobalLogCallback(); + if (globalLogCallbackFunction !== undefined) { + globalCallbackDefined = true; + + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalLogCallbackFunction(log); + } catch (err) { + console.log("Exception thrown inside global LogCallback block.", err.stack); + } + } + + // EXECUTE THE LOG STRATEGY + switch (activeLogRedirectionStrategy) { + case LogRedirectionStrategy.NEVER_PRINT_LOGS: { + return; + } + case LogRedirectionStrategy.PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED: { + if (globalCallbackDefined) { + return; + } + } + break; + case LogRedirectionStrategy.PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED: { + if (sessionCallbackDefined) { + return; + } + } + break; + case LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED: { + if (globalCallbackDefined || sessionCallbackDefined) { + return; + } + } + break; + case LogRedirectionStrategy.ALWAYS_PRINT_LOGS: { + } + break; + } + + // PRINT LOGS + switch (level) { + case Level.AV_LOG_QUIET: { + // PRINT NO OUTPUT + } + break; + default: { + console.log(text); + } + } + }); + } + + static processStatisticsCallbackEvent(event) { + let statistics = FFmpegKitFactory.mapToStatistics(event); + let sessionId = event.sessionId; + + FFmpegKitConfig.getSession(sessionId).then(session => { + if (session.isFFmpeg()) { + if (session.getStatisticsCallback() !== undefined) { + try { + // NOTIFY SESSION CALLBACK DEFINED + session.getStatisticsCallback()(statistics); + } catch (err) { + console.log("Exception thrown inside session StatisticsCallback block.", err.stack); + } + } + } + + let globalStatisticsCallbackFunction = FFmpegKitFactory.getGlobalStatisticsCallback(); + if (globalStatisticsCallbackFunction !== undefined) { + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalStatisticsCallbackFunction(statistics); + } catch (err) { + console.log("Exception thrown inside global StatisticsCallback block.", err.stack); + } + } + }); + } + + static processExecuteCallbackEvent(event) { + let sessionId = event.sessionId; + + FFmpegKitConfig.getSession(sessionId).then(session => { + if (session.getExecuteCallback() !== undefined) { + try { + // NOTIFY SESSION CALLBACK DEFINED + session.getExecuteCallback()(session); + } catch (err) { + console.log("Exception thrown inside session ExecuteCallback block.", err.stack); + } + } + + let globalExecuteCallbackFunction = FFmpegKitFactory.getGlobalExecuteCallback(); + if (globalExecuteCallbackFunction !== undefined) { + try { + // NOTIFY GLOBAL CALLBACK DEFINED + globalExecuteCallbackFunction(session); + } catch (err) { + console.log("Exception thrown inside global ExecuteCallback block.", err.stack); + } + } + }); + } + + static async initialize() { + if (this.#initialized) { + return; + } else { + this.#initialized = true; + } + + console.log("Loading ffmpeg-kit-react-native."); + + this.#eventEmitter.addListener(eventLogCallbackEvent, FFmpegKitInitializer.processLogCallbackEvent); + this.#eventEmitter.addListener(eventStatisticsCallbackEvent, FFmpegKitInitializer.processStatisticsCallbackEvent); + this.#eventEmitter.addListener(eventExecuteCallbackEvent, FFmpegKitInitializer.processExecuteCallbackEvent); + + FFmpegKitFactory.setLogLevel(await FFmpegKitReactNativeModule.getLogLevel()); + const version = FFmpegKitFactory.getVersion(); + const platform = await FFmpegKitConfig.getPlatform(); + const arch = await ArchDetect.getArch(); + const packageName = await Packages.getPackageName(); + await FFmpegKitConfig.enableRedirection(); + + console.log(`Loaded ffmpeg-kit-react-native-${platform}-${packageName}-${arch}-${version}.`); + } + +} + +/** + *

An FFmpeg session. + */ +export class FFmpegSession extends AbstractSession { + + /** + * Creates an empty FFmpeg session. + */ + constructor() { + super(); + } + + /** + * Creates a new FFmpeg session. + * + * @param argumentsArray FFmpeg command arguments + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @param statisticsCallback callback that will receive statistics + * @param logRedirectionStrategy defines how logs will be redirected + * @returns FFmpeg session created + */ + static async create(argumentsArray, executeCallback, logCallback, statisticsCallback, logRedirectionStrategy) { + const session = await AbstractSession.createFFmpegSession(argumentsArray, logRedirectionStrategy); + const sessionId = session.getSessionId(); + + FFmpegKitFactory.setExecuteCallback(sessionId, executeCallback); + FFmpegKitFactory.setLogCallback(sessionId, logCallback); + FFmpegKitFactory.setStatisticsCallback(sessionId, statisticsCallback); + + return session; + } + + /** + * Creates a new FFmpeg session from the given map. + * + * @param sessionMap map that includes session fields as map keys + * @returns FFmpeg session created + */ + static fromMap(sessionMap) { + return AbstractSession.createFFmpegSessionFromMap(sessionMap); + } + + /** + * Returns the session specific statistics callback function. + * + * @return session specific statistics callback function + */ + getStatisticsCallback() { + return FFmpegKitFactory.getStatisticsCallback(this.getSessionId()); + } + + /** + * Returns all statistics entries generated for this session. If there are asynchronous + * messages that are not delivered yet, this method waits for them until the given timeout. + * + * @param waitTimeout wait timeout for asynchronous messages in milliseconds + * @return list of statistics entries generated for this session + */ + async getAllStatistics(waitTimeout) { + await FFmpegKitConfig.init(); + + const allStatistics = await FFmpegKitReactNativeModule.ffmpegSessionGetAllStatistics(this.getSessionId(), FFmpegKitFactory.optionalNumericParameter(waitTimeout)); + return allStatistics.map(FFmpegKitFactory.mapToStatistics); + } + + /** + * Returns all statistics entries delivered for this session. Note that if there are + * asynchronous messages that are not delivered yet, this method will not wait for + * them and will return immediately. + * + * @return list of statistics entries received for this session + */ + async getStatistics() { + await FFmpegKitConfig.init(); + + const statistics = await FFmpegKitReactNativeModule.ffmpegSessionGetStatistics(this.getSessionId()); + return statistics.map(FFmpegKitFactory.mapToStatistics); + } + + /** + * Returns the last received statistics entry. + * + * @return the last received statistics entry or undefined if there are not any statistics entries + * received + */ + async getLastReceivedStatistics() { + let statistics = await this.getStatistics(); + + if (statistics.length > 0) { + return statistics[0]; + } else { + return undefined; + } + } + + isFFmpeg() { + return true; + } + + isFFprobe() { + return false; + } + +} + +/** + *

Main class to run FFprobe commands. + */ +export class FFprobeKit { + + /** + *

Asynchronously executes FFprobe with arguments provided. + * + * @param command FFprobe command + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @return FFprobe session created for this execution + */ + static async executeAsync(command, executeCallback, logCallback) { + return FFprobeKit.executeWithArgumentsAsync(FFmpegKitConfig.parseArguments(command), executeCallback, logCallback); + } + + /** + *

Asynchronously executes FFprobe with arguments provided. + * + * @param commandArguments FFprobe command options/arguments as string array + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @return FFprobe session created for this execution + */ + static async executeWithArgumentsAsync(commandArguments, executeCallback, logCallback) { + let session = await FFprobeSession.create(commandArguments, executeCallback, logCallback); + + await FFmpegKitConfig.asyncFFprobeExecute(session); + + return session; + } + + /** + *

Extracts the media information for the specified file asynchronously. + * + * @param path path or uri of a media file + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive logs + * @param waitTimeout max time to wait until media information is transmitted + * @return media information session created for this execution + */ + static async getMediaInformationAsync(path, executeCallback, logCallback, waitTimeout) { + const commandArguments = ["-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path]; + return FFprobeKit.getMediaInformationFromCommandArgumentsAsync(commandArguments, executeCallback, logCallback, waitTimeout); + } + + /** + *

Extracts media information using the command provided asynchronously. + * + * @param command FFprobe command that prints media information for a file in JSON format + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive logs + * @param waitTimeout max time to wait until media information is transmitted + * @return media information session created for this execution + */ + static async getMediaInformationFromCommandAsync(command, executeCallback, logCallback, waitTimeout) { + return FFprobeKit.getMediaInformationFromCommandArgumentsAsync(FFmpegKitConfig.parseArguments(command), executeCallback, logCallback, waitTimeout); + } + + /** + *

Extracts media information using the command arguments provided asynchronously. + * + * @param commandArguments FFprobe command arguments that prints media information for a file in JSON format + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive logs + * @param waitTimeout max time to wait until media information is transmitted + * @return media information session created for this execution + */ + static async getMediaInformationFromCommandArgumentsAsync(commandArguments, executeCallback, logCallback, waitTimeout) { + let session = await MediaInformationSession.create(commandArguments, executeCallback, logCallback); + + await FFmpegKitConfig.asyncGetMediaInformationExecute(session, waitTimeout); + + return session; + } + + /** + *

Lists all FFprobe sessions in the session history. + * + * @return all FFprobe sessions in the session history + */ + static async listSessions() { + await FFmpegKitConfig.init(); + + const sessionArray = await FFmpegKitReactNativeModule.getFFprobeSessions(); + return sessionArray.map(FFmpegKitFactory.mapToSession); + } + +} + +/** + *

An FFprobe session. + */ +export class FFprobeSession extends AbstractSession { + + /** + * Creates an empty FFprobe session. + */ + constructor() { + super(); + } + + /** + * Creates a new FFprobe session. + * + * @param argumentsArray FFprobe command arguments + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @param logRedirectionStrategy defines how logs will be redirected + * @returns FFprobe session created + */ + static async create(argumentsArray, executeCallback, logCallback, logRedirectionStrategy) { + const session = await AbstractSession.createFFprobeSession(argumentsArray, logRedirectionStrategy); + const sessionId = session.getSessionId(); + + FFmpegKitFactory.setExecuteCallback(sessionId, executeCallback); + FFmpegKitFactory.setLogCallback(sessionId, logCallback); + + return session; + } + + /** + * Creates a new FFprobe session from the given map. + * + * @param sessionMap map that includes session fields as map keys + * @returns FFprobe session created + */ + static fromMap(sessionMap) { + return AbstractSession.createFFprobeSessionFromMap(sessionMap); + } + + isFFmpeg() { + return false; + } + + isFFprobe() { + return true; + } + +} + +/** + *

Defines log levels. + */ +export class Level { + + /** + * This log level is used to specify logs printed to stderr by ffmpeg. + * Logs that has this level are not filtered and always redirected. + */ + static AV_LOG_STDERR = -16; + + /** + * Print no output. + */ + static AV_LOG_QUIET = -8; + + /** + * Something went really wrong and we will crash now. + */ + static AV_LOG_PANIC = 0; + + /** + * Something went wrong and recovery is not possible. + * For example, no header was found for a format which depends + * on headers or an illegal combination of parameters is used. + */ + static AV_LOG_FATAL = 8; + + /** + * Something went wrong and cannot losslessly be recovered. + * However, not all future data is affected. + */ + static AV_LOG_ERROR = 16; + + /** + * Something somehow does not look correct. This may or may not + * lead to problems. An example would be the use of '-vstrict -2'. + */ + static AV_LOG_WARNING = 24; + + /** + * Standard information. + */ + static AV_LOG_INFO = 32; + + /** + * Detailed information. + */ + static AV_LOG_VERBOSE = 40; + + /** + * Stuff which is only useful for libav* developers. + */ + static AV_LOG_DEBUG = 48; + + /** + * Extremely verbose debugging, useful for libav* development. + */ + static AV_LOG_TRACE = 56; + + /** + * Returns log level string. + * + * @param level log level integer + * @returns log level string + */ + static levelToString(level) { + switch (level) { + case Level.AV_LOG_TRACE: + return "TRACE"; + case Level.AV_LOG_DEBUG: + return "DEBUG"; + case Level.AV_LOG_VERBOSE: + return "VERBOSE"; + case Level.AV_LOG_INFO: + return "INFO"; + case Level.AV_LOG_WARNING: + return "WARNING"; + case Level.AV_LOG_ERROR: + return "ERROR"; + case Level.AV_LOG_FATAL: + return "FATAL"; + case Level.AV_LOG_PANIC: + return "PANIC"; + case Level.AV_LOG_STDERR: + return "STDERR"; + case Level.AV_LOG_QUIET: + default: + return ""; + } + } + +} + +/** + *

Log entry for an FFmpegKit session. + */ +export class Log { + #sessionId; + #level; + #message; + + constructor(sessionId, level, message) { + this.#sessionId = sessionId; + this.#level = level; + this.#message = message; + } + + getSessionId() { + return this.#sessionId; + } + + getLevel() { + return this.#level; + } + + getMessage() { + return this.#message; + } + +} + +/** + * Media information class. + */ +export class MediaInformation { + + static KEY_MEDIA_PROPERTIES = "format"; + static KEY_FILENAME = "filename"; + static KEY_FORMAT = "format_name"; + static KEY_FORMAT_LONG = "format_long_name"; + static KEY_START_TIME = "start_time"; + static KEY_DURATION = "duration"; + static KEY_SIZE = "size"; + static KEY_BIT_RATE = "bit_rate"; + static KEY_TAGS = "tags"; + + #allProperties; + + constructor(properties) { + this.#allProperties = properties; + } + + /** + * Returns file name. + * + * @return media file name + */ + getFilename() { + return this.getStringProperty(MediaInformation.KEY_FILENAME); + } + + /** + * Returns format. + * + * @return media format + */ + getFormat() { + return this.getStringProperty(MediaInformation.KEY_FORMAT); + } + + /** + * Returns long format. + * + * @return media long format + */ + getLongFormat() { + return this.getStringProperty(MediaInformation.KEY_FORMAT_LONG); + } + + /** + * Returns duration. + * + * @return media duration in milliseconds + */ + getDuration() { + return this.getStringProperty(MediaInformation.KEY_DURATION); + } + + /** + * Returns start time. + * + * @return media start time in milliseconds + */ + getStartTime() { + return this.getStringProperty(MediaInformation.KEY_START_TIME); + } + + /** + * Returns size. + * + * @return media size in bytes + */ + getSize() { + return this.getStringProperty(MediaInformation.KEY_SIZE); + } + + /** + * Returns bitrate. + * + * @return media bitrate in kb/s + */ + getBitrate() { + return this.getStringProperty(MediaInformation.KEY_BIT_RATE); + } + + /** + * Returns all tags. + * + * @return tags dictionary + */ + getTags() { + return this.getProperties(MediaInformation.KEY_TAGS); + } + + /** + * Returns the streams found as array. + * + * @returns StreamInformation array + */ + getStreams() { + let list = []; + let streamList; + + if (this.#allProperties !== undefined) { + streamList = this.#allProperties.streams; + } + + if (streamList !== undefined) { + streamList.forEach((stream) => { + list.push(new StreamInformation(stream)); + }); + } + + return list; + } + + /** + * Returns the media property associated with the key. + * + * @param key property key + * @return media property as string or undefined if the key is not found + */ + getStringProperty(key) { + let mediaProperties = this.getMediaProperties(); + if (mediaProperties !== undefined) { + return mediaProperties[key]; + } else { + return undefined; + } + } + + /** + * Returns the media property associated with the key. + * + * @param key property key + * @return media property as number or undefined if the key is not found + */ + getNumberProperty(key) { + let mediaProperties = this.getMediaProperties(); + if (mediaProperties !== undefined) { + return mediaProperties[key]; + } else { + return undefined; + } + } + + /** + * Returns the media properties associated with the key. + * + * @param key properties key + * @return media properties as an object or undefined if the key is not found + */ + getProperties(key) { + let mediaProperties = this.getMediaProperties(); + if (mediaProperties !== undefined) { + return mediaProperties[key]; + } else { + return undefined; + } + } + + /** + * Returns all media properties. + * + * @returns an object where media properties can be accessed by property names + */ + getMediaProperties() { + if (this.#allProperties !== undefined) { + return this.#allProperties.format; + } else { + return undefined; + } + } + + /** + * Returns all properties found, including stream properties. + * + * @returns an object in which properties can be accessed by property names + */ + getAllProperties() { + return this.#allProperties; + } +} + +/** + * A parser that constructs {@link MediaInformation} from FFprobe's json output. + */ +export class MediaInformationJsonParser { + + /** + * Extracts MediaInformation from the given FFprobe json output. Note that this + * method does not fail as {@link #fromWithError(String)} does and returns undefined on error. + * + * @param ffprobeJsonOutput FFprobe json output + * @return created {@link MediaInformation} instance of undefined if a parsing error occurs + */ + static async from(ffprobeJsonOutput) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.mediaInformationJsonParserFrom(ffprobeJsonOutput).map(properties => new MediaInformation(properties)); + } + + /** + * Extracts MediaInformation from the given FFprobe json output. + * + * @param ffprobeJsonOutput ffprobe json output + * @return created {@link MediaInformation} instance + */ + static async fromWithError(ffprobeJsonOutput) { + await FFmpegKitConfig.init(); + + return FFmpegKitReactNativeModule.mediaInformationJsonParserFrom(ffprobeJsonOutput).map(properties => new MediaInformation(properties)); + } + +} + +/** + *

A custom FFprobe session, which produces a MediaInformation object using the + * FFprobe output. + */ +export class MediaInformationSession extends FFprobeSession { + #mediaInformation; + + /** + * Creates an empty MediaInformationSession. + */ + constructor() { + super(); + } + + /** + * Creates a new MediaInformationSession session. + * + * @param argumentsArray FFprobe command arguments + * @param executeCallback callback that will be called when the execution is completed + * @param logCallback callback that will receive logs + * @returns MediaInformationSession session created + */ + static async create(argumentsArray, executeCallback, logCallback) { + const session = await AbstractSession.createMediaInformationSession(argumentsArray); + const sessionId = session.getSessionId(); + + FFmpegKitFactory.setExecuteCallback(sessionId, executeCallback); + FFmpegKitFactory.setLogCallback(sessionId, logCallback); + + return session; + } + + /** + * Creates a new MediaInformationSession from the given map. + * + * @param sessionMap map that includes session fields as map keys + * @returns MediaInformationSession created + */ + static fromMap(sessionMap) { + return AbstractSession.createMediaInformationSessionFromMap(sessionMap); + } + + /** + * Returns the media information extracted in this session. + * + * @return media information extracted or undefined if the command failed or the output can not be + * parsed + */ + getMediaInformation() { + return this.#mediaInformation; + } + + /** + * Sets the media information extracted in this session. + * + * @param mediaInformation media information extracted + */ + setMediaInformation(mediaInformation) { + this.#mediaInformation = mediaInformation; + } + +} + +/** + *

Helper class to extract binary package information. + */ +export class Packages { + + /** + * Returns the FFmpegKit ReactNative binary package name. + * + * @return predicted FFmpegKit ReactNative binary package name + */ + static getPackageName() { + return FFmpegKitReactNativeModule.getPackageName(); + } + + /** + * Returns enabled external libraries by FFmpeg. + * + * @return enabled external libraries + */ + static getExternalLibraries() { + return FFmpegKitReactNativeModule.getExternalLibraries(); + } + +} + +export class ReturnCode { + + static SUCCESS = 0; + + static CANCEL = 255; + + #value; + + constructor(value) { + this.#value = value; + } + + static isSuccess(returnCode) { + return (returnCode !== undefined && returnCode.getValue() === ReturnCode.SUCCESS); + } + + static isCancel(returnCode) { + return (returnCode !== undefined && returnCode.getValue() === ReturnCode.CANCEL); + } + + getValue() { + return this.#value; + } + + isValueSuccess() { + return (this.#value === ReturnCode.SUCCESS); + } + + isValueError() { + return ((this.#value !== ReturnCode.SUCCESS) && (this.#value !== ReturnCode.CANCEL)); + } + + isValueCancel() { + return (this.#value === ReturnCode.CANCEL); + } + + toString() { + return this.#value; + } + +} + +/** + *

Statistics entry for an FFmpeg execute session. + */ +export class Statistics { + #sessionId; + #videoFrameNumber; + #videoFps; + #videoQuality; + #size; + #time; + #bitrate; + #speed; + + constructor(sessionId, videoFrameNumber, videoFps, videoQuality, size, time, bitrate, speed) { + this.#sessionId = sessionId; + this.#videoFrameNumber = videoFrameNumber; + this.#videoFps = videoFps; + this.#videoQuality = videoQuality; + this.#size = size; + this.#time = time; + this.#bitrate = bitrate; + this.#speed = speed; + } + + getSessionId() { + return this.#sessionId; + } + + setSessionId(sessionId) { + this.#sessionId = sessionId; + } + + getVideoFrameNumber() { + return this.#videoFrameNumber; + } + + setVideoFrameNumber(videoFrameNumber) { + this.#videoFrameNumber = videoFrameNumber; + } + + getVideoFps() { + return this.#videoFps; + } + + setVideoFps(videoFps) { + this.#videoFps = videoFps; + } + + getVideoQuality() { + return this.#videoQuality; + } + + setVideoQuality(videoQuality) { + this.#videoQuality = videoQuality; + } + + getSize() { + return this.#size; + } + + setSize(size) { + this.#size = size; + } + + getTime() { + return this.#time; + } + + setTime(time) { + this.#time = time; + } + + getBitrate() { + return this.#bitrate; + } + + setBitrate(bitrate) { + this.#bitrate = bitrate; + } + + getSpeed() { + return this.#speed; + } + + setSpeed(speed) { + this.#speed = speed; + } + +} + +/** + * Stream information class. + */ +export class StreamInformation { + + static KEY_INDEX = "index"; + static KEY_TYPE = "codec_type"; + static KEY_CODEC = "codec_name"; + static KEY_CODEC_LONG = "codec_long_name"; + static KEY_FORMAT = "pix_fmt"; + static KEY_WIDTH = "width"; + static KEY_HEIGHT = "height"; + static KEY_BIT_RATE = "bit_rate"; + static KEY_SAMPLE_RATE = "sample_rate"; + static KEY_SAMPLE_FORMAT = "sample_fmt"; + static KEY_CHANNEL_LAYOUT = "channel_layout"; + static KEY_SAMPLE_ASPECT_RATIO = "sample_aspect_ratio"; + static KEY_DISPLAY_ASPECT_RATIO = "display_aspect_ratio"; + static KEY_AVERAGE_FRAME_RATE = "avg_frame_rate"; + static KEY_REAL_FRAME_RATE = "r_frame_rate"; + static KEY_TIME_BASE = "time_base"; + static KEY_CODEC_TIME_BASE = "codec_time_base"; + static KEY_TAGS = "tags"; + + #allProperties; + + constructor(properties) { + this.#allProperties = properties; + } + + /** + * Returns stream index. + * + * @return stream index, starting from zero + */ + getIndex() { + return this.getNumberProperty(StreamInformation.KEY_INDEX); + } + + /** + * Returns stream type. + * + * @return stream type; audio or video + */ + getType() { + return this.getStringProperty(StreamInformation.KEY_TYPE); + } + + /** + * Returns stream codec. + * + * @return stream codec + */ + getCodec() { + return this.getStringProperty(StreamInformation.KEY_CODEC); + } + + /** + * Returns stream codec in long format. + * + * @return stream codec with additional profile and mode information + */ + getCodecLong() { + return this.getStringProperty(StreamInformation.KEY_CODEC_LONG); + } + + /** + * Returns stream format. + * + * @return stream format + */ + getFormat() { + return this.getStringProperty(StreamInformation.KEY_FORMAT); + } + + /** + * Returns width. + * + * @return width in pixels + */ + getWidth() { + return this.getNumberProperty(StreamInformation.KEY_WIDTH); + } + + /** + * Returns height. + * + * @return height in pixels + */ + getHeight() { + return this.getNumberProperty(StreamInformation.KEY_HEIGHT); + } + + /** + * Returns bitrate. + * + * @return bitrate in kb/s + */ + getBitrate() { + return this.getStringProperty(StreamInformation.KEY_BIT_RATE); + } + + /** + * Returns sample rate. + * + * @return sample rate in hz + */ + getSampleRate() { + return this.getStringProperty(StreamInformation.KEY_SAMPLE_RATE); + } + + /** + * Returns sample format. + * + * @return sample format + */ + getSampleFormat() { + return this.getStringProperty(StreamInformation.KEY_SAMPLE_FORMAT); + } + + /** + * Returns channel layout. + * + * @return channel layout + */ + getChannelLayout() { + return this.getStringProperty(StreamInformation.KEY_CHANNEL_LAYOUT); + } + + /** + * Returns sample aspect ratio. + * + * @return sample aspect ratio + */ + getSampleAspectRatio() { + return this.getStringProperty(StreamInformation.KEY_SAMPLE_ASPECT_RATIO); + } + + /** + * Returns display aspect ratio. + * + * @return display aspect ratio + */ + getDisplayAspectRatio() { + return this.getStringProperty(StreamInformation.KEY_DISPLAY_ASPECT_RATIO); + } + + /** + * Returns display aspect ratio. + * + * @return average frame rate in fps + */ + getAverageFrameRate() { + return this.getStringProperty(StreamInformation.KEY_AVERAGE_FRAME_RATE); + } + + /** + * Returns real frame rate. + * + * @return real frame rate in tbr + */ + getRealFrameRate() { + return this.getStringProperty(StreamInformation.KEY_REAL_FRAME_RATE); + } + + /** + * Returns time base. + * + * @return time base in tbn + */ + getTimeBase() { + return this.getStringProperty(StreamInformation.KEY_TIME_BASE); + } + + /** + * Returns codec time base. + * + * @return codec time base in tbc + */ + getCodecTimeBase() { + return this.getStringProperty(StreamInformation.KEY_CODEC_TIME_BASE); + } + + /** + * Returns all tags. + * + * @return tags object + */ + getTags() { + return this.getProperties(StreamInformation.KEY_TAGS); + } + + /** + * Returns the stream property associated with the key. + * + * @param key property key + * @return stream property as string or undefined if the key is not found + */ + getStringProperty(key) { + if (this.#allProperties !== undefined) { + return this.#allProperties[key]; + } else { + return undefined; + } + } + + /** + * Returns the stream property associated with the key. + * + * @param key property key + * @return stream property as number or undefined if the key is not found + */ + getNumberProperty(key) { + if (this.#allProperties !== undefined) { + return this.#allProperties[key]; + } else { + return undefined; + } + } + + /** + * Returns the stream properties associated with the key. + * + * @param key properties key + * @return stream properties as an object or undefined if the key is not found + */ + getProperties(key) { + if (this.#allProperties !== undefined) { + return this.#allProperties[key]; + } else { + return undefined; + } + } + + /** + * Returns all properties found. + * + * @returns an object in which properties can be accessed by property names + */ + getAllProperties() { + return this.#allProperties; + } +}