@capgo/native-audio 7.3.37 → 7.5.1

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.
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
10
10
  s.homepage = package['repository']['url']
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
- s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
14
  s.ios.deployment_target = '14.0'
15
15
  s.dependency 'Capacitor'
16
16
  end
package/Package.swift ADDED
@@ -0,0 +1,31 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "CapgoNativeAudio",
6
+ platforms: [
7
+ .iOS(.v14)
8
+ ],
9
+ products: [
10
+ .library(
11
+ name: "CapgoNativeAudio",
12
+ targets: ["NativeAudioPlugin"]
13
+ )
14
+ ],
15
+ dependencies: [
16
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0")
17
+ ],
18
+ targets: [
19
+ .target(
20
+ name: "NativeAudioPlugin",
21
+ dependencies: [
22
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
23
+ .product(name: "Cordova", package: "capacitor-swift-pm")
24
+ ],
25
+ path: "ios/Sources/NativeAudioPlugin"),
26
+ .testTarget(
27
+ name: "NativeAudioPluginTests",
28
+ dependencies: ["NativeAudioPlugin"],
29
+ path: "ios/Tests/NativeAudioPluginTests")
30
+ ]
31
+ )
package/README.md CHANGED
@@ -75,6 +75,10 @@ npx cap sync
75
75
 
76
76
  On iOS, Android and Web, no further steps are needed.
77
77
 
78
+ ### Swift Package Manager
79
+
80
+ You can also consume the iOS implementation via Swift Package Manager. In Xcode open **File → Add Package…**, point it at `https://github.com/Cap-go/capacitor-native-audio.git`, and select the `CapgoNativeAudio` library product. The package supports iOS 14 and newer alongside Capacitor 7.
81
+
78
82
  ## Configuration
79
83
 
80
84
  No configuration required for this plugin.
@@ -106,6 +110,21 @@ No configuration required for this plugin.
106
110
 
