├── .watchmanconfig ├── .npmignore ├── example ├── ios │ ├── .xcode.env │ ├── File.swift │ ├── MediaLibraryExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── AppDelegate.mm │ │ ├── PrivacyInfo.xcprivacy │ │ ├── Info.plist │ │ └── LaunchScreen.storyboard │ ├── MediaLibraryExample-Bridging-Header.h │ ├── MediaLibraryExample.xcworkspace │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── contents.xcworkspacedata │ ├── Podfile │ └── MediaLibraryExample.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── MediaLibraryExample.xcscheme ├── .bundle │ └── config ├── assets │ └── tombstone.png ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── drawable │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── reactnativemedialibrary │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ ├── newarchitecture │ │ │ │ │ ├── components │ │ │ │ │ │ └── MainComponentsRegistry.java │ │ │ │ │ └── modules │ │ │ │ │ │ └── MainApplicationTurboModuleManagerDelegate.java │ │ │ │ │ └── MainApplication.java │ │ │ │ └── AndroidManifest.xml │ │ ├── proguard-rules.pro │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew.bat │ └── gradlew ├── index.js ├── react-native.config.js ├── babel.config.js ├── src │ ├── screens │ │ ├── index.ts │ │ └── modal │ │ │ ├── Base64Image.tsx │ │ │ ├── SloMo.tsx │ │ │ ├── CollectionsList.tsx │ │ │ ├── ExportVideo.tsx │ │ │ ├── CombineImages.tsx │ │ │ └── ImagesList.tsx │ ├── CropImageExample.tsx │ └── App.tsx ├── metro.config.js ├── patches │ └── react-native-fast-image+8.6.3.patch └── package.json ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── ios ├── MediaLibrary.h ├── MediaLibrary-Bridging-Header.h ├── FetchVideoFrame.h ├── Base64Downloader.swift ├── MediaAssetFileNative.h ├── Helpers.h ├── LibraryCombineImages.swift ├── MediaAssetFileNative.cpp ├── LibraryImageResize.swift ├── FetchVideoFrame.mm ├── Macros.h ├── Helpers.mm ├── LibraryImageSize.swift └── LibrarySaveToCameraRoll.swift ├── .yarnrc ├── .editorconfig ├── android ├── gradle.properties ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── reactnativemedialibrary │ │ │ ├── files.utils.ts.kt │ │ │ ├── AssetItemKeys.kt │ │ │ ├── Base64Downloader.kt │ │ │ ├── GetAssetsCallback.java │ │ │ ├── MediaLibraryPackage.kt │ │ │ ├── MediaLibraryModule.kt │ │ │ ├── FetchVideoFrame.kt │ │ │ ├── MedialLibraryCreateAsset.kt │ │ │ ├── AppCursor.kt │ │ │ ├── MediaLibrary.kt │ │ │ ├── ManipulateImages.kt │ │ │ ├── MediaLibraryUtils.kt │ │ │ └── AppContentResolver.kt │ │ ├── AndroidManifest.xml │ │ └── cpp │ │ ├── MediaAssetFileNative.h │ │ ├── MediaLibrary.h │ │ ├── MediaAssetFileNative.cpp │ │ ├── ThreadPool.h │ │ └── Macros.h ├── CMakeLists.txt └── build.gradle ├── lefthook.yml ├── tsconfig.json ├── scripts └── bootstrap.js ├── .gitignore ├── react-native-media-library.podspec ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── README.md ├── CONTRIBUTING.md └── src └── index.tsx /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | example/ 3 | .vscode/ 4 | .idea/ 5 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | export NODE_BINARY=$(command -v node) 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // MediaLibraryExample 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /example/assets/tombstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/assets/tombstone.png -------------------------------------------------------------------------------- /ios/MediaLibrary.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MediaLibrary : NSObject 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/debug.keystore -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MediaLibrary Example 3 | 4 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/MediaLibrary-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './src/App'; 3 | 4 | AppRegistry.registerComponent('main', () => App); 5 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeymild/react-native-media-library/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | dependencies: { 5 | 'react-native-media-library2': { 6 | root: path.join(__dirname, '..'), 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'MediaLibraryExample' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/@react-native/gradle-plugin') 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | MediaLibrary_kotlin_version=1.7.0 2 | MediaLibrary_coroutines=1.6.0 3 | MediaLibrary_minSdkVersion=21 4 | MediaLibrary_targetSdkVersion=31 5 | MediaLibrary_compileSdkVersion=31 6 | MediaLibrary_ndkversion=21.4.7075529 7 | MediaLibrary_exifinterfaceVersion=1.3.5 8 | MediaLibrary_mediastore=1.0.0-alpha06 9 | MediaLibrary_okio=3.0.0 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/files.utils.ts.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | fun String.fixFilePathFromJs(): String { 4 | return if (this.startsWith("file://")) this.substring(7) else this 5 | } 6 | 7 | fun String.fixFilePathToJs(): String { 8 | if (this.startsWith("http")) return this 9 | return if (!this.startsWith("file://")) "file://$this" else this 10 | } 11 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/AssetItemKeys.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | @Suppress("EnumEntryName") 4 | enum class AssetItemKeys { 5 | filename, 6 | id, 7 | mediaType, 8 | location, 9 | creationTime, 10 | modificationTime, 11 | duration, 12 | width, 13 | height, 14 | url, 15 | uri 16 | } 17 | 18 | @Suppress("EnumEntryName") 19 | enum class AssetMediaType { 20 | video, 21 | audio, 22 | photo 23 | } 24 | -------------------------------------------------------------------------------- /ios/FetchVideoFrame.h: -------------------------------------------------------------------------------- 1 | // 2 | // FetchVideoFrame.h 3 | // MediaLibrary 4 | // 5 | // Created by Sergei Golishnikov on 01/12/2022. 6 | // Copyright © 2022 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "json.h" 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface FetchVideoFrame : NSObject 14 | +(nullable NSString*)fetchVideoFrame:(NSString*)url time:(double)time quality:(double)quality; 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: ['module:@react-native/babel-preset'], 6 | plugins: [ 7 | ['react-native-reanimated/plugin'], 8 | [ 9 | 'module-resolver', 10 | { 11 | extensions: ['.tsx', '.ts', '.js', '.json'], 12 | alias: { 13 | [pak.name]: path.join(__dirname, '..', pak.source), 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/Base64Downloader.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.util.Base64 4 | import android.util.Base64OutputStream 5 | import java.io.ByteArrayOutputStream 6 | import java.net.URL 7 | 8 | object Base64Downloader { 9 | 10 | fun download(url: String): String { 11 | val byteArray = URL(url).readBytes() 12 | val out = ByteArrayOutputStream() 13 | Base64OutputStream(out, Base64.DEFAULT).use { 14 | it.write(byteArray) 15 | } 16 | return out.toString() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | -keep class com.facebook.hermes.unicode.** { *; } 13 | -keep class com.facebook.jni.** { *; } 14 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/GetAssetsCallback.java: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | 6 | @DoNotStrip 7 | public class GetAssetsCallback { 8 | @DoNotStrip 9 | private final HybridData mHybridData; 10 | 11 | @DoNotStrip 12 | public GetAssetsCallback(HybridData mHybridData) { 13 | System.out.println("🥸 GetAssetsCallback.constructor"); 14 | this.mHybridData = mHybridData; 15 | } 16 | 17 | public synchronized void destroy() { 18 | if (mHybridData != null) { 19 | mHybridData.resetNative(); 20 | } 21 | } 22 | 23 | @SuppressWarnings("JavaJniMissingFunction") 24 | native void onChange(String type); 25 | } 26 | -------------------------------------------------------------------------------- /ios/Base64Downloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64Downloader.swift 3 | // MediaLibrary 4 | // 5 | // Created by Sergei Golishnikov on 02/01/2024. 6 | // Copyright © 2024 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc 12 | public class Base64Downloader: NSObject { 13 | @objc 14 | public static func download(url: String, completion: @escaping (String?) -> Void) { 15 | guard let url = URL(string: url) else { return completion(nil) } 16 | let request = URLRequest(url: url) 17 | URLSession.shared.dataTask(with: request) { data, _, _ in 18 | guard let data else { return completion(nil) } 19 | completion("{\"base64\": \"\(data.base64EncodedString())\"}") 20 | }.resume() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.taskdefs.condition.Os 2 | 3 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 4 | 5 | buildscript { 6 | ext { 7 | buildToolsVersion = "34.0.0" 8 | minSdkVersion = 23 9 | compileSdkVersion = 34 10 | targetSdkVersion = 34 11 | ndkVersion = "26.1.10909125" 12 | kotlinVersion = "1.9.22" 13 | kotlin_version = "1.9.22" 14 | } 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | dependencies { 20 | classpath("com.android.tools.build:gradle") 21 | classpath("com.facebook.react:react-native-gradle-plugin") 22 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 23 | } 24 | } 25 | 26 | apply plugin: "com.facebook.react.rootproject" 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-media-library2": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "importsNotUsedAsValues": "error", 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "lib": ["esnext"], 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noImplicitUseStrict": false, 19 | "noStrictGenericChecks": false, 20 | "noUncheckedIndexedAccess": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "target": "esnext" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/MediaLibraryPackage.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import com.facebook.react.ReactPackage 4 | import com.facebook.react.bridge.NativeModule 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.uimanager.ViewManager 7 | import com.reactnativemedialibrary.MediaLibraryModule 8 | import java.util.ArrayList 9 | 10 | class MediaLibraryPackage : ReactPackage { 11 | override fun createNativeModules(reactContext: ReactApplicationContext): List { 12 | val modules: MutableList = ArrayList() 13 | modules.add(MediaLibraryModule(reactContext)) 14 | return modules 15 | } 16 | 17 | override fun createViewManagers(reactContext: ReactApplicationContext): List> { 18 | return emptyList() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /ios/MediaAssetFileNative.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAssetFileNative.hpp 3 | // react-native-media-library 4 | // 5 | // Created by Sergei Golishnikov on 19/04/2024. 6 | // 7 | 8 | 9 | #ifndef MediaAssetFileNative_h 10 | #define MediaAssetFileNative_h 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace MediaAssetFileNative { 19 | 20 | struct File { 21 | std::string name; 22 | std::string absolutePath; 23 | long size; 24 | long lastModificationTime; 25 | bool isDir; 26 | long filesCount; 27 | }; 28 | 29 | typedef std::vector fileVector_t; 30 | 31 | void getFilesList(const char *path, const char *sortType, fileVector_t *fileList); 32 | MediaAssetFileNative::File getFileNative(const char *path); 33 | } 34 | 35 | #endif /* MediaAssetFileNative_h */ 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/* 65 | 66 | # generated by bob 67 | lib/ 68 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"main"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | return [self bundleURL]; 20 | } 21 | 22 | - (NSURL *)bundleURL 23 | { 24 | #if DEBUG 25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 26 | #else 27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 28 | #endif 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /android/src/main/cpp/MediaAssetFileNative.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAssetFileNative.hpp 3 | // react-native-media-library 4 | // 5 | // Created by Sergei Golishnikov on 19/04/2024. 6 | // 7 | 8 | 9 | #ifndef MediaAssetFileNative_h 10 | #define MediaAssetFileNative_h 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace MediaAssetFileNative { 20 | 21 | struct File { 22 | std::string name; 23 | std::string absolutePath; 24 | long size; 25 | long lastModificationTime; 26 | bool isDir; 27 | long filesCount; 28 | }; 29 | 30 | typedef std::vector fileVector_t; 31 | 32 | void getFilesList(const char *path, const char *sortType, fileVector_t *fileList); 33 | MediaAssetFileNative::File getFileNative(const char *path); 34 | } 35 | 36 | #endif /* MediaAssetFileNative_h */ 37 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/MediaLibraryModule.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import com.facebook.react.bridge.ReactApplicationContext 4 | import com.facebook.react.bridge.ReactContextBaseJavaModule 5 | import com.facebook.react.bridge.ReactMethod 6 | import com.facebook.react.module.annotations.ReactModule 7 | 8 | @ReactModule(name = MediaLibraryModule.NAME) 9 | class MediaLibraryModule(reactContext: ReactApplicationContext?) : 10 | ReactContextBaseJavaModule(reactContext) { 11 | private val mediaLibrary: MediaLibrary 12 | 13 | init { 14 | mediaLibrary = MediaLibrary(reactContext!!) 15 | } 16 | 17 | override fun getName(): String { 18 | return NAME 19 | } 20 | 21 | @ReactMethod(isBlockingSynchronousMethod = true) 22 | fun install() { 23 | mediaLibrary.install(reactApplicationContext) 24 | } 25 | 26 | companion object { 27 | const val NAME = "MediaLibrary" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /react-native-media-library.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 5 | 6 | Pod::Spec.new do |s| 7 | s.name = "react-native-media-library" 8 | s.version = package["version"] 9 | s.summary = package["description"] 10 | s.homepage = package["homepage"] 11 | s.license = package["license"] 12 | s.authors = package["author"] 13 | 14 | s.platforms = { :ios => "15.0" } 15 | s.source = { :git => "https://github.com/sergeymild/react-native-media-library.git", :tag => "#{s.version}" } 16 | 17 | s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" 18 | s.public_header_files = 'ios/MediaLibrary-Bridging-Header.h' 19 | 20 | s.pod_target_xcconfig = { 21 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", 22 | } 23 | 24 | s.dependency "React-jsi" 25 | s.dependency "React" 26 | 27 | 28 | end 29 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/reactnativemedialibrary/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.reactnativemedialibrary; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.ReactRootView; 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 8 | 9 | public class MainActivity extends ReactActivity { 10 | 11 | /** 12 | * Returns the name of the main component registered from JavaScript. This is used to schedule 13 | * rendering of the component. 14 | */ 15 | @Override 16 | protected String getMainComponentName() { 17 | return "main"; 18 | } 19 | 20 | 21 | @Override 22 | protected ReactActivityDelegate createReactActivityDelegate() { 23 | return new DefaultReactActivityDelegate( 24 | this, 25 | getMainComponentName(), 26 | DefaultNewArchitectureEntryPoint.getFabricEnabled() 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/src/screens/index.ts: -------------------------------------------------------------------------------- 1 | export const screens = [ 2 | { 3 | title: 'Media', 4 | data: [ 5 | { 6 | name: 'CollectionsList', 7 | slug: 'CollectionsList', 8 | getScreen: () => require('./modal/CollectionsList').CollectionsList, 9 | }, 10 | { 11 | name: 'ImagesList', 12 | slug: 'ImagesList', 13 | getScreen: () => require('./modal/ImagesList').ImagesList, 14 | }, 15 | { 16 | name: 'SloMo', 17 | slug: 'SloMo', 18 | getScreen: () => require('./modal/SloMo').SloMo, 19 | }, 20 | { 21 | name: 'ExportVideo', 22 | slug: 'ExportVideo', 23 | getScreen: () => require('./modal/ExportVideo').ExportVideo, 24 | }, 25 | { 26 | name: 'Base64Image', 27 | slug: 'Base64Image', 28 | getScreen: () => require('./modal/Base64Image').Base64Image, 29 | }, 30 | { 31 | name: 'CombineImages', 32 | slug: 'CombineImages', 33 | getScreen: () => require('./modal/CombineImages').CombineImages, 34 | }, 35 | ], 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SergeyMild 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 4 | const pak = require('../package.json'); 5 | const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const config = { 14 | projectRoot: __dirname, 15 | watchFolders: [root], 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we block them at the root, and alias them to the versions in example's node_modules 19 | resolver: { 20 | blacklistRE: exclusionList( 21 | modules.map( 22 | (m) => 23 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 24 | ) 25 | ), 26 | 27 | extraNodeModules: modules.reduce((acc, name) => { 28 | acc[name] = path.join(__dirname, 'node_modules', name); 29 | return acc; 30 | }, {}), 31 | }, 32 | } 33 | 34 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 35 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, '15.0' 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'MediaLibraryExample' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | :app_path => "#{Pod::Config.instance.installation_root}/.." 23 | ) 24 | 25 | pod 'SDWebImagePhotosPlugin' 26 | 27 | post_install do |installer| 28 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 29 | react_native_post_install( 30 | installer, 31 | config[:reactNativePath], 32 | :mac_catalyst_enabled => false, 33 | # :ccache_enabled => true 34 | ) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /example/patches/react-native-fast-image+8.6.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m 2 | index 84ca94e..3c97354 100644 3 | --- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m 4 | +++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m 5 | @@ -3,11 +3,25 @@ 6 | 7 | #import 8 | #import 9 | +#import 10 | 11 | @implementation FFFastImageViewManager 12 | 13 | RCT_EXPORT_MODULE(FastImageView) 14 | 15 | + 16 | +- (id) init 17 | +{ 18 | + self = [super init]; 19 | + 20 | + // Supports HTTP URL as well as Photos URL globally 21 | + SDImageLoadersManager.sharedManager.loaders = @[SDWebImageDownloader.sharedDownloader, SDImagePhotosLoader.sharedLoader]; 22 | + // Replace default manager's loader implementation 23 | + SDWebImageManager.defaultImageLoader = SDImageLoadersManager.sharedManager; 24 | + 25 | + return self; 26 | +} 27 | + 28 | - (FFFastImageView*)view { 29 | return [[FFFastImageView alloc] init]; 30 | } 31 | -------------------------------------------------------------------------------- /example/src/screens/modal/Base64Image.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Image, Text, TextInput, TouchableOpacity, View } from 'react-native'; 3 | import { mediaLibrary } from 'react-native-media-library2'; 4 | 5 | export const Base64Image: React.FC = () => { 6 | const [url, setUrl] = useState( 7 | 'https://fastly.picsum.photos/id/4/200/300.jpg?hmac=y6_DgDO4ccUuOHUJcEWirdjxlpPwMcEZo7fz1MpuaWg' 8 | ); 9 | 10 | const [image, setImage] = useState(); 11 | 12 | return ( 13 | 14 | { 16 | if (url.length === 0) return; 17 | const response = await mediaLibrary.downloadAsBase64({ url: url }); 18 | setImage(response?.base64); 19 | }} 20 | > 21 | 22 | 23 | 24 | 29 | 30 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /android/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set (CMAKE_VERBOSE_MAKEFILE ON) 4 | set (CMAKE_CXX_STANDARD 17) 5 | set (PACKAGE_NAME "react-native-media-library") 6 | 7 | set (BUILD_DIR ${CMAKE_SOURCE_DIR}/build) 8 | include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake") 9 | add_compile_options(${folly_FLAGS}) 10 | 11 | # Consume shared libraries and headers from prefabs 12 | find_package(fbjni REQUIRED CONFIG) 13 | find_package(ReactAndroid REQUIRED CONFIG) 14 | 15 | add_library( 16 | ${PACKAGE_NAME} 17 | SHARED 18 | src/main/cpp/MediaLibrary.cpp 19 | src/main/cpp/MediaAssetFileNative.cpp 20 | ) 21 | 22 | # includes 23 | 24 | target_include_directories( 25 | ${PACKAGE_NAME} 26 | PRIVATE 27 | "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule" 28 | "${REACT_NATIVE_DIR}/ReactCommon" 29 | "${REACT_NATIVE_DIR}/ReactCommon/callinvoker" 30 | "src/main/cpp" 31 | ) 32 | 33 | find_library( 34 | LOG_LIB 35 | log 36 | ) 37 | 38 | target_link_libraries( 39 | ${PACKAGE_NAME} 40 | ${LOG_LIB} 41 | ReactAndroid::jsi 42 | ReactAndroid::reactnativejni 43 | fbjni::fbjni 44 | android 45 | ) 46 | -------------------------------------------------------------------------------- /ios/Helpers.h: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.h 3 | // MediaLibrary 4 | // 5 | // Created by Sergei Golishnikov on 03/03/2023. 6 | // Copyright © 2023 Facebook. All rights reserved. 7 | // 8 | #import 9 | #include 10 | #import 11 | #import 12 | #import 13 | #import "react_native_media_library-Swift.h" 14 | #import "json.h" 15 | 16 | 17 | using namespace facebook; 18 | 19 | NS_ASSUME_NONNULL_BEGIN 20 | 21 | @interface Helpers : NSObject 22 | 23 | +(jsi::String) toJSIString:(NSString*)value runtime_:(jsi::Runtime*)runtime_; 24 | +(const char*) toCString:(NSString *)value; 25 | +(NSString*) toString:(jsi::String)value runtime_:(jsi::Runtime*)runtime_; 26 | +(double) _exportDate:(NSDate *)date; 27 | +(NSString*) _toSdUrl:(NSString *)localId; 28 | +(NSString*) _assetIdFromLocalId:(NSString*)localId; 29 | +(NSString*) _assetUriForLocalId:(NSString *)localId; 30 | +(NSString *) _stringifyMediaType:(PHAssetMediaType)mediaType; 31 | +(PHAssetMediaType) _assetTypeForUri:(NSString *)localUri; 32 | +(NSURL*) _normalizeAssetURLFromUri:(NSString *)uri; 33 | +(NSSortDescriptor*) _sortDescriptorFrom:(jsi::Runtime*)runtime_ sortBy:(jsi::Value)sortBy sortOrder:(jsi::Value)sortOrder; 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | 38 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/reactnativemedialibrary/newarchitecture/components/MainComponentsRegistry.java: -------------------------------------------------------------------------------- 1 | package com.example.reactnativemedialibrary.newarchitecture.components; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | import com.facebook.react.fabric.ComponentFactory; 6 | import com.facebook.soloader.SoLoader; 7 | 8 | /** 9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a 10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 11 | * folder for you). 12 | * 13 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/src/screens/modal/SloMo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { View } from 'react-native'; 3 | import { mediaLibrary } from 'react-native-media-library2'; 4 | import Video from 'react-native-video'; 5 | 6 | export const SloMo: React.FC = () => { 7 | const [asset, setAsset] = useState(); 8 | 9 | useEffect(() => { 10 | mediaLibrary 11 | .getAssets({ 12 | mediaType: ['video'], 13 | sortBy: 'creationTime', 14 | sortOrder: 'desc', 15 | limit: 1, 16 | }) 17 | .then(async (r) => { 18 | //setAsset(r[0]); 19 | mediaLibrary.getAsset(r[0].id).then((rr) => { 20 | console.log('[SloMo.2]', rr?.url); 21 | setAsset(rr); 22 | }); 23 | }); 24 | }, []); 25 | 26 | console.log('[SloMo.SloMo!!]', asset?.uri); 27 | return ( 28 | 29 | {!!asset && ( 30 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/FetchVideoFrame.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.media.MediaMetadataRetriever 6 | import android.net.Uri 7 | import com.reactnativemedialibrary.MediaLibraryUtils.withRetriever 8 | import org.json.JSONObject 9 | import java.io.File 10 | import java.io.FileOutputStream 11 | 12 | 13 | fun Context.fetchFrame(input: JSONObject): JSONObject? { 14 | try { 15 | var response: JSONObject? = null 16 | val time = input.long("time") ?: 0 17 | val url = input.getString("url") 18 | val quality = input.long("quality") ?: 1 19 | withRetriever(contentResolver, Uri.parse(url)) { retriever -> 20 | retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)?.let { thumbnail -> 21 | val path = MediaLibraryUtils.generateOutputPath(cacheDir, "VideoThumbnails", "jpg") 22 | FileOutputStream(path).use { output -> 23 | thumbnail.compress(Bitmap.CompressFormat.JPEG, (quality * 100).toInt(), output) 24 | response = JSONObject().also { 25 | it.put("url", Uri.fromFile(File(path)).toString()) 26 | it.put("width", thumbnail.width) 27 | it.put("height", thumbnail.height) 28 | } 29 | } 30 | } 31 | } 32 | return response 33 | } catch (e: java.lang.RuntimeException) { 34 | return null 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /example/src/screens/modal/CollectionsList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { CollectionItem, mediaLibrary } from 'react-native-media-library2'; 3 | import { Dimensions, FlatList, Text, TouchableOpacity } from 'react-native'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | 6 | const width = Dimensions.get('window').width; 7 | export const CollectionsList: React.FC = () => { 8 | const navigation = useNavigation(); 9 | const [images, setImages] = useState([]); 10 | 11 | useEffect(() => { 12 | mediaLibrary.getCollections().then(setImages); 13 | }, []); 14 | 15 | return ( 16 | 17 | numColumns={3} 18 | data={images} 19 | renderItem={(info) => { 20 | return ( 21 | props.setOpenCollection(info.item.id)} 23 | onPress={() => { 24 | //@ts-ignore 25 | navigation.navigate('ImagesList', { collectionId: info.item.id }); 26 | }} 27 | style={{ 28 | width: width / 3, 29 | height: width / 3, 30 | alignItems: 'center', 31 | justifyContent: 'center', 32 | borderWidth: 1, 33 | }} 34 | > 35 | {info.item.filename} 36 | 37 | ); 38 | }} 39 | /> 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-media-library-example", 3 | "description": "Example app for react-native-media-library", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "android": "react-native run-android", 8 | "ios": "react-native run-ios --simulator='iPhone X'", 9 | "start": "react-native start", 10 | "pods": "pod-install --quiet", 11 | "postinstall": "patch-package" 12 | }, 13 | "dependencies": { 14 | "react": "18.2.0", 15 | "react-native": "0.74.5", 16 | "react-native-video": "^6.4.5", 17 | "react-native-fast-image": "^8.6.3", 18 | "react-native-fs": "^2.20.0", 19 | "@gorhom/showcase-template": "^2.1.0", 20 | "@react-navigation/native": "^6.1.18", 21 | "@react-navigation/native-stack": "^6.11.0", 22 | "@react-navigation/stack": "^6.4.1", 23 | "react-native-safe-area-context": "4.10.9", 24 | "react-native-screens": "^3.34.0", 25 | "react-native-reanimated": "3.14.0", 26 | "react-native-gesture-handler": "^2.18.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.23.3", 30 | "@babel/runtime": "^7.23.4", 31 | "babel-plugin-module-resolver": "^5.0.0", 32 | "metro-react-native-babel-preset": "^0.77.0", 33 | "patch-package": "^8.0.0", 34 | "postinstall-postinstall": "^2.1.0", 35 | 36 | "@react-native/babel-preset": "0.74.87", 37 | "@react-native/eslint-config": "0.74.87", 38 | "@react-native/metro-config": "0.74.87", 39 | "@react-native/typescript-config": "0.74.87", 40 | "@types/react": "^18.2.6", 41 | "eslint": "^8.19.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/src/screens/modal/ExportVideo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { View } from 'react-native'; 3 | import { mediaLibrary } from 'react-native-media-library2'; 4 | import Video from 'react-native-video'; 5 | 6 | export const ExportVideo: React.FC = () => { 7 | const [asset, setAsset] = useState(); 8 | 9 | useEffect(() => { 10 | mediaLibrary 11 | .getAssets({ 12 | mediaType: ['video'], 13 | sortBy: 'creationTime', 14 | sortOrder: 'desc', 15 | limit: 1, 16 | }) 17 | .then(async (r) => { 18 | //setAsset(r[0]); 19 | const resultSavePath = `${mediaLibrary.cacheDir}/file.mp4`; 20 | console.log('[ExportVideo.tryExport]', r[0].id, resultSavePath); 21 | mediaLibrary 22 | .exportVideo({ identifier: r[0].id, resultSavePath }) 23 | .then((isSuccess) => { 24 | console.log('[ExportVideo.video]', isSuccess); 25 | setAsset(resultSavePath); 26 | }); 27 | }); 28 | }, []); 29 | 30 | console.log('[SloMo.SloMo!!]', asset); 31 | return ( 32 | 33 | {!!asset && ( 34 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /example/src/screens/modal/CombineImages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Image, Text, TouchableOpacity, View } from 'react-native'; 3 | import { mediaLibrary } from 'react-native-media-library2'; 4 | 5 | export const CombineImages: React.FC = () => { 6 | const [url] = useState( 7 | 'https://fastly.picsum.photos/id/4/200/300.jpg?hmac=y6_DgDO4ccUuOHUJcEWirdjxlpPwMcEZo7fz1MpuaWg' 8 | ); 9 | 10 | const [image, setImage] = useState(); 11 | const [render, setRender] = useState(0); 12 | console.log('🍓[CombineImages.CombineImages]'); 13 | 14 | return ( 15 | 16 | { 18 | const path = `file://${mediaLibrary.cacheDir}/combined.png`; 19 | console.log('🍓[CombineImages.]', path); 20 | const response = await mediaLibrary.combineImages({ 21 | mainImageIndex: 0, 22 | images: [ 23 | url, 24 | { 25 | image: require('../../../assets/tombstone.png'), 26 | positions: { x: 0, y: 0 }, 27 | }, 28 | ], 29 | resultSavePath: path, 30 | }); 31 | console.log('🍓[CombineImages.]', response); 32 | setRender(Date.now); 33 | setImage(path); 34 | }} 35 | > 36 | 37 | 38 | 39 | 43 | 44 | 45 | 46 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | hermesEnabled=true 42 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | NSPhotoLibraryUsageDescription 16 | 3232 17 | NSPhotoLibraryAddUsageDescription 18 | 3232ee 19 | CFBundleName 20 | $(PRODUCT_NAME) 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | 1.0 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | 1 29 | LSRequiresIPhoneOS 30 | 31 | NSAppTransportSecurity 32 | 33 | NSExceptionDomains 34 | 35 | localhost 36 | 37 | NSExceptionAllowsInsecureHTTPLoads 38 | 39 | 40 | 41 | 42 | NSLocationWhenInUseUsageDescription 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UIViewControllerBasedStatusBarAppearance 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/reactnativemedialibrary/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.example.reactnativemedialibrary.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("example_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/reactnativemedialibrary/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.reactnativemedialibrary; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactNativeHost; 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 9 | import com.facebook.react.defaults.DefaultReactNativeHost; 10 | import com.facebook.soloader.SoLoader; 11 | import com.facebook.react.PackageList; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new DefaultReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | return packages; 28 | } 29 | 30 | @Override 31 | protected String getJSMainModuleName() { 32 | return "index"; 33 | } 34 | 35 | @Override 36 | protected boolean isNewArchEnabled() { 37 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 38 | } 39 | 40 | @Override 41 | protected Boolean isHermesEnabled() { 42 | return BuildConfig.IS_HERMES_ENABLED; 43 | } 44 | }; 45 | 46 | @Override 47 | public ReactNativeHost getReactNativeHost() { 48 | return mReactNativeHost; 49 | } 50 | 51 | @Override 52 | public void onCreate() { 53 | super.onCreate(); 54 | SoLoader.init(this, /* native exopackage */ false); 55 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 56 | // If you opted-in for the New Architecture, we load the native entry point for this app. 57 | DefaultNewArchitectureEntryPoint.load(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "com.facebook.react" 3 | apply plugin: "org.jetbrains.kotlin.android" 4 | 5 | import com.android.build.OutputFile 6 | 7 | 8 | react {} 9 | 10 | def enableSeparateBuildPerCPUArchitecture = false 11 | def enableProguardInReleaseBuilds = false 12 | 13 | def reactNativeArchitectures() { 14 | def value = project.getProperties().get("reactNativeArchitectures") 15 | return value ? value.split(",") : ["armeabi-v7a", "arm64-v8a"] 16 | } 17 | 18 | android { 19 | ndkVersion rootProject.ext.ndkVersion 20 | 21 | buildToolsVersion rootProject.ext.buildToolsVersion 22 | compileSdk rootProject.ext.compileSdkVersion 23 | 24 | namespace "com.example.reactnativemedialibrary" 25 | 26 | defaultConfig { 27 | applicationId "com.example.reactnativemedialibrary" 28 | minSdkVersion rootProject.ext.minSdkVersion 29 | targetSdkVersion rootProject.ext.targetSdkVersion 30 | versionCode 1 31 | versionName "1.0" 32 | } 33 | 34 | splits { 35 | abi { 36 | reset() 37 | enable enableSeparateBuildPerCPUArchitecture 38 | universalApk false // If true, also generate a universal APK 39 | include(*reactNativeArchitectures()) 40 | } 41 | } 42 | signingConfigs { 43 | debug { 44 | storeFile file('debug.keystore') 45 | storePassword 'android' 46 | keyAlias 'androiddebugkey' 47 | keyPassword 'android' 48 | } 49 | } 50 | buildTypes { 51 | debug { 52 | signingConfig signingConfigs.debug 53 | debuggable true 54 | } 55 | release { 56 | debuggable true 57 | // Caution! In production, you need to generate your own keystore file. 58 | // see https://reactnative.dev/docs/signed-apk-android. 59 | signingConfig signingConfigs.debug 60 | minifyEnabled enableProguardInReleaseBuilds 61 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 62 | } 63 | } 64 | } 65 | 66 | dependencies { 67 | // The version of react-native is set by the React Native Gradle Plugin 68 | implementation("com.facebook.react:react-android") 69 | 70 | implementation("com.facebook.react:hermes-android") 71 | } 72 | 73 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 74 | -------------------------------------------------------------------------------- /ios/LibraryCombineImages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryCombineImages.swift 3 | // MediaLibrary 4 | // 5 | // Created by sergeymild on 05/08/2023. 6 | // Copyright © 2023 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc 12 | public class LibraryCombineImages: NSObject { 13 | @objc 14 | public static func combineImages( 15 | images: [[String: Any]], 16 | resultSavePath: NSString, 17 | mainImageIndex: NSInteger, 18 | backgroundColor: UIColor 19 | ) -> String? { 20 | if images.isEmpty { 21 | return "LibraryCombineImages.combineImages.emptyArray" 22 | } 23 | 24 | let mainJson = images[mainImageIndex] 25 | let mainImage = mainJson["image"] as! UIImage 26 | let parentCenterX = mainImage.size.width / 2 27 | let parentCenterY = mainImage.size.height / 2 28 | let newImageSize = CGSize(width: mainImage.size.width, height: mainImage.size.height) 29 | 30 | UIGraphicsBeginImageContextWithOptions(newImageSize, false, 1.0) 31 | backgroundColor.setFill() 32 | UIGraphicsGetCurrentContext()!.fill(CGRect(x: 0, y: 0, width: newImageSize.width, height: newImageSize.height)) 33 | 34 | for (index, json) in images.enumerated() { 35 | let image = json["image"] as! UIImage 36 | var x = parentCenterX - image.size.width / 2 37 | var y = parentCenterY - image.size.height / 2 38 | if let positions = json["positions"] as? [String: Double], let pX = positions["x"], let pY = positions["y"] { 39 | x = pX 40 | y = pY 41 | if x > mainImage.size.width { 42 | x = mainImage.size.width - image.size.width 43 | } 44 | if y > mainImage.size.height { 45 | y = mainImage.size.height - image.size.height 46 | } 47 | if x < 0 {x = 0} 48 | if y <= 0 {y = 0} 49 | } 50 | image.draw(at: .init(x: x, y: y)) 51 | } 52 | let finalImage = UIGraphicsGetImageFromCurrentImageContext() 53 | UIGraphicsEndImageContext() 54 | 55 | guard let finalImage = finalImage else { 56 | return "CombineImages.combineImages.emptyContext"; 57 | } 58 | 59 | return LibraryImageSize.save(image: finalImage, format: "png", path: resultSavePath) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/src/main/cpp/MediaLibrary.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #import "map" 6 | 7 | namespace mediaLibrary { 8 | 9 | using namespace facebook::jsi; 10 | 11 | class GetAssetsCallback : public facebook::jni::HybridClass { 12 | public: 13 | static auto constexpr kJavaDescriptor = 14 | "Lcom/reactnativemedialibrary/GetAssetsCallback;"; 15 | 16 | void onChange(std::string data) { 17 | __android_log_print(ANDROID_LOG_DEBUG, "MediaLibrary", "🥸 GetAssetsCallback.onChange"); 18 | callback_(data); 19 | } 20 | 21 | static void registerNatives() { 22 | __android_log_print(ANDROID_LOG_DEBUG, "MediaLibrary", "🥸 GetAssetsCallback.registerNatives"); 23 | registerHybrid({ 24 | makeNativeMethod("onChange", GetAssetsCallback::onChange) 25 | }); 26 | } 27 | 28 | private: 29 | friend HybridBase; 30 | 31 | explicit GetAssetsCallback(std::function callback) 32 | : callback_(std::move(callback)) {} 33 | std::function callback_; 34 | }; 35 | 36 | 37 | class MediaLibrary : public facebook::jni::HybridClass { 38 | public: 39 | static constexpr auto kJavaDescriptor = "Lcom/reactnativemedialibrary/MediaLibrary;"; 40 | 41 | static facebook::jni::local_ref initHybrid( 42 | facebook::jni::alias_ref jThis, 43 | jlong jsContext, 44 | facebook::jni::alias_ref jsCallInvokerHolder 45 | ); 46 | 47 | static void registerNatives(); 48 | 49 | void installJSIBindings(); 50 | std::function createCallback(const std::shared_ptr& resolve, bool returnUndefinedOnEmpty); 51 | 52 | private: 53 | friend HybridBase; 54 | facebook::jni::global_ref javaPart_; 55 | facebook::jsi::Runtime *runtime_; 56 | std::shared_ptr jsCallInvoker_; 57 | 58 | explicit MediaLibrary( 59 | facebook::jni::alias_ref jThis, 60 | facebook::jsi::Runtime *rt, 61 | std::shared_ptr jsCallInvoker 62 | ); 63 | }; 64 | 65 | } 66 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | default: 5 | docker: 6 | - image: circleci/node:16 7 | working_directory: ~/project 8 | 9 | commands: 10 | attach_project: 11 | steps: 12 | - attach_workspace: 13 | at: ~/project 14 | 15 | jobs: 16 | install-dependencies: 17 | executor: default 18 | steps: 19 | - checkout 20 | - attach_project 21 | - restore_cache: 22 | keys: 23 | - dependencies-{{ checksum "package.json" }} 24 | - dependencies- 25 | - restore_cache: 26 | keys: 27 | - dependencies-example-{{ checksum "example/package.json" }} 28 | - dependencies-example- 29 | - run: 30 | name: Install dependencies 31 | command: | 32 | yarn install --cwd example --frozen-lockfile 33 | yarn install --frozen-lockfile 34 | - save_cache: 35 | key: dependencies-{{ checksum "package.json" }} 36 | paths: node_modules 37 | - save_cache: 38 | key: dependencies-example-{{ checksum "example/package.json" }} 39 | paths: example/node_modules 40 | - persist_to_workspace: 41 | root: . 42 | paths: . 43 | 44 | lint: 45 | executor: default 46 | steps: 47 | - attach_project 48 | - run: 49 | name: Lint files 50 | command: | 51 | yarn lint 52 | 53 | typescript: 54 | executor: default 55 | steps: 56 | - attach_project 57 | - run: 58 | name: Typecheck files 59 | command: | 60 | yarn typescript 61 | 62 | unit-tests: 63 | executor: default 64 | steps: 65 | - attach_project 66 | - run: 67 | name: Run unit tests 68 | command: | 69 | yarn test --coverage 70 | - store_artifacts: 71 | path: coverage 72 | destination: coverage 73 | 74 | build-package: 75 | executor: default 76 | steps: 77 | - attach_project 78 | - run: 79 | name: Build package 80 | command: | 81 | yarn prepare 82 | 83 | workflows: 84 | build-and-test: 85 | jobs: 86 | - install-dependencies 87 | - lint: 88 | requires: 89 | - install-dependencies 90 | - typescript: 91 | requires: 92 | - install-dependencies 93 | - unit-tests: 94 | requires: 95 | - install-dependencies 96 | - build-package: 97 | requires: 98 | - install-dependencies 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-media-library2", 3 | "version": "2.1.21", 4 | "description": "test", 5 | "main": "src/index", 6 | "module": "src/index", 7 | "react-native": "src/index", 8 | "source": "src/index", 9 | "files": [ 10 | "src", 11 | "lib", 12 | "android", 13 | "ios", 14 | "cpp", 15 | "react-native-media-library.podspec", 16 | "!lib/typescript/example", 17 | "!android/build", 18 | "!ios/build", 19 | "!**/__tests__", 20 | "!**/__fixtures__", 21 | "!**/__mocks__" 22 | ], 23 | "scripts": { 24 | "test": "jest", 25 | "typescript": "tsc --noEmit", 26 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 27 | "example": "yarn --cwd example", 28 | "bootstrap": "yarn example && yarn && yarn example pods" 29 | }, 30 | "keywords": [ 31 | "react-native", 32 | "ios", 33 | "android" 34 | ], 35 | "repository": "https://github.com/sergeymild/react-native-media-library", 36 | "author": "SergeyMild (https://github.com/sergeymild)", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/sergeymild/react-native-media-library/issues" 40 | }, 41 | "homepage": "https://github.com/sergeymild/react-native-media-library#readme", 42 | "publishConfig": { 43 | "registry": "https://registry.npmjs.org/" 44 | }, 45 | "devDependencies": { 46 | "@babel/eslint-parser": "^7.23.3", 47 | "@commitlint/config-conventional": "^18.4.3", 48 | "@react-native-community/eslint-config": "^3.2.0", 49 | "@types/react": "~18.2.38", 50 | "@types/react-native": "0.72.7", 51 | "eslint": "^8.54.0", 52 | "eslint-config-prettier": "^9.0.0", 53 | "eslint-plugin-prettier": "^5.0.1", 54 | "pod-install": "^0.1.39", 55 | "prettier": "^3.1.0", 56 | "react": "18.2.0", 57 | "react-native": "0.72.7", 58 | "typescript": "^5.3.2", 59 | "@react-native/babel-preset": "0.74.87", 60 | "@react-native/eslint-config": "0.74.87", 61 | "@react-native/metro-config": "0.74.87", 62 | "@react-native/typescript-config": "0.74.87" 63 | }, 64 | "peerDependencies": { 65 | "react": "*", 66 | "react-native": "*" 67 | }, 68 | "eslintConfig": { 69 | "root": true, 70 | "parser": "@babel/eslint-parser", 71 | "extends": [ 72 | "@react-native-community", 73 | "prettier" 74 | ], 75 | "rules": { 76 | "prettier/prettier": [ 77 | "error", 78 | { 79 | "quoteProps": "consistent", 80 | "singleQuote": true, 81 | "tabWidth": 2, 82 | "trailingComma": "es5", 83 | "useTabs": false 84 | } 85 | ] 86 | } 87 | }, 88 | "eslintIgnore": [ 89 | "node_modules/", 90 | "lib/" 91 | ], 92 | "prettier": { 93 | "quoteProps": "consistent", 94 | "singleQuote": true, 95 | "tabWidth": 2, 96 | "trailingComma": "es5", 97 | "useTabs": false 98 | } 99 | } -------------------------------------------------------------------------------- /android/src/main/cpp/MediaAssetFileNative.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "MediaAssetFileNative.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static int exclude_extradir_cb(const dirent* de) { 9 | int retVal = 1; 10 | if (de->d_type == DT_DIR) { 11 | char *mutablePath = strdup(de->d_name); 12 | char *filename = basename(mutablePath); 13 | retVal = (strcmp(filename, ".") != 0 && strcmp(filename, "..") != 0); 14 | free(mutablePath); 15 | } 16 | return retVal; 17 | } 18 | 19 | MediaAssetFileNative::File createFrom(const char *parentPath, dirent *entr) { 20 | MediaAssetFileNative::File file; 21 | const char *filename = entr->d_name; 22 | 23 | char fullPath[1024]; 24 | snprintf(fullPath, sizeof(fullPath), "%s/%s", parentPath, filename); 25 | 26 | file.name = filename; 27 | file.isDir = entr->d_type == DT_DIR; 28 | file.absolutePath = fullPath; 29 | file.lastModificationTime = 0; 30 | file.size = 0; 31 | 32 | struct stat fileAttrs; 33 | if (stat(fullPath, &fileAttrs) == 0) { 34 | file.size = fileAttrs.st_size; 35 | file.lastModificationTime = fileAttrs.st_mtim.tv_sec * 1000; 36 | } 37 | 38 | return file; 39 | } 40 | 41 | struct sort_by_date { 42 | int multiplier; 43 | sort_by_date(int _multiplier): multiplier(_multiplier) {}; 44 | 45 | inline bool operator() (const MediaAssetFileNative::File& lhs, const MediaAssetFileNative::File& rhs) { 46 | if (lhs.isDir && !rhs.isDir) return true; 47 | if (!lhs.isDir && rhs.isDir) return false; 48 | 49 | return multiplier > 0 50 | ? lhs.lastModificationTime < rhs.lastModificationTime 51 | : lhs.lastModificationTime > rhs.lastModificationTime; 52 | } 53 | }; 54 | 55 | 56 | void MediaAssetFileNative::getFilesList(const char *path, const char *sortType, fileVector_t *fileList) { 57 | fileList->clear(); 58 | dirent **namelist; 59 | int filesCount; 60 | filesCount = scandir(path, &namelist, &exclude_extradir_cb, nullptr); 61 | if (filesCount < 0) { 62 | return; 63 | } 64 | 65 | fileList->reserve(filesCount); 66 | 67 | for(int i = 0; i < filesCount; i++) { 68 | const char *filename = namelist[i]->d_name; 69 | auto isHidden = (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0); 70 | if (isHidden) { 71 | continue; 72 | } 73 | MediaAssetFileNative::File file = createFrom(path, namelist[i]); 74 | 75 | fileList->push_back(file); 76 | free(namelist[i]); 77 | } 78 | 79 | free(namelist); 80 | 81 | if (strcmp(sortType, "modificationTime_asc") == 0) { 82 | std::sort(fileList->begin(), fileList->end(), sort_by_date(1)); 83 | } else if (strcmp(sortType, "modificationTime_desc") == 0) { 84 | std::sort(fileList->begin(), fileList->end(), sort_by_date(-1)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ios/MediaAssetFileNative.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "MediaAssetFileNative.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static int exclude_extradir_cb(const dirent* de) { 9 | int retVal = 1; 10 | if (de->d_type == DT_DIR) { 11 | char *mutablePath = strdup(de->d_name); 12 | char *filename = basename(mutablePath); 13 | retVal = (strcmp(filename, ".") != 0 && strcmp(filename, "..") != 0); 14 | free(mutablePath); 15 | } 16 | return retVal; 17 | } 18 | 19 | MediaAssetFileNative::File createFrom(const char *parentPath, dirent *entr) { 20 | MediaAssetFileNative::File file; 21 | const char *filename = entr->d_name; 22 | 23 | char fullPath[1024]; 24 | snprintf(fullPath, sizeof(fullPath), "%s/%s", parentPath, filename); 25 | 26 | file.name = filename; 27 | file.isDir = entr->d_type == DT_DIR; 28 | file.absolutePath = fullPath; 29 | file.lastModificationTime = 0; 30 | file.size = 0; 31 | 32 | struct stat fileAttrs; 33 | if (stat(fullPath, &fileAttrs) == 0) { 34 | file.size = fileAttrs.st_size; 35 | file.lastModificationTime = fileAttrs.st_mtimespec.tv_sec * 1000; 36 | } 37 | 38 | return file; 39 | } 40 | 41 | struct sort_by_date { 42 | int multiplier; 43 | sort_by_date(int _multiplier): multiplier(_multiplier) {}; 44 | 45 | inline bool operator() (const MediaAssetFileNative::File& lhs, const MediaAssetFileNative::File& rhs) { 46 | if (lhs.isDir && !rhs.isDir) return true; 47 | if (!lhs.isDir && rhs.isDir) return false; 48 | 49 | return multiplier > 0 50 | ? lhs.lastModificationTime < rhs.lastModificationTime 51 | : lhs.lastModificationTime > rhs.lastModificationTime; 52 | } 53 | }; 54 | 55 | 56 | void MediaAssetFileNative::getFilesList(const char *path, const char *sortType, fileVector_t *fileList) { 57 | fileList->clear(); 58 | dirent **namelist; 59 | int filesCount; 60 | filesCount = scandir(path, &namelist, &exclude_extradir_cb, nullptr); 61 | if (filesCount < 0) { 62 | return; 63 | } 64 | 65 | fileList->reserve(filesCount); 66 | 67 | for(int i = 0; i < filesCount; i++) { 68 | const char *filename = namelist[i]->d_name; 69 | auto isHidden = (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0); 70 | if (isHidden) { 71 | continue; 72 | } 73 | MediaAssetFileNative::File file = createFrom(path, namelist[i]); 74 | 75 | fileList->push_back(file); 76 | free(namelist[i]); 77 | } 78 | 79 | free(namelist); 80 | 81 | if (strcmp(sortType, "modificationTime_asc") == 0) { 82 | std::sort(fileList->begin(), fileList->end(), sort_by_date(1)); 83 | } else if (strcmp(sortType, "modificationTime_desc") == 0) { 84 | std::sort(fileList->begin(), fileList->end(), sort_by_date(-1)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/MedialLibraryCreateAsset.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.ContentUris 4 | import android.content.Context 5 | import android.media.MediaScannerConnection 6 | import android.net.Uri 7 | import android.util.Log 8 | import com.facebook.proguard.annotations.DoNotStrip 9 | import org.json.JSONObject 10 | import java.io.File 11 | 12 | fun JSONObject.string(key: String): String? { 13 | if (has(key)) return getString(key) 14 | return null 15 | } 16 | 17 | fun JSONObject.long(key: String): Long? { 18 | if (has(key)) return getLong(key) 19 | return null 20 | } 21 | 22 | object MedialLibraryCreateAsset { 23 | 24 | private fun isFileExtensionPresent(uri: String): Boolean { 25 | return !uri.substring(uri.lastIndexOf(".") + 1).isEmpty() 26 | } 27 | 28 | private fun normalizeAssetUri(uri: String): Uri { 29 | return if (uri.startsWith("/")) { 30 | Uri.fromFile(File(uri)) 31 | } else Uri.parse(uri) 32 | } 33 | 34 | private inline fun createAssetFileLegacy( 35 | context: Context, 36 | uri: Uri, 37 | album: String?, 38 | callback: (String?, File?) -> Unit 39 | ) { 40 | try { 41 | 42 | val localFile = File(uri.path) 43 | var destDir = MediaLibraryUtils.getEnvDirectoryForAssetType( 44 | MediaLibraryUtils.getMimeType(context.contentResolver, uri), 45 | false 46 | ) ?: return callback("E_COULD_NOT_GUESS_FILE_TYPE", null) 47 | if (!album.isNullOrEmpty()) { 48 | destDir = File(destDir, album) 49 | if (!destDir.exists() && !destDir.mkdirs()) { 50 | return callback("E_WRITE_EXTERNAL_STORAGE_CREATE_ALBUM", null) 51 | } 52 | } 53 | val result = MediaLibraryUtils.safeCopyFile(localFile, destDir) 54 | if (result is String) return callback(result, null) 55 | val destFile = result as File 56 | if (!destFile.exists() || !destFile.isFile) { 57 | return callback("E_COULD_NOT_CREATE_ASSET_RELATED_FILE_IS_NOT_EXISTING", null); 58 | } 59 | callback(null, destFile) 60 | 61 | } 62 | catch (error: Exception) { 63 | Log.e("MediaLibrary", "Exception caught while executing createAssetFileLegacy:$error") 64 | return callback("E_EXCEPTION", null); 65 | } 66 | } 67 | 68 | 69 | 70 | @DoNotStrip 71 | fun saveToLibrary( 72 | params: JSONObject, 73 | context: Context, 74 | callback: (String?, String?) -> Unit 75 | ) { 76 | val localUrl = params.getString("localUrl") 77 | if (!isFileExtensionPresent(localUrl)) { 78 | callback.invoke("E_NO_FILE_EXTENSION", null) 79 | return 80 | } 81 | val uri = normalizeAssetUri(localUrl) 82 | 83 | createAssetFileLegacy(context, uri, params.string("album")) { error, asset -> 84 | if (error != null) return callback(JSONObject().apply { put("error", error) }.toString(), null) 85 | 86 | MediaScannerConnection.scanFile( 87 | context, 88 | arrayOf(asset!!.path), 89 | null 90 | ) { path: String?, newUri: Uri? -> 91 | if (newUri == null) return@scanFile callback("E_UNABLE_COPY_FILE_TO_EXTERNAL_STORAGE", null) 92 | callback(null, ContentUris.parseId(newUri).toString()) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample.xcodeproj/xcshareddata/xcschemes/MediaLibraryExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ios/LibraryImageResize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryImageResize.swift 3 | // MediaLibrary 4 | // 5 | // Created by sergeymild on 05/08/2023. 6 | // Copyright © 2023 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import React 11 | 12 | @objc 13 | public class LibraryImageResize: NSObject { 14 | @objc 15 | public static func resize( 16 | uri: NSString, 17 | width: NSNumber, 18 | height: NSNumber, 19 | format: NSString, 20 | resultSavePath: NSString 21 | ) -> String? { 22 | guard let image = LibraryImageSize.image(path: uri as String) else { 23 | return "LibraryImageResize.image.notExists" 24 | } 25 | let imageWidth = image.size.width 26 | let imageHeight = image.size.height 27 | let imageRatio = imageWidth / imageHeight 28 | var targetSize: CGSize = .zero 29 | 30 | if (width.floatValue >= 0) { 31 | targetSize.width = CGFloat(width.floatValue) 32 | targetSize.height = targetSize.width / imageRatio 33 | } 34 | 35 | if (height.floatValue >= 0) { 36 | targetSize.height = CGFloat(height.floatValue) 37 | targetSize.width = targetSize.width <= 0 ? imageRatio * targetSize.height : targetSize.width 38 | } 39 | UIGraphicsBeginImageContextWithOptions(targetSize, false, 1.0) 40 | image.draw(in: .init(origin: .zero, size: targetSize)) 41 | let finalImage = UIGraphicsGetImageFromCurrentImageContext() 42 | UIGraphicsEndImageContext() 43 | 44 | guard let finalImage = finalImage else { 45 | return "LibraryImageResize.image.emptyContext" 46 | } 47 | 48 | return LibraryImageSize.save(image: finalImage, format: format, path: resultSavePath) 49 | } 50 | 51 | @objc 52 | public static func crop( 53 | uri: NSString, 54 | x: NSNumber, 55 | y: NSNumber, 56 | width: NSNumber, 57 | height: NSNumber, 58 | format: NSString, 59 | resultSavePath: NSString 60 | ) -> String? { 61 | guard let image = LibraryImageSize.image(path: uri as String) else { 62 | return "LibraryImageResize.crop.notExists" 63 | } 64 | let originalSize = image.size 65 | var fX = CGFloat(x.floatValue) * originalSize.width 66 | var fY = CGFloat(y.floatValue) * originalSize.height 67 | let fWidth = CGFloat(width.floatValue) 68 | let fHeight = CGFloat(height.floatValue) 69 | 70 | if (fX + fWidth > image.size.width) { 71 | fX = image.size.width - fWidth; 72 | } 73 | 74 | if (fY + fHeight > image.size.height) { 75 | fY = image.size.height - fHeight; 76 | } 77 | 78 | let targetSize = CGSize(width: fWidth, height: fHeight) 79 | let targetRect = CGRect(x: -fX, y: -fY, width: image.size.width, height: image.size.height) 80 | let transform = RCTTransformFromTargetRect(image.size, targetRect) 81 | guard let finalImage = RCTTransformImage(image, targetSize, image.scale, transform) else { 82 | return "LibraryImageResize.crop.errorTransform" 83 | } 84 | return LibraryImageSize.save(image: finalImage, format: format, path: resultSavePath) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ios/FetchVideoFrame.mm: -------------------------------------------------------------------------------- 1 | // 2 | // FetchVideoFrame.m 3 | // MediaLibrary 4 | // 5 | // Created by Sergei Golishnikov on 01/12/2022. 6 | // Copyright © 2022 Facebook. All rights reserved. 7 | // 8 | 9 | #import "FetchVideoFrame.h" 10 | 11 | #import 12 | #import 13 | 14 | @implementation FetchVideoFrame 15 | 16 | + (BOOL)ensureDirExistsWithPath:(NSString *)path 17 | { 18 | BOOL isDir = NO; 19 | NSError *error; 20 | BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; 21 | if (!(exists && isDir)) { 22 | [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; 23 | if (error) { 24 | return NO; 25 | } 26 | } 27 | return YES; 28 | } 29 | 30 | +(NSString*)createVideoThumbnailsFolder { 31 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); 32 | NSString *cacheDirectory = [paths objectAtIndex:0]; 33 | auto path = [[NSURL URLWithString:cacheDirectory] URLByAppendingPathComponent:@"VideoThumbnails"]; 34 | [self ensureDirExistsWithPath:path.absoluteString]; 35 | return path.absoluteString; 36 | } 37 | 38 | 39 | +(nullable NSString*)fetchVideoFrame:(NSString*)url time:(double)time quality:(double)quality { 40 | NSLog(@"fetchVideoFrame: time: %f, quality: %f url: %@", time, quality, url); 41 | NSString *p = [url stringByReplacingOccurrencesOfString:@"file://" withString:@""]; 42 | NSURL *nsUrl = [NSURL URLWithString:url]; 43 | if (![[NSFileManager defaultManager] fileExistsAtPath:p]) { 44 | return NULL; 45 | } 46 | 47 | auto asset = [[AVURLAsset alloc] initWithURL:nsUrl options:nil]; 48 | auto generator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; 49 | generator.appliesPreferredTrackTransform = YES; 50 | generator.requestedTimeToleranceBefore = kCMTimeZero; 51 | generator.requestedTimeToleranceAfter = kCMTimeZero; 52 | 53 | NSError *err = NULL; 54 | CMTime cmTime = CMTimeMake(time * 1000, 1000); 55 | CGImageRef imgRef = [generator copyCGImageAtTime:cmTime actualTime:NULL error:&err]; 56 | if (err) { 57 | NSLog(@"error: %@", err.localizedFailureReason); 58 | return NULL; 59 | } 60 | 61 | UIImage *thumbnail = [UIImage imageWithCGImage:imgRef]; 62 | 63 | 64 | NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:@".jpg"]; 65 | NSString *newPath = [[self createVideoThumbnailsFolder] stringByAppendingPathComponent:fileName]; 66 | NSLog(@"writeTo: %@", newPath); 67 | NSData *data = UIImageJPEGRepresentation(thumbnail, quality); 68 | 69 | if (![data writeToFile:newPath atomically:YES]) { 70 | NSLog(@"error:Can't write to file"); 71 | return NULL; 72 | } 73 | 74 | NSURL *fileURL = [NSURL fileURLWithPath:newPath]; 75 | NSString *filePath = [fileURL absoluteString]; 76 | 77 | CGImageRelease(imgRef); 78 | json::object response; 79 | 80 | response.insert("url", [filePath cStringUsingEncoding:NSUTF8StringEncoding]); 81 | response.insert("width", (double)thumbnail.size.width); 82 | response.insert("width", (double)thumbnail.size.height); 83 | 84 | return [[NSString alloc] initWithCString:json::stringify(response).c_str() encoding:NSUTF8StringEncoding]; 85 | 86 | } 87 | @end 88 | -------------------------------------------------------------------------------- /android/src/main/cpp/ThreadPool.h: -------------------------------------------------------------------------------- 1 | #ifndef THREAD_POOL_H 2 | #define THREAD_POOL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace mediaLibrary { 15 | 16 | class ThreadPool { 17 | public: 18 | ThreadPool(size_t); 19 | 20 | template 21 | auto enqueue(F &&f, Args &&... args) 22 | -> std::future::type>; 23 | 24 | ~ThreadPool(); 25 | 26 | private: 27 | // need to keep track of threads so we can join them 28 | std::vector workers; 29 | // the task queue 30 | std::queue > tasks; 31 | 32 | // synchronization 33 | std::mutex queue_mutex; 34 | std::condition_variable condition; 35 | bool stop; 36 | }; 37 | 38 | // the constructor just launches some amount of workers 39 | inline ThreadPool::ThreadPool(size_t threads) : stop(false) { 40 | for (size_t i = 0; i < threads; ++i) 41 | workers.emplace_back([this] { 42 | for (;;) { 43 | std::function task; 44 | 45 | { 46 | std::unique_lock lock(this->queue_mutex); 47 | this->condition.wait(lock, 48 | [this] { return this->stop || !this->tasks.empty(); }); 49 | if (this->stop && this->tasks.empty()) 50 | return; 51 | task = std::move(this->tasks.front()); 52 | this->tasks.pop(); 53 | } 54 | 55 | task(); 56 | } 57 | } 58 | ); 59 | } 60 | 61 | // add new work item to the pool 62 | template 63 | auto ThreadPool::enqueue(F &&f, Args &&... args) 64 | -> std::future::type> { 65 | using return_type = typename std::result_of::type; 66 | 67 | auto task = std::make_shared >( 68 | std::bind(std::forward(f), std::forward(args)...) 69 | ); 70 | 71 | std::future res = task->get_future(); 72 | { 73 | std::unique_lock lock(queue_mutex); 74 | // don't allow enqueueing after stopping the pool 75 | if (stop) throw std::runtime_error("enqueue on stopped ThreadPool"); 76 | tasks.emplace([task]() { (*task)(); }); 77 | } 78 | condition.notify_one(); 79 | return res; 80 | } 81 | 82 | // the destructor joins all threads 83 | inline ThreadPool::~ThreadPool() { 84 | { 85 | std::unique_lock lock(queue_mutex); 86 | stop = true; 87 | } 88 | condition.notify_all(); 89 | for (std::thread &worker: workers) 90 | worker.join(); 91 | } 92 | 93 | 94 | } // namespace mediaLibrary 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /ios/Macros.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifndef Macros_h 5 | #define Macros_h 6 | 7 | #define JSI_HOST_FUNCTION(NAME, ARGS_COUNT) \ 8 | jsi::Function::createFromHostFunction( \ 9 | *runtime_, \ 10 | jsi::PropNameID::forUtf8(*runtime_, NAME), \ 11 | ARGS_COUNT, \ 12 | [=](jsi::Runtime &runtime, \ 13 | const jsi::Value &thisArg, \ 14 | const jsi::Value *args, \ 15 | size_t count) -> jsi::Value 16 | 17 | 18 | #define STR_FROM_ARGS(index) \ 19 | args[index].asString(runtime).utf8(runtime); 20 | 21 | #define PAIR(first, second) \ 22 | std::make_pair(first, second); 23 | 24 | #define BOOL_FROM_ARGS(index) \ 25 | args[index].getBool(); 26 | 27 | #define NUM_FROM_ARGS(index) \ 28 | args[index].asNumber(); 29 | 30 | 31 | #define TASK_START(...) \ 32 | auto callback = make_shared(callbackHolder.asObject(runtime)); \ 33 | auto task = [&runtime, callback, invoker = jsCallInvoker_, __VA_ARGS__]() 34 | 35 | #define TASK_END() \ 36 | pool->queueWork(task); \ 37 | return jsi::Value::undefined(); 38 | 39 | #define IF_ASYNC(index, ...) \ 40 | const jsi::Value &callbackHolder = args[index]; \ 41 | if (callbackHolder.isObject() && callbackHolder.asObject(runtime).isFunction(runtime)) 42 | 43 | #define INVOKE_CALLBACK(value) \ 44 | callback->asObject(runtime).asFunction(runtime).call(runtime, value); 45 | 46 | #define INVOKE_ASYNC(...) \ 47 | invoker->invokeAsync([&runtime, callback, __VA_ARGS__]() \ 48 | 49 | #define STARTS_WITH(first, second) \ 50 | first.find(second) == 0 51 | 52 | #define NOT_STARTS_WITH(first, second) \ 53 | first.find(second) != 0 54 | 55 | #define ANDROID_DATA_PROVIDER_FUNCTION(...) \ 56 | DataProvider dataProvider = [__VA_ARGS__, sd = sdCardDir_, an = android](const std::shared_ptr& callback, bool useFastImplementation) 57 | 58 | #define IIO(index, ...) \ 59 | const jsi::Value &callbackHolder = args[index]; \ 60 | if (callbackHolder.isObject() && callbackHolder.asObject(runtime).isFunction(runtime)) { \ 61 | auto callback = make_shared(callbackHolder.asObject(runtime)); \ 62 | auto task = [&runtime, callback, invoker = jsCallInvoker_, __VA_ARGS__]() { \ 63 | \ 64 | }; \ 65 | pool->queueWork(task); \ 66 | return jsi::Value::undefined(); \ 67 | } 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /android/src/main/cpp/Macros.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifndef Macros_h 5 | #define Macros_h 6 | 7 | #define JSI_HOST_FUNCTION(NAME, ARGS_COUNT) \ 8 | jsi::Function::createFromHostFunction( \ 9 | *runtime_, \ 10 | jsi::PropNameID::forUtf8(*runtime_, NAME), \ 11 | ARGS_COUNT, \ 12 | [=](jsi::Runtime &runtime, \ 13 | const jsi::Value &thisArg, \ 14 | const jsi::Value *args, \ 15 | size_t count) -> jsi::Value 16 | 17 | 18 | #define STR_FROM_ARGS(index) \ 19 | args[index].asString(runtime).utf8(runtime); 20 | 21 | #define PAIR(first, second) \ 22 | std::make_pair(first, second); 23 | 24 | #define BOOL_FROM_ARGS(index) \ 25 | args[index].getBool(); 26 | 27 | #define NUM_FROM_ARGS(index) \ 28 | args[index].asNumber(); 29 | 30 | 31 | #define TASK_START(...) \ 32 | auto callback = make_shared(callbackHolder.asObject(runtime)); \ 33 | auto task = [&runtime, callback, invoker = jsCallInvoker_, __VA_ARGS__]() 34 | 35 | #define TASK_END() \ 36 | pool->queueWork(task); \ 37 | return jsi::Value::undefined(); 38 | 39 | #define IF_ASYNC(index, ...) \ 40 | const jsi::Value &callbackHolder = args[index]; \ 41 | if (callbackHolder.isObject() && callbackHolder.asObject(runtime).isFunction(runtime)) 42 | 43 | #define INVOKE_CALLBACK(value) \ 44 | callback->asObject(runtime).asFunction(runtime).call(runtime, value); 45 | 46 | #define INVOKE_ASYNC(...) \ 47 | invoker->invokeAsync([&runtime, callback, __VA_ARGS__]() \ 48 | 49 | #define STARTS_WITH(first, second) \ 50 | first.find(second) == 0 51 | 52 | #define NOT_STARTS_WITH(first, second) \ 53 | first.find(second) != 0 54 | 55 | #define ANDROID_DATA_PROVIDER_FUNCTION(...) \ 56 | DataProvider dataProvider = [__VA_ARGS__, sd = sdCardDir_, an = android](const std::shared_ptr& callback, bool useFastImplementation) 57 | 58 | #define IIO(index, ...) \ 59 | const jsi::Value &callbackHolder = args[index]; \ 60 | if (callbackHolder.isObject() && callbackHolder.asObject(runtime).isFunction(runtime)) { \ 61 | auto callback = make_shared(callbackHolder.asObject(runtime)); \ 62 | auto task = [&runtime, callback, invoker = jsCallInvoker_, __VA_ARGS__]() { \ 63 | \ 64 | }; \ 65 | pool->queueWork(task); \ 66 | return jsi::Value::undefined(); \ 67 | } 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /example/src/CropImageExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | Dimensions, 4 | Image, 5 | Platform, 6 | Text, 7 | TextInput, 8 | TouchableOpacity, 9 | View, 10 | } from 'react-native'; 11 | import { AssetItem, mediaLibrary } from 'react-native-media-library2'; 12 | import FastImage from 'react-native-fast-image'; 13 | 14 | export const CropImageExample: React.FC = () => { 15 | const [original, setOriginal] = useState(); 16 | const [cropped, setCropped] = useState(); 17 | const [x, setX] = useState('0'); 18 | const [y, setY] = useState('0.9'); 19 | const [w, setW] = useState('500'); 20 | const [h, setH] = useState('500'); 21 | 22 | useEffect(() => { 23 | mediaLibrary 24 | .getAssets({ 25 | limit: 1, 26 | offset: 0, 27 | mediaType: ['photo'], 28 | sortBy: 'creationTime', 29 | sortOrder: 'desc', 30 | }) 31 | .then((r) => { 32 | console.log('[CropImageExample.]', r[0]); 33 | setOriginal(r[0]); 34 | }); 35 | }, []); 36 | 37 | const onCropPress = async () => { 38 | console.log( 39 | '[CropImageExample.onCropPress]', 40 | `${mediaLibrary.cacheDir}/resize.png` 41 | ); 42 | const asset = await mediaLibrary.getAsset(original!.id); 43 | console.log('[CropImageExample.onCropPress]', asset?.url); 44 | const response = await mediaLibrary.imageCrop({ 45 | resultSavePath: `${mediaLibrary.cacheDir}/resize.png`, 46 | uri: asset!.url, 47 | x: parseFloat(x), 48 | y: parseFloat(y), 49 | width: parseInt(w, 10), 50 | height: parseInt(h, 10), 51 | }); 52 | setCropped({ 53 | ...asset, 54 | uri: 55 | Platform.OS === 'ios' 56 | ? `${mediaLibrary.cacheDir}/resize.png` 57 | : `file://${mediaLibrary.cacheDir}/resize.png`, 58 | }); 59 | console.log('[CropImageExample.onCropPress]', response); 60 | }; 61 | 62 | return ( 63 | 64 | 65 | {!!original && ( 66 | 77 | )} 78 | {!!cropped && ( 79 | 90 | )} 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /ios/Helpers.mm: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.m 3 | // MediaLibrary 4 | // 5 | // Created by Sergei Golishnikov on 03/03/2023. 6 | // Copyright © 2023 Facebook. All rights reserved. 7 | // 8 | 9 | #import "Helpers.h" 10 | 11 | 12 | using namespace facebook; 13 | 14 | 15 | 16 | 17 | @implementation Helpers 18 | NSString *const AssetMediaTypeAudio = @"audio"; 19 | NSString *const AssetMediaTypePhoto = @"photo"; 20 | NSString *const AssetMediaTypeVideo = @"video"; 21 | NSString *const AssetMediaTypeUnknown = @"unknown"; 22 | NSString *const AssetMediaTypeAll = @"all"; 23 | 24 | +(jsi::String) toJSIString:(NSString*)value runtime_:(jsi::Runtime*)runtime_ { 25 | return jsi::String::createFromUtf8(*runtime_, [value UTF8String] ?: ""); 26 | } 27 | 28 | +(const char*) toCString:(NSString *)value { 29 | return [value cStringUsingEncoding:NSUTF8StringEncoding]; 30 | } 31 | 32 | +(NSString*) toString:(jsi::String)value runtime_:(jsi::Runtime*)runtime_ { 33 | return [[NSString alloc] initWithCString:value.utf8(*runtime_).c_str() encoding:NSUTF8StringEncoding]; 34 | } 35 | 36 | 37 | +(double) _exportDate:(NSDate *)date { 38 | if (!date) return 0.0; 39 | NSTimeInterval interval = date.timeIntervalSince1970; 40 | NSUInteger intervalMs = interval * 1000; 41 | return [[NSNumber numberWithUnsignedInteger:intervalMs] doubleValue]; 42 | } 43 | 44 | +(NSString*) _assetIdFromLocalId:(NSString*)localId { 45 | // PHAsset's localIdentifier looks like `8B51C35E-E1F3-4D18-BF90-22CC905737E9/L0/001` 46 | // however `/L0/001` doesn't take part in URL to the asset, so we need to strip it out. 47 | return [localId stringByReplacingOccurrencesOfString:@"/.*" withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, localId.length)]; 48 | } 49 | 50 | +(NSString*) _assetUriForLocalId:(NSString *)localId { 51 | NSString *assetId = [Helpers _assetIdFromLocalId:localId]; 52 | return [NSString stringWithFormat:@"ph://%@", assetId]; 53 | } 54 | 55 | 56 | +(NSString*) _toSdUrl:(NSString *)localId { 57 | return [[NSURL URLWithString:[NSString stringWithFormat:@"ph://%@", localId]] absoluteString]; 58 | } 59 | 60 | 61 | +(NSString *) _stringifyMediaType:(PHAssetMediaType)mediaType { 62 | switch (mediaType) { 63 | case PHAssetMediaTypeAudio: 64 | return AssetMediaTypeAudio; 65 | case PHAssetMediaTypeImage: 66 | return AssetMediaTypePhoto; 67 | case PHAssetMediaTypeVideo: 68 | return AssetMediaTypeVideo; 69 | default: 70 | return AssetMediaTypeUnknown; 71 | } 72 | } 73 | 74 | 75 | +(PHAssetMediaType) _assetTypeForUri:(NSString *)localUri { 76 | CFStringRef fileExtension = (__bridge CFStringRef)[localUri pathExtension]; 77 | CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL); 78 | 79 | if (UTTypeConformsTo(fileUTI, kUTTypeImage)) { 80 | return PHAssetMediaTypeImage; 81 | } 82 | if (UTTypeConformsTo(fileUTI, kUTTypeMovie)) { 83 | return PHAssetMediaTypeVideo; 84 | } 85 | if (UTTypeConformsTo(fileUTI, kUTTypeAudio)) { 86 | return PHAssetMediaTypeAudio; 87 | } 88 | return PHAssetMediaTypeUnknown; 89 | } 90 | 91 | +(NSURL*) _normalizeAssetURLFromUri:(NSString *)uri { 92 | if ([uri hasPrefix:@"/"]) { 93 | return [NSURL URLWithString:[@"file://" stringByAppendingString:uri]]; 94 | } 95 | return [NSURL URLWithString:uri]; 96 | } 97 | 98 | +(NSSortDescriptor*) _sortDescriptorFrom:(jsi::Runtime*)runtime_ sortBy:(jsi::Value)sortBy sortOrder:(jsi::Value)sortOrder { 99 | auto sortKey = [Helpers toString:sortBy.asString(*runtime_) runtime_:runtime_];; 100 | if ([sortKey isEqual: @"creationTime"] || [sortKey isEqual: @"modificationTime"]) { 101 | bool ascending = false; 102 | if (!sortOrder.isUndefined() && sortOrder.asString(*runtime_).utf8(*runtime_) == "asc") { 103 | ascending = true; 104 | } 105 | return [NSSortDescriptor sortDescriptorWithKey:sortKey ascending:ascending]; 106 | } 107 | return nil; 108 | } 109 | 110 | @end 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /example/ios/MediaLibraryExample/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/LibraryImageSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryImageSize.swift 3 | // MediaLibrary 4 | // 5 | // Created by sergeymild on 03/08/2023. 6 | // Copyright © 2023 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import React 11 | 12 | private struct Size: Codable { 13 | let width: Double 14 | let height: Double 15 | } 16 | 17 | public func toFilePath(path: NSString) -> URL { 18 | if path.hasPrefix("file://") { 19 | return URL(string: path as String)! 20 | } 21 | return URL(string: "file://\(path)")! 22 | } 23 | 24 | public func ensurePath(path: NSString) { 25 | let folderPath = path.deletingLastPathComponent 26 | try? FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: true) 27 | if FileManager.default.fileExists(atPath: path as String) { 28 | try? FileManager.default.removeItem(atPath: path as String) 29 | } 30 | } 31 | 32 | @objc 33 | public class LibraryImageSize: NSObject { 34 | private static func imageSource(path: String) -> RCTImageSource? { 35 | return RCTImageSource( 36 | urlRequest: RCTConvert.nsurlRequest(path), 37 | size: .zero, 38 | scale: 1.0 39 | ) 40 | } 41 | 42 | @objc 43 | public static func image(path: String) -> UIImage? { 44 | guard let source = imageSource(path: path), 45 | let url = source.request.url, 46 | let scheme = url.scheme?.lowercased() 47 | else { return nil } 48 | 49 | var image: UIImage? = nil 50 | if scheme == "file" { 51 | image = RCTImageFromLocalAssetURL(url) ?? RCTImageFromLocalBundleAssetURL(url) 52 | } else if scheme == "data" || scheme.hasPrefix("http") { 53 | guard let data = try? Data(contentsOf: url) else { return nil } 54 | return UIImage(data: data) 55 | } 56 | 57 | var scale = source.scale 58 | if scale == 1.0 && source.size.width > 0, let image = image { 59 | // If no scale provided, set scale to image width / source width 60 | scale = CGFloat(image.cgImage?.width ?? 1) / source.size.width; 61 | } 62 | 63 | if scale > 1.0, let i = image, let cgImage = i.cgImage { 64 | image = UIImage( 65 | cgImage: cgImage, 66 | scale: scale, 67 | orientation: i.imageOrientation 68 | ) 69 | } 70 | 71 | return image 72 | } 73 | 74 | 75 | @objc 76 | public static func getSizes(paths: [String], completion: @escaping (String) -> Void) { 77 | Task { 78 | var sizes: [Size] = [] 79 | for path in paths { 80 | if path.hasPrefix("ph://") { 81 | if let asset = MediaAssetManager.fetchRawAsset(identifier: path) { 82 | let size = Size(width: Double(asset.pixelWidth), height: Double(asset.pixelHeight)) 83 | sizes.append(size) 84 | } 85 | continue 86 | } 87 | 88 | guard let image = image(path: path) else { 89 | continue 90 | } 91 | sizes.append(.init(width: image.size.width, height: image.size.height)) 92 | } 93 | let result = String(data: (try! JSONEncoder().encode(sizes)), encoding: .utf8) ?? "[]" 94 | completion(result) 95 | } 96 | } 97 | 98 | @objc 99 | public static func save(image: UIImage, format: NSString, path: NSString) -> String? { 100 | ensurePath(path: path) 101 | do { 102 | let finalPath = toFilePath(path: path) 103 | if format == "png" { 104 | guard let data = image.pngData() else { return "LibraryImageSize.failConvertToPNG" } 105 | try data.write(to: finalPath, options: .atomic) 106 | return nil 107 | } 108 | guard let data = image.jpegData(compressionQuality: 1.0) else { return "LibraryImageSize.failConvertToJPG" } 109 | try data.write(to: finalPath, options: .atomic) 110 | return nil 111 | } catch { 112 | return "LibraryImageSize.catch.error: \(error.localizedDescription)" 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-media-library2 2 | React Native JSI access to user's media library 3 | 4 | 5 | ### Configure for iOS 6 | Add NSPhotoLibraryUsageDescription, and NSPhotoLibraryAddUsageDescription keys to your Info.plist: 7 | 8 | ```ts 9 | NSPhotoLibraryUsageDescription 10 | Give $(PRODUCT_NAME) permission to access your photos 11 | NSPhotoLibraryAddUsageDescription 12 | Give $(PRODUCT_NAME) permission to save photos 13 | ``` 14 | 15 | ### Configure for Android 16 | This package automatically adds the `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions. They are used when accessing the user's images or videos. 17 | 18 | ```ts 19 | 20 | 21 | 22 | ``` 23 | 24 | ## Installation 25 | 26 | ```sh 27 | add this line to `package.json` 28 | "react-native-media-library2": "*" 29 | yarn 30 | npx pod-install 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```js 36 | import { mediaLibrary } from "react-native-media-library2"; 37 | 38 | // ... 39 | 40 | export interface CollectionItem { 41 | readonly filename: string; 42 | readonly id: string; 43 | readonly count: number; 44 | } 45 | 46 | interface Options { 47 | mediaType?: MediaType[]; 48 | sortBy?: 'creationTime' | 'modificationTime'; 49 | sortOrder?: 'asc' | 'desc'; 50 | extensions?: string[]; 51 | requestUrls?: boolean; 52 | limit?: number; 53 | offset?: number; 54 | collectionId?: string; 55 | } 56 | 57 | interface SaveToLibrary { 58 | localUrl: string; 59 | album?: string; 60 | } 61 | 62 | export type MediaType = 'photo' | 'video' | 'audio' | 'unknown'; 63 | export interface AssetItem { 64 | readonly filename: string; 65 | readonly id: string; 66 | readonly creationTime: number; 67 | readonly modificationTime: number; 68 | readonly mediaType: MediaType; 69 | readonly duration: number; 70 | readonly width: number; 71 | readonly height: number; 72 | readonly uri: string; 73 | } 74 | 75 | export interface FullAssetItem extends AssetItem { 76 | readonly url: string; 77 | } 78 | 79 | export interface FetchThumbnailOptions { 80 | url: string; 81 | time?: number; 82 | quality?: number; 83 | } 84 | 85 | export interface Thumbnail { 86 | url: string; 87 | width: number; 88 | height: number; 89 | } 90 | 91 | export interface ImageResizeParams { 92 | uri: ImageRequireSource | string; 93 | width?: number; 94 | height?: number; 95 | format?: 'jpeg' | 'png'; 96 | resultSavePath: string; 97 | } 98 | 99 | export interface ImageCropParams { 100 | uri: ImageRequireSource | string; 101 | // offset between 0 and 1 percents of original image 102 | x: number; 103 | // offset between 0 and 1 percents of original image 104 | y: number; 105 | width: number; 106 | height: number; 107 | format?: 'jpeg' | 'png'; 108 | resultSavePath: string; 109 | } 110 | 111 | mediaLibrary.getCollections(): Promise 112 | mediaLibrary.getAssets(options?: Options): Promise 113 | mediaLibrary.getAsset(id: string): Promise 114 | // will return save asset or error string 115 | mediaLibrary.saveToLibrary(params: SaveToLibrary): Promise 116 | // returns cache directory 117 | mediaLibrary.cacheDir: string 118 | 119 | // retrieve frame from video with passed params 120 | mediaLibrary.fetchVideoFrame(params: FetchThumbnailOptions): Promise 121 | 122 | // combine passed images in one 123 | mediaLibrary.combineImages(params: { 124 | images: (ImageRequireSource | string)[]; 125 | resultSavePath: string; 126 | }): Promise<{ result: boolean }> 127 | 128 | // resize image based on passed width and height 129 | mediaLibrary.imageResize(params: ImageResizeParams): Promise<{ result: boolean }> 130 | // crop image based on passed offset 131 | mediaLibrary.imageCrop(params: ImageCropParams): Promise<{ result: boolean }> 132 | 133 | // resolve passed images sizes 134 | mediaLibrary.imageSizes(params: { 135 | images: (ImageRequireSource | string)[]; 136 | }): Promise<{ result: { width: number; height: number; size: number }[] }> 137 | ``` 138 | 139 | ## Contributing 140 | 141 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 142 | 143 | ## License 144 | 145 | MIT 146 | 147 | --- 148 | 149 | Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) 150 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // Buildscript is evaluated before everything else so we can't use getExtOrDefault 3 | def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["MediaLibrary_kotlin_version"] 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | maven { 9 | url "https://plugins.gradle.org/m2/" 10 | } 11 | } 12 | 13 | dependencies { 14 | classpath 'com.android.tools.build:gradle:4.2.2' 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | } 17 | } 18 | 19 | apply plugin: 'com.android.library' 20 | apply plugin: 'kotlin-android' 21 | 22 | 23 | def getExtOrDefault(name) { 24 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['MediaLibrary_' + name] 25 | } 26 | 27 | def safeExtGet(prop, fallback) { 28 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 29 | } 30 | 31 | def reactNativeArchitectures() { 32 | def value = project.getProperties().get("reactNativeArchitectures") 33 | return value ? value.split(",") : ["armeabi-v7a", "arm64-v8a"] 34 | } 35 | 36 | def resolveReactNativeDirectory() { 37 | def reactNativeLocation = safeExtGet("REACT_NATIVE_NODE_MODULES_DIR", null) 38 | if (reactNativeLocation != null) { 39 | return file(reactNativeLocation) 40 | } 41 | 42 | // monorepo workaround 43 | // react-native can be hoisted or in project's own node_modules 44 | def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native") 45 | if (reactNativeFromProjectNodeModules.exists()) { 46 | return reactNativeFromProjectNodeModules 47 | } 48 | 49 | throw new GradleException( 50 | "[Reanimated] Unable to resolve react-native location in " + 51 | "node_modules. You should project extension property (in app/build.gradle) " + 52 | "`REACT_NATIVE_NODE_MODULES_DIR` with path to react-native." 53 | ) 54 | } 55 | 56 | def reactNativeRootDir = resolveReactNativeDirectory() 57 | 58 | android { 59 | buildFeatures { 60 | prefab true 61 | } 62 | 63 | 64 | compileSdkVersion getExtOrDefault('compileSdkVersion') 65 | buildToolsVersion getExtOrDefault('buildToolsVersion') 66 | ndkVersion getExtOrDefault('ndkVersion') 67 | 68 | defaultConfig { 69 | minSdkVersion getExtOrDefault('minSdkVersion') 70 | targetSdkVersion getExtOrDefault('targetSdkVersion') 71 | 72 | externalNativeBuild { 73 | cmake { 74 | cppFlags "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID" 75 | abiFilters "armeabi-v7a", "arm64-v8a" 76 | arguments '-DANDROID_STL=c++_shared', 77 | "-DREACT_NATIVE_DIR=${reactNativeRootDir.path}", 78 | "-DANDROID_CPP_FEATURES=rtti exceptions", 79 | '-DANDROID_ARM_NEON=TRUE' 80 | } 81 | } 82 | ndk { 83 | abiFilters (*reactNativeArchitectures()) 84 | } 85 | } 86 | 87 | dexOptions { 88 | javaMaxHeapSize "4g" 89 | } 90 | 91 | externalNativeBuild { 92 | cmake { 93 | path "CMakeLists.txt" 94 | } 95 | } 96 | 97 | packagingOptions { 98 | // Exclude all Libraries that are already present in the user's app (through React Native or by him installing REA) 99 | excludes = [ 100 | "META-INF", 101 | "META-INF/**", 102 | "**/libc++_shared.so", 103 | "**/libfbjni.so", 104 | "**/libjsi.so", 105 | "**/libfolly_json.so", 106 | "**/libfolly_runtime.so", 107 | "**/libglog.so", 108 | "**/libhermes.so", 109 | "**/libhermes-executor-debug.so", 110 | "**/libhermes_executor.so", 111 | "**/libreactnativejni.so", 112 | "**/libturbomodulejsijni.so", 113 | "**/libreact_nativemodule_core.so", 114 | "**/libjscexecutor.so", 115 | "**/libv8executor.so", 116 | ] 117 | exclude "META-INF/**" 118 | } 119 | 120 | buildTypes { 121 | release { 122 | minifyEnabled false 123 | } 124 | } 125 | 126 | lintOptions { 127 | disable 'GradleCompatible' 128 | } 129 | compileOptions { 130 | sourceCompatibility JavaVersion.VERSION_1_8 131 | targetCompatibility JavaVersion.VERSION_1_8 132 | } 133 | 134 | configurations { 135 | extractHeaders 136 | extractJNI 137 | } 138 | } 139 | 140 | repositories { 141 | mavenCentral() 142 | google() 143 | } 144 | 145 | 146 | dependencies { 147 | implementation "com.facebook.react:react-android" 148 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${getExtOrDefault('coroutines')}" 149 | implementation "androidx.exifinterface:exifinterface:${getExtOrDefault('exifinterfaceVersion')}" 150 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${getExtOrDefault('kotlin_version')}" 151 | } 152 | -------------------------------------------------------------------------------- /example/src/screens/modal/ImagesList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { 3 | AssetItem, 4 | FetchAssetsOptions, 5 | mediaLibrary, 6 | } from 'react-native-media-library2'; 7 | import { 8 | Dimensions, 9 | FlatList, 10 | Text, 11 | TouchableOpacity, 12 | View, 13 | } from 'react-native'; 14 | import FastImage from 'react-native-fast-image'; 15 | import { useRoute } from '@react-navigation/native'; 16 | 17 | const width = Dimensions.get('window').width; 18 | export const ImagesList: React.FC<{ collection: string | undefined }> = ( 19 | props 20 | ) => { 21 | const { params } = useRoute(); 22 | const [images, setImages] = useState([]); 23 | const options = useRef({ 24 | collectionId: params?.collectionId, 25 | limit: 50, 26 | sortBy: 'creationTime', 27 | sortOrder: 'desc', 28 | }); 29 | 30 | useEffect(() => { 31 | const strat = Date.now(); 32 | console.log('[ImagesList.]', options.current); 33 | mediaLibrary.getAssets(options.current).then((r) => { 34 | console.log( 35 | '[ImagesList.initial]', 36 | (Date.now() - strat) / 1000, 37 | r.length 38 | ); 39 | setImages(r); 40 | }); 41 | }, []); 42 | 43 | const mediaType = options.current!.mediaType ?? []; 44 | const sortBy = options.current!.sortBy; 45 | let title = 'both'; 46 | if (mediaType.length === 2) title = 'photo'; 47 | if (mediaType.length === 1 && mediaType.includes('video')) title = 'both'; 48 | if (mediaType.length === 1 && mediaType.includes('photo')) title = 'video'; 49 | 50 | return ( 51 | 52 | 53 | { 56 | options.current!.sortBy = 57 | sortBy === 'creationTime' ? 'modificationTime' : 'creationTime'; 58 | mediaLibrary.getAssets(options.current).then(setImages); 59 | }} 60 | > 61 | 66 | 67 | { 70 | options.current!.sortBy = 'creationTime'; 71 | options.current!.sortOrder = 72 | options.current!.sortOrder === 'desc' ? 'asc' : 'desc'; 73 | mediaLibrary.getAssets(options.current).then(setImages); 74 | }} 75 | > 76 | 79 | 80 | { 82 | options.current!.mediaType = 83 | title === 'both' 84 | ? ['photo', 'video'] 85 | : title === 'video' 86 | ? ['video'] 87 | : ['photo']; 88 | mediaLibrary.getAssets(options.current).then(setImages); 89 | }} 90 | > 91 | 92 | 93 | 94 | 95 | numColumns={3} 96 | data={images} 97 | initialNumToRender={50} 98 | getItemLayout={(_, index) => ({ 99 | length: width / 3, 100 | offset: (width / 3) * index, 101 | index, 102 | })} 103 | onEndReached={() => { 104 | const strat = Date.now(); 105 | console.log('[ImagesList.onEndReached]'); 106 | mediaLibrary 107 | .getAssets({ 108 | ...options.current, 109 | limit: 50, 110 | offset: images.length - 1, 111 | }) 112 | .then((r) => { 113 | console.log( 114 | '[ImagesList.more]', 115 | (Date.now() - strat) / 1000, 116 | r.length 117 | ); 118 | setImages([...images, ...r]); 119 | }); 120 | }} 121 | renderItem={(info) => { 122 | return ( 123 | { 125 | const d = await mediaLibrary.getAsset(info.item.id); 126 | console.log('[ImagesList.]', JSON.stringify(d, undefined, 2)); 127 | }} 128 | > 129 | 144 | 145 | ); 146 | }} 147 | /> 148 | 149 | ); 150 | }; 151 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/AppCursor.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.ContentResolver 4 | import android.database.Cursor 5 | import android.graphics.BitmapFactory 6 | import android.media.ExifInterface 7 | import android.net.Uri 8 | import android.provider.MediaStore 9 | import android.util.Log 10 | import com.reactnativemedialibrary.AssetItemKeys.* 11 | import org.json.JSONArray 12 | import org.json.JSONObject 13 | import java.io.IOException 14 | import kotlin.math.abs 15 | 16 | private fun toSet(array: JSONArray): Set { 17 | val strings = HashSet(array.length()) 18 | for (i in 0 until array.length()) { 19 | strings.add(array.getString(i)) 20 | } 21 | return strings 22 | } 23 | 24 | private fun exportMediaType(mediaType: Int): String { 25 | return when (mediaType) { 26 | MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> AssetMediaType.photo.name 27 | MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO, MediaStore.Files.FileColumns.MEDIA_TYPE_PLAYLIST -> AssetMediaType.audio.name 28 | MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> AssetMediaType.video.name 29 | else -> "unknown" 30 | } 31 | } 32 | 33 | private fun maybeRotateAssetSize(width: Int, height: Int, orientation: Int): IntArray { 34 | // given width and height might need to be swapped if the orientation is -90 or 90 35 | return if (abs(orientation) % 180 == 90) { 36 | intArrayOf(height, width) 37 | } else { 38 | intArrayOf(width, height) 39 | } 40 | } 41 | 42 | private fun getOrientation(cursor: Cursor, uri: String, mediaType: Int): Int { 43 | val orientationIndex = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION) 44 | var orientation = cursor.getInt(orientationIndex) 45 | if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) { 46 | var exifInterface: ExifInterface? = null 47 | try { 48 | exifInterface = ExifInterface(uri) 49 | } catch (e: IOException) { 50 | Log.w("media-library", "Could not parse EXIF tags for $uri") 51 | e.printStackTrace() 52 | } 53 | if (exifInterface != null) { 54 | val exifOrientation = exifInterface.getAttributeInt( 55 | ExifInterface.TAG_ORIENTATION, 56 | ExifInterface.ORIENTATION_NORMAL 57 | ) 58 | if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 || 59 | exifOrientation == ExifInterface.ORIENTATION_ROTATE_270 || 60 | exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE || 61 | exifOrientation == ExifInterface.ORIENTATION_TRANSVERSE 62 | ) { 63 | orientation = 90 64 | } 65 | } 66 | } 67 | return orientation 68 | } 69 | 70 | private fun getAssetDimensionsFromCursor( 71 | cursor: Cursor, 72 | contentResolver: ContentResolver, 73 | mediaType: Int, 74 | localUriColumnIndex: Int 75 | ): IntArray { 76 | val size = IntArray(2) 77 | val uri = cursor.getString(localUriColumnIndex) 78 | 79 | val widthIndex = cursor.getColumnIndex(MediaStore.MediaColumns.WIDTH) 80 | val heightIndex = cursor.getColumnIndex(MediaStore.MediaColumns.HEIGHT) 81 | var width = cursor.getInt(widthIndex) 82 | var height = cursor.getInt(heightIndex) 83 | val orientation = getOrientation(cursor, uri, mediaType) 84 | val isNoWH = (width <= 0 || height <= 0) 85 | 86 | if (isNoWH && mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) { 87 | val videoUri = Uri.parse("file://$uri") 88 | MediaLibraryUtils.retrieveWidthHeightFromMedia(contentResolver, videoUri, size) 89 | if (size[0] > 0 && size[1] > 0) return maybeRotateAssetSize(size[0], size[1], orientation) 90 | } 91 | 92 | // If the image doesn't have the required information, we can get them from Bitmap.Options 93 | if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE && (width <= 0 || height <= 0)) { 94 | val options = BitmapFactory.Options() 95 | options.inJustDecodeBounds = true 96 | BitmapFactory.decodeFile(uri, options) 97 | width = options.outWidth 98 | height = options.outHeight 99 | } 100 | return maybeRotateAssetSize(width, height, orientation) 101 | } 102 | 103 | fun Cursor.mapToJson( 104 | contentResolver: ContentResolver, 105 | array: JSONArray, 106 | input: JSONObject, 107 | limit: Int 108 | ) { 109 | if (count == 0) return 110 | val idIndex = getColumnIndex(MediaStore.Images.Media._ID) 111 | val filenameIndex = getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME) 112 | val mediaTypeIndex = getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE) 113 | val creationDateIndex = getColumnIndex(MediaLibrary.dateAdded) 114 | val modificationDateIndex = 115 | getColumnIndex(MediaStore.Files.FileColumns.DATE_MODIFIED) 116 | val durationIndex = getColumnIndex(MediaStore.Video.VideoColumns.DURATION) 117 | val localUriIndex = getColumnIndex(MediaStore.Images.Media.DATA) 118 | var extensions: Set? = null 119 | if (input.has("extensions")) { 120 | extensions = toSet(input.getJSONArray("extensions")) 121 | } 122 | while (moveToNext()) { 123 | val assetId = getString(idIndex) 124 | val path = getString(localUriIndex) 125 | val extension = path.substring(path.lastIndexOf(".") + 1) 126 | if (extensions != null && !extensions.contains(extension)) { 127 | continue 128 | } 129 | val localUri = "file://$path" 130 | val mediaType = getInt(mediaTypeIndex) 131 | val widthHeight = 132 | getAssetDimensionsFromCursor(this, contentResolver, mediaType, localUriIndex) 133 | val `object` = JSONObject() 134 | `object`.put(filename.name, getString(filenameIndex)) 135 | `object`.put(id.name, assetId) 136 | `object`.put(creationTime.name, getLong(creationDateIndex) * 1000.0) 137 | `object`.put(modificationTime.name, getLong(modificationDateIndex) * 1000.0) 138 | `object`.put(AssetItemKeys.mediaType.name, exportMediaType(mediaType)) 139 | `object`.put(duration.name, getInt(durationIndex) / 1000.0) 140 | `object`.put(width.name, widthHeight[0]) 141 | `object`.put(height.name, widthHeight[1]) 142 | `object`.put(uri.name, localUri) 143 | array.put(`object`) 144 | if (limit == 1) break 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/MediaLibrary.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.provider.MediaStore 6 | import android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE 7 | import com.facebook.jni.HybridData 8 | import com.facebook.proguard.annotations.DoNotStrip 9 | import com.facebook.react.bridge.ReactApplicationContext 10 | import com.facebook.react.common.annotations.FrameworkAPI 11 | import com.facebook.react.turbomodule.core.CallInvokerHolderImpl 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.SupervisorJob 15 | import kotlinx.coroutines.launch 16 | import org.json.JSONObject 17 | import java.lang.ref.WeakReference 18 | 19 | @DoNotStrip 20 | class MediaLibrary(context: Context) { 21 | @DoNotStrip 22 | private var mHybridData: HybridData? = null 23 | 24 | @OptIn(FrameworkAPI::class) 25 | @Suppress("KotlinJniMissingFunction") 26 | external fun initHybrid( 27 | jsContext: Long, 28 | jsCallInvokerHolder: CallInvokerHolderImpl 29 | ): HybridData? 30 | 31 | @Suppress("KotlinJniMissingFunction") 32 | external fun installJSIBindings() 33 | private val context: Context 34 | private val manipulateImages: ManipulateImages 35 | 36 | private val job = SupervisorJob() 37 | val scope = CoroutineScope(Dispatchers.IO + job) 38 | 39 | init { 40 | this.context = context.applicationContext 41 | manipulateImages = ManipulateImages(context.applicationContext) 42 | MediaLibrary.context = WeakReference(context.applicationContext) 43 | } 44 | 45 | @OptIn(FrameworkAPI::class) 46 | fun install(context: ReactApplicationContext): Boolean { 47 | System.loadLibrary("react-native-media-library") 48 | val jsContext = context.javaScriptContextHolder!! 49 | val jsCallInvokerHolder = context.catalystInstance.jsCallInvokerHolder 50 | mHybridData = initHybrid( 51 | jsContext.get(), 52 | jsCallInvokerHolder as CallInvokerHolderImpl 53 | ) 54 | installJSIBindings() 55 | return true 56 | } 57 | 58 | @DoNotStrip 59 | fun getAssets(params: String, callback: GetAssetsCallback) { 60 | scope.launch { 61 | val contentResolver = context.contentResolver 62 | val jsonArray = contentResolver.listQuery( 63 | EXTERNAL_CONTENT_URI, 64 | context, 65 | params.asJsonInput() 66 | ) 67 | callback.onChange(jsonArray.toString()) 68 | } 69 | } 70 | 71 | @DoNotStrip 72 | fun getCollections(callback: GetAssetsCallback) { 73 | println("😀 getCollections") 74 | scope.launch { 75 | val contentResolver = context.contentResolver 76 | val jsonArray = contentResolver.getCollections(MEDIA_TYPE_IMAGE) 77 | callback.onChange(jsonArray.toString()) 78 | } 79 | } 80 | 81 | @DoNotStrip 82 | fun getAsset(id: String, callback: GetAssetsCallback) { 83 | scope.launch { 84 | val contentResolver = context.contentResolver 85 | val jsonArray = contentResolver.singleQuery( 86 | EXTERNAL_CONTENT_URI, 87 | context, 88 | JSONObject(), 89 | id 90 | ) 91 | if (jsonArray.length() == 0) { 92 | return@launch callback.onChange("") 93 | } 94 | val media = jsonArray.getJSONObject(0) 95 | MediaLibraryUtils.getMediaLocation(media, contentResolver) 96 | callback.onChange(media.toString()) 97 | } 98 | } 99 | 100 | @DoNotStrip 101 | fun saveToLibrary(params: String, callback: GetAssetsCallback) { 102 | scope.launch { 103 | val input = params.asJsonInput() 104 | MedialLibraryCreateAsset.saveToLibrary(input, context) { error, id -> 105 | if (error != null) { 106 | callback.onChange(error) 107 | } else { 108 | getAsset(id!!, callback) 109 | } 110 | } 111 | } 112 | } 113 | 114 | @DoNotStrip 115 | fun fetchVideoFrame(params: String, callback: GetAssetsCallback) { 116 | scope.launch { 117 | val input = params.asJsonInput() 118 | val response = context.fetchFrame(input) 119 | if (response == null) { 120 | callback.onChange("") 121 | } else { 122 | callback.onChange(response.toString()) 123 | } 124 | } 125 | } 126 | 127 | @DoNotStrip 128 | fun combineImages(params: String, callback: GetAssetsCallback) { 129 | scope.launch { 130 | val input = params.asJsonInput() 131 | if (manipulateImages.combineImages(input)) { 132 | callback.onChange("{\"result\": true}") 133 | } else { 134 | callback.onChange("{\"result\": false}") 135 | } 136 | } 137 | } 138 | 139 | @DoNotStrip 140 | fun imageResize(params: String, callback: GetAssetsCallback) { 141 | scope.launch { 142 | val input = params.asJsonInput() 143 | if (manipulateImages.imageResize(input)) { 144 | callback.onChange("{\"result\": true}") 145 | } else { 146 | callback.onChange("{\"result\": false}") 147 | } 148 | } 149 | } 150 | 151 | @DoNotStrip 152 | fun imageCrop(params: String, callback: GetAssetsCallback) { 153 | scope.launch { 154 | val input = params.asJsonInput() 155 | if (manipulateImages.imageCrop(input)) { 156 | callback.onChange("{\"result\": true}") 157 | } else { 158 | callback.onChange("{\"result\": false}") 159 | } 160 | } 161 | } 162 | 163 | @DoNotStrip 164 | fun imageSizes(params: String, callback: GetAssetsCallback) { 165 | scope.launch { 166 | val input = params.asJsonInput() 167 | callback.onChange(manipulateImages.imageSizes(input).toString()) 168 | } 169 | } 170 | 171 | @DoNotStrip 172 | fun downloadAsBase64(params: String, callback: GetAssetsCallback) { 173 | scope.launch { 174 | val input = params.asJsonInput() 175 | val base64String = Base64Downloader.download(input.getString("url")) 176 | val response = JSONObject() 177 | response.put("base64", base64String) 178 | callback.onChange(response.toString()) 179 | } 180 | } 181 | 182 | @DoNotStrip 183 | fun cacheDir(): String { 184 | return context.cacheDir.absolutePath 185 | } 186 | 187 | companion object { 188 | var EXTERNAL_CONTENT_URI: Uri = MediaStore.Files.getContentUri("external") 189 | var dateAdded = MediaStore.Files.FileColumns.DATE_ADDED 190 | var context: WeakReference? = null 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /ios/LibrarySaveToCameraRoll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibrarySaveToCameraRoll.swift 3 | // MediaLibrary 4 | // 5 | // Created by sergeymild on 03/08/2023. 6 | // Copyright © 2023 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreServices 11 | import Photos 12 | 13 | class Result { 14 | internal init(error: String? = nil, result: String? = nil) { 15 | self.error = error 16 | self.result = result 17 | } 18 | 19 | var error: String? 20 | var result: String? 21 | } 22 | 23 | @objc 24 | public class LibrarySaveToCameraRoll: NSObject { 25 | private static func assetTypeFor(uri: NSString) -> PHAssetMediaType { 26 | let fileExtension = uri.pathExtension 27 | guard let fileUTI = UTTypeCreatePreferredIdentifierForTag( 28 | kUTTagClassFilenameExtension, 29 | fileExtension as CFString, 30 | nil 31 | ) else { return .unknown } 32 | let value = fileUTI.takeRetainedValue() 33 | if (UTTypeConformsTo(value, kUTTypeImage)) { return .image } 34 | if (UTTypeConformsTo(value, kUTTypeMovie)) { return .video } 35 | if (UTTypeConformsTo(value, kUTTypeAudio)) { return .audio } 36 | return .unknown 37 | } 38 | 39 | private static func normalizeAssetURLFromUri(uri: NSString) -> URL? { 40 | if uri.hasPrefix("/") { 41 | return URL(string: "file://\(uri)") 42 | } 43 | return URL(string: uri as String) 44 | } 45 | 46 | private static func saveBlock( 47 | assetType: PHAssetMediaType, 48 | url: URL, 49 | collection: PHAssetCollection? 50 | ) async -> Result { 51 | let result = Result() 52 | do { 53 | try await PHPhotoLibrary.shared().performChanges { 54 | let changeRequest = assetType == .video 55 | ? PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url) 56 | : PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url) 57 | 58 | let assetPlaceholder = changeRequest!.placeholderForCreatedAsset 59 | 60 | if let collection = collection { 61 | let photosAsset = PHAsset.fetchAssets( 62 | in: collection, 63 | options: nil 64 | ) 65 | let albumChangeRequest = PHAssetCollectionChangeRequest( 66 | for: collection, 67 | assets: photosAsset 68 | ) 69 | albumChangeRequest?.addAssets([assetPlaceholder] as NSFastEnumeration) 70 | } 71 | result.error = nil 72 | result.result = assetPlaceholder!.localIdentifier 73 | } 74 | 75 | } 76 | catch let error { 77 | print(error.localizedDescription) 78 | result.error = error.localizedDescription 79 | } 80 | return result 81 | } 82 | 83 | private static func saveWithOptions( 84 | album: String?, 85 | assetType: PHAssetMediaType, 86 | url: URL, 87 | collection: PHAssetCollection? 88 | ) async -> Result { 89 | if album == nil || album!.isEmpty { 90 | return await saveBlock(assetType: assetType, url: url, collection: collection) 91 | } 92 | 93 | let fetchOptions = PHFetchOptions() 94 | fetchOptions.predicate = NSPredicate(format: "title = %@", argumentArray: [album!]) 95 | 96 | let collection = PHAssetCollection.fetchAssetCollections( 97 | with: .album, 98 | subtype: .any, 99 | options: fetchOptions 100 | ) 101 | if let collection = collection.firstObject { 102 | return await saveBlock(assetType: assetType, url: url, collection: collection) 103 | } 104 | 105 | // create new collection 106 | var placeholder: PHObjectPlaceholder? 107 | 108 | let result = Result() 109 | do { 110 | try await PHPhotoLibrary.shared().performChanges { 111 | let createAlbum = PHAssetCollectionChangeRequest.creationRequestForAssetCollection( 112 | withTitle: album! 113 | ) 114 | placeholder = createAlbum.placeholderForCreatedAssetCollection 115 | } 116 | // if collection was created fetch it 117 | if let placeholder = placeholder { 118 | let collection = PHAssetCollection.fetchAssetCollections( 119 | withLocalIdentifiers: [placeholder.localIdentifier], 120 | options: nil 121 | ).firstObject 122 | if let collection = collection { 123 | return await Self.saveBlock( 124 | assetType: assetType, 125 | url: url, 126 | collection: collection 127 | ) 128 | } 129 | } 130 | } 131 | catch let error { 132 | print(error.localizedDescription) 133 | result.error = error.localizedDescription 134 | } 135 | 136 | return result 137 | } 138 | 139 | @objc 140 | public static func saveToCameraRoll( 141 | localUri: NSString, 142 | album: String, 143 | callback: @escaping (String?, String?) -> Void 144 | ) { 145 | Task { 146 | if Bundle.main.object(forInfoDictionaryKey: "NSPhotoLibraryAddUsageDescription") == nil { 147 | return callback("kErrorNoPermissions", nil) 148 | } 149 | if localUri.pathExtension.isEmpty { 150 | return callback("kErrorNoFileExtension", nil); 151 | } 152 | 153 | let assetType = Self.assetTypeFor(uri: localUri) 154 | if assetType == .audio || assetType == .unknown { 155 | return callback("kErrorUnsupportedAsset", nil) 156 | } 157 | 158 | guard let assetUrl = Self.normalizeAssetURLFromUri(uri: localUri) else { 159 | return callback("kErrorNotValidUri", nil) 160 | } 161 | 162 | let result = await saveWithOptions( 163 | album: album, 164 | assetType: assetType, 165 | url: assetUrl, 166 | collection: nil 167 | ) 168 | if let error = result.error { 169 | return callback(error, nil) 170 | } 171 | if let id = result.result { 172 | return MediaAssetManager.fetchAsset(identifier: id) { data in 173 | if let data = data { 174 | return callback(nil, data) 175 | } 176 | callback("kErrorFetchAssetData", nil) 177 | } 178 | } 179 | return callback("kErrorSaveToCameraRoll", nil) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/ManipulateImages.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapFactory 7 | import android.graphics.Canvas 8 | import android.graphics.Paint 9 | import android.util.Log 10 | import androidx.annotation.ColorInt 11 | import com.facebook.react.uimanager.PixelUtil 12 | import org.json.JSONArray 13 | import org.json.JSONObject 14 | import java.io.ByteArrayOutputStream 15 | import java.io.File 16 | import java.io.InputStream 17 | import java.lang.RuntimeException 18 | import java.net.URI 19 | import java.net.URL 20 | 21 | 22 | fun toCompressFormat(format: String): Bitmap.CompressFormat { 23 | return when (format) { 24 | "jpeg" -> Bitmap.CompressFormat.JPEG 25 | "jpg" -> Bitmap.CompressFormat.JPEG 26 | "png" -> Bitmap.CompressFormat.PNG 27 | else -> Bitmap.CompressFormat.PNG 28 | } 29 | } 30 | 31 | class ManipulateImages(private val context: Context) { 32 | fun combineImages(input: JSONObject): Boolean { 33 | val imagesArray = input.getJSONArray("images") 34 | val resultSavePath = input.getString("resultSavePath").fixFilePathFromJs() 35 | val mainImageIndex = input.optInt("mainImageIndex", -1).takeIf { it != -1 } ?: 0 36 | 37 | @ColorInt 38 | val backgroundColor = input.optInt("backgroundColor", -1).takeIf { it != -1 } 39 | val file = File(resultSavePath) 40 | 41 | return try { 42 | val mainImageJson = imagesArray.getJSONObject(mainImageIndex) 43 | val mainImage = mainImageJson.getString("image").fixFilePathFromJs() 44 | println("🗡️ $mainImage") 45 | val mainBitmap = getBitmapFromUrl(mainImage)?.copy(Bitmap.Config.ARGB_8888, true) 46 | mainBitmap ?: return false 47 | val canvas = Canvas(mainBitmap) 48 | val parentCenterX = canvas.width / 2F 49 | val parentCenterY = canvas.height / 2F 50 | 51 | if (backgroundColor != null) { 52 | canvas.drawPaint(Paint().apply { 53 | color = backgroundColor 54 | style = Paint.Style.FILL 55 | }) 56 | } 57 | 58 | for (i in 0 until (imagesArray.length())) { 59 | val obj = imagesArray.getJSONObject(i) 60 | val url = obj.getString("image").fixFilePathFromJs() 61 | if (mainImageIndex == i) continue 62 | println("🗡️ $url") 63 | val bitmap = getBitmapFromUrl(url) ?: return false 64 | val positions = obj.optJSONObject("positions") 65 | var x = parentCenterX - bitmap.width / 2F 66 | var y = parentCenterY - bitmap.height / 2F 67 | if (positions != null) { 68 | x = positions.getDouble("x").toFloat() 69 | y = positions.getDouble("y").toFloat() 70 | if (x > canvas.width) x = (canvas.width - bitmap.width).toFloat() 71 | if (y > canvas.height) y = (canvas.height - bitmap.height).toFloat() 72 | if (x < 0) x = 0F 73 | if (y <= 0) y = 0F 74 | } 75 | canvas.drawBitmap(bitmap, x, y, null) 76 | bitmap.recycle() 77 | } 78 | if (file.exists()) file.delete() 79 | if (file.parentFile?.exists() != true) { 80 | if (file.parentFile?.mkdirs() != true) return false 81 | } 82 | val byteArrayOutputStream = ByteArrayOutputStream() 83 | mainBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) 84 | val byteArray = byteArrayOutputStream.toByteArray() 85 | file.writeBytes(byteArray) 86 | mainBitmap.recycle() 87 | true 88 | } catch (e: Throwable) { 89 | Log.e("CombineImages", null, e) 90 | false 91 | } 92 | } 93 | 94 | fun imageResize(input: JSONObject): Boolean { 95 | val uri = input.getString("uri").fixFilePathFromJs() 96 | val width = input.getDouble("width") 97 | val height = input.getDouble("height") 98 | val format = input.getString("format") 99 | val resultSavePath = input.getString("resultSavePath").fixFilePathFromJs() 100 | val file = File(resultSavePath) 101 | 102 | return try { 103 | var bitmap = getBitmapFromUrl(uri) ?: return false 104 | val currentImageRatio = bitmap.width.toFloat() / bitmap.height 105 | var newWidth = 0 106 | var newHeight = 0 107 | if (width >= 0) { 108 | newWidth = width.toInt() 109 | newHeight = (newWidth / currentImageRatio).toInt() 110 | } 111 | 112 | if (height >= 0) { 113 | newHeight = height.toInt() 114 | newWidth = if (newWidth == 0) (currentImageRatio * newHeight).toInt() else newWidth 115 | } 116 | bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) 117 | 118 | file.outputStream().use { fileOut -> 119 | bitmap.compress(toCompressFormat(format), 100, fileOut) 120 | } 121 | true 122 | } catch (e: Throwable) { 123 | Log.e("CombineImages", null, e) 124 | false 125 | } 126 | } 127 | 128 | fun imageCrop(input: JSONObject): Boolean { 129 | val uri = input.getString("uri").fixFilePathFromJs() 130 | val x = input.getDouble("x") 131 | val y = input.getDouble("y") 132 | val width = input.getDouble("width") 133 | val height = input.getDouble("height") 134 | val format = input.getString("format") 135 | val resultSavePath = input.getString("resultSavePath").fixFilePathFromJs() 136 | val file = File(resultSavePath) 137 | 138 | return try { 139 | var bitmap = getBitmapFromUrl(uri) ?: return false 140 | var cropX = (x * bitmap.width).toInt() 141 | if (cropX + width > bitmap.width) { 142 | cropX = bitmap.width - width.toInt() 143 | } 144 | 145 | var cropY = (y * bitmap.height).toInt() 146 | if (cropY + height > bitmap.height) { 147 | cropY = bitmap.height - height.toInt() 148 | } 149 | 150 | 151 | bitmap = Bitmap.createBitmap(bitmap, cropX, cropY, width.toInt(), height.toInt(), null, true) 152 | 153 | file.outputStream().use { fileOut -> 154 | bitmap.compress(toCompressFormat(format), 100, fileOut) 155 | } 156 | true 157 | } catch (e: Throwable) { 158 | Log.e("CombineImages", null, e) 159 | false 160 | } 161 | } 162 | 163 | // TODO: rename to getImagesDimensions 164 | fun imageSizes(input: JSONObject): JSONArray { 165 | val imagesArray = input.getJSONArray("images") 166 | val imagesDimensions = JSONArray() 167 | 168 | Log.d("MediaLibrary", "getImagesDimensions") 169 | 170 | try { 171 | for (i in 0 until imagesArray.length()) { 172 | val url = imagesArray.getString(i).fixFilePathFromJs() 173 | val bitmap = getBitmapFromUrl(url) ?: throw RuntimeException("failCreateBitmap") 174 | 175 | val imageDimensions = JSONObject() 176 | imageDimensions.put("width", bitmap.width) 177 | imageDimensions.put("height", bitmap.height) 178 | imagesDimensions.put(imageDimensions) 179 | } 180 | } catch (error: Throwable) { 181 | Log.e("MediaLibrary", "Couldn't determine Image Dimensions", error) 182 | } 183 | 184 | return imagesDimensions 185 | } 186 | 187 | private fun getBitmapFromUrl(source: String?): Bitmap? { 188 | source ?: return null 189 | val resourceId: Int = 190 | context.resources.getIdentifier(source, "drawable", context.packageName) 191 | 192 | return if (resourceId == 0) { 193 | val uri = URI(source.fixFilePathToJs()) 194 | BitmapFactory.decodeStream(uri.toURL().openConnection().getInputStream()) 195 | } else { 196 | BitmapFactory.decodeResource(context.resources, resourceId) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { PermissionsAndroid, StyleSheet, View } from 'react-native'; 4 | import { ShowcaseApp } from '@gorhom/showcase-template'; 5 | import { screens } from './screens'; 6 | 7 | const requestCameraPermission = async () => { 8 | try { 9 | const granted = await PermissionsAndroid.request( 10 | 'android.permission.READ_EXTERNAL_STORAGE', 11 | 'android.permission.ACCESS_MEDIA_LOCATION' 12 | ); 13 | if (granted === PermissionsAndroid.RESULTS.GRANTED) { 14 | console.log('You can use the camera'); 15 | } else { 16 | console.log('Camera permission denied'); 17 | } 18 | } catch (err) { 19 | console.warn(err); 20 | } 21 | }; 22 | 23 | const author = { 24 | username: 'SergeyMild', 25 | url: 'https://github.com/sergeymild', 26 | }; 27 | 28 | 29 | export default function App() { 30 | return ( 31 | 32 | 39 | 40 | ); 41 | } 42 | 43 | // export default function App() { 44 | // const [image, setImage] = useState(); 45 | // const [openCollection, setOpenCollection] = useState(); 46 | // // requestCameraPermission(); 47 | // // if (true) { 48 | // // return ; 49 | // // } 50 | // 51 | // // if (openCollection) { 52 | // // return ; 53 | // // } 54 | // 55 | // // return ; 56 | // 57 | // // return ; 58 | // 59 | // return ( 60 | // 61 | // { 64 | // const start = Date.now(); 65 | // 66 | // // 67 | // // const result = await mediaLibrary.getAssets({ 68 | // // limit: 10, 69 | // // sortBy: 'creationTime', 70 | // // sortOrder: 'desc', 71 | // // }); 72 | // // console.log('[App.]', result.length); 73 | // // for (let assetItem of result) { 74 | // // console.log('[App.]', await mediaLibrary.getAsset(assetItem.id)); 75 | // // } 76 | // 77 | // console.log('[App.]', mediaLibrary.cacheDir); 78 | // // console.log('[App.]'); 79 | // // const t = await mediaLibrary.imageResize({ 80 | // // width: 200, 81 | // // format: 'png', 82 | // // uri: `${mediaLibrary.cacheDir}/3.jpeg`, 83 | // // resultSavePath: `${mediaLibrary.cacheDir}/result.png`, 84 | // // }); 85 | // // console.log('[App.---- ]', t); 86 | // // const s = await mediaLibrary.getAssets({}); 87 | // // console.log( 88 | // // '[App.]', 89 | // // s.find((ss) => ss.filename.startsWith('image.png')) 90 | // // ); 91 | // // console.log( 92 | // // '[App.]', 93 | // // await mediaLibrary.imageSizes({ 94 | // // images: [ 95 | // // (await mediaLibrary.getAssets({ limit: 1 }))[0].uri, 96 | // // `${mediaLibrary.cacheDir}/3.png`, 97 | // // 'https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png', 98 | // // require('../assets/3.png'), 99 | // // ], 100 | // // }) 101 | // // ); 102 | // // console.log('[App.imageSizes]'); 103 | // const isSuccess = await mediaLibrary.combineImages({ 104 | // images: [ 105 | // `https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png`, 106 | // require('../assets/3.png'), 107 | // ], 108 | // resultSavePath: `${__mediaLibrary.cacheDir()}/tmp/re/result.png`, 109 | // }); 110 | // console.log( 111 | // '[App.save]', 112 | // isSuccess, 113 | // `${__mediaLibrary.cacheDir()}/tmp/re/result.png` 114 | // ); 115 | // // const saveResponse = await mediaLibrary.saveToLibrary( 116 | // // // `/data/user/0/com.example.reactnativemedialibrary/files/2222.jpg` 117 | // // `${__mediaLibrary.docDir()}/ls.jpg` 118 | // // ); 119 | // // console.log('[App.save]', saveResponse); 120 | // 121 | // // requestCameraPermission(); 122 | // // console.log( 123 | // // '[App.]', 124 | // // await FS.exists(`${FS.CachesDirectoryPath}/3.jpg`) 125 | // // ); 126 | // // const response = await mediaLibrary.getAssets({ 127 | // // //onlyFavorites: true, 128 | // // mediaType: ['video'], 129 | // // }); 130 | // // for (let assetItem of response.filter( 131 | // // (s) => s.mediaType === 'video' 132 | // // )) { 133 | // // let newVar = await mediaLibrary.getAsset(assetItem.id); 134 | // // console.log( 135 | // // '[App.]', 136 | // // newVar?.location, 137 | // // newVar?.width, 138 | // // newVar?.height 139 | // // ); 140 | // // } 141 | // 142 | // // console.log( 143 | // // '[App.]', 144 | // // await mediaLibrary.fetchVideoFrame({ 145 | // // time: 6, 146 | // // url: ( 147 | // // await mediaLibrary.getAsset( 148 | // // response.filter((s) => s.mediaType === 'video')[0].id 149 | // // ) 150 | // // )?.url, 151 | // // quality: 0.4, 152 | // // }) 153 | // // ); 154 | // 155 | // // const saveR = await mediaLibrary.saveToLibrary({ 156 | // // // localUrl: `${__mediaLibrary.docDir()}/ls.jpg`, 157 | // // localUrl: `/data/user/0/com.example.reactnativemedialibrary/files/2222.jpg`, 158 | // // album: 'some', 159 | // // }); 160 | // // console.log('[App.]', saveR); 161 | // // const assets = mediaLibrary.getAssets({ 162 | // // requestUrls: false, 163 | // // limit: 1, 164 | // // mediaType: ['photo'], 165 | // // extensions: ['jpg'], 166 | // // sortBy: 'creationTime', 167 | // // sortOrder: 'asc', 168 | // // }); 169 | // // console.log( 170 | // // '[App.]', 171 | // // assets.map((e) => e.creationTime) 172 | // // ); 173 | // // const end = start - Date.now(); 174 | // // // console.log('[App.]', JSON.stringify(assets, undefined, 2)); 175 | // // console.log( 176 | // // '[App.]', 177 | // // assets.length, 178 | // // assets[0], 179 | // // end 180 | // // //mediaLibrary.getAssetUrl(assets[0].id) 181 | // // ); 182 | // // console.log('[App.]', mediaLibrary.getAsset(assets[0].id)); 183 | // // setImage(assets[1].uri); 184 | // }} 185 | // > 186 | // Photos 187 | // 188 | // {!!image && ( 189 | // 193 | // )} 194 | // 195 | // ); 196 | // } 197 | 198 | const styles = StyleSheet.create({ 199 | container: { 200 | flex: 1, 201 | flexGrow: 1, 202 | } 203 | }); 204 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/MediaLibraryUtils.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.ContentResolver 4 | import android.graphics.Bitmap 5 | import android.media.MediaMetadataRetriever 6 | import android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT 7 | import android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.os.Environment 11 | import android.os.ParcelFileDescriptor 12 | import android.provider.MediaStore 13 | import android.webkit.MimeTypeMap 14 | import android.webkit.URLUtil 15 | import androidx.exifinterface.media.ExifInterface 16 | import org.json.JSONException 17 | import org.json.JSONObject 18 | import java.io.File 19 | import java.io.IOException 20 | import java.util.* 21 | import java.util.regex.Matcher 22 | import java.util.regex.Pattern 23 | 24 | 25 | object MediaLibraryUtils { 26 | 27 | val retriever: MediaMetadataRetriever 28 | get() = MediaMetadataRetriever() 29 | 30 | private val iSO6709LocationPattern = Pattern.compile("([+\\-][0-9.]+)([+\\-][0-9.]+)") 31 | 32 | fun getMimeTypeFromFileUrl(url: String?): String? { 33 | val extension = MimeTypeMap.getFileExtensionFromUrl(url) ?: return null 34 | return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) 35 | } 36 | 37 | fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? { 38 | val type = contentResolver.getType(uri) 39 | return type ?: getMimeTypeFromFileUrl(uri.toString()) 40 | } 41 | 42 | fun getRelativePathForAssetType(mimeType: String?, useCameraDir: Boolean): String { 43 | if (mimeType != null && (mimeType.contains("image") || mimeType.contains("video"))) { 44 | return if (useCameraDir) Environment.DIRECTORY_DCIM else Environment.DIRECTORY_PICTURES 45 | } else if (mimeType != null && mimeType.contains("audio")) { 46 | return Environment.DIRECTORY_MUSIC 47 | } 48 | return if (useCameraDir) Environment.DIRECTORY_DCIM else Environment.DIRECTORY_PICTURES 49 | } 50 | 51 | fun getEnvDirectoryForAssetType(mimeType: String?, useCameraDir: Boolean): File { 52 | return Environment.getExternalStoragePublicDirectory( 53 | getRelativePathForAssetType(mimeType, useCameraDir) 54 | ) 55 | } 56 | 57 | fun copyBitmapToFile( 58 | bitmap: Bitmap, 59 | path: String, 60 | format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, 61 | quality: Int = 100, 62 | ): Boolean { 63 | val file = File(path) 64 | if (file.extension.isEmpty()) { 65 | println("ERROR: extension must be present, FILE: ${file.absolutePath}") 66 | return false 67 | } 68 | return File(path).outputStream().use { output -> 69 | return@use bitmap.compress(format, quality, output) 70 | } 71 | } 72 | 73 | @Throws(IOException::class, JSONException::class) 74 | fun safeCopyFile(src: File, destDir: File?): Any { 75 | println("👌 ${src.absolutePath}") 76 | var newFile = File(destDir, src.name) 77 | var suffix = 0 78 | val filename = src.nameWithoutExtension 79 | val extension = src.extension 80 | val suffixLimit = Short.MAX_VALUE.toInt() 81 | while (newFile.exists()) { 82 | newFile = File(destDir, "${filename}_$suffix.$extension") 83 | suffix++ 84 | if (suffix > suffixLimit) { 85 | return "E_FILE_NAME_SUFFIX_REACHED" 86 | } 87 | } 88 | 89 | src.inputStream().channel.use { input -> 90 | newFile.outputStream().channel.use { output -> 91 | val transferred = input.transferTo(0, input.size(), output) 92 | if (transferred != input.size()) { 93 | newFile.delete() 94 | return "E_COULD_NOT_SAVE_FILE" 95 | } 96 | } 97 | } 98 | return newFile 99 | } 100 | 101 | inline fun withRetriever(contentResolver: ContentResolver, uri: Uri, handler: (MediaMetadataRetriever) -> Unit) { 102 | try { 103 | val path = uri.path ?: return 104 | val r = retriever 105 | var openFileDescriptor: ParcelFileDescriptor? = null 106 | if (URLUtil.isFileUrl(path)) { 107 | r.setDataSource(path.replace("file://", "")) 108 | } else if (URLUtil.isContentUrl(path)) { 109 | openFileDescriptor = contentResolver.openFileDescriptor(uri, "r") 110 | val fileDescriptor = openFileDescriptor?.fileDescriptor 111 | r.setDataSource(fileDescriptor) 112 | } else { 113 | r.setDataSource(path) 114 | } 115 | handler(r) 116 | openFileDescriptor?.close() 117 | r.release() 118 | } catch (e: java.lang.RuntimeException) { 119 | println(e.message) 120 | } 121 | } 122 | 123 | fun retrieveWidthHeightFromMedia(contentResolver: ContentResolver, videoUri: Uri, size: IntArray) { 124 | withRetriever(contentResolver, videoUri) { 125 | val videoWidth = it.extractMetadata(METADATA_KEY_VIDEO_WIDTH) 126 | val videoHeight = it.extractMetadata(METADATA_KEY_VIDEO_HEIGHT) 127 | size[0] = videoWidth?.toInt() ?: 0 128 | size[1] = videoHeight?.toInt() ?: 0 129 | } 130 | } 131 | 132 | private fun parseStringLocation(location: String?): DoubleArray? { 133 | if (location == null) return null 134 | val m: Matcher = iSO6709LocationPattern.matcher(location) 135 | if (m.find() && m.groupCount() == 2) { 136 | val latstr = m.group(1) ?: return null 137 | val lonstr = m.group(2) ?: return null 138 | try { 139 | val lat = latstr.toDouble() 140 | val lon = lonstr.toDouble() 141 | return doubleArrayOf(lat, lon) 142 | } catch (ignored: NumberFormatException) { 143 | } 144 | } 145 | return null 146 | } 147 | 148 | fun getMediaLocation(media: JSONObject, contentResolver: ContentResolver) { 149 | val latLong = DoubleArray(2) 150 | val localUri = media.getString("uri") 151 | val uri = Uri.parse(localUri) 152 | 153 | if (media.getString(AssetItemKeys.mediaType.name) == AssetMediaType.video.name) { 154 | withRetriever(contentResolver, uri) { 155 | val locationMetadata = it.extractMetadata( 156 | MediaMetadataRetriever.METADATA_KEY_LOCATION 157 | ) 158 | parseStringLocation(locationMetadata)?.let { result -> 159 | val location = JSONObject() 160 | location.put("latitude", result[0]) 161 | location.put("longitude", result[1]) 162 | media.put("location", location) 163 | } 164 | } 165 | } else { 166 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 167 | MediaStore.setRequireOriginal(uri) 168 | } 169 | contentResolver.openInputStream(uri)?.use { 170 | val exifInterface = ExifInterface(it) 171 | exifInterface.latLong?.let { latLng -> 172 | latLong[0] = latLng[0] 173 | latLong[1] = latLng[1] 174 | } 175 | } 176 | } 177 | if (latLong[0] != 0.0 && latLong[1] != 0.0) { 178 | val location = JSONObject() 179 | location.put("latitude", latLong[0]) 180 | location.put("longitude", latLong[1]) 181 | media.put("location", location) 182 | } 183 | } 184 | 185 | private fun ensureDirExists(dir: File): File? { 186 | if (!(dir.isDirectory || dir.mkdirs())) { 187 | throw IOException("Couldn't create directory '$dir'") 188 | } 189 | return dir 190 | } 191 | 192 | fun generateOutputPath(internalDirectory: File, dirName: String, extension: String): String { 193 | val directory = File(internalDirectory.toString() + File.separator + dirName) 194 | ensureDirExists(directory) 195 | val filename = UUID.randomUUID().toString() 196 | return directory.toString() + File.separator + filename + if (extension.startsWith(".")) extension else ".$extension" 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativemedialibrary/AppContentResolver.kt: -------------------------------------------------------------------------------- 1 | package com.reactnativemedialibrary 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.provider.MediaStore 9 | import android.provider.MediaStore.Files.FileColumns.* 10 | import androidx.annotation.RequiresApi 11 | import org.json.JSONArray 12 | import org.json.JSONObject 13 | 14 | fun String?.asJsonInput(): JSONObject { 15 | if (this != null) return JSONObject(this) 16 | return JSONObject() 17 | } 18 | 19 | var ASSET_PROJECTION = arrayOf( 20 | _ID, 21 | DISPLAY_NAME, 22 | DATA, 23 | //MediaStore.Files.FileColumns.IS_FAVORITE, 24 | MEDIA_TYPE, 25 | MediaStore.MediaColumns.WIDTH, 26 | MediaStore.MediaColumns.WIDTH, 27 | MediaStore.MediaColumns.HEIGHT, 28 | MediaLibrary.dateAdded, 29 | DATE_MODIFIED, 30 | MediaStore.Images.Media.ORIENTATION, 31 | MediaStore.Video.VideoColumns.DURATION, 32 | MediaStore.Images.Media.BUCKET_ID 33 | ) 34 | 35 | @RequiresApi(Build.VERSION_CODES.O) 36 | fun Bundle.addLimitOffset(input: JSONObject) { 37 | if (input.has("limit")) putInt(ContentResolver.QUERY_ARG_LIMIT, input.getInt("limit")) 38 | if (input.has("offset")) putInt(ContentResolver.QUERY_ARG_OFFSET, input.getInt("offset")) 39 | } 40 | 41 | @RequiresApi(Build.VERSION_CODES.O) 42 | fun Bundle.addSort(input: JSONObject) { 43 | // Sort function 44 | var field = DATE_MODIFIED 45 | if (input.has("sortBy") && input.getString("sortBy") == "creationTime") { 46 | field = DATE_ADDED 47 | } 48 | putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(field)) 49 | 50 | var direction = ContentResolver.QUERY_SORT_DIRECTION_DESCENDING 51 | if (input.has("sortOrder") && input.getString("sortOrder") == "asc") { 52 | direction = ContentResolver.QUERY_SORT_DIRECTION_ASCENDING 53 | } 54 | 55 | putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, direction) 56 | } 57 | 58 | 59 | @RequiresApi(Build.VERSION_CODES.O) 60 | fun Bundle.sqlSelection(input: String) { 61 | putString(ContentResolver.QUERY_ARG_SQL_SELECTION, input) 62 | } 63 | 64 | @RequiresApi(Build.VERSION_CODES.O) 65 | fun Bundle.sqlArgs(selectionArgs: Array) { 66 | putStringArray( 67 | ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, 68 | selectionArgs 69 | ) 70 | } 71 | 72 | 73 | fun addLegacySort(input: JSONObject): String { 74 | var field = DATE_MODIFIED 75 | if (input.has("sortBy") && input.getString("sortBy") == "creationTime") { 76 | field = DATE_ADDED 77 | } 78 | 79 | var direction = "DESC" 80 | if (input.has("sortOrder") && input.getString("sortOrder") == "asc") { 81 | direction = "ASC" 82 | } 83 | 84 | return "$field $direction" 85 | } 86 | 87 | data class Tuple(val selection: String, val arguments: Array) 88 | fun queryByMediaType(input: JSONObject): Tuple { 89 | val selection = mutableListOf() 90 | val arguments = mutableListOf() 91 | if (input.has(AssetItemKeys.mediaType.name)) { 92 | val jsonArray = input.getJSONArray(AssetItemKeys.mediaType.name) 93 | for (i in (0 until jsonArray.length())) { 94 | when (jsonArray.getString(i)) { 95 | AssetMediaType.video.name -> { 96 | selection.add("$MEDIA_TYPE = ?") 97 | arguments.add(MEDIA_TYPE_VIDEO.toString()) 98 | } 99 | AssetMediaType.audio.name -> { 100 | selection.add("$MEDIA_TYPE = ?") 101 | arguments.add(MEDIA_TYPE_AUDIO.toString()) 102 | } 103 | AssetMediaType.photo.name -> { 104 | selection.add("$MEDIA_TYPE = ?") 105 | arguments.add(MEDIA_TYPE_IMAGE.toString()) 106 | } 107 | } 108 | } 109 | } 110 | if (selection.isEmpty()) { 111 | selection.add("$MEDIA_TYPE = ?") 112 | selection.add("$MEDIA_TYPE = ?") 113 | selection.add("$MEDIA_TYPE = ?") 114 | arguments.add(MEDIA_TYPE_VIDEO.toString()) 115 | arguments.add(MEDIA_TYPE_AUDIO.toString()) 116 | arguments.add(MEDIA_TYPE_IMAGE.toString()) 117 | } 118 | return Tuple(selection.joinToString(" OR "), arguments.toTypedArray()) 119 | } 120 | 121 | fun ContentResolver.listQuery( 122 | uri: Uri, 123 | context: Context, 124 | input: JSONObject, 125 | ): JSONArray { 126 | var (selection, arguments) = queryByMediaType(input) 127 | if (input.has("collectionId")) { 128 | if (selection.isNotEmpty()) selection = "($selection) AND " 129 | selection += "${MediaStore.Images.Media.BUCKET_ID} = ?" 130 | arguments = arrayOf(*arguments, input.getString("collectionId")) 131 | } 132 | println("⚽️ SELECT: $selection, ${arguments.contentToString()}") 133 | return makeQuery(uri, context, input, selection, arguments) 134 | } 135 | 136 | fun ContentResolver.getCollections(mediaType: Int): JSONArray { 137 | var contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 138 | if (mediaType == MEDIA_TYPE_VIDEO) { 139 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI 140 | } 141 | 142 | val projection = arrayOf(MediaStore.Images.ImageColumns.BUCKET_ID, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME) 143 | val cursor = this.query(contentUri, projection, null, null, null) 144 | 145 | val array = JSONArray() 146 | val buckets = mutableMapOf() 147 | if (cursor != null) { 148 | while (cursor.moveToNext()) { 149 | val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)) 150 | val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID)) 151 | if (buckets.containsKey(bucketId)) { 152 | var count = buckets[bucketId]!!.getInt("count") 153 | if (count == 0) count = 1 154 | buckets[bucketId]!!.put("count", count + 1) 155 | continue 156 | } 157 | val item = JSONObject() 158 | item.put("filename", name) 159 | item.put("id", bucketId) 160 | item.put("count", 0) 161 | buckets[bucketId] = item 162 | array.put(item) 163 | } 164 | cursor.close() 165 | 166 | } 167 | return array 168 | 169 | } 170 | 171 | fun ContentResolver.singleQuery( 172 | uri: Uri, 173 | context: Context, 174 | input: JSONObject, 175 | id: String 176 | ): JSONArray { 177 | val selection = "$_ID = ?" 178 | val selectionArgs = arrayOf(id) 179 | input.put("limit", 1) 180 | return makeQuery(uri, context, input, selection, selectionArgs) 181 | } 182 | 183 | fun ContentResolver.makeQuery( 184 | uri: Uri, 185 | context: Context, 186 | input: JSONObject, 187 | selection: String, 188 | selectionArgs: Array 189 | ): JSONArray { 190 | val galleryImageUrls = JSONArray() 191 | 192 | val limit = if (input.has("limit")) input.getInt("limit") else -1 193 | val offset = if (input.has("offset")) input.getInt("offset") else -1 194 | /** 195 | * Change the way to fetch Media Store 196 | */ 197 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 198 | // Get All data in Cursor by sorting in DESC order 199 | context.contentResolver.query( 200 | uri, 201 | ASSET_PROJECTION, 202 | Bundle().apply { 203 | addLimitOffset(input) 204 | addSort(input) 205 | sqlSelection(selection) 206 | sqlArgs(selectionArgs) 207 | }, null 208 | ) 209 | } else { 210 | var sortOrder = addLegacySort(input) 211 | if (limit > 0) sortOrder += " LIMIT $limit" 212 | if (offset > 0) sortOrder += " OFFSET $offset" 213 | // Get All data in Cursor by sorting in DESC order 214 | context.contentResolver.query( 215 | uri, 216 | ASSET_PROJECTION, 217 | selection, 218 | selectionArgs, 219 | sortOrder 220 | ) 221 | }?.use { cursor -> 222 | cursor.mapToJson(this, galleryImageUrls, input, limit) 223 | } 224 | 225 | return galleryImageUrls 226 | } 227 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 4 | 5 | ## Development workflow 6 | 7 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 8 | 9 | ```sh 10 | yarn 11 | ``` 12 | 13 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 14 | 15 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 16 | 17 | To start the packager: 18 | 19 | ```sh 20 | yarn example start 21 | ``` 22 | 23 | To run the example app on Android: 24 | 25 | ```sh 26 | yarn example android 27 | ``` 28 | 29 | To run the example app on iOS: 30 | 31 | ```sh 32 | yarn example ios 33 | ``` 34 | 35 | 36 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 37 | 38 | ```sh 39 | yarn typescript 40 | yarn lint 41 | ``` 42 | 43 | To fix formatting errors, run the following: 44 | 45 | ```sh 46 | yarn lint --fix 47 | ``` 48 | 49 | Remember to add tests for your change if possible. Run the unit tests by: 50 | 51 | ```sh 52 | yarn test 53 | ``` 54 | To edit the Objective-C files, open `example/ios/MediaLibraryExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-media-library2`. 55 | 56 | To edit the Kotlin files, open `example/android` in Android studio and find the source files at `reactnativemedialibrary` under `Android`. 57 | ### Commit message convention 58 | 59 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 60 | 61 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 62 | - `feat`: new features, e.g. add new method to the module. 63 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 64 | - `docs`: changes into documentation, e.g. add usage example for the module.. 65 | - `test`: adding or updating tests, e.g. add integration tests using detox. 66 | - `chore`: tooling changes, e.g. change CI config. 67 | 68 | Our pre-commit hooks verify that your commit message matches this format when committing. 69 | 70 | ### Linting and tests 71 | 72 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 73 | 74 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 75 | 76 | Our pre-commit hooks verify that the linter and tests pass when committing. 77 | 78 | ### Publishing to npm 79 | 80 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 81 | 82 | To publish new versions, run the following: 83 | 84 | ```sh 85 | yarn release 86 | ``` 87 | 88 | ### Scripts 89 | 90 | The `package.json` file contains various scripts for common tasks: 91 | 92 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 93 | - `yarn typescript`: type-check files with TypeScript. 94 | - `yarn lint`: lint files with ESLint. 95 | - `yarn test`: run unit tests with Jest. 96 | - `yarn example start`: start the Metro server for the example app. 97 | - `yarn example android`: run the example app on Android. 98 | - `yarn example ios`: run the example app on iOS. 99 | 100 | ### Sending a pull request 101 | 102 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 103 | 104 | When you're sending a pull request: 105 | 106 | - Prefer small pull requests focused on one change. 107 | - Verify that linters and tests are passing. 108 | - Review the documentation to make sure it looks good. 109 | - Follow the pull request template when opening a pull request. 110 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 111 | 112 | ## Code of Conduct 113 | 114 | ### Our Pledge 115 | 116 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 117 | 118 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 119 | 120 | ### Our Standards 121 | 122 | Examples of behavior that contributes to a positive environment for our community include: 123 | 124 | - Demonstrating empathy and kindness toward other people 125 | - Being respectful of differing opinions, viewpoints, and experiences 126 | - Giving and gracefully accepting constructive feedback 127 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 128 | - Focusing on what is best not just for us as individuals, but for the overall community 129 | 130 | Examples of unacceptable behavior include: 131 | 132 | - The use of sexualized language or imagery, and sexual attention or 133 | advances of any kind 134 | - Trolling, insulting or derogatory comments, and personal or political attacks 135 | - Public or private harassment 136 | - Publishing others' private information, such as a physical or email 137 | address, without their explicit permission 138 | - Other conduct which could reasonably be considered inappropriate in a 139 | professional setting 140 | 141 | ### Enforcement Responsibilities 142 | 143 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 144 | 145 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 146 | 147 | ### Scope 148 | 149 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 150 | 151 | ### Enforcement 152 | 153 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 154 | 155 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 156 | 157 | ### Enforcement Guidelines 158 | 159 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 160 | 161 | #### 1. Correction 162 | 163 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 164 | 165 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 166 | 167 | #### 2. Warning 168 | 169 | **Community Impact**: A violation through a single incident or series of actions. 170 | 171 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 172 | 173 | #### 3. Temporary Ban 174 | 175 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 176 | 177 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 178 | 179 | #### 4. Permanent Ban 180 | 181 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 182 | 183 | **Consequence**: A permanent ban from any sort of public interaction within the community. 184 | 185 | ### Attribution 186 | 187 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 188 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 189 | 190 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 191 | 192 | [homepage]: https://www.contributor-covenant.org 193 | 194 | For answers to common questions about this code of conduct, see the FAQ at 195 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 196 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Image, 3 | ImageRequireSource, 4 | NativeModules, 5 | Platform, 6 | ColorValue, 7 | processColor, 8 | type ProcessedColorValue, 9 | } from 'react-native'; 10 | 11 | const LINKING_ERROR = 12 | `The package 'react-native-media-library2' doesn't seem to be linked. Make sure: \n\n` + 13 | Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + 14 | '- You rebuilt the app after installing the package\n' + 15 | '- You are not using Expo managed workflow\n'; 16 | 17 | const MediaLibrary = NativeModules.MediaLibrary 18 | ? NativeModules.MediaLibrary 19 | : new Proxy( 20 | {}, 21 | { 22 | get() { 23 | throw new Error(LINKING_ERROR); 24 | }, 25 | } 26 | ); 27 | 28 | MediaLibrary.install(); 29 | 30 | declare global { 31 | var __mediaLibrary: { 32 | getAsset( 33 | id: string, 34 | callback: (item: FullAssetItem | undefined) => void 35 | ): void; 36 | exportVideo( 37 | params: { 38 | identifier: string; 39 | resultSavePath: string; 40 | }, 41 | callback: (item: FullAssetItem | undefined) => void 42 | ): void; 43 | getAssets( 44 | options: FetchAssetsOptions, 45 | callback: (items: AssetItem[]) => void 46 | ): void; 47 | getFromDisk( 48 | options: { path: string; extensions?: string }, 49 | callback: (items: DiskAssetItem[]) => void 50 | ): void; 51 | getCollections(callback: (items: CollectionItem[]) => void): void; 52 | saveToLibrary( 53 | params: SaveToLibrary, 54 | callback: (item: AssetItem | { error: string }) => void 55 | ): void; 56 | 57 | fetchVideoFrame( 58 | params: FetchThumbnailOptions, 59 | callback: (item: Thumbnail) => void 60 | ): void; 61 | combineImages( 62 | params: { 63 | readonly images: CombineImage[]; 64 | readonly resultSavePath: string; 65 | readonly mainImageIndex?: number; 66 | readonly backgroundColor?: ProcessedColorValue | null | undefined; 67 | }, 68 | callback: (item: { result: boolean }) => void 69 | ): void; 70 | 71 | imageResize( 72 | params: ImageResizeParams, 73 | callback: (item: { result: boolean }) => void 74 | ): void; 75 | imageCrop( 76 | params: ImageCropParams, 77 | callback: (item: { result: boolean }) => void 78 | ): void; 79 | 80 | imageSizes( 81 | params: { images: string[] }, 82 | callback: ( 83 | items: { 84 | width: number; 85 | height: number; 86 | size: number; 87 | }[] 88 | ) => void 89 | ): void; 90 | 91 | downloadAsBase64( 92 | params: { url: string }, 93 | callback: (data: { base64: string } | undefined) => void 94 | ): void; 95 | 96 | cacheDir(): string; 97 | }; 98 | } 99 | 100 | type ImagesTypes = ImageRequireSource | string; 101 | 102 | export interface FetchAssetsOptions { 103 | mediaType?: MediaType[]; 104 | sortBy?: 'creationTime' | 'modificationTime'; 105 | sortOrder?: 'asc' | 'desc'; 106 | extensions?: string[]; 107 | requestUrls?: boolean; 108 | limit?: number; 109 | offset?: number; 110 | onlyFavorites?: boolean; 111 | collectionId?: string; 112 | } 113 | 114 | export interface FetchThumbnailOptions { 115 | url: string; 116 | time?: number; 117 | quality?: number; 118 | } 119 | 120 | export interface Thumbnail { 121 | url: string; 122 | width: number; 123 | height: number; 124 | } 125 | 126 | interface SaveToLibrary { 127 | localUrl: string; 128 | album?: string; 129 | } 130 | 131 | export type MediaType = 'photo' | 'video' | 'audio' | 'unknown'; 132 | export type MediaSubType = 133 | | 'photoPanorama' 134 | | 'photoHDR' 135 | | 'photoScreenshot' 136 | | 'photoLive' 137 | | 'photoDepthEffect' 138 | | 'videoStreamed' 139 | | 'videoHighFrameRate' 140 | | 'videoTimelapse' 141 | | 'videoCinematic' 142 | | 'unknown'; 143 | export interface AssetItem { 144 | readonly filename: string; 145 | readonly id: string; 146 | readonly creationTime?: number; 147 | readonly modificationTime?: number; 148 | readonly mediaType: MediaType; 149 | readonly duration: number; 150 | readonly width: number; 151 | readonly height: number; 152 | readonly uri: string; 153 | // only on IOS 154 | readonly subtypes?: MediaSubType[]; 155 | } 156 | 157 | export interface DiskAssetItem { 158 | readonly isDirectory: boolean; 159 | readonly filename: string; 160 | readonly creationTime: number; 161 | readonly size: number; 162 | readonly uri: string; 163 | } 164 | 165 | export interface CollectionItem { 166 | readonly filename: string; 167 | readonly id: string; 168 | // On Android it will be approximate count 169 | readonly count: number; 170 | } 171 | 172 | export interface ImageResizeParams { 173 | uri: ImagesTypes; 174 | width?: number; 175 | height?: number; 176 | format?: 'jpeg' | 'png'; 177 | resultSavePath: string; 178 | } 179 | 180 | export interface ImageCropParams { 181 | uri: ImagesTypes; 182 | x: number; 183 | y: number; 184 | width: number; 185 | height: number; 186 | format?: 'jpeg' | 'png'; 187 | resultSavePath: string; 188 | } 189 | 190 | interface CombineImage { 191 | image: ImagesTypes; 192 | positions?: { x: number; y: number }; 193 | } 194 | []; 195 | 196 | export interface FullAssetItem extends AssetItem { 197 | // on android, it will be available only from API 24 (N) 198 | readonly location?: { latitude: number; longitude: number }; 199 | } 200 | 201 | const prepareImages = (images: ImagesTypes[]): string[] => { 202 | return images.map((image) => { 203 | if (typeof image === 'string') return image; 204 | return Image.resolveAssetSource(image).uri; 205 | }); 206 | }; 207 | 208 | const prepareCombineImages = (images: CombineImage[]): CombineImage[] => { 209 | return images.map((image) => { 210 | if (typeof image.image === 'string') return image; 211 | return { 212 | image: Image.resolveAssetSource(image.image).uri, 213 | positions: image.positions, 214 | }; 215 | }); 216 | }; 217 | 218 | const prepareImage = (image: ImagesTypes): string => { 219 | if (typeof image === 'string') return image; 220 | return Image.resolveAssetSource(image).uri; 221 | }; 222 | 223 | export const mediaLibrary = { 224 | get cacheDir(): string { 225 | return __mediaLibrary.cacheDir().replace(/\/$/, ''); 226 | }, 227 | 228 | getAssets(options?: FetchAssetsOptions): Promise { 229 | const params = { 230 | mediaType: options?.mediaType ?? ['photo', 'video'], 231 | sortBy: options?.sortBy, 232 | sortOrder: options?.sortOrder, 233 | limit: options?.limit, 234 | offset: options?.offset, 235 | onlyFavorites: options?.onlyFavorites ?? false, 236 | collectionId: options?.collectionId, 237 | }; 238 | if (params.offset && !params.limit) { 239 | throw new Error( 240 | 'limit parameter must be present in order to make a pagination' 241 | ); 242 | } 243 | return new Promise((resolve) => { 244 | __mediaLibrary.getAssets(params, (response) => resolve(response)); 245 | }); 246 | }, 247 | getFromDisk(options: { 248 | path: string; 249 | extensions?: string[]; 250 | }): Promise { 251 | return new Promise((resolve) => { 252 | __mediaLibrary.getFromDisk( 253 | { 254 | ...options, 255 | extensions: options.extensions 256 | ? options.extensions.join(',') 257 | : undefined, 258 | }, 259 | (response) => resolve(response) 260 | ); 261 | }); 262 | }, 263 | 264 | getCollections(): Promise { 265 | return new Promise((resolve) => { 266 | __mediaLibrary.getCollections((response) => resolve(response)); 267 | }); 268 | }, 269 | 270 | getAsset(id: string): Promise { 271 | return new Promise((resolve) => { 272 | __mediaLibrary.getAsset(id, (response) => resolve(response)); 273 | }); 274 | }, 275 | 276 | exportVideo(params: { 277 | identifier: string; 278 | resultSavePath: string; 279 | }): Promise { 280 | return new Promise((resolve) => { 281 | __mediaLibrary.exportVideo(params, (response) => resolve(response)); 282 | }); 283 | }, 284 | 285 | saveToLibrary(params: SaveToLibrary) { 286 | return new Promise((resolve, reject) => { 287 | __mediaLibrary.saveToLibrary(params, (response) => { 288 | if ('error' in response) { 289 | reject(response.error); 290 | } else { 291 | resolve(response); 292 | } 293 | }); 294 | }); 295 | }, 296 | 297 | fetchVideoFrame(params: FetchThumbnailOptions) { 298 | return new Promise((resolve) => { 299 | __mediaLibrary.fetchVideoFrame( 300 | { 301 | time: params.time ?? 0, 302 | quality: params.quality ?? 1, 303 | url: params.url, 304 | }, 305 | (response) => resolve(response) 306 | ); 307 | }); 308 | }, 309 | 310 | combineImages(params: { 311 | readonly images: (CombineImage | ImagesTypes)[]; 312 | readonly resultSavePath: string; 313 | readonly mainImageIndex?: number; 314 | readonly backgroundColor?: ColorValue | undefined; 315 | }) { 316 | return new Promise<{ result: boolean }>((resolve) => { 317 | const images = params.images.map((img) => 318 | typeof img === 'object' ? img : { image: img } 319 | ); 320 | __mediaLibrary.combineImages( 321 | { 322 | images: prepareCombineImages(images), 323 | resultSavePath: params.resultSavePath, 324 | mainImageIndex: params.mainImageIndex ?? 0, 325 | backgroundColor: params.backgroundColor 326 | ? processColor(params.backgroundColor) 327 | : processColor('transparent'), 328 | }, 329 | resolve 330 | ); 331 | }); 332 | }, 333 | 334 | imageResize(params: ImageResizeParams) { 335 | return new Promise<{ result: boolean }>((resolve) => { 336 | __mediaLibrary.imageResize( 337 | { 338 | uri: prepareImage(params.uri), 339 | resultSavePath: params.resultSavePath, 340 | format: params.format ?? 'png', 341 | height: params.height ?? -1, 342 | width: params.width ?? -1, 343 | }, 344 | resolve 345 | ); 346 | }); 347 | }, 348 | 349 | imageCrop(params: ImageCropParams) { 350 | return new Promise<{ result: boolean }>((resolve) => { 351 | __mediaLibrary.imageCrop( 352 | { 353 | ...params, 354 | uri: prepareImage(params.uri), 355 | format: params.format ?? 'png', 356 | }, 357 | resolve 358 | ); 359 | }); 360 | }, 361 | 362 | imageSizes(params: { images: ImagesTypes[] }): Promise< 363 | { 364 | width: number; 365 | height: number; 366 | size: number; 367 | }[] 368 | > { 369 | return new Promise((resolve) => { 370 | __mediaLibrary.imageSizes( 371 | { images: prepareImages(params.images) }, 372 | resolve 373 | ); 374 | }); 375 | }, 376 | 377 | downloadAsBase64(params: { 378 | url: string; 379 | }): Promise<{ base64: string } | undefined> { 380 | return new Promise((resolve) => { 381 | __mediaLibrary.downloadAsBase64(params, resolve); 382 | }); 383 | }, 384 | }; 385 | --------------------------------------------------------------------------------