also looking at this
fix(native): interaction, audio, document and checkpoint gaps stranded after #401
#415
19 files
+987
-50
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -26,7 +26,9 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 26 | 26 | var volume: Float, |
| 27 | 27 | var paused: Boolean = false, |
| 28 | 28 | var interrupted: Boolean = false, |
| 29 | var prepared: Boolean = false, | |
| 29 | 30 | var fade: Runnable? = null, |
| 31 | var pending: ((CraftNativeCapabilityResult) -> Unit)? = null, | |
| 30 | 32 | ) |
| 31 | 33 | |
| 32 | 34 | private val main = Handler(Looper.getMainLooper()) |
| @@ -58,24 +60,23 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 58 | 60 | val fadeIn = options.optDouble("fadeIn", 0.0) |
| 59 | 61 | if (!fadeIn.isFinite() || fadeIn < 0) return done(failure("INVALID_ARGUMENT", "audio.play fadeIn must be a non-negative finite number")) |
| 60 | 62 | val player = MediaPlayer() |
| 61 | val sound = Sound(player, volume) | |
| 63 | val sound = Sound(player, if (fadeIn > 0) 0f else volume, pending = done) | |
| 62 | 64 | sounds[id] = sound |
| 63 | var settled = false | |
| 64 | 65 | fun settle(result: CraftNativeCapabilityResult) { |
| 65 | if (settled) return | |
| 66 | settled = true | |
| 67 | done(result) | |
| 66 | val answer = sound.pending ?: return | |
| 67 | sound.pending = null | |
| 68 | answer(result) | |
| 68 | 69 | } |
| 69 | 70 | fun failed(message: String) { |
| 70 | 71 | if (sounds[id] !== sound) return |
| 71 | remove(id) | |
| 72 | 72 | settle(failure("AUDIO_ERROR", message)) |
| 73 | remove(id) | |
| 73 | 74 | emit?.invoke(JSONObject().put("id", id).put("event", "error").put("message", message)) |
| 74 | 75 | } |
| 75 | 76 | |
| 76 | 77 | if (audioManager.requestAudioFocus(focusRequest) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| 77 | remove(id) | |
| 78 | 78 | settle(failure("AUDIO_FOCUS_DENIED", "Audio focus was denied by the operating system")) |
| 79 | remove(id) | |
| 79 | 80 | emit?.invoke(JSONObject().put("id", id).put("event", "error").put("message", "Audio focus was denied by the operating system")) |
| 80 | 81 | return |
| 81 | 82 | } |
| @@ -89,9 +90,13 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 89 | 90 | player.setVolume(if (fadeIn > 0) 0f else volume, if (fadeIn > 0) 0f else volume) |
| 90 | 91 | player.setOnPreparedListener { |
| 91 | 92 | if (sounds[id] !== sound) return@setOnPreparedListener |
| 93 | sound.prepared = true | |
| 94 | if (!sound.paused && !sound.interrupted) player.start() | |
| 92 | 95 | settle(CraftNativeCapabilityResult(data = true)) |
| 93 | player.start() | |
| 94 | if (fadeIn > 0) ramp(sound, volume, fadeIn / 1000.0) | |
| 96 | if (fadeIn > 0) { | |
| 97 | if (!sound.paused) ramp(sound, volume, fadeIn / 1000.0) | |
| 98 | else { sound.volume = volume; player.setVolume(volume, volume) } | |
| 99 | } | |
| 95 | 100 | } |
| 96 | 101 | player.setOnCompletionListener { |
| 97 | 102 | if (sounds[id] !== sound) return@setOnCompletionListener |
| @@ -106,25 +111,32 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 106 | 111 | player.prepareAsync() |
| 107 | 112 | }.onFailure { error -> |
| 108 | 113 | if (sounds[id] === sound) { |
| 109 | remove(id) | |
| 110 | 114 | settle(failure("AUDIO_ERROR", error.message ?: "The sound could not be loaded")) |
| 115 | remove(id) | |
| 111 | 116 | emit?.invoke(JSONObject().put("id", id).put("event", "error").put("message", error.message ?: "The sound could not be loaded")) |
| 112 | 117 | } |
| 113 | 118 | } |
| 114 | 119 | } |
| 115 | 120 | |
| 121 | /** Cancellation belongs to this playback, not a later reuse of its id. */ | |
| 122 | fun cancellation(id: String): () -> Unit { | |
| 123 | val sound = sounds[id] | |
| 124 | return { if (sound != null && sounds[id] === sound) remove(id) } | |
| 125 | } | |
| 126 | ||
| 116 | 127 | fun pause(id: String): Boolean { |
| 117 | 128 | val sound = sounds[id] ?: return false |
| 118 | 129 | sound.fade?.let(main::removeCallbacks) |
| 119 | 130 | sound.fade = null |
| 120 | 131 | runCatching { if (sound.player.isPlaying) sound.player.pause() } |
| 121 | 132 | sound.paused = true |
| 133 | sound.interrupted = false | |
| 122 | 134 | return true |
| 123 | 135 | } |
| 124 | 136 | |
| 125 | 137 | fun resume(id: String): Boolean { |
| 126 | 138 | val sound = sounds[id] ?: return false |
| 127 | runCatching { sound.player.start() }.onFailure { return false } | |
| 139 | if (sound.prepared) runCatching { sound.player.start() }.onFailure { return false } | |
| 128 | 140 | sound.paused = false |
| 129 | 141 | sound.interrupted = false |
| 130 | 142 | return true |
| @@ -137,7 +149,10 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 137 | 149 | sound.fade?.let(main::removeCallbacks) |
| 138 | 150 | sound.fade = null |
| 139 | 151 | if (fade > 0 && !sound.paused) ramp(sound, target, fade / 1000.0) |
| 140 | else sound.player.setVolume(target, target) | |
| 152 | else { | |
| 153 | sound.volume = target | |
| 154 | sound.player.setVolume(target, target) | |
| 155 | } | |
| 141 | 156 | return true |
| 142 | 157 | } |
| 143 | 158 | |
| @@ -181,6 +196,9 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 181 | 196 | } |
| 182 | 197 | sound.player.release() |
| 183 | 198 | if (sounds.isEmpty()) audioManager.abandonAudioFocusRequest(focusRequest) |
| 199 | val answer = sound.pending | |
| 200 | sound.pending = null | |
| 201 | answer?.invoke(failure("CANCELLED", "Audio playback was stopped or replaced")) | |
| 184 | 202 | } |
| 185 | 203 | |
| 186 | 204 | private fun onAudioFocusChange(change: Int) { |
| @@ -194,18 +212,18 @@ internal class CraftNativeSounds(private val context: Context) { | ||
| 194 | 212 | } |
| 195 | 213 | } |
| 196 | 214 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> sounds.values.toList().forEach { sound -> |
| 197 | if (!sound.paused && runCatching { sound.player.isPlaying }.getOrDefault(false)) { | |
| 215 | if (!sound.paused) { | |
| 198 | 216 | sound.interrupted = true |
| 199 | runCatching { sound.player.pause() } | |
| 217 | if (sound.prepared) runCatching { sound.player.pause() } | |
| 200 | 218 | } |
| 201 | 219 | } |
| 202 | 220 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> sounds.values.toList().forEach { sound -> |
| 203 | 221 | runCatching { sound.player.setVolume(sound.volume * 0.2f, sound.volume * 0.2f) } |
| 204 | 222 | } |
| 205 | 223 | AudioManager.AUDIOFOCUS_GAIN -> sounds.values.toList().forEach { sound -> |
| 206 | if (sound.interrupted) { | |
| 224 | if (sound.interrupted && !sound.paused) { | |
| 207 | 225 | sound.interrupted = false |
| 208 | runCatching { sound.player.start() } | |
| 226 | if (sound.prepared) runCatching { sound.player.start() } | |
| 209 | 227 | } else { |
| 210 | 228 | runCatching { sound.player.setVolume(sound.volume, sound.volume) } |
| 211 | 229 | } |
| @@ -7663,10 +7663,10 @@ class MainActivity : AppCompatActivity() { | ||
| 7663 | 7663 | } else if (module == "Audio") { |
| 7664 | 7664 | { completion -> |
| 7665 | 7665 | val id = args.optString(0) |
| 7666 | if (method == "play" && id.isNotBlank()) { | |
| 7667 | screen.soundRequestCancellers[token] = { screen.sounds.stop(id, 0.0) } | |
| 7668 | } | |
| 7669 | 7666 | performAudio(screen, method, args, completion) |
| 7667 | if (method == "play" && id.isNotBlank() && screen.pendingCapabilities.contains(token)) { | |
| 7668 | screen.soundRequestCancellers[token] = screen.sounds.cancellation(id) | |
| 7669 | } | |
| 7670 | 7670 | } |
| 7671 | 7671 | } else if (services.handles(module)) { |
| 7672 | 7672 | { completion -> services.perform(module, method, args, completion) } |