107
111
  [Example repository](https://github.com/bazuka5801/native-audio-example)
108
112
 
113
+ ## Example app
114
+
115
+ This repository now ships with an interactive Capacitor project under `example/` that exercises the main APIs on web, iOS, and Android shells.
116
+
117
+ ```bash
118
+ cd example
119
+ npm install
120
+ npm run dev # start the web playground
121
+ npm run sync # optional: generate iOS/Android platforms
122
+ npm run ios # open the iOS shell app
123
+ npm run android # open the Android shell app
124
+ ```
125
+
126
+ The UI demonstrates local asset preloading, remote streaming, playback controls, looping, live position updates, and cache clearing for remote audio.
127
+
109
128
  ```typescript
110
129
  import {NativeAudio} from '@capgo/native-audio'
111
130
 
@@ -352,7 +352,8 @@ public class StreamAudioAsset extends AudioAsset {
352
352
  }
353
353
 
354
354
  private void startPlaybackWithFade(Double time) {
355
- if (!player.isPlayingAd()) { // Make sure we're not in an ad
355
+ if (!player.isPlayingAd()) {
356
+ // Make sure we're not in an ad
356
357
  if (time != null) {
357
358
  player.seekTo(Math.round(time * 1000));
358
359
  } else if (player.isCurrentMediaItemLive()) {
@@ -393,7 +394,8 @@ public class StreamAudioAsset extends AudioAsset {
393
394
  public void run() {
394
395
  if (player.isPlaying()) {
395
396
  fadeIn();
396
- } else if (attempts < 10) { // Try for 5 seconds (10 * 500ms)
397
+ } else if (attempts < 10) {
398
+ // Try for 5 seconds (10 * 500ms)
397
399
  attempts++;
398
400
  handler.postDelayed(this, 500);
399
401
  }
@@ -17,7 +17,7 @@ public class RemoteAudioAsset: AudioAsset {
17
17
 
18
18
  owner.executeOnAudioQueue { [weak self] in
19
19
  guard let self = self else { return }
20
-
20
+
21
21
  guard let url = URL(string: path ?? "") else {
22
22
  print("Invalid URL: \(String(describing: path))")
23
23
  return
@@ -158,13 +158,13 @@ public class RemoteAudioAsset: AudioAsset {
158
158
  forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
159
159
  object: player.currentItem,
160
160
  queue: OperationQueue.main) { [weak self, weak player] notification in
161
- guard let strongSelf = self, let strongPlayer = player else { return }
161
+ guard let strongSelf = self, let strongPlayer = player else { return }
162
162
 
163
- if let currentItem = notification.object as? AVPlayerItem,
164
- strongPlayer.currentItem == currentItem {
165
- strongSelf.playerDidFinishPlaying(player: strongPlayer)
166
- }
163
+ if let currentItem = notification.object as? AVPlayerItem,
164
+ strongPlayer.currentItem === currentItem {
165
+ strongSelf.playerDidFinishPlaying(player: strongPlayer)
167
166
  }
167
+ }
168
168
  notificationObservers.append(observer)
169
169
  startCurrentTimeUpdates()
170
170
  }
@@ -205,14 +205,14 @@ public class RemoteAudioAsset: AudioAsset {
205
205
  forName: .AVPlayerItemDidPlayToEndTime,
206
206
  object: playerItem,
207
207
  queue: OperationQueue.main) { [weak self, weak player] notification in
208
- guard let strongPlayer = player,
209
- let strongSelf = self,
210
- let item = notification.object as? AVPlayerItem,
211
- strongPlayer.currentItem === item else { return }
208
+ guard let strongPlayer = player,
209
+ let strongSelf = self,
210
+ let item = notification.object as? AVPlayerItem,
211
+ strongPlayer.currentItem === item else { return }
212
212
 
213
- strongPlayer.seek(to: .zero)
214
- strongPlayer.play()
215
- }
213
+ strongPlayer.seek(to: .zero)
214
+ strongPlayer.play()
215
+ }
216
216
 
217
217
  notificationObservers.append(observer)
218
218
 
@@ -0,0 +1,308 @@
1
+ import XCTest
2
+ import Capacitor
3
+ import AVFoundation
4
+ @testable import NativeAudio
5
+
6
+ class PluginTests: XCTestCase {
7
+
8
+ var plugin: NativeAudio!
9
+ var tempFileURL: URL!
10
+ var testAssetId = "testAssetId"
11
+ var testRemoteAssetId = "testRemoteAssetId"
12
+
13
+ override func setUp() {
14
+ super.setUp()
15
+ plugin = NativeAudio()
16
+
17
+ // Create a temporary audio file for testing
18
+ let audioFilePath = NSTemporaryDirectory().appending("testAudio.wav")
19
+ tempFileURL = URL(fileURLWithPath: audioFilePath)
20
+
21
+ // Create a simple test audio file if needed
22
+ if !FileManager.default.fileExists(atPath: audioFilePath) {
23
+ createTestAudioFile(at: audioFilePath)
24
+ }
25
+ }
26
+
27
+ override func tearDown() {
28
+ // Clean up any audio assets
29
+ plugin.executeOnAudioQueue {
30
+ if let asset = self.plugin.audioList[self.testAssetId] as? AudioAsset {
31
+ asset.unload()
32
+ }
33
+ if let asset = self.plugin.audioList[self.testRemoteAssetId] as? RemoteAudioAsset {
34
+ asset.unload()
35
+ }
36
+ self.plugin.audioList.removeAll()
37
+ }
38
+
39
+ // Try to delete the temporary file
40
+ try? FileManager.default.removeItem(at: tempFileURL)
41
+
42
+ plugin = nil
43
+ super.tearDown()
44
+ }
45
+
46
+ // Helper method to create a simple test audio file
47
+ private func createTestAudioFile(at path: String) {
48
+ // This is a placeholder for a real implementation
49
+ // In a real scenario, you would create a small audio file for testing
50
+ // For now, we'll just create an empty file
51
+ FileManager.default.createFile(atPath: path, contents: Data(), attributes: nil)
52
+ }
53
+
54
+ func testAudioAssetInitialization() {
55
+ let expectation = XCTestExpectation(description: "Initialize AudioAsset")
56
+
57
+ plugin.executeOnAudioQueue {
58
+ // Create an audio asset
59
+ let asset = AudioAsset(
60
+ owner: self.plugin,
61
+ withAssetId: self.testAssetId,
62
+ withPath: self.tempFileURL.path,
63
+ withChannels: 1,
64
+ withVolume: 0.5,
65
+ withFadeDelay: 0.5
66
+ )
67
+
68
+ // Add it to the plugin's audio list
69
+ self.plugin.audioList[self.testAssetId] = asset
70
+
71
+ // Verify initial values
72
+ XCTAssertEqual(asset.assetId, self.testAssetId)
73
+ XCTAssertEqual(asset.initialVolume, 0.5)
74
+ XCTAssertEqual(asset.fadeDelay, 0.5)
75
+
76
+ expectation.fulfill()
77
+ }
78
+
79
+ wait(for: [expectation], timeout: 5.0)
80
+ }
81
+
82
+ func testAudioAssetVolumeControl() {
83
+ let expectation = XCTestExpectation(description: "Test volume control")
84
+
85
+ plugin.executeOnAudioQueue {
86
+ // Create an audio asset
87
+ let asset = AudioAsset(
88
+ owner: self.plugin,
89
+ withAssetId: self.testAssetId,
90
+ withPath: self.tempFileURL.path,
91
+ withChannels: 1,
92
+ withVolume: 1.0,
93
+ withFadeDelay: 0.5
94
+ )
95
+
96
+ // Add it to the plugin's audio list
97
+ self.plugin.audioList[self.testAssetId] = asset
98
+
99
+ // Test setting volume
100
+ let testVolume: Float = 0.7
101
+ asset.setVolume(volume: NSNumber(value: testVolume))
102
+
103
+ // We can't directly check player.volume as it may take time to set
104
+ // So we'll just verify the method doesn't crash
105
+
106
+ expectation.fulfill()
107
+ }
108
+
109
+ wait(for: [expectation], timeout: 5.0)
110
+ }
111
+
112
+ func testRemoteAudioAssetInitialization() {
113
+ let expectation = XCTestExpectation(description: "Initialize RemoteAudioAsset")
114
+
115
+ // Use a publicly accessible test audio URL
116
+ let testURL = "https://file-examples.com/storage/fe5947fd2362a2f06a86851/2017/11/file_example_MP3_700KB.mp3"
117
+
118
+ plugin.executeOnAudioQueue {
119
+ // Create a remote audio asset
120
+ let asset = RemoteAudioAsset(
121
+ owner: self.plugin,
122
+ withAssetId: self.testRemoteAssetId,
123
+ withPath: testURL,
124
+ withChannels: 1,
125
+ withVolume: 0.6,
126
+ withFadeDelay: 0.3
127
+ )
128
+
129
+ // Add it to the plugin's audio list
130
+ self.plugin.audioList[self.testRemoteAssetId] = asset
131
+
132
+ // Verify initial values
133
+ XCTAssertEqual(asset.assetId, self.testRemoteAssetId)
134
+ XCTAssertEqual(asset.initialVolume, 0.6)
135
+ XCTAssertEqual(asset.fadeDelay, 0.3)
136
+ XCTAssertNotNil(asset.asset, "AVURLAsset should be created")
137
+
138
+ expectation.fulfill()
139
+ }
140
+
141
+ wait(for: [expectation], timeout: 5.0)
142
+ }
143
+
144
+ func testPluginPreloadMethod() {
145
+ // Create a plugin call to test the preload method
146
+ let call = CAPPluginCall(callbackId: "test", options: [
147
+ "assetId": testAssetId,
148
+ "assetPath": tempFileURL.path,
149
+ "volume": 0.8,
150
+ "audioChannelNum": 2
151
+ ], success: { (_, _) in
152
+ // Success case
153
+ }, error: { (_) in
154
+ XCTFail("Preload shouldn't fail")
155
+ })
156
+
157
+ // Call the plugin method
158
+ plugin.preload(call)
159
+
160
+ // Verify the asset was loaded by checking if it exists in the audioList
161
+ plugin.executeOnAudioQueue {
162
+ XCTAssertNotNil(self.plugin.audioList[self.testAssetId])
163
+ if let asset = self.plugin.audioList[self.testAssetId] as? AudioAsset {
164
+ XCTAssertEqual(asset.assetId, self.testAssetId)
165
+ XCTAssertEqual(asset.initialVolume, 0.8)
166
+ } else {
167
+ XCTFail("Asset should be of type AudioAsset")
168
+ }
169
+ }
170
+ }
171
+
172
+ func testFadeEffects() {
173
+ let expectation = XCTestExpectation(description: "Test fade effects")
174
+
175
+ plugin.executeOnAudioQueue {
176
+ // Create an audio asset
177
+ let asset = AudioAsset(
178
+ owner: self.plugin,
179
+ withAssetId: self.testAssetId,
180
+ withPath: self.tempFileURL.path,
181
+ withChannels: 1,
182
+ withVolume: 1.0,
183
+ withFadeDelay: 0.2
184
+ )
185
+
186
+ // Test fade functionality (just make sure it doesn't crash)
187
+ asset.playWithFade(time: 0)
188
+
189
+ // Wait a short time for fade to start
190
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
191
+ // Then test stop with fade
192
+ asset.stopWithFade()
193
+
194
+ // Wait for fade to complete
195
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
196
+ expectation.fulfill()
197
+ }
198
+ }
199
+ }
200
+
201
+ wait(for: [expectation], timeout: 5.0)
202
+ }
203
+
204
+ // Test the ClearCache functionality
205
+ func testClearCache() {
206
+ // This is mostly a method call test to ensure it doesn't crash
207
+ RemoteAudioAsset.clearCache()
208
+
209
+ // We can't easily verify the cache was cleared without complex setup,
210
+ // but we can ensure the method completes without errors
211
+ XCTAssertTrue(true)
212
+ }
213
+
214
+ // Test notification observer pattern in RemoteAudioAsset
215
+ func testNotificationObserverPattern() {
216
+ let expectation = XCTestExpectation(description: "Test notification observer")
217
+
218
+ // Use a publicly accessible test audio URL
219
+ let testURL = "https://file-examples.com/storage/fe5947fd2362a2f06a86851/2017/11/file_example_MP3_700KB.mp3"
220
+
221
+ plugin.executeOnAudioQueue {
222
+ // Create a remote audio asset
223
+ let asset = RemoteAudioAsset(
224
+ owner: self.plugin,
225
+ withAssetId: self.testRemoteAssetId,
226
+ withPath: testURL,
227
+ withChannels: 1,
228
+ withVolume: 0.6,
229
+ withFadeDelay: 0.3
230
+ )
231
+
232
+ // Add it to the plugin's audio list
233
+ self.plugin.audioList[self.testRemoteAssetId] = asset
234
+
235
+ // Verify initial values
236
+ XCTAssertEqual(asset.notificationObservers.count, 0, "Should start with zero notification observers")
237
+
238
+ // Test the resume method which sets up a notification observer
239
+ asset.resume()
240
+
241
+ // Check that a notification observer was added
242
+ XCTAssertGreaterThan(asset.notificationObservers.count, 0, "Should have added notification observers")
243
+
244
+ // Now test cleanup
245
+ asset.cleanupNotificationObservers()
246
+ XCTAssertEqual(asset.notificationObservers.count, 0, "Should have removed all notification observers")
247
+
248
+ expectation.fulfill()
249
+ }
250
+
251
+ wait(for: [expectation], timeout: 5.0)
252
+ }
253
+
254
+ // Test the fade timer functionality, which was a key part of our fixes
255
+ func testFadeTimerFunctionality() {
256
+ let expectation = XCTestExpectation(description: "Test fade timer")
257
+
258
+ plugin.executeOnAudioQueue {
259
+ // Create an audio asset
260
+ let asset = AudioAsset(
261
+ owner: self.plugin,
262
+ withAssetId: self.testAssetId,
263
+ withPath: self.tempFileURL.path,
264
+ withChannels: 1,
265
+ withVolume: 1.0,
266
+ withFadeDelay: 0.5
267
+ )
268
+
269
+ // Ensure the fade timer is nil initially
270
+ XCTAssertNil(asset.fadeTimer, "Fade timer should be nil initially")
271
+
272
+ // Access the private method using reflection
273
+ // (this is a test-only approach to access private methods)
274
+ let selector = NSSelectorFromString("startVolumeRamp:to:player:")
275
+ if asset.responds(to: selector) {
276
+ // Create a test mock for AVAudioPlayer
277
+ guard let player = asset.channels.first else {
278
+ XCTFail("No audio player available")
279
+ expectation.fulfill()
280
+ return
281
+ }
282
+
283
+ // Set initial volume
284
+ player.volume = 1.0
285
+
286
+ // Invoke using performSelector
287
+ asset.perform(selector, with: NSNumber(value: 1.0), with: NSNumber(value: 0.0), with: player)
288
+
289
+ // Check that the fade timer was created
290
+ XCTAssertNotNil(asset.fadeTimer, "Fade timer should be created")
291
+
292
+ // Wait for fade to complete
293
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
294
+ // Fade should be complete, timer should be nil again
295
+ XCTAssertNil(asset.fadeTimer, "Fade timer should be nil after completion")
296
+ XCTAssertEqual(player.volume, 0.0, "Volume should be 0 after fade out")
297
+
298
+ expectation.fulfill()
299
+ }
300
+ } else {
301
+ XCTFail("startVolumeRamp method not available")
302
+ expectation.fulfill()
303
+ }
304
+ }
305
+
306
+ wait(for: [expectation], timeout: 5.0)
307
+ }
308
+ }
@@ -0,0 +1,39 @@
1
+ # Native Audio Plugin Tests
2
+
3
+ This directory contains tests for the Capacitor Native Audio Plugin.
4
+
5
+ ## Running Tests
6
+
7
+ The easiest way to run these tests is through Xcode:
8
+
9
+ 1. Open the `Plugin.xcworkspace` file in Xcode
10
+ 2. Select the "Plugin" scheme
11
+ 3. Go to Product > Test (or press ⌘+U)
12
+
13
+ ## Test Coverage
14
+
15
+ The tests cover the following functionality:
16
+
17
+ - Basic initialization of `AudioAsset` and `RemoteAudioAsset` classes
18
+ - Volume control
19
+ - Fade effects (fade in/out)
20
+ - Notification observer pattern
21
+ - Cache clearing
22
+ - Plugin preloading
23
+
24
+ ## Notes for Test Development
25
+
26
+ - The tests use a temporary audio file created at runtime
27
+ - For remote audio tests, a publicly accessible MP3 file is used
28
+ - Some tests use reflection to access private methods (for testing purposes only)
29
+ - All tests run on the plugin's audio queue to maintain thread safety
30
+
31
+ ## Troubleshooting
32
+
33
+ If tests fail:
34
+
35
+ 1. Make sure the iOS simulator is available
36
+ 2. Check that the audio session is properly configured
37
+ 3. For remote tests, ensure network connectivity is available
38
+
39
+ To view detailed test logs, expand the test navigator in Xcode and click on the failing test.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "7.3.37",
3
+ "version": "7.5.1",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MIT",
6
6
  "main": "dist/plugin.cjs.js",
@@ -11,7 +11,9 @@
11
11
  "android/src/main/",
12
12
  "android/build.gradle",
13
13
  "dist/",
14
- "ios/Plugin/",
14
+ "ios/Sources",
15
+ "ios/Tests",
16
+ "Package.swift",
15
17
  "CapgoNativeAudio.podspec"
16
18
  ],
17
19
  "author": "Martin Donadieu",
@@ -32,7 +34,7 @@
32
34
  ],
33
35
  "scripts": {
34
36
  "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
35
- "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin && cd ..",
37
+ "verify:ios": "xcodebuild -scheme CapgoNativeAudio -destination generic/platform=iOS",
36
38
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
37
39
  "verify:web": "npm run build",
38
40
  "test": "npm run test:ios",
@@ -1,24 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>CFBundleDevelopmentRegion</key>
6
- <string>$(DEVELOPMENT_LANGUAGE)</string>
7
- <key>CFBundleExecutable</key>
8
- <string>$(EXECUTABLE_NAME)</string>
9
- <key>CFBundleIdentifier</key>
10
- <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
- <key>CFBundleInfoDictionaryVersion</key>
12
- <string>6.0</string>
13
- <key>CFBundleName</key>
14
- <string>$(PRODUCT_NAME)</string>
15
- <key>CFBundlePackageType</key>
16
- <string>FMWK</string>
17
- <key>CFBundleShortVersionString</key>
18
- <string>$(MARKETING_VERSION)</string>
19
- <key>CFBundleVersion</key>
20
- <string>$(CURRENT_PROJECT_VERSION)</string>
21
- <key>NSPrincipalClass</key>
22
- <string></string>
23
- </dict>
24
- </plist>