sezo-audio-engine 0.0.15 → 0.0.16
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 +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/expo/modules/audioengine/ExpoAudioEngineModule.kt +227 -49
- package/dist/AudioEngineModule.js +1 -1
- package/dist/AudioEngineModule.types.d.ts +53 -2
- package/ios/ExpoAudioEngineModule.swift +183 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ allprojects {
|
|
|
38
38
|
Optionally pin the engine version in `android/gradle.properties`:
|
|
39
39
|
|
|
40
40
|
```properties
|
|
41
|
-
sezoAudioEngineVersion=android-engine-v0.1.
|
|
41
|
+
sezoAudioEngineVersion=android-engine-v0.1.8
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
If you want to build from source instead, include the local engine module in
|
package/android/build.gradle
CHANGED
|
@@ -46,7 +46,7 @@ dependencies {
|
|
|
46
46
|
if (androidEngineProject != null) {
|
|
47
47
|
implementation androidEngineProject
|
|
48
48
|
} else {
|
|
49
|
-
def sezoAudioEngineVersion = project.findProperty("sezoAudioEngineVersion") ?: "android-engine-v0.1.
|
|
49
|
+
def sezoAudioEngineVersion = project.findProperty("sezoAudioEngineVersion") ?: "android-engine-v0.1.8"
|
|
50
50
|
implementation "com.github.Sepzie:SezoAudioEngine:${sezoAudioEngineVersion}"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -62,7 +62,14 @@ class ExpoAudioEngineModule : Module() {
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
private fun requireEngine(): AudioEngine {
|
|
65
|
-
return audioEngine ?:
|
|
65
|
+
return audioEngine ?: run {
|
|
66
|
+
emitEngineError(
|
|
67
|
+
code = "ENGINE_NOT_INITIALIZED",
|
|
68
|
+
message = "Engine not initialized",
|
|
69
|
+
source = "engine"
|
|
70
|
+
)
|
|
71
|
+
throw CodedException("ENGINE_NOT_INITIALIZED", "Engine not initialized", null)
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
override fun definition() = ModuleDefinition {
|
|
@@ -70,6 +77,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
70
77
|
|
|
71
78
|
OnActivityEntersBackground {
|
|
72
79
|
Log.d(TAG, "Activity entering background")
|
|
80
|
+
emitDebugLog("debug", "Activity entering background")
|
|
73
81
|
val engine = audioEngine ?: return@OnActivityEntersBackground
|
|
74
82
|
|
|
75
83
|
if (backgroundPlaybackEnabled) {
|
|
@@ -81,31 +89,33 @@ class ExpoAudioEngineModule : Module() {
|
|
|
81
89
|
if (wasPlayingBeforePause) {
|
|
82
90
|
Log.d(TAG, "Pausing engine before background (was playing)")
|
|
83
91
|
pausePlaybackInternal(engine, fromSystem = true, keepAudioFocus = false)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"wasPlaying" to true
|
|
87
|
-
)
|
|
92
|
+
emitEngineStateChanged(
|
|
93
|
+
reason = "backgrounded",
|
|
94
|
+
payload = mapOf("wasPlaying" to true)
|
|
95
|
+
)
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
OnActivityEntersForeground {
|
|
92
100
|
Log.d(TAG, "Activity entering foreground")
|
|
101
|
+
emitDebugLog("debug", "Activity entering foreground")
|
|
93
102
|
val engine = audioEngine ?: return@OnActivityEntersForeground
|
|
94
103
|
|
|
95
104
|
// Check stream health on resume
|
|
96
105
|
if (!engine.isStreamHealthy()) {
|
|
97
106
|
Log.w(TAG, "Stream unhealthy on resume, attempting restart")
|
|
98
107
|
val restarted = engine.restartStream()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"success" to restarted
|
|
102
|
-
)
|
|
108
|
+
emitEngineStateChanged(
|
|
109
|
+
reason = "streamRestarted",
|
|
110
|
+
payload = mapOf("success" to restarted)
|
|
111
|
+
)
|
|
103
112
|
if (!restarted) {
|
|
104
113
|
Log.e(TAG, "Failed to restart stream on resume")
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
emitEngineError(
|
|
115
|
+
code = "STREAM_DISCONNECTED",
|
|
116
|
+
message = "Audio stream could not be recovered after returning to foreground",
|
|
117
|
+
source = "engine"
|
|
118
|
+
)
|
|
109
119
|
return@OnActivityEntersForeground
|
|
110
120
|
}
|
|
111
121
|
}
|
|
@@ -115,10 +125,9 @@ class ExpoAudioEngineModule : Module() {
|
|
|
115
125
|
Log.d(TAG, "Resuming playback after returning to foreground")
|
|
116
126
|
val resumed = startPlaybackInternal(engine, fromSystem = true)
|
|
117
127
|
wasPlayingBeforePause = false
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
mapOf(
|
|
121
|
-
"reason" to "resumed",
|
|
128
|
+
emitEngineStateChanged(
|
|
129
|
+
reason = "resumed",
|
|
130
|
+
payload = mapOf(
|
|
122
131
|
"wasPlaying" to true,
|
|
123
132
|
"success" to resumed
|
|
124
133
|
)
|
|
@@ -148,6 +157,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
148
157
|
|
|
149
158
|
AsyncFunction("initialize") { config: Map<String, Any?> ->
|
|
150
159
|
Log.d(TAG, "Initialize called with config: $config")
|
|
160
|
+
emitDebugLog("info", "Initialize called", mapOf("config" to config))
|
|
151
161
|
|
|
152
162
|
val sampleRate = (config["sampleRate"] as? Number)?.toInt() ?: 44100
|
|
153
163
|
val maxTracks = (config["maxTracks"] as? Number)?.toInt() ?: 8
|
|
@@ -156,6 +166,11 @@ class ExpoAudioEngineModule : Module() {
|
|
|
156
166
|
val success = audioEngine?.initialize(sampleRate, maxTracks) ?: false
|
|
157
167
|
|
|
158
168
|
if (!success) {
|
|
169
|
+
emitEngineError(
|
|
170
|
+
code = "ENGINE_INIT_FAILED",
|
|
171
|
+
message = "Failed to initialize audio engine",
|
|
172
|
+
source = "engine"
|
|
173
|
+
)
|
|
159
174
|
throw CodedException("ENGINE_INIT_FAILED", "Failed to initialize audio engine", null)
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -171,6 +186,13 @@ class ExpoAudioEngineModule : Module() {
|
|
|
171
186
|
"durationMs" to durationMs
|
|
172
187
|
)
|
|
173
188
|
)
|
|
189
|
+
sendEvent(
|
|
190
|
+
"positionUpdate",
|
|
191
|
+
mapOf(
|
|
192
|
+
"positionMs" to positionMs,
|
|
193
|
+
"durationMs" to durationMs
|
|
194
|
+
)
|
|
195
|
+
)
|
|
174
196
|
handlePlaybackStateChange(state)
|
|
175
197
|
}
|
|
176
198
|
|
|
@@ -179,6 +201,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
179
201
|
|
|
180
202
|
AsyncFunction("release") {
|
|
181
203
|
Log.d(TAG, "Release called")
|
|
204
|
+
emitDebugLog("info", "Release called")
|
|
182
205
|
teardownBackgroundPlayback(clearMetadata = false, stopService = true)
|
|
183
206
|
audioEngine?.let { engine ->
|
|
184
207
|
synchronized(pendingExtractions) {
|
|
@@ -203,15 +226,47 @@ class ExpoAudioEngineModule : Module() {
|
|
|
203
226
|
|
|
204
227
|
tracks.forEach { track ->
|
|
205
228
|
val id = track["id"] as? String
|
|
206
|
-
?:
|
|
229
|
+
?: run {
|
|
230
|
+
emitEngineError(
|
|
231
|
+
code = "TRACK_LOAD_FAILED",
|
|
232
|
+
message = "Missing track id",
|
|
233
|
+
details = track,
|
|
234
|
+
source = "engine"
|
|
235
|
+
)
|
|
236
|
+
throw CodedException("TRACK_LOAD_FAILED", "Missing track id", null)
|
|
237
|
+
}
|
|
207
238
|
val uri = track["uri"] as? String
|
|
208
|
-
?:
|
|
239
|
+
?: run {
|
|
240
|
+
emitEngineError(
|
|
241
|
+
code = "TRACK_LOAD_FAILED",
|
|
242
|
+
message = "Missing track uri",
|
|
243
|
+
details = mapOf("trackId" to id),
|
|
244
|
+
source = "engine"
|
|
245
|
+
)
|
|
246
|
+
throw CodedException("TRACK_LOAD_FAILED", "Missing track uri", null)
|
|
247
|
+
}
|
|
209
248
|
val startTimeMs = (track["startTimeMs"] as? Number)?.toDouble() ?: 0.0
|
|
210
249
|
|
|
211
250
|
Log.d(TAG, "Loading track: id=$id, uri=$uri")
|
|
212
|
-
val filePath =
|
|
251
|
+
val filePath = try {
|
|
252
|
+
convertUriToPath(uri)
|
|
253
|
+
} catch (error: CodedException) {
|
|
254
|
+
emitEngineError(
|
|
255
|
+
code = "UNSUPPORTED_URI",
|
|
256
|
+
message = error.message ?: "Unsupported URI",
|
|
257
|
+
details = mapOf("trackId" to id, "uri" to uri),
|
|
258
|
+
source = "system"
|
|
259
|
+
)
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
213
262
|
|
|
214
263
|
if (!engine.loadTrack(id, filePath, startTimeMs)) {
|
|
264
|
+
emitEngineError(
|
|
265
|
+
code = "TRACK_LOAD_FAILED",
|
|
266
|
+
message = "Failed to load track: $id from $filePath",
|
|
267
|
+
details = mapOf("trackId" to id, "uri" to uri),
|
|
268
|
+
source = "engine"
|
|
269
|
+
)
|
|
215
270
|
throw CodedException(
|
|
216
271
|
"TRACK_LOAD_FAILED",
|
|
217
272
|
"Failed to load track: $id from $filePath",
|
|
@@ -221,6 +276,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
221
276
|
|
|
222
277
|
loadedTrackIds.add(id)
|
|
223
278
|
Log.d(TAG, "Track loaded successfully: $id")
|
|
279
|
+
sendEvent("trackLoaded", mapOf("trackId" to id))
|
|
224
280
|
}
|
|
225
281
|
}
|
|
226
282
|
|
|
@@ -228,18 +284,27 @@ class ExpoAudioEngineModule : Module() {
|
|
|
228
284
|
val engine = requireEngine()
|
|
229
285
|
|
|
230
286
|
if (!engine.unloadTrack(trackId)) {
|
|
287
|
+
emitEngineError(
|
|
288
|
+
code = "TRACK_UNLOAD_FAILED",
|
|
289
|
+
message = "Failed to unload track: $trackId",
|
|
290
|
+
details = mapOf("trackId" to trackId),
|
|
291
|
+
source = "engine"
|
|
292
|
+
)
|
|
231
293
|
throw CodedException("TRACK_UNLOAD_FAILED", "Failed to unload track: $trackId", null)
|
|
232
294
|
}
|
|
233
295
|
|
|
234
296
|
loadedTrackIds.remove(trackId)
|
|
235
297
|
Log.d(TAG, "Track unloaded: $trackId")
|
|
298
|
+
sendEvent("trackUnloaded", mapOf("trackId" to trackId))
|
|
236
299
|
}
|
|
237
300
|
|
|
238
301
|
AsyncFunction("unloadAllTracks") {
|
|
239
302
|
val engine = requireEngine()
|
|
303
|
+
val trackIdsToUnload = loadedTrackIds.toList()
|
|
240
304
|
engine.unloadAllTracks()
|
|
241
305
|
loadedTrackIds.clear()
|
|
242
306
|
Log.d(TAG, "All tracks unloaded")
|
|
307
|
+
trackIdsToUnload.forEach { sendEvent("trackUnloaded", mapOf("trackId" to it)) }
|
|
243
308
|
}
|
|
244
309
|
|
|
245
310
|
Function("getLoadedTracks") {
|
|
@@ -249,6 +314,11 @@ class ExpoAudioEngineModule : Module() {
|
|
|
249
314
|
AsyncFunction("play") {
|
|
250
315
|
val engine = requireEngine()
|
|
251
316
|
if (!startPlaybackInternal(engine, fromSystem = false)) {
|
|
317
|
+
emitEngineError(
|
|
318
|
+
code = "PLAYBACK_START_FAILED",
|
|
319
|
+
message = "Failed to start playback",
|
|
320
|
+
source = "playback"
|
|
321
|
+
)
|
|
252
322
|
throw CodedException("PLAYBACK_START_FAILED", "Failed to start playback", null)
|
|
253
323
|
}
|
|
254
324
|
Log.d(TAG, "Playback started")
|
|
@@ -387,11 +457,23 @@ class ExpoAudioEngineModule : Module() {
|
|
|
387
457
|
)
|
|
388
458
|
|
|
389
459
|
if (!success) {
|
|
460
|
+
emitEngineError(
|
|
461
|
+
code = "RECORDING_START_FAILED",
|
|
462
|
+
message = "Failed to start recording",
|
|
463
|
+
details = mapOf(
|
|
464
|
+
"format" to format,
|
|
465
|
+
"sampleRate" to sampleRate,
|
|
466
|
+
"channels" to channels
|
|
467
|
+
),
|
|
468
|
+
source = "recording"
|
|
469
|
+
)
|
|
390
470
|
throw CodedException("RECORDING_START_FAILED", "Failed to start recording", null)
|
|
391
471
|
}
|
|
392
472
|
|
|
393
473
|
activeRecordingFormat = format
|
|
394
474
|
Log.d(TAG, "Recording started successfully")
|
|
475
|
+
sendEvent("recordingStarted", mapOf<String, Any>())
|
|
476
|
+
emitEngineStateChanged("recordingStarted")
|
|
395
477
|
}
|
|
396
478
|
|
|
397
479
|
AsyncFunction("stopRecording") {
|
|
@@ -401,6 +483,12 @@ class ExpoAudioEngineModule : Module() {
|
|
|
401
483
|
val result = engine.stopRecording()
|
|
402
484
|
|
|
403
485
|
if (!result.success) {
|
|
486
|
+
emitEngineError(
|
|
487
|
+
code = "RECORDING_STOP_FAILED",
|
|
488
|
+
message = "Failed to stop recording: ${result.errorMessage}",
|
|
489
|
+
details = mapOf("errorMessage" to result.errorMessage),
|
|
490
|
+
source = "recording"
|
|
491
|
+
)
|
|
404
492
|
throw CodedException(
|
|
405
493
|
"RECORDING_STOP_FAILED",
|
|
406
494
|
"Failed to stop recording: ${result.errorMessage}",
|
|
@@ -423,6 +511,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
423
511
|
"fileSize" to result.fileSize
|
|
424
512
|
)
|
|
425
513
|
)
|
|
514
|
+
emitEngineStateChanged("recordingStopped")
|
|
426
515
|
|
|
427
516
|
mapOf(
|
|
428
517
|
"uri" to "file://${result.outputPath}",
|
|
@@ -469,6 +558,12 @@ class ExpoAudioEngineModule : Module() {
|
|
|
469
558
|
)
|
|
470
559
|
|
|
471
560
|
if (jobId <= 0L) {
|
|
561
|
+
emitEngineError(
|
|
562
|
+
code = "EXTRACTION_FAILED",
|
|
563
|
+
message = "Failed to start extraction",
|
|
564
|
+
details = mapOf("trackId" to trackId),
|
|
565
|
+
source = "extraction"
|
|
566
|
+
)
|
|
472
567
|
promise.reject("EXTRACTION_FAILED", "Failed to start extraction", null)
|
|
473
568
|
return@AsyncFunction
|
|
474
569
|
}
|
|
@@ -508,6 +603,11 @@ class ExpoAudioEngineModule : Module() {
|
|
|
508
603
|
)
|
|
509
604
|
|
|
510
605
|
if (jobId <= 0L) {
|
|
606
|
+
emitEngineError(
|
|
607
|
+
code = "EXTRACTION_FAILED",
|
|
608
|
+
message = "Failed to start extraction",
|
|
609
|
+
source = "extraction"
|
|
610
|
+
)
|
|
511
611
|
promise.reject("EXTRACTION_FAILED", "Failed to start extraction", null)
|
|
512
612
|
return@AsyncFunction
|
|
513
613
|
}
|
|
@@ -551,7 +651,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
551
651
|
requestAudioFocus()
|
|
552
652
|
}
|
|
553
653
|
syncBackgroundService(engine.isPlaying())
|
|
554
|
-
|
|
654
|
+
emitEngineStateChanged("backgroundPlaybackEnabled")
|
|
555
655
|
}
|
|
556
656
|
Function("updateNowPlayingInfo") { metadata: Map<String, Any?> ->
|
|
557
657
|
mergeNowPlayingMetadata(metadata)
|
|
@@ -568,7 +668,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
568
668
|
abandonAudioFocus()
|
|
569
669
|
}
|
|
570
670
|
nowPlayingMetadata.clear()
|
|
571
|
-
|
|
671
|
+
emitEngineStateChanged("backgroundPlaybackDisabled")
|
|
572
672
|
}
|
|
573
673
|
|
|
574
674
|
Events(
|
|
@@ -582,6 +682,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
582
682
|
"extractionProgress",
|
|
583
683
|
"extractionComplete",
|
|
584
684
|
"engineStateChanged",
|
|
685
|
+
"debugLog",
|
|
585
686
|
"error"
|
|
586
687
|
)
|
|
587
688
|
}
|
|
@@ -621,6 +722,21 @@ class ExpoAudioEngineModule : Module() {
|
|
|
621
722
|
|
|
622
723
|
if (!result.success) {
|
|
623
724
|
Log.e(TAG, "Extraction failed for job=$jobId: ${result.errorMessage}")
|
|
725
|
+
val code = if (result.errorMessage == "Extraction cancelled") {
|
|
726
|
+
"EXTRACTION_CANCELLED"
|
|
727
|
+
} else {
|
|
728
|
+
"EXTRACTION_FAILED"
|
|
729
|
+
}
|
|
730
|
+
emitEngineError(
|
|
731
|
+
code = code,
|
|
732
|
+
message = result.errorMessage ?: "Unknown extraction error",
|
|
733
|
+
details = mapOf(
|
|
734
|
+
"jobId" to jobId,
|
|
735
|
+
"trackId" to pending.trackId,
|
|
736
|
+
"operation" to pending.operation
|
|
737
|
+
),
|
|
738
|
+
source = "extraction"
|
|
739
|
+
)
|
|
624
740
|
sendEvent(
|
|
625
741
|
"extractionComplete",
|
|
626
742
|
mapOf(
|
|
@@ -634,11 +750,6 @@ class ExpoAudioEngineModule : Module() {
|
|
|
634
750
|
"operation" to pending.operation
|
|
635
751
|
)
|
|
636
752
|
)
|
|
637
|
-
val code = if (result.errorMessage == "Extraction cancelled") {
|
|
638
|
-
"EXTRACTION_CANCELLED"
|
|
639
|
-
} else {
|
|
640
|
-
"EXTRACTION_FAILED"
|
|
641
|
-
}
|
|
642
753
|
pending.promise.reject(code, result.errorMessage, null)
|
|
643
754
|
} else {
|
|
644
755
|
Log.d(TAG, "Extraction successful: ${result.fileSize} bytes, ${result.durationSamples} samples")
|
|
@@ -696,12 +807,10 @@ class ExpoAudioEngineModule : Module() {
|
|
|
696
807
|
Log.w(TAG, "Stream unhealthy before play, attempting restart")
|
|
697
808
|
if (!engine.restartStream()) {
|
|
698
809
|
if (!fromSystem) {
|
|
699
|
-
|
|
700
|
-
"
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
"message" to "Audio stream is disconnected and could not be recovered"
|
|
704
|
-
)
|
|
810
|
+
emitEngineError(
|
|
811
|
+
code = "STREAM_DISCONNECTED",
|
|
812
|
+
message = "Audio stream is disconnected and could not be recovered",
|
|
813
|
+
source = "engine"
|
|
705
814
|
)
|
|
706
815
|
}
|
|
707
816
|
return false
|
|
@@ -711,12 +820,10 @@ class ExpoAudioEngineModule : Module() {
|
|
|
711
820
|
if (!requestAudioFocus()) {
|
|
712
821
|
Log.w(TAG, "Audio focus request denied")
|
|
713
822
|
if (!fromSystem) {
|
|
714
|
-
|
|
715
|
-
"
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
"message" to "Could not gain audio focus for playback"
|
|
719
|
-
)
|
|
823
|
+
emitEngineError(
|
|
824
|
+
code = "AUDIO_FOCUS_DENIED",
|
|
825
|
+
message = "Could not gain audio focus for playback",
|
|
826
|
+
source = "focus"
|
|
720
827
|
)
|
|
721
828
|
}
|
|
722
829
|
return false
|
|
@@ -750,7 +857,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
750
857
|
}
|
|
751
858
|
|
|
752
859
|
if (fromSystem) {
|
|
753
|
-
|
|
860
|
+
emitEngineStateChanged("pausedFromSystem")
|
|
754
861
|
}
|
|
755
862
|
}
|
|
756
863
|
|
|
@@ -769,7 +876,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
769
876
|
}
|
|
770
877
|
|
|
771
878
|
if (fromSystem) {
|
|
772
|
-
|
|
879
|
+
emitEngineStateChanged("stoppedFromSystem")
|
|
773
880
|
}
|
|
774
881
|
}
|
|
775
882
|
|
|
@@ -826,7 +933,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
826
933
|
if (engine.isPlaying()) {
|
|
827
934
|
shouldResumeAfterTransientFocusLoss = true
|
|
828
935
|
pausePlaybackInternal(engine, fromSystem = true, keepAudioFocus = true)
|
|
829
|
-
|
|
936
|
+
emitEngineStateChanged("audioFocusLossTransient")
|
|
830
937
|
}
|
|
831
938
|
}
|
|
832
939
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
@@ -834,12 +941,9 @@ class ExpoAudioEngineModule : Module() {
|
|
|
834
941
|
volumeBeforeDuck = engine.getMasterVolume()
|
|
835
942
|
val duckedVolume = (volumeBeforeDuck!! * 0.3f).coerceAtLeast(0.05f)
|
|
836
943
|
engine.setMasterVolume(duckedVolume)
|
|
837
|
-
|
|
838
|
-
"
|
|
839
|
-
mapOf(
|
|
840
|
-
"reason" to "audioFocusDuck",
|
|
841
|
-
"volume" to duckedVolume.toDouble()
|
|
842
|
-
)
|
|
944
|
+
emitEngineStateChanged(
|
|
945
|
+
reason = "audioFocusDuck",
|
|
946
|
+
payload = mapOf("volume" to duckedVolume.toDouble())
|
|
843
947
|
)
|
|
844
948
|
}
|
|
845
949
|
}
|
|
@@ -847,7 +951,7 @@ class ExpoAudioEngineModule : Module() {
|
|
|
847
951
|
shouldResumeAfterTransientFocusLoss = false
|
|
848
952
|
if (engine.isPlaying()) {
|
|
849
953
|
pausePlaybackInternal(engine, fromSystem = true)
|
|
850
|
-
|
|
954
|
+
emitEngineStateChanged("audioFocusLoss")
|
|
851
955
|
} else {
|
|
852
956
|
abandonAudioFocus()
|
|
853
957
|
}
|
|
@@ -900,6 +1004,80 @@ class ExpoAudioEngineModule : Module() {
|
|
|
900
1004
|
abandonAudioFocus()
|
|
901
1005
|
}
|
|
902
1006
|
|
|
1007
|
+
private fun emitEngineStateChanged(
|
|
1008
|
+
reason: String,
|
|
1009
|
+
payload: Map<String, Any?> = emptyMap()
|
|
1010
|
+
) {
|
|
1011
|
+
val eventPayload = payload.toMutableMap()
|
|
1012
|
+
eventPayload["reason"] = reason
|
|
1013
|
+
sendEvent("engineStateChanged", eventPayload)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private fun emitDebugLog(
|
|
1017
|
+
level: String,
|
|
1018
|
+
message: String,
|
|
1019
|
+
context: Map<String, Any?>? = null
|
|
1020
|
+
) {
|
|
1021
|
+
val payload = mutableMapOf<String, Any?>(
|
|
1022
|
+
"level" to level,
|
|
1023
|
+
"message" to message
|
|
1024
|
+
)
|
|
1025
|
+
if (context != null) {
|
|
1026
|
+
payload["context"] = context
|
|
1027
|
+
}
|
|
1028
|
+
sendEvent("debugLog", payload)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private fun emitEngineError(
|
|
1032
|
+
code: String,
|
|
1033
|
+
message: String,
|
|
1034
|
+
details: Any? = null,
|
|
1035
|
+
source: String? = null
|
|
1036
|
+
) {
|
|
1037
|
+
val resolvedSource = source ?: resolveErrorSource(code)
|
|
1038
|
+
val (severity, recoverable) = classifyError(code)
|
|
1039
|
+
val payload = mutableMapOf<String, Any?>(
|
|
1040
|
+
"code" to code,
|
|
1041
|
+
"message" to message,
|
|
1042
|
+
"severity" to severity,
|
|
1043
|
+
"recoverable" to recoverable,
|
|
1044
|
+
"source" to resolvedSource,
|
|
1045
|
+
"timestampMs" to System.currentTimeMillis(),
|
|
1046
|
+
"platform" to "android"
|
|
1047
|
+
)
|
|
1048
|
+
if (details != null) {
|
|
1049
|
+
payload["details"] = details
|
|
1050
|
+
}
|
|
1051
|
+
sendEvent("error", payload)
|
|
1052
|
+
emitDebugLog(
|
|
1053
|
+
level = if (severity == "fatal") "error" else "warn",
|
|
1054
|
+
message = message,
|
|
1055
|
+
context = payload
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private fun resolveErrorSource(code: String): String {
|
|
1060
|
+
return when {
|
|
1061
|
+
code == "AUDIO_SESSION_FAILED" -> "session"
|
|
1062
|
+
code == "AUDIO_FOCUS_DENIED" -> "focus"
|
|
1063
|
+
code.startsWith("PLAYBACK_") || code == "NO_TRACKS_LOADED" -> "playback"
|
|
1064
|
+
code.startsWith("RECORDING_") -> "recording"
|
|
1065
|
+
code.startsWith("EXTRACTION_") -> "extraction"
|
|
1066
|
+
code.startsWith("ENGINE_") || code.startsWith("TRACK_") || code == "STREAM_DISCONNECTED" -> "engine"
|
|
1067
|
+
else -> "system"
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private fun classifyError(code: String): Pair<String, Boolean> {
|
|
1072
|
+
return when (code) {
|
|
1073
|
+
"ENGINE_INIT_FAILED",
|
|
1074
|
+
"ENGINE_START_FAILED",
|
|
1075
|
+
"AUDIO_SESSION_FAILED",
|
|
1076
|
+
"STREAM_DISCONNECTED" -> "fatal" to false
|
|
1077
|
+
else -> "warning" to true
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
903
1081
|
private fun mapToBundle(map: Map<String, Any?>): Bundle {
|
|
904
1082
|
val bundle = Bundle()
|
|
905
1083
|
map.forEach { (key, value) ->
|
|
@@ -42,5 +42,5 @@ export const AudioEngineModule = {
|
|
|
42
42
|
enableBackgroundPlayback: (metadata) => NativeAudioEngineModule.enableBackgroundPlayback(metadata),
|
|
43
43
|
updateNowPlayingInfo: (metadata) => NativeAudioEngineModule.updateNowPlayingInfo(metadata),
|
|
44
44
|
disableBackgroundPlayback: () => NativeAudioEngineModule.disableBackgroundPlayback(),
|
|
45
|
-
addListener: (event, callback) => NativeAudioEngineModule.addListener(event, callback)
|
|
45
|
+
addListener: ((event, callback) => NativeAudioEngineModule.addListener(event, callback))
|
|
46
46
|
};
|
|
@@ -64,11 +64,62 @@ export interface PlaybackCardOptions {
|
|
|
64
64
|
showStop?: boolean;
|
|
65
65
|
seekStepMs?: number;
|
|
66
66
|
}
|
|
67
|
-
export type AudioEngineEvent = 'playbackStateChange' | 'positionUpdate' | 'playbackComplete' | 'trackLoaded' | 'trackUnloaded' | 'recordingStarted' | 'recordingStopped' | 'extractionProgress' | 'extractionComplete' | 'error';
|
|
67
|
+
export type AudioEngineEvent = 'playbackStateChange' | 'positionUpdate' | 'playbackComplete' | 'trackLoaded' | 'trackUnloaded' | 'recordingStarted' | 'recordingStopped' | 'extractionProgress' | 'extractionComplete' | 'engineStateChanged' | 'debugLog' | 'error';
|
|
68
|
+
export type AudioEngineErrorSeverity = 'warning' | 'fatal';
|
|
69
|
+
export type AudioEngineErrorSource = 'engine' | 'session' | 'playback' | 'recording' | 'extraction' | 'focus' | 'system';
|
|
68
70
|
export interface AudioEngineError {
|
|
69
71
|
code: string;
|
|
70
72
|
message: string;
|
|
71
73
|
details?: unknown;
|
|
74
|
+
severity: AudioEngineErrorSeverity;
|
|
75
|
+
recoverable: boolean;
|
|
76
|
+
source: AudioEngineErrorSource;
|
|
77
|
+
timestampMs: number;
|
|
78
|
+
platform: 'ios' | 'android';
|
|
79
|
+
}
|
|
80
|
+
export interface AudioEngineEventMap {
|
|
81
|
+
playbackStateChange: {
|
|
82
|
+
state: string;
|
|
83
|
+
positionMs: number;
|
|
84
|
+
durationMs: number;
|
|
85
|
+
};
|
|
86
|
+
positionUpdate: {
|
|
87
|
+
positionMs: number;
|
|
88
|
+
durationMs: number;
|
|
89
|
+
};
|
|
90
|
+
playbackComplete: {
|
|
91
|
+
positionMs: number;
|
|
92
|
+
durationMs: number;
|
|
93
|
+
};
|
|
94
|
+
trackLoaded: {
|
|
95
|
+
trackId: string;
|
|
96
|
+
};
|
|
97
|
+
trackUnloaded: {
|
|
98
|
+
trackId: string;
|
|
99
|
+
};
|
|
100
|
+
recordingStarted: Record<string, never>;
|
|
101
|
+
recordingStopped: {
|
|
102
|
+
uri: string;
|
|
103
|
+
duration: number;
|
|
104
|
+
startTimeMs: number;
|
|
105
|
+
startTimeSamples?: number;
|
|
106
|
+
sampleRate: number;
|
|
107
|
+
channels: number;
|
|
108
|
+
format: 'aac' | 'm4a' | 'mp3' | 'wav';
|
|
109
|
+
fileSize: number;
|
|
110
|
+
};
|
|
111
|
+
extractionProgress: Record<string, unknown>;
|
|
112
|
+
extractionComplete: Record<string, unknown>;
|
|
113
|
+
engineStateChanged: {
|
|
114
|
+
reason: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
};
|
|
117
|
+
debugLog: {
|
|
118
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
119
|
+
message: string;
|
|
120
|
+
context?: unknown;
|
|
121
|
+
};
|
|
122
|
+
error: AudioEngineError;
|
|
72
123
|
}
|
|
73
124
|
export interface AudioEngine {
|
|
74
125
|
initialize(config: AudioEngineConfig): Promise<void>;
|
|
@@ -112,7 +163,7 @@ export interface AudioEngine {
|
|
|
112
163
|
enableBackgroundPlayback(metadata: MediaMetadata): Promise<void>;
|
|
113
164
|
updateNowPlayingInfo(metadata: Partial<MediaMetadata>): void;
|
|
114
165
|
disableBackgroundPlayback(): Promise<void>;
|
|
115
|
-
addListener(event:
|
|
166
|
+
addListener<K extends keyof AudioEngineEventMap>(event: K, callback: (payload: AudioEngineEventMap[K]) => void): {
|
|
116
167
|
remove: () => void;
|
|
117
168
|
};
|
|
118
169
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
2
3
|
|
|
3
4
|
public class ExpoAudioEngineModule: Module {
|
|
4
5
|
private let engine = AudioEngineFacade()
|
|
@@ -6,6 +7,7 @@ public class ExpoAudioEngineModule: Module {
|
|
|
6
7
|
public func definition() -> ModuleDefinition {
|
|
7
8
|
Name("ExpoAudioEngineModule")
|
|
8
9
|
OnCreate {
|
|
10
|
+
emitDebugLog(level: "info", message: "ExpoAudioEngineModule created")
|
|
9
11
|
engine.onPlaybackStateChange = { [weak self] state, positionMs, durationMs in
|
|
10
12
|
DispatchQueue.main.async {
|
|
11
13
|
self?.sendEvent(
|
|
@@ -16,6 +18,13 @@ public class ExpoAudioEngineModule: Module {
|
|
|
16
18
|
"durationMs": durationMs
|
|
17
19
|
]
|
|
18
20
|
)
|
|
21
|
+
self?.sendEvent(
|
|
22
|
+
"positionUpdate",
|
|
23
|
+
[
|
|
24
|
+
"positionMs": positionMs,
|
|
25
|
+
"durationMs": durationMs
|
|
26
|
+
]
|
|
27
|
+
)
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
engine.onPlaybackComplete = { [weak self] positionMs, durationMs in
|
|
@@ -31,14 +40,7 @@ public class ExpoAudioEngineModule: Module {
|
|
|
31
40
|
}
|
|
32
41
|
engine.onError = { [weak self] code, message, details in
|
|
33
42
|
DispatchQueue.main.async {
|
|
34
|
-
|
|
35
|
-
"code": code,
|
|
36
|
-
"message": message
|
|
37
|
-
]
|
|
38
|
-
if let details = details {
|
|
39
|
-
payload["details"] = details
|
|
40
|
-
}
|
|
41
|
-
self?.sendEvent("error", payload)
|
|
43
|
+
self?.emitEngineError(code: code, message: message, details: details)
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -46,13 +48,18 @@ public class ExpoAudioEngineModule: Module {
|
|
|
46
48
|
engine.onPlaybackStateChange = nil
|
|
47
49
|
engine.onPlaybackComplete = nil
|
|
48
50
|
engine.onError = nil
|
|
51
|
+
emitDebugLog(level: "info", message: "ExpoAudioEngineModule destroyed")
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
AsyncFunction("initialize") { (config: [String: Any]) in
|
|
52
55
|
engine.initialize(config: config)
|
|
56
|
+
emitEngineStateChanged(reason: "initialized")
|
|
57
|
+
emitDebugLog(level: "info", message: "Audio engine initialized", context: ["config": config])
|
|
53
58
|
}
|
|
54
59
|
AsyncFunction("release") {
|
|
55
60
|
engine.releaseResources()
|
|
61
|
+
emitEngineStateChanged(reason: "released")
|
|
62
|
+
emitDebugLog(level: "info", message: "Audio engine released")
|
|
56
63
|
}
|
|
57
64
|
AsyncFunction("loadTracks") { (tracks: [[String: Any]]) throws in
|
|
58
65
|
if let failure = engine.loadTracks(tracks) {
|
|
@@ -60,14 +67,25 @@ public class ExpoAudioEngineModule: Module {
|
|
|
60
67
|
let message = failedCount > 0
|
|
61
68
|
? "Failed to load \(failedCount) track(s)."
|
|
62
69
|
: "Failed to load tracks."
|
|
70
|
+
emitEngineError(code: "TRACK_LOAD_FAILED", message: message, details: failure, source: "engine")
|
|
63
71
|
throw AudioEngineError("TRACK_LOAD_FAILED", message, details: failure)
|
|
64
72
|
}
|
|
73
|
+
for track in tracks {
|
|
74
|
+
if let trackId = track["id"] as? String {
|
|
75
|
+
sendEvent("trackLoaded", ["trackId": trackId])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
65
78
|
}
|
|
66
79
|
AsyncFunction("unloadTrack") { (trackId: String) in
|
|
67
80
|
engine.unloadTrack(trackId)
|
|
81
|
+
sendEvent("trackUnloaded", ["trackId": trackId])
|
|
68
82
|
}
|
|
69
83
|
AsyncFunction("unloadAllTracks") {
|
|
84
|
+
let loadedTrackIds = engine.getLoadedTracks().compactMap { $0["id"] as? String }
|
|
70
85
|
engine.unloadAllTracks()
|
|
86
|
+
for trackId in loadedTrackIds {
|
|
87
|
+
sendEvent("trackUnloaded", ["trackId": trackId])
|
|
88
|
+
}
|
|
71
89
|
}
|
|
72
90
|
Function("getLoadedTracks") {
|
|
73
91
|
return engine.getLoadedTracks()
|
|
@@ -84,6 +102,13 @@ public class ExpoAudioEngineModule: Module {
|
|
|
84
102
|
}
|
|
85
103
|
Function("seek") { (positionMs: Double) in
|
|
86
104
|
engine.seek(positionMs: positionMs)
|
|
105
|
+
sendEvent(
|
|
106
|
+
"positionUpdate",
|
|
107
|
+
[
|
|
108
|
+
"positionMs": engine.getCurrentPosition(),
|
|
109
|
+
"durationMs": engine.getDuration()
|
|
110
|
+
]
|
|
111
|
+
)
|
|
87
112
|
}
|
|
88
113
|
Function("isPlaying") {
|
|
89
114
|
return engine.isPlaying()
|
|
@@ -147,17 +172,23 @@ public class ExpoAudioEngineModule: Module {
|
|
|
147
172
|
let didStart = engine.startRecording(config: config)
|
|
148
173
|
if !didStart {
|
|
149
174
|
let detail = engine.getLastRecordingError() ?? "audio input format is invalid or the audio session is not ready"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
code:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
let message = "Failed to start recording. \(detail)."
|
|
176
|
+
emitEngineError(
|
|
177
|
+
code: "RECORDING_START_FAILED",
|
|
178
|
+
message: message,
|
|
179
|
+
details: ["detail": detail],
|
|
180
|
+
source: "recording"
|
|
156
181
|
)
|
|
182
|
+
throw AudioEngineError("RECORDING_START_FAILED", message, details: ["detail": detail])
|
|
157
183
|
}
|
|
184
|
+
sendEvent("recordingStarted", [String: Any]())
|
|
185
|
+
emitEngineStateChanged(reason: "recordingStarted")
|
|
158
186
|
}
|
|
159
187
|
AsyncFunction("stopRecording") {
|
|
160
|
-
|
|
188
|
+
let result = engine.stopRecording()
|
|
189
|
+
sendEvent("recordingStopped", result)
|
|
190
|
+
emitEngineStateChanged(reason: "recordingStopped")
|
|
191
|
+
return result
|
|
161
192
|
}
|
|
162
193
|
Function("isRecording") {
|
|
163
194
|
return engine.isRecording()
|
|
@@ -167,11 +198,67 @@ public class ExpoAudioEngineModule: Module {
|
|
|
167
198
|
}
|
|
168
199
|
|
|
169
200
|
AsyncFunction("extractTrack") { (trackId: String, config: [String: Any]?) throws in
|
|
170
|
-
|
|
201
|
+
do {
|
|
202
|
+
let result = try engine.extractTrack(trackId: trackId, config: config)
|
|
203
|
+
sendEvent("extractionProgress", ["trackId": trackId, "progress": 1.0, "operation": "track"])
|
|
204
|
+
var completionPayload: [String: Any] = [
|
|
205
|
+
"success": true,
|
|
206
|
+
"trackId": trackId
|
|
207
|
+
]
|
|
208
|
+
for (key, value) in result {
|
|
209
|
+
completionPayload[key] = value
|
|
210
|
+
}
|
|
211
|
+
sendEvent("extractionComplete", completionPayload)
|
|
212
|
+
return result
|
|
213
|
+
} catch let error as AudioEngineError {
|
|
214
|
+
emitEngineError(
|
|
215
|
+
code: error.code,
|
|
216
|
+
message: error.description,
|
|
217
|
+
details: error.details,
|
|
218
|
+
source: "extraction"
|
|
219
|
+
)
|
|
220
|
+
sendEvent(
|
|
221
|
+
"extractionComplete",
|
|
222
|
+
[
|
|
223
|
+
"success": false,
|
|
224
|
+
"trackId": trackId,
|
|
225
|
+
"errorMessage": error.description
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
171
230
|
}
|
|
172
231
|
|
|
173
232
|
AsyncFunction("extractAllTracks") { (config: [String: Any]?) throws in
|
|
174
|
-
|
|
233
|
+
do {
|
|
234
|
+
let results = try engine.extractAllTracks(config: config)
|
|
235
|
+
sendEvent("extractionProgress", ["progress": 1.0, "operation": "mix"])
|
|
236
|
+
sendEvent(
|
|
237
|
+
"extractionComplete",
|
|
238
|
+
[
|
|
239
|
+
"success": true,
|
|
240
|
+
"operation": "mix",
|
|
241
|
+
"count": results.count
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
return results
|
|
245
|
+
} catch let error as AudioEngineError {
|
|
246
|
+
emitEngineError(
|
|
247
|
+
code: error.code,
|
|
248
|
+
message: error.description,
|
|
249
|
+
details: error.details,
|
|
250
|
+
source: "extraction"
|
|
251
|
+
)
|
|
252
|
+
sendEvent(
|
|
253
|
+
"extractionComplete",
|
|
254
|
+
[
|
|
255
|
+
"success": false,
|
|
256
|
+
"operation": "mix",
|
|
257
|
+
"errorMessage": error.description
|
|
258
|
+
]
|
|
259
|
+
)
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
175
262
|
}
|
|
176
263
|
|
|
177
264
|
Function("cancelExtraction") { (jobId: Double?) in
|
|
@@ -190,12 +277,14 @@ public class ExpoAudioEngineModule: Module {
|
|
|
190
277
|
|
|
191
278
|
AsyncFunction("enableBackgroundPlayback") { (metadata: [String: Any]) in
|
|
192
279
|
engine.enableBackgroundPlayback(metadata: metadata)
|
|
280
|
+
emitEngineStateChanged(reason: "backgroundPlaybackEnabled")
|
|
193
281
|
}
|
|
194
282
|
Function("updateNowPlayingInfo") { (metadata: [String: Any]) in
|
|
195
283
|
engine.updateNowPlayingInfo(metadata: metadata)
|
|
196
284
|
}
|
|
197
285
|
AsyncFunction("disableBackgroundPlayback") {
|
|
198
286
|
engine.disableBackgroundPlayback()
|
|
287
|
+
emitEngineStateChanged(reason: "backgroundPlaybackDisabled")
|
|
199
288
|
}
|
|
200
289
|
|
|
201
290
|
Events(
|
|
@@ -208,8 +297,85 @@ public class ExpoAudioEngineModule: Module {
|
|
|
208
297
|
"recordingStopped",
|
|
209
298
|
"extractionProgress",
|
|
210
299
|
"extractionComplete",
|
|
300
|
+
"engineStateChanged",
|
|
211
301
|
"error",
|
|
212
302
|
"debugLog"
|
|
213
303
|
)
|
|
214
304
|
}
|
|
305
|
+
|
|
306
|
+
private func emitEngineStateChanged(reason: String, payload: [String: Any] = [:]) {
|
|
307
|
+
var statePayload = payload
|
|
308
|
+
statePayload["reason"] = reason
|
|
309
|
+
sendEvent("engineStateChanged", statePayload)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private func emitDebugLog(level: String, message: String, context: [String: Any]? = nil) {
|
|
313
|
+
var payload: [String: Any] = [
|
|
314
|
+
"level": level,
|
|
315
|
+
"message": message
|
|
316
|
+
]
|
|
317
|
+
if let context = context {
|
|
318
|
+
payload["context"] = context
|
|
319
|
+
}
|
|
320
|
+
sendEvent("debugLog", payload)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private func emitEngineError(
|
|
324
|
+
code: String,
|
|
325
|
+
message: String,
|
|
326
|
+
details: [String: Any]? = nil,
|
|
327
|
+
source: String? = nil
|
|
328
|
+
) {
|
|
329
|
+
let sourceValue = source ?? resolveErrorSource(code: code)
|
|
330
|
+
let classification = classifyError(code: code)
|
|
331
|
+
var payload: [String: Any] = [
|
|
332
|
+
"code": code,
|
|
333
|
+
"message": message,
|
|
334
|
+
"severity": classification.severity,
|
|
335
|
+
"recoverable": classification.recoverable,
|
|
336
|
+
"source": sourceValue,
|
|
337
|
+
"timestampMs": Int(Date().timeIntervalSince1970 * 1000.0),
|
|
338
|
+
"platform": "ios"
|
|
339
|
+
]
|
|
340
|
+
if let details = details {
|
|
341
|
+
payload["details"] = details
|
|
342
|
+
}
|
|
343
|
+
sendEvent("error", payload)
|
|
344
|
+
emitDebugLog(level: classification.severity == "fatal" ? "error" : "warn", message: message, context: payload)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private func resolveErrorSource(code: String) -> String {
|
|
348
|
+
if code == "AUDIO_SESSION_FAILED" {
|
|
349
|
+
return "session"
|
|
350
|
+
}
|
|
351
|
+
if code == "AUDIO_FOCUS_DENIED" {
|
|
352
|
+
return "focus"
|
|
353
|
+
}
|
|
354
|
+
if code.hasPrefix("PLAYBACK_") || code == "NO_TRACKS_LOADED" {
|
|
355
|
+
return "playback"
|
|
356
|
+
}
|
|
357
|
+
if code.hasPrefix("RECORDING_") {
|
|
358
|
+
return "recording"
|
|
359
|
+
}
|
|
360
|
+
if code.hasPrefix("EXTRACTION_") {
|
|
361
|
+
return "extraction"
|
|
362
|
+
}
|
|
363
|
+
if code.hasPrefix("ENGINE_") || code.hasPrefix("TRACK_") || code == "STREAM_DISCONNECTED" {
|
|
364
|
+
return "engine"
|
|
365
|
+
}
|
|
366
|
+
return "system"
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private func classifyError(code: String) -> (severity: String, recoverable: Bool) {
|
|
370
|
+
let fatalCodes: Set<String> = [
|
|
371
|
+
"ENGINE_INIT_FAILED",
|
|
372
|
+
"ENGINE_START_FAILED",
|
|
373
|
+
"AUDIO_SESSION_FAILED",
|
|
374
|
+
"STREAM_DISCONNECTED"
|
|
375
|
+
]
|
|
376
|
+
if fatalCodes.contains(code) {
|
|
377
|
+
return ("fatal", false)
|
|
378
|
+
}
|
|
379
|
+
return ("warning", true)
|
|
380
|
+
}
|
|
215
381
|
}
|
package/package.json
CHANGED