@capgo/native-audio 7.10.2 → 7.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,7 +103,40 @@ You can also consume the iOS implementation via Swift Package Manager. In Xcode
103
103
 
104
104
  ## Configuration
105
105
 
106
- No configuration required for this plugin.
106
+ ### Optional HLS/m3u8 Streaming (Android)
107
+
108
+ By default, HLS streaming support is **enabled** for backward compatibility. However, it adds approximately **4MB** to your Android APK size due to the `media3-exoplayer-hls` dependency.
109
+
110
+ If you don't need HLS/m3u8 streaming support, you can disable it to reduce your APK size:
111
+
112
+ ```typescript
113
+ // capacitor.config.ts
114
+ import type { CapacitorConfig } from '@capacitor/cli';
115
+
116
+ const config: CapacitorConfig = {
117
+ appId: 'com.example.app',
118
+ appName: 'My App',
119
+ plugins: {
120
+ NativeAudio: {
121
+ hls: false // Disable HLS to reduce APK size by ~4MB
122
+ }
123
+ }
124
+ };
125
+
126
+ export default config;
127
+ ```
128
+
129
+ After changing the configuration, run:
130
+
131
+ ```bash
132
+ npx cap sync
133
+ ```
134
+
135
+ **Notes:**
136
+ - iOS uses native AVPlayer for HLS, so this setting only affects Android
137
+ - If HLS is disabled and you try to play an `.m3u8` file, you'll get a clear error message explaining how to enable it
138
+ - The default is `hls: true` to maintain backward compatibility
139
+
107
140
  <docgen-config>
108
141
  <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
109
142
 
@@ -3,6 +3,10 @@ ext {
3
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
+
7
+ // Read HLS configuration from gradle.properties (set by hook script)
8
+ // Default to 'true' for backward compatibility
9
+ includeHls = project.findProperty('nativeAudio.hls.include') ?: 'true'
6
10
  }
7
11
 
8
12
  buildscript {
@@ -40,6 +44,16 @@ android {
40
44
  sourceCompatibility JavaVersion.VERSION_21
41
45
  targetCompatibility JavaVersion.VERSION_21
42
46
  }
47
+
48
+ // Exclude StreamAudioAsset when HLS is disabled
49
+ // StreamAudioAsset depends on HlsMediaSource which is only available with media3-exoplayer-hls
50
+ sourceSets {
51
+ main {
52
+ if (includeHls != 'true') {
53
+ java.exclude '**/StreamAudioAsset.java'
54
+ }
55
+ }
56
+ }
43
57
  }
44
58
 
