diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 9b9c733..fd512e1 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -24,8 +24,12 @@ on: jobs: build: - name: Build Android Example App + name: Build Android Example App (audio=${{ matrix.enableAudio }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + enableAudio: ['true', 'false'] steps: - uses: actions/checkout@v4 @@ -60,7 +64,14 @@ jobs: - name: Run Gradle Build for examples/bare/android/ working-directory: examples/bare/android - run: ./gradlew assembleDebug --no-daemon --build-cache + run: ./gradlew assembleDebug --no-daemon --build-cache -PCompressor_enableAudio=${{ matrix.enableAudio }} + + - name: Check that the APK has the LAME encoder only when audio is enabled + working-directory: examples/bare/android + run: | + if unzip -l app/build/outputs/apk/debug/app-debug.apk | grep -q libandroidlame.so; then found=true; else found=false; fi + echo "libandroidlame.so in APK: $found" + test "$found" = "${{ matrix.enableAudio }}" - name: Stop Gradle Daemon if: always() diff --git a/README.md b/README.md index 1d784dc..afbd8f3 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ We should use **react-native-compressor** instead of **FFmpeg** because **react- - [Cancel Video Compression](#cancel-video-compression) - [Video Api Docs](#video-1) - [Audio](#audio) + - [Build without audio compression (Android)](#build-without-audio-compression-android) - [Background Upload](#background-upload) - [Cancel Background Upload](#cancel-background-upload) - [Download File](#download) @@ -292,6 +293,16 @@ const result = await Audio.compress( ); ``` +##### Build without audio compression (Android) + +If your app does not use `Audio.compress`, add this line to `android/gradle.properties`: + +```properties +Compressor_enableAudio=false +``` + +The Android build then leaves out audio compression, the `TAndroidLame` dependency (`libandroidlame.so`), `jlayer` and the JitPack repository. `Audio.compress` on Android rejects with an error whose message starts with `[E_AUDIO_DISABLED]`. The default is `true`. iOS does not change. + ### Background Upload ```js diff --git a/TRIAGE.md b/TRIAGE.md index f18ad50..b441dd1 100644 --- a/TRIAGE.md +++ b/TRIAGE.md @@ -13,6 +13,7 @@ Legend: | Issue | Triage | Notes | | --- | --- | --- | +| #427 | feature, added here | Every Android app shipped `libandroidlame.so` (TAndroidLame, from JitPack) even when it never calls `Audio.compress`. This branch adds the `Compressor_enableAudio` Gradle property (default `true`). With `false`, the build leaves out the LAME-based `AudioCompressor.kt`, TAndroidLame, jlayer and the JitPack repository, and `Audio.compress` rejects with `[E_AUDIO_DISABLED]`. | | #428 | real, fixed here | Android `Video.compress` rejected with the bare `CodecException` message, which is usually empty, so every codec failure read the same. This branch appends `CodecException.diagnosticInfo` (e.g. `android.media.MediaCodec.error_neg_12` = `NO_MEMORY`) to the rejection message. | | #423 | real, fixed here | Android `Video.compress` rejects with `NO_MEMORY` on Samsung Exynos encoders (`c2.exynos.h264.encoder`) at higher resolutions (e.g. 1080p manual mode). #399 set `KEY_PRIORITY = 0` + `KEY_OPERATING_RATE = Short.MAX_VALUE`; Exynos accepts them in `configure()` but fails `start()` ("real-time(32767 fps) is not supported"), which the `prepareEncoder` fallback doesn't cover. Small `auto` outputs (360×640) still fit the realtime budget. This branch removes both keys. Verified on a Galaxy A56 (Exynos 1580): 1080p manual fails with the keys and succeeds without them (94.1 MB → 32.0 MB). | | #406 | real, fixed here | Nitro module not linked in Android **release** builds (works in debug). R8 strips the `HybridCompressor` implementation, which is instantiated from C++/JNI by class name (`NitroCompressorOnLoad.cpp` `kJavaDescriptor`) and was the only Nitro class lacking keep protection — the generated spec has `@DoNotStrip @Keep`, but the hand-written impl didn't. This branch annotates `HybridCompressor` with `@DoNotStrip @Keep` (the same convention nitrogen and other Nitro modules like react-native-mmkv use), so no consumer ProGuard rules are needed. Verified with a minified release build: without the annotation R8 lists the class in `usage.txt` (removed); with it, the class + no-arg constructor are kept in `mapping.txt`. | @@ -102,3 +103,4 @@ These should be closed upstream unless a current repro still exists on the lates - Android: rewrite `NitroPromiseAdapter` in Java so the Nitro module compiles across all React Native versions regardless of `Promise.reject`'s `code` nullability (#404) - Android: drop `KEY_PRIORITY`/`KEY_OPERATING_RATE` from the encoder format so Exynos H.264 encoders don't fail `start()` with `NO_MEMORY` (#423) - Android: name the codec error (`CodecException.diagnosticInfo`) when video compression fails (#428) +- Android: `Compressor_enableAudio=false` builds without audio compression and the TAndroidLame dependency (#427) diff --git a/__tests__/compressor.test.ts b/__tests__/compressor.test.ts index d48c7c4..4f367fa 100644 --- a/__tests__/compressor.test.ts +++ b/__tests__/compressor.test.ts @@ -206,6 +206,14 @@ describe('react-native-compressor JS wrapper API', () => { }); }); + it('passes a native audio rejection to the caller', async () => { + mockCompressor.compress_audio.mockRejectedValueOnce( + new Error('[E_AUDIO_DISABLED] Audio compression is disabled in this build (Compressor_enableAudio=false).'), + ); + + await expect(api.Audio.compress(localAudioUri)).rejects.toThrow('E_AUDIO_DISABLED'); + }); + it('wraps native utility functions and normalizes image metadata', async () => { reactNative.Platform.OS = 'android'; mockCompressor.generateFilePath.mockResolvedValue('/tmp/output.jpg'); diff --git a/android/build.gradle b/android/build.gradle index 21bb364..5d1f858 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -39,6 +39,15 @@ def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Compressor_" + name]).toInteger() } +def getBooleanExtOrDefault(String name, boolean fallback) { + def key = "Compressor_" + name + def value = rootProject.ext.has(key) ? rootProject.ext.get(key) : project.findProperty(key) + return value == null ? fallback : value.toString().toBoolean() +} + +// Set Compressor_enableAudio=false to build without audio compression and the TAndroidLame dependency. +def enableAudio = getBooleanExtOrDefault("enableAudio", true) + android { namespace "com.reactnativecompressor" @@ -48,6 +57,7 @@ android { sourceSets { main { manifest.srcFile "src/main/AndroidManifestNew.xml" + java.srcDirs += [enableAudio ? "src/audio/java" : "src/noaudio/java"] } } @@ -114,7 +124,9 @@ android { repositories { mavenCentral() google() - maven { url "https://jitpack.io" } + if (enableAudio) { + maven { url "https://jitpack.io" } + } } def kotlin_version = getExtOrDefault("kotlinVersion") @@ -131,6 +143,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation 'org.mp4parser:isoparser:1.9.56' - implementation 'com.github.kaushik-naik:TAndroidLame:277c2ab4b0' - implementation 'javazoom:jlayer:1.0.1' + if (enableAudio) { + implementation 'com.github.kaushik-naik:TAndroidLame:277c2ab4b0' + implementation 'javazoom:jlayer:1.0.1' + } } diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt b/android/src/audio/java/com/reactnativecompressor/Audio/AudioCompressor.kt similarity index 100% rename from android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt rename to android/src/audio/java/com/reactnativecompressor/Audio/AudioCompressor.kt diff --git a/android/src/noaudio/java/com/reactnativecompressor/Audio/AudioCompressor.kt b/android/src/noaudio/java/com/reactnativecompressor/Audio/AudioCompressor.kt new file mode 100644 index 0000000..63fb9e7 --- /dev/null +++ b/android/src/noaudio/java/com/reactnativecompressor/Audio/AudioCompressor.kt @@ -0,0 +1,25 @@ +package com.reactnativecompressor.Audio + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap + +// Built instead of the LAME-based AudioCompressor when Compressor_enableAudio=false. +class AudioCompressor { + companion object { + val TAG = "AudioMain" + private const val ERROR_CODE = "E_AUDIO_DISABLED" + private const val ERROR_MESSAGE = + "[E_AUDIO_DISABLED] Audio compression is disabled in this build (Compressor_enableAudio=false)." + + @JvmStatic + fun CompressAudio( + fileUrl: String, + optionMap: ReadableMap, + context: ReactApplicationContext, + promise: Promise, + ) { + promise.reject(ERROR_CODE, ERROR_MESSAGE) + } + } +}