expo-audio 55.0.5 → 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.
Files changed (103) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/AndroidManifest.xml +0 -17
  4. package/android/src/main/java/expo/modules/audio/AudioExceptions.kt +35 -0
  5. package/android/src/main/java/expo/modules/audio/AudioModule.kt +6 -4
  6. package/android/src/main/java/expo/modules/audio/AudioPlayer.kt +71 -37
  7. package/android/src/main/java/expo/modules/audio/AudioRecorder.kt +56 -19
  8. package/android/src/main/java/expo/modules/audio/AudioUtils.kt +9 -0
  9. package/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt +146 -124
  10. package/android/src/main/java/expo/modules/audio/service/AudioPlaybackServiceConnection.kt +100 -0
  11. package/android/src/main/java/expo/modules/audio/service/AudioRecordingService.kt +64 -63
  12. package/android/src/main/java/expo/modules/audio/service/AudioRecordingServiceConnection.kt +153 -0
  13. package/android/src/main/java/expo/modules/audio/service/BaseServiceConnection.kt +141 -0
  14. package/build/Audio.types.d.ts +19 -1
  15. package/build/Audio.types.d.ts.map +1 -1
  16. package/build/Audio.types.js.map +1 -1
  17. package/build/AudioModule.types.d.ts +5 -0
  18. package/build/AudioModule.types.d.ts.map +1 -1
  19. package/build/AudioModule.types.js.map +1 -1
  20. package/build/AudioModule.web.d.ts +3 -67
  21. package/build/AudioModule.web.d.ts.map +1 -1
  22. package/build/AudioModule.web.js +3 -378
  23. package/build/AudioModule.web.js.map +1 -1
  24. package/build/AudioPlayer.web.d.ts +47 -0
  25. package/build/AudioPlayer.web.d.ts.map +1 -0
  26. package/build/AudioPlayer.web.js +267 -0
  27. package/build/AudioPlayer.web.js.map +1 -0
  28. package/build/AudioRecorder.web.d.ts +40 -0
  29. package/build/AudioRecorder.web.d.ts.map +1 -0
  30. package/build/AudioRecorder.web.js +239 -0
  31. package/build/AudioRecorder.web.js.map +1 -0
  32. package/build/AudioUtils.web.d.ts +7 -0
  33. package/build/AudioUtils.web.d.ts.map +1 -0
  34. package/build/AudioUtils.web.js +76 -0
  35. package/build/AudioUtils.web.js.map +1 -0
  36. package/build/ExpoAudio.d.ts +25 -0
  37. package/build/ExpoAudio.d.ts.map +1 -1
  38. package/build/ExpoAudio.js +30 -0
  39. package/build/ExpoAudio.js.map +1 -1
  40. package/build/MediaSessionController.web.d.ts +32 -0
  41. package/build/MediaSessionController.web.d.ts.map +1 -0
  42. package/build/MediaSessionController.web.js +156 -0
  43. package/build/MediaSessionController.web.js.map +1 -0
  44. package/expo-module.config.json +1 -1
  45. package/ios/AudioModule.swift +45 -2
  46. package/ios/AudioPlayer.swift +81 -21
  47. package/ios/AudioRecorder.swift +44 -4
  48. package/ios/AudioRecords.swift +1 -0
  49. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar +0 -0
  50. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.md5 +1 -0
  51. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.sha1 +1 -0
  52. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.sha256 +1 -0
  53. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6-sources.jar.sha512 +1 -0
  54. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar +0 -0
  55. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.md5 +1 -0
  56. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.sha1 +1 -0
  57. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.sha256 +1 -0
  58. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.aar.sha512 +1 -0
  59. package/local-maven-repo/expo/modules/audio/expo.modules.audio/{55.0.5/expo.modules.audio-55.0.5.module → 55.0.6/expo.modules.audio-55.0.6.module} +22 -22
  60. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.md5 +1 -0
  61. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.sha1 +1 -0
  62. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.sha256 +1 -0
  63. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.module.sha512 +1 -0
  64. package/local-maven-repo/expo/modules/audio/expo.modules.audio/{55.0.5/expo.modules.audio-55.0.5.pom → 55.0.6/expo.modules.audio-55.0.6.pom} +1 -1
  65. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.md5 +1 -0
  66. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.sha1 +1 -0
  67. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.sha256 +1 -0
  68. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.6/expo.modules.audio-55.0.6.pom.sha512 +1 -0
  69. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml +4 -4
  70. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.md5 +1 -1
  71. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha1 +1 -1
  72. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha256 +1 -1
  73. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha512 +1 -1
  74. package/package.json +2 -2
  75. package/plugin/build/withAudio.d.ts +1 -0
  76. package/plugin/build/withAudio.js +54 -4
  77. package/plugin/src/withAudio.ts +73 -4
  78. package/src/Audio.types.ts +19 -1
  79. package/src/AudioModule.types.ts +5 -0
  80. package/src/AudioModule.web.ts +4 -500
  81. package/src/AudioPlayer.web.ts +344 -0
  82. package/src/AudioRecorder.web.ts +310 -0
  83. package/src/AudioUtils.web.ts +91 -0
  84. package/src/ExpoAudio.ts +33 -0
  85. package/src/MediaSessionController.web.ts +191 -0
  86. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar +0 -0
  87. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.md5 +0 -1
  88. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.sha1 +0 -1
  89. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.sha256 +0 -1
  90. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.sha512 +0 -1
  91. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar +0 -0
  92. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.md5 +0 -1
  93. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.sha1 +0 -1
  94. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.sha256 +0 -1
  95. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.sha512 +0 -1
  96. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.md5 +0 -1
  97. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.sha1 +0 -1
  98. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.sha256 +0 -1
  99. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.sha512 +0 -1
  100. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.md5 +0 -1
  101. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.sha1 +0 -1
  102. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.sha256 +0 -1
  103. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,27 @@
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
+
13
34
  ## 55.0.5 — 2026-02-08