45
59
  repositories {
@@ -56,7 +70,14 @@ dependencies {
56
70
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
57
71
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
58
72
  implementation 'androidx.media3:media3-exoplayer:1.8.0'
59
- implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
73
+
74
+ // HLS (m3u8) streaming support - optional dependency
75
+ // When disabled, reduces APK size by ~4MB
76
+ // Configure via capacitor.config.ts: plugins.NativeAudio.hls = false
77
+ if (includeHls == 'true') {
78
+ implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
79
+ }
80
+
60
81
  implementation 'androidx.media3:media3-session:1.8.0'
61
82
  implementation 'androidx.media3:media3-transformer:1.5.1'
62
83
  implementation 'androidx.media3:media3-ui:1.5.1'
@@ -0,0 +1,84 @@
1
+ package ee.forgr.audio;
2
+
3
+ import android.util.Log;
4
+
5
+ /**
6
+ * Utility class to check if HLS (m3u8) streaming dependencies are available at runtime.
7
+ *
8
+ * This allows the plugin to gracefully handle cases where the HLS dependency
9
+ * (media3-exoplayer-hls) is excluded to reduce APK size.
10
+ *
11
+ * Users who don't need HLS streaming support can disable it in capacitor.config.ts:
12
+ *
13
+ * plugins: {
14
+ * NativeAudio: {
15
+ * hls: false // Reduces APK size by ~4MB
16
+ * }
17
+ * }
18
+ */
19
+ public class HlsAvailabilityChecker {
20
+
21
+ private static final String TAG = "HlsAvailabilityChecker";
22
+ private static Boolean hlsAvailable = null;
23
+
24
+ /**
25
+ * Check if a class is available at runtime using reflection.
26
+ */
27
+ private static boolean isClassAvailable(String className) {
28
+ try {
29
+ Class.forName(className);
30
+ return true;
31
+ } catch (ClassNotFoundException e) {
32
+ return false;
33
+ } catch (Exception e) {
34
+ Log.e(TAG, "Unexpected error checking class availability: " + className, e);
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Check if HLS streaming dependencies are available.
41
+ * Results are cached for performance.
42
+ *
43
+ * @return true if HLS classes are available, false otherwise
44
+ */
45
+ public static boolean isHlsAvailable() {
46
+ if (hlsAvailable != null) {
47
+ return hlsAvailable;
48
+ }
49
+
50
+ // Check for the critical HLS classes from media3-exoplayer-hls
51
+ String[] hlsClasses = { "androidx.media3.exoplayer.hls.HlsMediaSource", "androidx.media3.exoplayer.hls.HlsMediaSource$Factory" };
52
+
53
+ boolean allAvailable = true;
54
+ for (String className : hlsClasses) {
55
+ if (!isClassAvailable(className)) {
56
+ allAvailable = false;
57
+ Log.w(TAG, "HLS dependency class not available: " + className);
58
+ }
59
+ }
60
+
61
+ hlsAvailable = allAvailable;
62
+
63
+ if (!allAvailable) {
64
+ Log.i(
65
+ TAG,
66
+ "HLS streaming support is not available. " +
67
+ "To enable m3u8 streaming, set 'hls: true' in capacitor.config.ts under NativeAudio plugin config " +
68
+ "and run 'npx cap sync'."
69
+ );
70
+ } else {
71
+ Log.d(TAG, "HLS streaming support is available.");
72
+ }
73
+
74
+ return allAvailable;
75
+ }
76
+
77
+ /**
78
+ * Reset the cached availability check.
79
+ * Useful for testing purposes.
80
+ */
81
+ public static void resetCache() {
82
+ hlsAvailable = null;
83
+ }
84
+ }
@@ -665,8 +665,22 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
665
665
  }
666
666
 
667
667
  if (assetPath.endsWith(".m3u8")) {
668
- // HLS Stream - resolve immediately since it's a stream
669
- StreamAudioAsset streamAudioAsset = new StreamAudioAsset(this, audioId, uri, volume, requestHeaders);
668
+ // HLS Stream - check if HLS support is available
669
+ if (!HlsAvailabilityChecker.isHlsAvailable()) {
670
+ call.reject(
671
+ "HLS streaming (.m3u8) is not available. " +
672
+ "The media3-exoplayer-hls dependency is not included. " +
673
+ "To enable HLS support, set 'hls: true' in capacitor.config.ts under NativeAudio plugin config " +
674
+ "and run 'npx cap sync'. This will increase APK size by ~4MB."
675
+ );
676
+ return;
677
+ }
678
+ // HLS Stream - create via reflection to allow compile-time exclusion
679
+ AudioAsset streamAudioAsset = createStreamAudioAsset(audioId, uri, volume, requestHeaders);
680
+ if (streamAudioAsset == null) {
681
+ call.reject("Failed to create HLS stream player. HLS support may not be properly configured.");
682
+ return;
683
+ }
670
684
  audioAssetList.put(audioId, streamAudioAsset);
671
685
  call.resolve(status);
672
686
  } else {
@@ -784,6 +798,37 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
784
798
  return (value != null && !value.isEmpty() && !value.equals("null"));
785
799
  }
786
800
 
801
+ /**
802
+ * Creates a StreamAudioAsset via reflection.
803
+ * This allows the StreamAudioAsset class to be excluded at compile time when HLS is disabled,
804
+ * reducing APK size by ~4MB.
805
+ *
806
+ * @param audioId The unique identifier for the audio asset
807
+ * @param uri The URI of the HLS stream
808
+ * @param volume The initial volume (0.0 to 1.0)
809
+ * @param headers Optional HTTP headers for the request
810
+ * @return The created AudioAsset, or null if creation failed
811
+ */
812
+ private AudioAsset createStreamAudioAsset(String audioId, Uri uri, float volume, java.util.Map<String, String> headers) {
813
+ try {
814
+ Class<?> streamAudioAssetClass = Class.forName("ee.forgr.audio.StreamAudioAsset");
815
+ java.lang.reflect.Constructor<?> constructor = streamAudioAssetClass.getConstructor(
816
+ NativeAudio.class,
817
+ String.class,
818
+ Uri.class,
819
+ float.class,
820
+ java.util.Map.class
821
+ );
822
+ return (AudioAsset) constructor.newInstance(this, audioId, uri, volume, headers);
823
+ } catch (ClassNotFoundException e) {
824
+ Log.e(TAG, "StreamAudioAsset class not found. HLS support is not included in this build.", e);
825
+ return null;
826
+ } catch (Exception e) {
827
+ Log.e(TAG, "Failed to create StreamAudioAsset", e);
828
+ return null;
829
+ }
830
+ }
831
+
787
832
  private void stopAudio(String audioId) throws Exception {
788
833
  if (!audioAssetList.containsKey(audioId)) {
789
834
  throw new Exception(ERROR_ASSET_NOT_LOADED);
@@ -13,7 +13,7 @@ enum MyError: Error {
13
13
  // swiftlint:disable type_body_length file_length
14
14
  @objc(NativeAudio)
15
15
  public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
16
- private let pluginVersion: String = "7.10.2"
16
+ private let pluginVersion: String = "7.11.0"
17
17
  public let identifier = "NativeAudio"
18
18
  public let jsName = "NativeAudio"
19
19
  public let pluginMethods: [CAPPluginMethod] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "7.10.2",
3
+ "version": "7.11.0",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MPL-2.0",
6
6
  "main": "dist/plugin.cjs.js",
@@ -14,7 +14,8 @@
14
14
  "ios/Sources",
15
15
  "ios/Tests",
16
16
  "Package.swift",
17
- "CapgoNativeAudio.podspec"
17
+ "CapgoNativeAudio.podspec",
18
+ "scripts/configure-dependencies.js"
18
19
  ],
19
20
  "author": "Martin Donadieu",
20
21
  "repository": {
@@ -33,6 +34,7 @@
33
34
  "native"
34
35
  ],
35
36
  "scripts": {
37
+ "capacitor:sync:after": "node scripts/configure-dependencies.js",
36
38
  "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
37
39
  "verify:ios": "xcodebuild -scheme CapgoNativeAudio -destination generic/platform=iOS",
38
40
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
@@ -45,7 +47,8 @@
45
47
  "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
46
48
  "swiftlint": "node-swiftlint",
47
49
  "docgen": "docgen --api NativeAudio --output-readme README.md --output-json dist/docs.json",
48
- "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
50
+ "build": "npm run clean && npm run docgen && npm run build:scripts && tsc && rollup -c rollup.config.mjs",
51
+ "build:scripts": "tsc -p scripts/tsconfig.json",
49
52
  "clean": "rimraf ./dist",
50
53
  "watch": "tsc --watch",
51
54
  "prepublishOnly": "npm run build"
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Capacitor Hook Script: Configure Optional HLS Dependency
5
+ *
6
+ * This script runs during `npx cap sync` and configures whether to include
7
+ * the HLS (m3u8) streaming dependency based on capacitor.config.ts settings.
8
+ *
9
+ * By default, HLS is enabled for backward compatibility.
10
+ * To disable HLS and reduce APK size by ~4MB, set:
11
+ *
12
+ * plugins: {
13
+ * NativeAudio: {
14
+ * hls: false
15
+ * }
16
+ * }
17
+ *
18
+ * Environment variables provided by Capacitor:
19
+ * - CAPACITOR_ROOT_DIR: Root directory of the consuming app
20
+ * - CAPACITOR_CONFIG: JSON stringified config object
21
+ * - CAPACITOR_PLATFORM_NAME: Platform name (android, ios, web)
22
+ * - process.cwd(): Plugin root directory
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.getConfig = getConfig;
59
+ exports.configureAndroid = configureAndroid;
60
+ exports.configureIOS = configureIOS;
61
+ exports.configureWeb = configureWeb;
62
+ const fs = __importStar(require("fs"));
63
+ const path = __importStar(require("path"));
64
+ // Get environment variables
65
+ const PLUGIN_ROOT = process.cwd();
66
+ const CONFIG_JSON = process.env.CAPACITOR_CONFIG;
67
+ const PLATFORM = process.env.CAPACITOR_PLATFORM_NAME;
68
+ // File paths
69
+ const gradlePropertiesPath = path.join(PLUGIN_ROOT, 'android', 'gradle.properties');
70
+ // ============================================================================
71
+ // Logging Utilities
72
+ // ============================================================================
73
+ const colors = {
74
+ reset: '\x1b[0m',
75
+ bright: '\x1b[1m',
76
+ green: '\x1b[32m',
77
+ red: '\x1b[31m',
78
+ yellow: '\x1b[33m',
79
+ blue: '\x1b[34m',
80
+ cyan: '\x1b[36m',
81
+ gray: '\x1b[90m',
82
+ };
83
+ function log(message, emoji = '', color = '') {
84
+ const emojiPart = emoji ? `${emoji} ` : '';
85
+ const colorCode = color || colors.reset;
86
+ const resetCode = color ? colors.reset : '';
87
+ console.log(`${colorCode}${emojiPart}${message}${resetCode}`);
88
+ }
89
+ function logSuccess(message) {
90
+ log(message, '✔', colors.green);
91
+ }
92
+ function logError(message) {
93
+ log(message, '✖', colors.red);
94
+ }
95
+ function logInfo(message) {
96
+ log(message, 'ℹ', colors.blue);
97
+ }
98
+ function logWarning(message) {
99
+ log(message, '⚠', colors.yellow);
100
+ }
101
+ /**
102
+ * Parse NativeAudio configuration from Capacitor config
103
+ * Default: hls = true (for backward compatibility)
104
+ */
105
+ function getConfig() {
106
+ const defaultConfig = {
107
+ hls: true, // Enabled by default for backward compatibility
108
+ };
109
+ try {
110
+ if (!CONFIG_JSON) {
111
+ logInfo('No CAPACITOR_CONFIG found, using defaults (HLS enabled)');
112
+ return defaultConfig;
113
+ }
114
+ const config = JSON.parse(CONFIG_JSON);
115
+ const nativeAudioConfig = config.plugins?.NativeAudio || {};
116
+ return {
117
+ hls: nativeAudioConfig.hls !== false, // Default to true unless explicitly set to false
118
+ };
119
+ }
120
+ catch (error) {
121
+ logError(`Error parsing config: ${error.message}`);
122
+ return defaultConfig;
123
+ }
124
+ }
125
+ /**
126
+ * Log the current configuration status
127
+ */
128
+ function logConfig(config) {
129
+ log('\nNativeAudio configuration:', '', colors.bright);
130
+ if (config.hls) {
131
+ console.log(` ${colors.green}✔${colors.reset} ${colors.bright}HLS (m3u8)${colors.reset}: ${colors.green}enabled${colors.reset} (includes media3-exoplayer-hls, adds ~4MB to APK)`);
132
+ }
133
+ else {
134
+ console.log(` ${colors.yellow}○${colors.reset} ${colors.bright}HLS (m3u8)${colors.reset}: ${colors.yellow}disabled${colors.reset} (reduces APK size by ~4MB)`);
135
+ }
136
+ console.log('');
137
+ }
138
+ // ============================================================================
139
+ // Android: Gradle Configuration
140
+ // ============================================================================
141
+ /**
142
+ * Write gradle.properties file for Android
143
+ * Injects NativeAudio properties while preserving existing content
144
+ */
145
+ function configureAndroid(config) {
146
+ logInfo('Configuring Android dependencies...');
147
+ try {
148
+ // Read existing gradle.properties if it exists
149
+ let existingContent = '';
150
+ if (fs.existsSync(gradlePropertiesPath)) {
151
+ existingContent = fs.readFileSync(gradlePropertiesPath, 'utf8');
152
+ }
153
+ // Remove existing NativeAudio properties (if any)
154
+ const lines = existingContent.split('\n');
155
+ const filteredLines = [];
156
+ let inNativeAudioSection = false;
157
+ let lastWasEmpty = false;
158
+ for (const line of lines) {
159
+ // Check if this is a NativeAudio property or comment
160
+ if (line.trim().startsWith('# NativeAudio') ||
161
+ line.trim().startsWith('nativeAudio.') ||
162
+ line.trim() === '# Generated by NativeAudio hook script') {
163
+ inNativeAudioSection = true;
164
+ continue; // Skip this line
165
+ }
166
+ // If we were in NativeAudio section and hit a non-empty line, we're done
167
+ if (inNativeAudioSection && line.trim() !== '') {
168
+ inNativeAudioSection = false;
169
+ }
170
+ // Add non-NativeAudio lines, but avoid multiple consecutive empty lines
171
+ if (!inNativeAudioSection) {
172
+ if (line.trim() === '') {
173
+ if (!lastWasEmpty) {
174
+ filteredLines.push(line);
175
+ lastWasEmpty = true;
176
+ }
177
+ }
178
+ else {
179
+ filteredLines.push(line);
180
+ lastWasEmpty = false;
181
+ }
182
+ }
183
+ }
184
+ // Build new NativeAudio properties section
185
+ const nativeAudioProperties = [];
186
+ nativeAudioProperties.push('');
187
+ nativeAudioProperties.push('# NativeAudio Optional Dependencies (auto-generated)');
188
+ nativeAudioProperties.push('# Generated by NativeAudio hook script');
189
+ nativeAudioProperties.push(`nativeAudio.hls.include=${config.hls ? 'true' : 'false'}`);
190
+ // Combine: existing content + new NativeAudio properties
191
+ const newContent = filteredLines.join('\n') + '\n' + nativeAudioProperties.join('\n') + '\n';
192
+ fs.writeFileSync(gradlePropertiesPath, newContent, 'utf8');
193
+ logSuccess('Updated gradle.properties');
194
+ }
195
+ catch (error) {
196
+ logError(`Error updating gradle.properties: ${error.message}`);
197
+ }
198
+ }
199
+ // ============================================================================
200
+ // iOS: No Configuration Needed (yet)
201
+ // ============================================================================
202
+ /**
203
+ * iOS platform - HLS is handled natively by AVPlayer
204
+ */
205
+ function configureIOS() {
206
+ logInfo('iOS uses native AVPlayer for HLS - no additional configuration needed');
207
+ }
208
+ // ============================================================================
209
+ // Web: No Configuration Needed
210
+ // ============================================================================
211
+ /**
212
+ * Web platform doesn't need native dependency configuration
213
+ */
214
+ function configureWeb() {
215
+ logInfo('Web platform - no native dependency configuration needed');
216
+ }
217
+ // ============================================================================
218
+ // Main Execution
219
+ // ============================================================================
220
+ function main() {
221
+ const config = getConfig();
222
+ switch (PLATFORM) {
223
+ case 'android':
224
+ log('Configuring optional dependencies for NativeAudio', '🔧', colors.cyan);
225
+ logConfig(config);
226
+ configureAndroid(config);
227
+ logSuccess('Configuration complete\n');
228
+ break;
229
+ case 'ios':
230
+ log('Configuring NativeAudio for iOS', '🔧', colors.cyan);
231
+ logConfig(config);
232
+ configureIOS();
233
+ logSuccess('Configuration complete\n');
234
+ break;
235
+ case 'web':
236
+ configureWeb();
237
+ break;
238
+ default:
239
+ // If platform is not specified, configure all platforms (backward compatibility)
240
+ log('Configuring optional dependencies for NativeAudio', '🔧', colors.blue);
241
+ logConfig(config);
242
+ logWarning(`Unknown platform: ${PLATFORM || 'undefined'}, configuring Android`);
243
+ configureAndroid(config);
244
+ logSuccess('Configuration complete\n');
245
+ break;
246
+ }
247
+ }
248
+ // Run if executed directly
249
+ if (require.main === module) {
250
+ main();
251
+ }