expo-audio 55.0.4 → 55.0.6
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/CHANGELOG.md +25 -0
- package/android/build.gradle +2 -2
- package/android/src/main/AndroidManifest.xml +0 -17
- package/android/src/main/java/expo/modules/audio/AudioExceptions.kt +35 -0
- package/android/src/main/java/expo/modules/audio/AudioModule.kt +6 -4
- package/android/src/main/java/expo/modules/audio/AudioPlayer.kt +71 -37
- package/android/src/main/java/expo/modules/audio/AudioRecorder.kt +56 -19
- package/android/src/main/java/expo/modules/audio/AudioUtils.kt +9 -0
- package/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt +146 -124
- package/android/src/main/java/expo/modules/audio/service/AudioPlaybackServiceConnection.kt +100 -0
- package/android/src/main/java/expo/modules/audio/service/AudioRecordingService.kt +64 -63
- package/android/src/main/java/expo/modules/audio/service/AudioRecordingServiceConnection.kt +153 -0
- package/android/src/main/java/expo/modules/audio/service/BaseServiceConnection.kt +141 -0
- package/build/Audio.types.d.ts +22 -1
- package/build/Audio.types.d.ts.map +1 -1
- package/build/Audio.types.js.map +1 -1
- package/build/AudioModule.types.d.ts +5 -0
- package/build/AudioModule.types.d.ts.map +1 -1
- package/build/AudioModule.types.js.map +1 -1
- package/build/AudioModule.web.d.ts +3 -67
- package/build/AudioModule.web.d.ts.map +1 -1
- package/build/AudioModule.web.js +3 -378
- package/build/AudioModule.web.js.map +1 -1
- package/build/AudioPlayer.web.d.ts +47 -0
- package/build/AudioPlayer.web.d.ts.map +1 -0
- package/build/AudioPlayer.web.js +267 -0
- package/build/AudioPlayer.web.js.map +1 -0
- package/build/AudioRecorder.web.d.ts +40 -0
- package/build/AudioRecorder.web.d.ts.map +1 -0
- package/build/AudioRecorder.web.js +239 -0
- package/build/AudioRecorder.web.js.map +1 -0
- package/build/AudioUtils.web.d.ts +7 -0
- package/build/AudioUtils.web.d.ts.map +1 -0
- package/build/AudioUtils.web.js +76 -0
- package/build/AudioUtils.web.js.map +1 -0
- package/build/ExpoAudio.d.ts +25 -0
- package/build/ExpoAudio.d.ts.map +1 -1
- package/build/ExpoAudio.js +30 -0
- package/build/ExpoAudio.js.map +1 -1
- package/build/MediaSessionController.web.d.ts +32 -0
- package/build/MediaSessionController.web.d.ts.map +1 -0
- package/build/MediaSessionController.web.js +156 -0
- package/build/MediaSessionController.web.js.map +1 -0
- package/expo-module.config.json +1 -1
- package/ios/AudioModule.swift +45 -2
- package/ios/AudioPlayer.swift +81 -21
- package/ios/AudioRecorder.swift +44 -4
- package/ios/AudioRecords.swift +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar +0 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar +0 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/{55.0.4/expo.modules.audio-55.0.4.module → 55.0.6/expo.modules.audio-55.0.6.module} +22 -22
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.md5 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/{55.0.4/expo.modules.audio-55.0.4.pom → 55.0.6/expo.modules.audio-55.0.6.pom} +1 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.sha512 +1 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml +4 -4
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha512 +1 -1
- package/package.json +2 -2
- package/plugin/build/withAudio.d.ts +1 -0
- package/plugin/build/withAudio.js +54 -4
- package/plugin/src/withAudio.ts +73 -4
- package/src/Audio.types.ts +22 -1
- package/src/AudioModule.types.ts +5 -0
- package/src/AudioModule.web.ts +4 -500
- package/src/AudioPlayer.web.ts +344 -0
- package/src/AudioRecorder.web.ts +310 -0
- package/src/AudioUtils.web.ts +91 -0
- package/src/ExpoAudio.ts +33 -0
- package/src/MediaSessionController.web.ts +191 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4-sources.jar +0 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.aar +0 -0
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.module.md5 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.4/expo.modules.audio-55.0.4.pom.sha512 +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.6 — 2026-02-16
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Add support for `shouldRouteThroughEarpiece`. ([#43089](https://github.com/expo/expo/pull/43089) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
- [Android] Make it possible to add/remove the foreground service and foreground service permissions with a config plugin. ([#43014](https://github.com/expo/expo/pull/43014) by [@behenate](https://github.com/behenate))
|
|
19
|
+
- [Web] Add support for audio sampling. ([#43149](https://github.com/expo/expo/pull/43149) by [@alanjhughes](https://github.com/alanjhughes))
|
|
20
|
+
- [Web] Add support for media controls. ([#43150](https://github.com/expo/expo/pull/43150) by [@alanjhughes](https://github.com/alanjhughes))
|
|
21
|
+
- [Web] Add support for selecting recording inputs. ([#43151](https://github.com/expo/expo/pull/43151) by [@alanjhughes](https://github.com/alanjhughes))
|
|
22
|
+
- [Web] Add support for recording metering. ([#43152](https://github.com/expo/expo/pull/43152) by [@alanjhughes](https://github.com/alanjhughes))
|
|
23
|
+
|
|
24
|
+
### 🐛 Bug fixes
|
|
25
|
+
|
|
26
|
+
- [Android] Fix memory leaks when refreshing the app. ([#42785](https://github.com/expo/expo/pull/42785) by [@behenate](https://github.com/behenate))
|
|
27
|
+
- [iOS] Fixes `mediaServicesDidReset` not being correctly implemented. ([#42898](https://github.com/expo/expo/pull/42898) by [@alanjhughes](https://github.com/alanjhughes))
|
|
28
|
+
|
|
29
|
+
### 💡 Others
|
|
30
|
+
|
|
31
|
+
- [Android] Improve event handling. ([#43121](https://github.com/expo/expo/pull/43121) by [@alanjhughes](https://github.com/alanjhughes))
|
|
32
|
+
- [Android] Rework native audio service handling. ([#43015](https://github.com/expo/expo/pull/43015) by [@behenate](https://github.com/behenate))
|
|
33
|
+
|
|
34
|
+
## 55.0.5 — 2026-02-08
|
|
35
|
+
|
|
36
|
+
_This version does not introduce any user-facing changes._
|
|
37
|
+
|
|
13
38
|
## 55.0.4 — 2026-02-03
|
|
14
39
|
|
|
15
40
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -4,13 +4,13 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'expo.modules.audio'
|
|
7
|
-
version = '55.0.
|
|
7
|
+
version = '55.0.6'
|
|
8
8
|
|
|
9
9
|
android {
|
|
10
10
|
namespace "expo.modules.audio"
|
|
11
11
|
defaultConfig {
|
|
12
12
|
versionCode 1
|
|
13
|
-
versionName "55.0.
|
|
13
|
+
versionName "55.0.6"
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
compileOptions {
|
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
3
3
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
4
|
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
5
|
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
6
|
-
|
|
7
|
-
<application>
|
|
8
|
-
<service
|
|
9
|
-
android:name=".service.AudioControlsService"
|
|
10
|
-
android:exported="false"
|
|
11
|
-
android:foregroundServiceType="mediaPlayback">
|
|
12
|
-
<intent-filter>
|
|
13
|
-
<action android:name="androidx.media3.session.MediaSessionService" />
|
|
14
|
-
</intent-filter>
|
|
15
|
-
</service>
|
|
16
|
-
<service
|
|
17
|
-
android:name=".service.AudioRecordingService"
|
|
18
|
-
android:exported="false"
|
|
19
|
-
android:foregroundServiceType="microphone" />
|
|
20
|
-
</application>
|
|
21
4
|
</manifest>
|
|
@@ -2,9 +2,24 @@ package expo.modules.audio
|
|
|
2
2
|
|
|
3
3
|
import expo.modules.kotlin.exception.CodedException
|
|
4
4
|
|
|
5
|
+
private const val defaultPlaybackServiceTip =
|
|
6
|
+
"Make sure that the expo-audio config plugin is properly configured with " +
|
|
7
|
+
"'enableBackgroundPlayback: true' to avoid issues with lock screen controls " +
|
|
8
|
+
"and background audio playback."
|
|
9
|
+
|
|
10
|
+
private const val defaultRecordingServiceTip =
|
|
11
|
+
"Make sure that the expo-audio config plugin is properly configured with " +
|
|
12
|
+
"'enableBackgroundRecording: true' to avoid issues with background recording."
|
|
13
|
+
|
|
5
14
|
internal class AudioPermissionsException :
|
|
6
15
|
CodedException("RECORD_AUDIO permission has not been granted")
|
|
7
16
|
|
|
17
|
+
internal class NotificationPermissionsException :
|
|
18
|
+
CodedException(
|
|
19
|
+
"POST_NOTIFICATIONS permission has not been granted. This permission is required when using background recording (allowsBackgroundRecording: true). " +
|
|
20
|
+
"Request notification permissions using AudioModule.requestNotificationPermissionsAsync() before calling prepareRecording()."
|
|
21
|
+
)
|
|
22
|
+
|
|
8
23
|
internal class GetAudioInputNotSupportedException :
|
|
9
24
|
CodedException("Getting current audio input is not supported on devices running Android version lower than Android 9.0")
|
|
10
25
|
|
|
@@ -22,3 +37,23 @@ internal class AudioRecorderPrepareException(cause: Throwable) :
|
|
|
22
37
|
|
|
23
38
|
internal class AudioRecorderAlreadyPreparedException :
|
|
24
39
|
CodedException("AudioRecorder has already been prepared. Stop or release the current session before preparing again.")
|
|
40
|
+
|
|
41
|
+
internal fun getPlaybackServiceErrorMessage(
|
|
42
|
+
message: String?,
|
|
43
|
+
tip: String = defaultPlaybackServiceTip
|
|
44
|
+
): String {
|
|
45
|
+
return (message ?: "expo-audio playback service error") + ". $tip"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
internal fun getRecordingServiceErrorMessage(
|
|
49
|
+
message: String?,
|
|
50
|
+
tip: String = defaultRecordingServiceTip
|
|
51
|
+
): String {
|
|
52
|
+
return (message ?: "expo-audio recording service error") + ". $tip"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
internal class AudioRecordingServiceException(message: String?, cause: Throwable? = null) :
|
|
56
|
+
CodedException(getRecordingServiceErrorMessage(message), cause)
|
|
57
|
+
|
|
58
|
+
internal class AudioPlaybackServiceException(message: String?, cause: Throwable? = null) :
|
|
59
|
+
CodedException(getPlaybackServiceErrorMessage(message), cause)
|
|
@@ -28,10 +28,10 @@ import androidx.media3.exoplayer.hls.HlsMediaSource
|
|
|
28
28
|
import androidx.media3.exoplayer.smoothstreaming.SsMediaSource
|
|
29
29
|
import androidx.media3.exoplayer.source.MediaSource
|
|
30
30
|
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
|
31
|
-
import expo.modules.audio.service.AudioControlsService
|
|
32
31
|
import expo.modules.interfaces.permissions.Permissions
|
|
33
32
|
import expo.modules.kotlin.Promise
|
|
34
33
|
import expo.modules.kotlin.exception.Exceptions
|
|
34
|
+
import expo.modules.kotlin.functions.Coroutine
|
|
35
35
|
import expo.modules.kotlin.functions.Queues
|
|
36
36
|
import expo.modules.kotlin.modules.Module
|
|
37
37
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
@@ -210,6 +210,10 @@ class AudioModule : Module() {
|
|
|
210
210
|
Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.RECORD_AUDIO)
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
|
|
214
|
+
Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.POST_NOTIFICATIONS)
|
|
215
|
+
}
|
|
216
|
+
|
|
213
217
|
AsyncFunction("getRecordingPermissionsAsync") { promise: Promise ->
|
|
214
218
|
Permissions.getPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.RECORD_AUDIO)
|
|
215
219
|
}
|
|
@@ -269,8 +273,6 @@ class AudioModule : Module() {
|
|
|
269
273
|
recorders.values.forEach {
|
|
270
274
|
it.stopRecording()
|
|
271
275
|
}
|
|
272
|
-
|
|
273
|
-
AudioControlsService.clearSession()
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
|
|
@@ -494,7 +496,7 @@ class AudioModule : Module() {
|
|
|
494
496
|
recorder.getCurrentTimeSeconds()
|
|
495
497
|
}
|
|
496
498
|
|
|
497
|
-
AsyncFunction("prepareToRecordAsync") { recorder: AudioRecorder, options: RecordingOptions? ->
|
|
499
|
+
AsyncFunction("prepareToRecordAsync") Coroutine { recorder: AudioRecorder, options: RecordingOptions? ->
|
|
498
500
|
checkRecordingPermission()
|
|
499
501
|
recorder.prepareRecording(options)
|
|
500
502
|
}
|
|
@@ -13,12 +13,16 @@ import androidx.media3.common.Player
|
|
|
13
13
|
import androidx.media3.common.util.UnstableApi
|
|
14
14
|
import androidx.media3.exoplayer.ExoPlayer
|
|
15
15
|
import androidx.media3.exoplayer.source.MediaSource
|
|
16
|
-
import
|
|
16
|
+
import androidx.media3.session.MediaSession
|
|
17
|
+
import expo.modules.audio.service.AudioPlaybackServiceConnection
|
|
18
|
+
import expo.modules.audio.service.ServiceBindingState
|
|
17
19
|
import expo.modules.kotlin.AppContext
|
|
18
20
|
import expo.modules.kotlin.exception.Exceptions
|
|
19
21
|
import expo.modules.kotlin.sharedobjects.SharedRef
|
|
20
22
|
import kotlinx.coroutines.CoroutineScope
|
|
23
|
+
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
21
24
|
import kotlinx.coroutines.Dispatchers
|
|
25
|
+
import kotlinx.coroutines.GlobalScope
|
|
22
26
|
import kotlinx.coroutines.Job
|
|
23
27
|
import kotlinx.coroutines.cancel
|
|
24
28
|
import kotlinx.coroutines.delay
|
|
@@ -27,7 +31,7 @@ import kotlinx.coroutines.flow.launchIn
|
|
|
27
31
|
import kotlinx.coroutines.flow.onEach
|
|
28
32
|
import kotlinx.coroutines.flow.onStart
|
|
29
33
|
import kotlinx.coroutines.launch
|
|
30
|
-
import
|
|
34
|
+
import java.lang.ref.WeakReference
|
|
31
35
|
import java.util.UUID
|
|
32
36
|
|
|
33
37
|
private const val PLAYBACK_STATUS_UPDATE = "playbackStatusUpdate"
|
|
@@ -58,9 +62,12 @@ class AudioPlayer(
|
|
|
58
62
|
|
|
59
63
|
// Lock screen controls
|
|
60
64
|
var isActiveForLockScreen = false
|
|
61
|
-
|
|
65
|
+
internal var metadata: Metadata? = null
|
|
66
|
+
internal var lockScreenOptions: AudioLockScreenOptions? = null
|
|
67
|
+
internal var mediaSession: MediaSession = buildBasicMediaSession(context, ref)
|
|
68
|
+
val serviceConnection = AudioPlaybackServiceConnection(WeakReference(this), appContext)
|
|
62
69
|
|
|
63
|
-
private var playerScope = CoroutineScope(Dispatchers.
|
|
70
|
+
private var playerScope = CoroutineScope(Dispatchers.Main)
|
|
64
71
|
private var samplingEnabled = false
|
|
65
72
|
private var visualizer: Visualizer? = null
|
|
66
73
|
private var playing = false
|
|
@@ -106,22 +113,47 @@ class AudioPlayer(
|
|
|
106
113
|
fun setActiveForLockScreen(active: Boolean, metadata: Metadata? = null, options: AudioLockScreenOptions? = null) {
|
|
107
114
|
if (active) {
|
|
108
115
|
this.metadata = metadata
|
|
109
|
-
|
|
116
|
+
this.lockScreenOptions = options
|
|
117
|
+
this.isActiveForLockScreen = true
|
|
118
|
+
|
|
119
|
+
if (serviceConnection.bindingState == ServiceBindingState.UNBOUND) {
|
|
120
|
+
serviceConnection.bindWithService()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
val serviceBinder = serviceConnection.playbackServiceBinder
|
|
124
|
+
if (serviceBinder != null && serviceConnection.bindingState == ServiceBindingState.BOUND) {
|
|
125
|
+
serviceBinder.service.setPlayerOptions(this, metadata, options)
|
|
126
|
+
} else if (serviceConnection.bindingState == ServiceBindingState.BINDING) {
|
|
127
|
+
// The settings will be applied when the service connects
|
|
128
|
+
} else {
|
|
129
|
+
appContext?.jsLogger?.error(
|
|
130
|
+
getPlaybackServiceErrorMessage("Failed to activate lock screen controls - service binding failed")
|
|
131
|
+
)
|
|
132
|
+
}
|
|
110
133
|
} else if (isActiveForLockScreen) {
|
|
111
|
-
|
|
134
|
+
this.isActiveForLockScreen = false
|
|
135
|
+
serviceConnection.playbackServiceBinder?.service?.unregisterPlayer()
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
fun updateLockScreenMetadata(metadata: Metadata) {
|
|
116
140
|
if (isActiveForLockScreen) {
|
|
117
141
|
this.metadata = metadata
|
|
118
|
-
|
|
142
|
+
|
|
143
|
+
val serviceBinder = serviceConnection.playbackServiceBinder
|
|
144
|
+
if (serviceBinder != null && serviceConnection.bindingState == ServiceBindingState.BOUND) {
|
|
145
|
+
serviceBinder.service.setPlayerMetadata(this, metadata)
|
|
146
|
+
} else {
|
|
147
|
+
appContext?.jsLogger?.warn(
|
|
148
|
+
getPlaybackServiceErrorMessage("Cannot update lock screen metadata - service not connected")
|
|
149
|
+
)
|
|
150
|
+
}
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
|
|
122
154
|
fun clearLockScreenControls() {
|
|
123
155
|
if (isActiveForLockScreen) {
|
|
124
|
-
|
|
156
|
+
serviceConnection.playbackServiceBinder?.service?.unregisterPlayer()
|
|
125
157
|
}
|
|
126
158
|
}
|
|
127
159
|
|
|
@@ -158,15 +190,11 @@ class AudioPlayer(
|
|
|
158
190
|
if (isTransient) {
|
|
159
191
|
return
|
|
160
192
|
}
|
|
161
|
-
|
|
162
|
-
sendPlayerUpdate(mapOf("playing" to isPlaying))
|
|
163
|
-
}
|
|
193
|
+
sendPlayerUpdate(mapOf("playing" to isPlaying))
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
override fun onIsLoadingChanged(isLoading: Boolean) {
|
|
167
|
-
|
|
168
|
-
sendPlayerUpdate(mapOf("isLoaded" to !isLoading))
|
|
169
|
-
}
|
|
197
|
+
sendPlayerUpdate()
|
|
170
198
|
}
|
|
171
199
|
|
|
172
200
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
@@ -178,22 +206,18 @@ class AudioPlayer(
|
|
|
178
206
|
intendedPlayingState = false
|
|
179
207
|
}
|
|
180
208
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
updateMap["playing"] = false
|
|
188
|
-
}
|
|
189
|
-
sendPlayerUpdate(updateMap)
|
|
209
|
+
val updateMap = mutableMapOf<String, Any?>(
|
|
210
|
+
"playbackState" to playbackStateToString(playbackState)
|
|
211
|
+
)
|
|
212
|
+
if (justFinished) {
|
|
213
|
+
updateMap["didJustFinish"] = true
|
|
214
|
+
updateMap["playing"] = false
|
|
190
215
|
}
|
|
216
|
+
sendPlayerUpdate(updateMap)
|
|
191
217
|
}
|
|
192
218
|
|
|
193
219
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
194
|
-
|
|
195
|
-
sendPlayerUpdate()
|
|
196
|
-
}
|
|
220
|
+
sendPlayerUpdate()
|
|
197
221
|
}
|
|
198
222
|
|
|
199
223
|
override fun onPositionDiscontinuity(
|
|
@@ -202,9 +226,7 @@ class AudioPlayer(
|
|
|
202
226
|
reason: Int
|
|
203
227
|
) {
|
|
204
228
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
|
205
|
-
|
|
206
|
-
sendPlayerUpdate(mapOf("currentTime" to (newPosition.positionMs / 1000f)))
|
|
207
|
-
}
|
|
229
|
+
sendPlayerUpdate(mapOf("currentTime" to (newPosition.positionMs / 1000f)))
|
|
208
230
|
}
|
|
209
231
|
}
|
|
210
232
|
})
|
|
@@ -260,12 +282,16 @@ class AudioPlayer(
|
|
|
260
282
|
)
|
|
261
283
|
}
|
|
262
284
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
285
|
+
internal fun assignBasicMediaSession() {
|
|
286
|
+
mediaSession.release()
|
|
287
|
+
mediaSession = buildBasicMediaSession(context, ref)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private fun sendPlayerUpdate(map: Map<String, Any?>? = null) {
|
|
291
|
+
val data = currentStatus()
|
|
292
|
+
val body = map?.let { data + it } ?: data
|
|
293
|
+
emit(PLAYBACK_STATUS_UPDATE, body)
|
|
294
|
+
}
|
|
269
295
|
|
|
270
296
|
private fun sendAudioSampleUpdate(sample: List<Float>) {
|
|
271
297
|
val body = mapOf(
|
|
@@ -314,12 +340,20 @@ class AudioPlayer(
|
|
|
314
340
|
}
|
|
315
341
|
}
|
|
316
342
|
|
|
343
|
+
@OptIn(DelicateCoroutinesApi::class)
|
|
317
344
|
override fun sharedObjectDidRelease() {
|
|
318
345
|
super.sharedObjectDidRelease()
|
|
319
|
-
|
|
346
|
+
|
|
347
|
+
serviceConnection.release()
|
|
348
|
+
|
|
349
|
+
// Run on global scope (not appContext.mainQueue) so that reloading doesn't cancel the release process
|
|
350
|
+
// https://github.com/expo/expo/blob/cdf592a7fea56fc01b0149e9b2e5dbd294bcdc4c/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt#L277-L279
|
|
351
|
+
GlobalScope.launch(Dispatchers.Main) {
|
|
352
|
+
mediaSession.release()
|
|
320
353
|
if (isActiveForLockScreen) {
|
|
321
|
-
|
|
354
|
+
serviceConnection.playbackServiceBinder?.service?.unregisterPlayer()
|
|
322
355
|
}
|
|
356
|
+
serviceConnection.unbind()
|
|
323
357
|
playerScope.cancel()
|
|
324
358
|
visualizer?.release()
|
|
325
359
|
ref.release()
|
|
@@ -13,15 +13,19 @@ import android.os.Build
|
|
|
13
13
|
import android.os.Bundle
|
|
14
14
|
import androidx.core.content.ContextCompat
|
|
15
15
|
import androidx.core.net.toUri
|
|
16
|
-
import expo.modules.audio.service.
|
|
16
|
+
import expo.modules.audio.service.AudioRecordingServiceConnection
|
|
17
17
|
import expo.modules.kotlin.AppContext
|
|
18
|
+
import expo.modules.kotlin.exception.CodedException
|
|
19
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
18
20
|
import expo.modules.kotlin.sharedobjects.SharedObject
|
|
19
21
|
import kotlinx.coroutines.Job
|
|
20
22
|
import kotlinx.coroutines.delay
|
|
21
23
|
import kotlinx.coroutines.launch
|
|
22
24
|
import java.io.File
|
|
23
25
|
import java.io.IOException
|
|
26
|
+
import java.lang.ref.WeakReference
|
|
24
27
|
import java.util.UUID
|
|
28
|
+
import kotlin.coroutines.Continuation
|
|
25
29
|
import kotlin.math.log10
|
|
26
30
|
|
|
27
31
|
private const val RECORDING_STATUS_UPDATE = "recordingStatusUpdate"
|
|
@@ -45,7 +49,14 @@ class AudioRecorder(
|
|
|
45
49
|
var isPaused = false
|
|
46
50
|
private var recordingTimerJob: Job? = null
|
|
47
51
|
var useForegroundService = false
|
|
48
|
-
private var
|
|
52
|
+
private var bindingContinuation: Continuation<Boolean>? = null
|
|
53
|
+
|
|
54
|
+
val serviceConnection = AudioRecordingServiceConnection(WeakReference(this), appContext)
|
|
55
|
+
|
|
56
|
+
private val _appContext: AppContext
|
|
57
|
+
get() {
|
|
58
|
+
return appContext ?: throw Exceptions.AppContextLost()
|
|
59
|
+
}
|
|
49
60
|
|
|
50
61
|
private fun currentFileUrl(): String? =
|
|
51
62
|
filePath?.let(::File)?.toUri()?.toString()
|
|
@@ -71,25 +82,41 @@ class AudioRecorder(
|
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
|
|
74
|
-
fun prepareRecording(options: RecordingOptions?) {
|
|
85
|
+
suspend fun prepareRecording(options: RecordingOptions?) {
|
|
75
86
|
if (recorder != null || isPrepared || isRecording || isPaused) {
|
|
76
87
|
throw AudioRecorderAlreadyPreparedException()
|
|
77
88
|
}
|
|
89
|
+
|
|
90
|
+
if (useForegroundService && !hasNotificationPermissions()) {
|
|
91
|
+
throw NotificationPermissionsException()
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
val recordingOptions = options ?: this.options
|
|
79
95
|
val mediaRecorder = createRecorder(recordingOptions)
|
|
80
96
|
recorder = mediaRecorder
|
|
97
|
+
|
|
81
98
|
try {
|
|
99
|
+
if (useForegroundService) {
|
|
100
|
+
serviceConnection.bindWithService()
|
|
101
|
+
}
|
|
82
102
|
mediaRecorder.prepare()
|
|
83
103
|
isPrepared = true
|
|
84
|
-
} catch (
|
|
104
|
+
} catch (e: Exception) {
|
|
85
105
|
mediaRecorder.release()
|
|
86
106
|
recorder = null
|
|
87
107
|
isPrepared = false
|
|
88
|
-
|
|
108
|
+
|
|
109
|
+
throw e as? CodedException ?: AudioRecorderPrepareException(e)
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
fun record() {
|
|
114
|
+
if (useForegroundService) {
|
|
115
|
+
serviceConnection.recordingServiceBinder?.service?.registerRecorder(this) ?: run {
|
|
116
|
+
throw AudioRecordingServiceException("The service connection is not bound, but `allowsBackgroundRecording` is set to `true`")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
if (isPaused) {
|
|
94
121
|
recorder?.resume()
|
|
95
122
|
} else {
|
|
@@ -98,11 +125,6 @@ class AudioRecorder(
|
|
|
98
125
|
startTime = System.currentTimeMillis()
|
|
99
126
|
isRecording = true
|
|
100
127
|
isPaused = false
|
|
101
|
-
|
|
102
|
-
if (useForegroundService && !isRegisteredWithService) {
|
|
103
|
-
AudioRecordingService.startService(context, this)
|
|
104
|
-
isRegisteredWithService = true
|
|
105
|
-
}
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
fun recordWithOptions(atTimeSeconds: Double? = null, forDurationSeconds: Double? = null) {
|
|
@@ -147,9 +169,8 @@ class AudioRecorder(
|
|
|
147
169
|
var stopFailed = false
|
|
148
170
|
var stopError: String? = null
|
|
149
171
|
|
|
150
|
-
if (useForegroundService
|
|
151
|
-
|
|
152
|
-
isRegisteredWithService = false
|
|
172
|
+
if (useForegroundService) {
|
|
173
|
+
serviceConnection.recordingServiceBinder?.service?.unregisterRecorder(this)
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
try {
|
|
@@ -259,9 +280,16 @@ class AudioRecorder(
|
|
|
259
280
|
|
|
260
281
|
override fun sharedObjectDidRelease() {
|
|
261
282
|
super.sharedObjectDidRelease()
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
283
|
+
|
|
284
|
+
// Mark recorder as released
|
|
285
|
+
serviceConnection.release()
|
|
286
|
+
|
|
287
|
+
if (useForegroundService) {
|
|
288
|
+
serviceConnection.recordingServiceBinder?.service?.unregisterRecorder(this)
|
|
289
|
+
// Unbind service connection
|
|
290
|
+
serviceConnection.unbind()
|
|
291
|
+
// Clean up service connection resources
|
|
292
|
+
serviceConnection.cleanup()
|
|
265
293
|
}
|
|
266
294
|
reset()
|
|
267
295
|
}
|
|
@@ -319,9 +347,10 @@ class AudioRecorder(
|
|
|
319
347
|
MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED -> {
|
|
320
348
|
val url = currentFileUrl()
|
|
321
349
|
|
|
322
|
-
if (useForegroundService
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
if (useForegroundService) {
|
|
351
|
+
serviceConnection.recordingServiceBinder?.service?.unregisterRecorder(this)
|
|
352
|
+
// Unbind the service connection
|
|
353
|
+
serviceConnection.unbind()
|
|
325
354
|
}
|
|
326
355
|
|
|
327
356
|
try {
|
|
@@ -390,6 +419,14 @@ class AudioRecorder(
|
|
|
390
419
|
private fun hasRecordingPermissions() =
|
|
391
420
|
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
|
392
421
|
|
|
422
|
+
private fun hasNotificationPermissions(): Boolean {
|
|
423
|
+
// POST_NOTIFICATIONS permission is only required on Android 13+ (API 33+)
|
|
424
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
425
|
+
return true
|
|
426
|
+
}
|
|
427
|
+
return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
|
428
|
+
}
|
|
429
|
+
|
|
393
430
|
fun getAvailableInputs(audioManager: AudioManager) =
|
|
394
431
|
audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).mapNotNull { deviceInfo ->
|
|
395
432
|
val type = deviceInfo.type
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package expo.modules.audio
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
3
4
|
import android.media.AudioDeviceInfo
|
|
4
5
|
import android.os.Bundle
|
|
6
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
7
|
+
import androidx.media3.session.MediaSession
|
|
5
8
|
import java.io.File
|
|
6
9
|
import java.io.IOException
|
|
7
10
|
|
|
@@ -27,3 +30,9 @@ fun getMapFromDeviceInfo(deviceInfo: AudioDeviceInfo): Bundle {
|
|
|
27
30
|
map.putString("uid", deviceInfo.id.toString())
|
|
28
31
|
return map
|
|
29
32
|
}
|
|
33
|
+
|
|
34
|
+
fun buildBasicMediaSession(context: Context, player: ExoPlayer): MediaSession {
|
|
35
|
+
return MediaSession.Builder(context, player)
|
|
36
|
+
.setId("ExpoAudioBasicMediaSession_${player.hashCode()}")
|
|
37
|
+
.build()
|
|
38
|
+
}
|