14
35
 
15
36
  _This version does not introduce any user-facing changes._
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'expo.modules.audio'
7
- version = '55.0.5'
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.5"
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 expo.modules.audio.service.AudioControlsService
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 kotlinx.coroutines.withContext
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
- private var metadata: Metadata? = null
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.Default)
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
- AudioControlsService.setActivePlayer(context, this, metadata, options)
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
- AudioControlsService.setActivePlayer(context, null)
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
- AudioControlsService.updateMetadata(this, metadata)
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
- AudioControlsService.setActivePlayer(context, null)
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
- playerScope.launch {
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
- playerScope.launch {
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
- playerScope.launch {
182
- val updateMap = mutableMapOf<String, Any?>(
183
- "playbackState" to playbackStateToString(playbackState)
184
- )
185
- if (justFinished) {
186
- updateMap["didJustFinish"] = true
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
- playerScope.launch {
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
- playerScope.launch {
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
- private suspend fun sendPlayerUpdate(map: Map<String, Any?>? = null) =
264
- withContext(Dispatchers.Main) {
265
- val data = currentStatus()
266
- val body = map?.let { data + it } ?: data
267
- emit(PLAYBACK_STATUS_UPDATE, body)
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
- appContext?.mainQueue?.launch {
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
- AudioControlsService.clearSession()
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.AudioRecordingService
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 isRegisteredWithService = false
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 (cause: Exception) {
104
+ } catch (e: Exception) {
85
105
  mediaRecorder.release()
86
106
  recorder = null
87
107
  isPrepared = false
88
- throw AudioRecorderPrepareException(cause)
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 && isRegisteredWithService) {
151
- AudioRecordingService.getInstance()?.unregisterRecorder(this)
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
- if (useForegroundService && isRegisteredWithService) {
263
- AudioRecordingService.getInstance()?.unregisterRecorder(this)
264
- isRegisteredWithService = false
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 && isRegisteredWithService) {
323
- AudioRecordingService.getInstance()?.unregisterRecorder(this)
324
- isRegisteredWithService = false
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
+ }