expo-audio 55.0.5 → 55.0.7

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 (132) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/android/build.gradle +4 -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 +280 -49
  6. package/android/src/main/java/expo/modules/audio/AudioPlayer.kt +125 -80
  7. package/android/src/main/java/expo/modules/audio/AudioPlaylist.kt +347 -0
  8. package/android/src/main/java/expo/modules/audio/AudioPreloadManager.kt +55 -0
  9. package/android/src/main/java/expo/modules/audio/AudioRecorder.kt +56 -19
  10. package/android/src/main/java/expo/modules/audio/AudioRecords.kt +8 -1
  11. package/android/src/main/java/expo/modules/audio/AudioUtils.kt +9 -0
  12. package/android/src/main/java/expo/modules/audio/Playable.kt +48 -0
  13. package/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt +146 -124
  14. package/android/src/main/java/expo/modules/audio/service/AudioPlaybackServiceConnection.kt +100 -0
  15. package/android/src/main/java/expo/modules/audio/service/AudioRecordingService.kt +64 -63
  16. package/android/src/main/java/expo/modules/audio/service/AudioRecordingServiceConnection.kt +153 -0
  17. package/android/src/main/java/expo/modules/audio/service/BaseServiceConnection.kt +141 -0
  18. package/build/Audio.types.d.ts +142 -3
  19. package/build/Audio.types.d.ts.map +1 -1
  20. package/build/Audio.types.js.map +1 -1
  21. package/build/AudioEventKeys.d.ts +2 -0
  22. package/build/AudioEventKeys.d.ts.map +1 -1
  23. package/build/AudioEventKeys.js +2 -0
  24. package/build/AudioEventKeys.js.map +1 -1
  25. package/build/AudioModule.types.d.ts +147 -4
  26. package/build/AudioModule.types.d.ts.map +1 -1
  27. package/build/AudioModule.types.js.map +1 -1
  28. package/build/AudioModule.web.d.ts +10 -67
  29. package/build/AudioModule.web.d.ts.map +1 -1
  30. package/build/AudioModule.web.js +43 -372
  31. package/build/AudioModule.web.js.map +1 -1
  32. package/build/AudioPlayer.web.d.ts +52 -0
  33. package/build/AudioPlayer.web.d.ts.map +1 -0
  34. package/build/AudioPlayer.web.js +304 -0
  35. package/build/AudioPlayer.web.js.map +1 -0
  36. package/build/AudioPlaylist.web.d.ts +58 -0
  37. package/build/AudioPlaylist.web.d.ts.map +1 -0
  38. package/build/AudioPlaylist.web.js +449 -0
  39. package/build/AudioPlaylist.web.js.map +1 -0
  40. package/build/AudioRecorder.web.d.ts +40 -0
  41. package/build/AudioRecorder.web.d.ts.map +1 -0
  42. package/build/AudioRecorder.web.js +239 -0
  43. package/build/AudioRecorder.web.js.map +1 -0
  44. package/build/AudioUtils.web.d.ts +12 -0
  45. package/build/AudioUtils.web.d.ts.map +1 -0
  46. package/build/AudioUtils.web.js +81 -0
  47. package/build/AudioUtils.web.js.map +1 -0
  48. package/build/ExpoAudio.d.ts +146 -2
  49. package/build/ExpoAudio.d.ts.map +1 -1
  50. package/build/ExpoAudio.js +187 -6
  51. package/build/ExpoAudio.js.map +1 -1
  52. package/build/ExpoAudio.web.d.ts +8 -1
  53. package/build/ExpoAudio.web.d.ts.map +1 -1
  54. package/build/ExpoAudio.web.js +52 -57
  55. package/build/ExpoAudio.web.js.map +1 -1
  56. package/build/MediaSessionController.web.d.ts +32 -0
  57. package/build/MediaSessionController.web.d.ts.map +1 -0
  58. package/build/MediaSessionController.web.js +156 -0
  59. package/build/MediaSessionController.web.js.map +1 -0
  60. package/build/utils/resolveSource.d.ts +1 -0
  61. package/build/utils/resolveSource.d.ts.map +1 -1
  62. package/build/utils/resolveSource.js +9 -11
  63. package/build/utils/resolveSource.js.map +1 -1
  64. package/expo-module.config.json +1 -1
  65. package/ios/AudioComponentRegistry.swift +61 -0
  66. package/ios/AudioModule.swift +254 -6
  67. package/ios/AudioPlayer.swift +101 -22
  68. package/ios/AudioPlaylist.swift +374 -0
  69. package/ios/AudioRecorder.swift +44 -4
  70. package/ios/AudioRecords.swift +7 -0
  71. package/ios/AudioSource.swift +3 -0
  72. package/ios/AudioUtils.swift +2 -15
  73. package/ios/MediaController.swift +9 -9
  74. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7-sources.jar +0 -0
  75. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7-sources.jar.md5 +1 -0
  76. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7-sources.jar.sha1 +1 -0
  77. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7-sources.jar.sha256 +1 -0
  78. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7-sources.jar.sha512 +1 -0
  79. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.aar +0 -0
  80. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.aar.md5 +1 -0
  81. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.aar.sha1 +1 -0
  82. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.aar.sha256 +1 -0
  83. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.aar.sha512 +1 -0
  84. package/local-maven-repo/expo/modules/audio/expo.modules.audio/{55.0.5/expo.modules.audio-55.0.5.module → 55.0.7/expo.modules.audio-55.0.7.module} +36 -22
  85. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.module.md5 +1 -0
  86. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.module.sha1 +1 -0
  87. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.module.sha256 +1 -0
  88. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.module.sha512 +1 -0
  89. package/local-maven-repo/expo/modules/audio/expo.modules.audio/{55.0.5/expo.modules.audio-55.0.5.pom → 55.0.7/expo.modules.audio-55.0.7.pom} +25 -1
  90. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.pom.md5 +1 -0
  91. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.pom.sha1 +1 -0
  92. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.pom.sha256 +1 -0
  93. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.7/expo.modules.audio-55.0.7.pom.sha512 +1 -0
  94. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml +4 -4
  95. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.md5 +1 -1
  96. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha1 +1 -1
  97. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha256 +1 -1
  98. package/local-maven-repo/expo/modules/audio/expo.modules.audio/maven-metadata.xml.sha512 +1 -1
  99. package/package.json +2 -2
  100. package/plugin/build/withAudio.d.ts +1 -0
  101. package/plugin/build/withAudio.js +54 -4
  102. package/plugin/src/withAudio.ts +73 -4
  103. package/src/Audio.types.ts +150 -3
  104. package/src/AudioEventKeys.ts +2 -0
  105. package/src/AudioModule.types.ts +178 -3
  106. package/src/AudioModule.web.ts +44 -486
  107. package/src/AudioPlayer.web.ts +392 -0
  108. package/src/AudioPlaylist.web.ts +529 -0
  109. package/src/AudioRecorder.web.ts +310 -0
  110. package/src/AudioUtils.web.ts +98 -0
  111. package/src/ExpoAudio.ts +227 -7
  112. package/src/ExpoAudio.web.ts +71 -64
  113. package/src/MediaSessionController.web.ts +191 -0
  114. package/src/utils/resolveSource.ts +10 -12
  115. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar +0 -0
  116. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.md5 +0 -1
  117. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.sha1 +0 -1
  118. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.sha256 +0 -1
  119. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5-sources.jar.sha512 +0 -1
  120. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar +0 -0
  121. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.md5 +0 -1
  122. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.sha1 +0 -1
  123. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.sha256 +0 -1
  124. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.aar.sha512 +0 -1
  125. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.md5 +0 -1
  126. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.sha1 +0 -1
  127. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.sha256 +0 -1
  128. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.module.sha512 +0 -1
  129. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.md5 +0 -1
  130. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.sha1 +0 -1
  131. package/local-maven-repo/expo/modules/audio/expo.modules.audio/55.0.5/expo.modules.audio-55.0.5.pom.sha256 +0 -1
  132. 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,39 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.7 — 2026-02-20
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [Web] Fix potential `nan` duration. ([#43268](https://github.com/expo/expo/pull/43268) by [@alanjhughes](https://github.com/alanjhughes))
18
+
19
+ ### 💡 Others
20
+
21
+ - [android] Make in-memory preload cache. ([#43293](https://github.com/expo/expo/pull/43293) by [@alanjhughes](https://github.com/alanjhughes))
22
+
23
+ ## 55.0.6 — 2026-02-16
24
+
25
+ ### 🎉 New features
26
+
27
+ - [iOS] Add support for `shouldRouteThroughEarpiece`. ([#43089](https://github.com/expo/expo/pull/43089) by [@alanjhughes](https://github.com/alanjhughes))
28
+ - [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))
29
+ - [Web] Add support for audio sampling. ([#43149](https://github.com/expo/expo/pull/43149) by [@alanjhughes](https://github.com/alanjhughes))
30
+ - [Web] Add support for media controls. ([#43150](https://github.com/expo/expo/pull/43150) by [@alanjhughes](https://github.com/alanjhughes))
31
+ - [Web] Add support for selecting recording inputs. ([#43151](https://github.com/expo/expo/pull/43151) by [@alanjhughes](https://github.com/alanjhughes))
32
+ - [Web] Add support for recording metering. ([#43152](https://github.com/expo/expo/pull/43152) by [@alanjhughes](https://github.com/alanjhughes))
33
+ - [Web] Enable `setIsAudioActiveAsync`. ([#43142](https://github.com/expo/expo/pull/43142) by [@alanjhughes](https://github.com/alanjhughes))
34
+ - Add support for preloading audio sources. ([#43063](https://github.com/expo/expo/pull/43063) by [@alanjhughes](https://github.com/alanjhughes))
35
+
36
+ ### 🐛 Bug fixes
37
+
38
+ - [Android] Fix memory leaks when refreshing the app. ([#42785](https://github.com/expo/expo/pull/42785) by [@behenate](https://github.com/behenate))
39
+ - [iOS] Fixes `mediaServicesDidReset` not being correctly implemented. ([#42898](https://github.com/expo/expo/pull/42898) by [@alanjhughes](https://github.com/alanjhughes))
40
+
41
+ ### 💡 Others
42
+
43
+ - [Android] Improve event handling. ([#43121](https://github.com/expo/expo/pull/43121) by [@alanjhughes](https://github.com/alanjhughes))
44
+ - [Android] Rework native audio service handling. ([#43015](https://github.com/expo/expo/pull/43015) by [@behenate](https://github.com/behenate))
45
+
13
46
  ## 55.0.5 — 2026-02-08
14
47
 
15
48
  _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.7'
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.7"
14
14
  }
15
15
 
16
16
  compileOptions {
@@ -24,12 +24,14 @@ repositories {
24
24
 
25
25
  dependencies {
26
26
  implementation 'androidx.appcompat:appcompat:1.7.1'
27
+ implementation 'androidx.core:core-ktx:1.15.0'
27
28
 
28
29
  def androidxMedia3Version = "1.8.0"
29
30
  implementation "androidx.media3:media3-session:$androidxMedia3Version"
30
31
  implementation "androidx.media3:media3-ui:$androidxMedia3Version"
31
32
  implementation "androidx.media3:media3-exoplayer:$androidxMedia3Version"
32
33
  implementation "androidx.media3:media3-exoplayer-dash:$androidxMedia3Version"
34
+ implementation "androidx.media3:media3-datasource:$androidxMedia3Version"
33
35
  implementation "androidx.media3:media3-datasource-okhttp:$androidxMedia3Version"
34
36
  implementation "androidx.media3:media3-exoplayer-hls:$androidxMedia3Version"
35
37
  implementation "androidx.media3:media3-exoplayer-smoothstreaming:$androidxMedia3Version"
@@ -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)
@@ -17,7 +17,6 @@ import androidx.media3.common.C.CONTENT_TYPE_HLS
17
17
  import androidx.media3.common.C.CONTENT_TYPE_OTHER
18
18
  import androidx.media3.common.C.CONTENT_TYPE_SS
19
19
  import androidx.media3.common.MediaItem
20
- import androidx.media3.common.PlaybackParameters
21
20
  import androidx.media3.common.Player
22
21
  import androidx.media3.common.util.Util
23
22
  import androidx.media3.datasource.DataSource
@@ -28,20 +27,23 @@ import androidx.media3.exoplayer.hls.HlsMediaSource
28
27
  import androidx.media3.exoplayer.smoothstreaming.SsMediaSource
29
28
  import androidx.media3.exoplayer.source.MediaSource
30
29
  import androidx.media3.exoplayer.source.ProgressiveMediaSource
31
- import expo.modules.audio.service.AudioControlsService
32
30
  import expo.modules.interfaces.permissions.Permissions
33
31
  import expo.modules.kotlin.Promise
34
32
  import expo.modules.kotlin.exception.Exceptions
33
+ import expo.modules.kotlin.functions.Coroutine
35
34
  import expo.modules.kotlin.functions.Queues
36
35
  import expo.modules.kotlin.modules.Module
37
36
  import expo.modules.kotlin.modules.ModuleDefinition
37
+ import kotlinx.coroutines.DelicateCoroutinesApi
38
+ import kotlinx.coroutines.Dispatchers
39
+ import kotlinx.coroutines.GlobalScope
38
40
  import kotlinx.coroutines.launch
39
41
  import kotlinx.coroutines.runBlocking
40
42
  import okhttp3.OkHttpClient
41
43
  import java.io.File
42
44
  import java.util.concurrent.ConcurrentHashMap
43
- import kotlin.math.min
44
45
 
46
+ @DelicateCoroutinesApi
45
47
  @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
46
48
  class AudioModule : Module() {
47
49
  private lateinit var audioManager: AudioManager
@@ -51,6 +53,7 @@ class AudioModule : Module() {
51
53
 
52
54
  private val players = ConcurrentHashMap<String, AudioPlayer>()
53
55
  private val recorders = ConcurrentHashMap<String, AudioRecorder>()
56
+ private val playlists = ConcurrentHashMap<String, AudioPlaylist>()
54
57
  private var shouldPlayInBackground = false
55
58
  private var audioEnabled = true
56
59
  private var shouldRouteThroughEarpiece = false
@@ -58,40 +61,41 @@ class AudioModule : Module() {
58
61
  private var interruptionMode: InterruptionMode? = null
59
62
  private var allowsBackgroundRecording = false
60
63
 
64
+ private val allPlayables: Sequence<Playable>
65
+ get() = players.values.asSequence() + playlists.values.asSequence()
66
+
61
67
  private var audioFocusRequest: AudioFocusRequest? = null
62
68
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
63
69
  appContext.mainQueue.launch {
64
70
  when (focusChange) {
65
71
  AudioManager.AUDIOFOCUS_LOSS -> {
66
72
  focusAcquired = false
67
- players.values.forEach { player ->
68
- player.ref.pause()
69
- }
73
+ allPlayables.forEach { it.pause() }
70
74
  }
71
75
 
72
76
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
73
77
  focusAcquired = false
74
- players.values.forEach { player ->
75
- if (player.ref.isPlaying) {
76
- player.isPaused = true
77
- player.ref.pause()
78
+ allPlayables.forEach { playable ->
79
+ if (playable.isPlaying) {
80
+ playable.isPaused = true
81
+ playable.pause()
78
82
  }
79
83
  }
80
84
  }
81
85
 
82
86
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
83
87
  if (interruptionMode == InterruptionMode.DUCK_OTHERS) {
84
- players.values.forEach { player ->
85
- if (player.previousVolume != player.ref.volume) {
86
- player.previousVolume = player.ref.volume
88
+ allPlayables.forEach { playable ->
89
+ if (playable.previousVolume != playable.volume) {
90
+ playable.previousVolume = playable.volume
87
91
  }
88
- player.ref.volume = player.previousVolume * 0.5f
92
+ playable.setVolume(playable.previousVolume * 0.5f)
89
93
  }
90
94
  } else {
91
- players.values.forEach { player ->
92
- if (player.ref.isPlaying) {
93
- player.isPaused = true
94
- player.ref.pause()
95
+ allPlayables.forEach { playable ->
96
+ if (playable.isPlaying) {
97
+ playable.isPaused = true
98
+ playable.pause()
95
99
  }
96
100
  }
97
101
  }
@@ -99,11 +103,11 @@ class AudioModule : Module() {
99
103
 
100
104
  AudioManager.AUDIOFOCUS_GAIN -> {
101
105
  focusAcquired = true
102
- players.values.forEach { player ->
103
- player.setVolume(player.previousVolume)
104
- if (player.isPaused) {
105
- player.isPaused = false
106
- player.ref.play()
106
+ allPlayables.forEach { playable ->
107
+ playable.setVolume(playable.previousVolume)
108
+ if (playable.isPaused) {
109
+ playable.isPaused = false
110
+ playable.play()
107
111
  }
108
112
  }
109
113
  }
@@ -112,7 +116,7 @@ class AudioModule : Module() {
112
116
  }
113
117
 
114
118
  private fun shouldReleaseFocus(): Boolean {
115
- return players.values.none { it.ref.isPlaying }
119
+ return allPlayables.none { it.isPlaying }
116
120
  }
117
121
 
118
122
  private fun requestAudioFocus() {
@@ -197,9 +201,9 @@ class AudioModule : Module() {
197
201
  if (!enabled) {
198
202
  releaseAudioFocus()
199
203
  runOnMain {
200
- players.values.forEach {
201
- if (it.ref.isPlaying) {
202
- it.ref.pause()
204
+ allPlayables.forEach {
205
+ if (it.isPlaying) {
206
+ it.pause()
203
207
  }
204
208
  }
205
209
  }
@@ -210,17 +214,43 @@ class AudioModule : Module() {
210
214
  Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.RECORD_AUDIO)
211
215
  }
212
216
 
217
+ AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
218
+ Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.POST_NOTIFICATIONS)
219
+ }
220
+
213
221
  AsyncFunction("getRecordingPermissionsAsync") { promise: Promise ->
214
222
  Permissions.getPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.RECORD_AUDIO)
215
223
  }
216
224
 
225
+ AsyncFunction("preload") Coroutine { source: AudioSource, _: Double ->
226
+ val uri = source.uri ?: return@Coroutine
227
+ val factory = when (uri.toUri().scheme) {
228
+ "http", "https" -> httpDataSourceFactory(source.headers)
229
+ else -> DefaultDataSource.Factory(context)
230
+ }
231
+ AudioPreloadManager.preload(uri, factory)
232
+ }
233
+
234
+ AsyncFunction("clearPreloadedSource") Coroutine { source: AudioSource ->
235
+ val uri = source.uri ?: return@Coroutine
236
+ AudioPreloadManager.clearSource(uri)
237
+ }
238
+
239
+ AsyncFunction("clearAllPreloadedSources") Coroutine { ->
240
+ AudioPreloadManager.clearAll()
241
+ }
242
+
243
+ AsyncFunction("getPreloadedSources") {
244
+ AudioPreloadManager.getPreloadedSources()
245
+ }
246
+
217
247
  OnActivityEntersBackground {
218
248
  if (!shouldPlayInBackground) {
219
249
  releaseAudioFocus()
220
- players.values.forEach { player ->
221
- if (player.ref.isPlaying) {
222
- player.isPaused = true
223
- player.ref.pause()
250
+ allPlayables.forEach { playable ->
251
+ if (playable.isPlaying) {
252
+ playable.isPaused = true
253
+ playable.pause()
224
254
  }
225
255
  }
226
256
  }
@@ -235,15 +265,14 @@ class AudioModule : Module() {
235
265
 
236
266
  OnActivityEntersForeground {
237
267
  if (!shouldPlayInBackground) {
238
- val hasPlayersToResume = players.values.any { it.isPaused }
239
- if (hasPlayersToResume) {
268
+ if (allPlayables.any { it.isPaused }) {
240
269
  requestAudioFocus()
241
270
  }
242
271
 
243
- players.values.forEach { player ->
244
- if (player.isPaused) {
245
- player.isPaused = false
246
- player.ref.play()
272
+ allPlayables.forEach { playable ->
273
+ if (playable.isPaused) {
274
+ playable.isPaused = false
275
+ playable.play()
247
276
  }
248
277
  }
249
278
  }
@@ -260,29 +289,34 @@ class AudioModule : Module() {
260
289
  }
261
290
 
262
291
  OnDestroy {
263
- appContext.mainQueue.launch {
292
+ GlobalScope.launch(Dispatchers.Main) {
264
293
  releaseAudioFocus()
265
294
  players.values.forEach {
266
295
  it.ref.stop()
267
296
  }
268
297
 
298
+ playlists.values.forEach {
299
+ it.ref.stop()
300
+ }
301
+
269
302
  recorders.values.forEach {
270
303
  it.stopRecording()
271
304
  }
272
-
273
- AudioControlsService.clearSession()
305
+ AudioPreloadManager.clearAll()
274
306
  }
275
307
  }
276
308
 
277
309
  Class(AudioPlayer::class) {
278
- Constructor { source: AudioSource?, updateInterval: Double, keepAudioSessionActive: Boolean ->
310
+ Constructor { source: AudioSource?, updateInterval: Double, keepAudioSessionActive: Boolean, preferredForwardBufferDuration: Double ->
279
311
  val mediaSource = createMediaItem(source)
312
+ val bufferDurationMs = (preferredForwardBufferDuration * 1000).toLong()
280
313
  runOnMain {
281
314
  val player = AudioPlayer(
282
315
  context,
283
316
  appContext,
284
317
  mediaSource,
285
- updateInterval
318
+ updateInterval,
319
+ bufferDurationMs
286
320
  )
287
321
  player.onPlaybackStateChange = { isPlaying ->
288
322
  if (!isPlaying && shouldReleaseFocus()) {
@@ -453,9 +487,7 @@ class AudioModule : Module() {
453
487
 
454
488
  Function("setPlaybackRate") { player: AudioPlayer, rate: Float ->
455
489
  appContext.mainQueue.launch {
456
- val playbackRate = if (rate <= 0) 0.1f else min(rate, 2.0f)
457
- val pitch = if (player.preservesPitch) 1f else playbackRate
458
- player.ref.playbackParameters = PlaybackParameters(playbackRate, pitch)
490
+ player.setPlaybackRate(rate)
459
491
  }
460
492
  }
461
493
 
@@ -494,7 +526,7 @@ class AudioModule : Module() {
494
526
  recorder.getCurrentTimeSeconds()
495
527
  }
496
528
 
497
- AsyncFunction("prepareToRecordAsync") { recorder: AudioRecorder, options: RecordingOptions? ->
529
+ AsyncFunction("prepareToRecordAsync") Coroutine { recorder: AudioRecorder, options: RecordingOptions? ->
498
530
  checkRecordingPermission()
499
531
  recorder.prepareRecording(options)
500
532
  }
@@ -550,6 +582,200 @@ class AudioModule : Module() {
550
582
  }
551
583
  }
552
584
  }
585
+
586
+ Class(AudioPlaylist::class) {
587
+ Constructor { sources: List<AudioSource>, updateInterval: Double, loop: LoopMode ->
588
+ runOnMain {
589
+ val playlist = AudioPlaylist(
590
+ context,
591
+ appContext,
592
+ sources,
593
+ updateInterval,
594
+ DefaultDataSource.Factory(context)
595
+ )
596
+ playlist.loopMode = loop
597
+ playlist.setMediaItemCreator { source ->
598
+ source.uri?.let { uriString ->
599
+ val uri = uriString.toUri()
600
+ when {
601
+ isRawResource(uri) -> {
602
+ val file = getResourceName(uri, uriString)
603
+ MediaItem.fromUri(getRawResourceURI(file))
604
+ }
605
+ else -> MediaItem.fromUri(uri)
606
+ }
607
+ }
608
+ }
609
+ playlist.loadInitialPlaylist()
610
+ playlist.onPlaybackStateChange = { isPlaying ->
611
+ if (!isPlaying && shouldReleaseFocus()) {
612
+ releaseAudioFocus()
613
+ }
614
+ }
615
+ playlists[playlist.id] = playlist
616
+ playlist
617
+ }
618
+ }
619
+
620
+ Property("id") { playlist ->
621
+ playlist.id
622
+ }
623
+
624
+ Property("currentIndex") { playlist ->
625
+ runOnMain {
626
+ playlist.currentTrackIndex
627
+ }
628
+ }
629
+
630
+ Property("trackCount") { playlist ->
631
+ runOnMain {
632
+ playlist.trackCount
633
+ }
634
+ }
635
+
636
+ Property("sources") { playlist ->
637
+ playlist.getSources()
638
+ }
639
+
640
+ Property("playing") { playlist ->
641
+ runOnMain {
642
+ playlist.ref.isPlaying
643
+ }
644
+ }
645
+
646
+ Property("muted") { playlist ->
647
+ playlist.isMuted
648
+ }.set { playlist, muted: Boolean? ->
649
+ val newMuted = muted ?: false
650
+ playlist.isMuted = newMuted
651
+ playlist.setVolume(if (newMuted) 0f else playlist.previousVolume)
652
+ }
653
+
654
+ Property("isLoaded") { playlist ->
655
+ runOnMain {
656
+ playlist.ref.playbackState == Player.STATE_READY
657
+ }
658
+ }
659
+
660
+ Property("isBuffering") { playlist ->
661
+ runOnMain {
662
+ playlist.ref.playbackState == Player.STATE_BUFFERING
663
+ }
664
+ }
665
+
666
+ Property("currentTime") { playlist ->
667
+ runOnMain {
668
+ playlist.currentTime
669
+ }
670
+ }
671
+
672
+ Property("duration") { playlist ->
673
+ runOnMain {
674
+ playlist.duration
675
+ }
676
+ }
677
+
678
+ Property("volume") { playlist ->
679
+ runOnMain {
680
+ playlist.ref.volume
681
+ }
682
+ }.set { playlist, volume: Float? ->
683
+ playlist.setVolume(volume)
684
+ }
685
+
686
+ Property("playbackRate") { playlist ->
687
+ runOnMain {
688
+ playlist.ref.playbackParameters.speed
689
+ }
690
+ }.set { playlist, rate: Float ->
691
+ appContext.mainQueue.launch {
692
+ playlist.setPlaybackRate(rate)
693
+ }
694
+ }
695
+
696
+ Property("loop") { playlist ->
697
+ playlist.loopMode.value
698
+ }.set { playlist, mode: LoopMode ->
699
+ runOnMain {
700
+ playlist.loopMode = mode
701
+ }
702
+ }
703
+
704
+ Property("currentStatus") { playlist ->
705
+ runOnMain {
706
+ playlist.currentStatus()
707
+ }
708
+ }
709
+
710
+ Function("play") { playlist: AudioPlaylist ->
711
+ if (!audioEnabled) {
712
+ Log.e(TAG, "Audio has been disabled. Re-enable to start playing")
713
+ return@Function
714
+ }
715
+ runOnMain {
716
+ if (!focusAcquired) {
717
+ requestAudioFocus()
718
+ }
719
+ playlist.ref.play()
720
+ }
721
+ }
722
+
723
+ Function("pause") { playlist: AudioPlaylist ->
724
+ runOnMain {
725
+ playlist.ref.pause()
726
+ }
727
+ }
728
+
729
+ Function("next") { playlist: AudioPlaylist ->
730
+ runOnMain {
731
+ playlist.next()
732
+ }
733
+ }
734
+
735
+ Function("previous") { playlist: AudioPlaylist ->
736
+ runOnMain {
737
+ playlist.previous()
738
+ }
739
+ }
740
+
741
+ Function("skipTo") { playlist: AudioPlaylist, index: Int ->
742
+ runOnMain {
743
+ playlist.skipTo(index)
744
+ }
745
+ }
746
+
747
+ AsyncFunction("seekTo") { playlist: AudioPlaylist, seconds: Double ->
748
+ playlist.seekTo(seconds)
749
+ }.runOnQueue(Queues.MAIN)
750
+
751
+ Function("add") { playlist: AudioPlaylist, source: AudioSource ->
752
+ runOnMain {
753
+ playlist.add(source)
754
+ }
755
+ }
756
+
757
+ Function("insert") { playlist: AudioPlaylist, source: AudioSource, index: Int ->
758
+ runOnMain {
759
+ playlist.insert(source, index)
760
+ }
761
+ }
762
+
763
+ Function("remove") { playlist: AudioPlaylist, index: Int ->
764
+ runOnMain {
765
+ playlist.remove(index)
766
+ }
767
+ }
768
+
769
+ Function("clear") { playlist: AudioPlaylist ->
770
+ runOnMain {
771
+ playlist.clear()
772
+ }
773
+ }
774
+
775
+ Function("destroy") { playlist: AudioPlaylist ->
776
+ playlists.remove(playlist.id)
777
+ }
778
+ }
553
779
  }
554
780
 
555
781
  private fun createMediaItem(source: AudioSource?): MediaSource? = source?.uri?.let { uriString ->
@@ -562,9 +788,14 @@ class AudioModule : Module() {
562
788
  else -> MediaItem.fromUri(uri)
563
789
  }
564
790
 
565
- val factory = when (uri.scheme) {
566
- "http", "https" -> httpDataSourceFactory(source.headers)
567
- else -> DefaultDataSource.Factory(context)
791
+ val preloadedBytes = AudioPreloadManager.get(uriString)
792
+ val factory: DataSource.Factory = if (preloadedBytes != null) {
793
+ InMemoryDataSourceFactory(preloadedBytes)
794
+ } else {
795
+ when (uri.scheme) {
796
+ "http", "https" -> httpDataSourceFactory(source.headers)
797
+ else -> DefaultDataSource.Factory(context)
798
+ }
568
799
  }
569
800
  return buildMediaSourceFactory(factory, mediaItem)
570
801
  }