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.
| @@ -1378,6 +1378,8 @@ only when the system grants the `shouldResume` option. A permanent Android | ||
| 1378 | 1378 | focus loss releases every player and sends each sound's error event; replacement, |
| 1379 | 1379 | decode failure, cancellation, interruption, and teardown also release their |
| 1380 | 1380 | player resources. |
| 1381 | Control calls resolve `false` when their sound id is not owned by the screen, | |
| 1382 | matching the browser host instead of reporting a false success. | |
| 1381 | 1383 | |
| 1382 | 1384 | Checkpoint names use the same bounded filename contract on both native hosts: |
| 1383 | 1385 | letters, digits, `.`, `_`, and `-`, up to 128 characters, without a leading |
| @@ -2139,6 +2139,9 @@ describe('Craft iOS builder', () => { | ||
| 2139 | 2139 | expect(services).toContain('AVAudioSession.interruptionNotification') |
| 2140 | 2140 | expect(services).toContain('sound.interrupted = true') |
| 2141 | 2141 | expect(services).toContain('AVAudioSession.InterruptionOptions.shouldResume') |
| 2142 | expect(screen).toContain('reply(.success(sounds.pause(id: id)))') | |
| 2143 | expect(screen).toContain('reply(.success(sounds.resume(id: id)))') | |
| 2144 | expect(screen).toContain('reply(.success(sounds.stop(id: id, fadeOut: number(1, "fadeOut") / 1000)))') | |
| 2142 | 2145 | expect(plist).not.toContain('<string>audio</string>') |
| 2143 | 2146 | // A route change or an interruption stops AVAudioEngine; it starts again. |
| 2144 | 2147 | expect(audio).toContain('forName: .AVAudioEngineConfigurationChange, object: engine') |
| @@ -7762,20 +7762,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 7762 | 7762 | screenRequestCancellers[requestToken] = { [weak self] in self?.sounds.stop(id: id, fadeOut: 0) } |
| 7763 | 7763 | sounds.play(id: id, url: url, options: args.count > 2 ? args[2] as? [String: Any] ?? [:] : [:], answer: reply) |
| 7764 | 7764 | case ("Audio", "pause"): |
| 7765 | sounds.pause(id: id) | |
| 7766 | reply(.success(true)) | |
| 7765 | reply(.success(sounds.pause(id: id))) | |
| 7767 | 7766 | case ("Audio", "resume"): |
| 7768 | sounds.resume(id: id) | |
| 7769 | reply(.success(true)) | |
| 7767 | reply(.success(sounds.resume(id: id))) | |
| 7770 | 7768 | case ("Audio", "setVolume"): |
| 7771 | sounds.setVolume(id: id, volume: number(1), fade: number(2, "fade") / 1000) | |
| 7772 | reply(.success(true)) | |
| 7769 | reply(.success(sounds.setVolume(id: id, volume: number(1), fade: number(2, "fade") / 1000))) | |
| 7773 | 7770 | case ("Audio", "stop"): |
| 7774 | sounds.stop(id: id, fadeOut: number(1, "fadeOut") / 1000) | |
| 7775 | reply(.success(true)) | |
| 7771 | reply(.success(sounds.stop(id: id, fadeOut: number(1, "fadeOut") / 1000))) | |
| 7776 | 7772 | case ("Audio", "stopAll"): |
| 7777 | sounds.stopAll(fadeOut: number(0, "fadeOut") / 1000) | |
| 7778 | reply(.success(true)) | |
| 7773 | reply(.success(sounds.stopAll(fadeOut: number(0, "fadeOut") / 1000))) | |
| 7779 | 7774 | default: |
| 7780 | 7775 | return false |
| 7781 | 7776 | } |
| @@ -395,42 +395,52 @@ final class CraftNativeSounds { | ||
| 395 | 395 | if fadeIn > 0 { ramp(sound, to: volume, over: fadeIn) } |
| 396 | 396 | } |
| 397 | 397 | |
| 398 | func pause(id: String) { | |
| 399 | guard let sound = sounds[id] else { return } | |
| 398 | @discardableResult | |
| 399 | func pause(id: String) -> Bool { | |
| 400 | guard let sound = sounds[id] else { return false } | |
| 400 | 401 | sound.paused = true |
| 401 | 402 | sound.interrupted = false |
| 402 | 403 | sound.fade?.invalidate() |
| 403 | 404 | sound.player.pause() |
| 405 | return true | |
| 404 | 406 | } |
| 405 | 407 | |
| 406 | func resume(id: String) { | |
| 407 | guard let sound = sounds[id], sound.paused else { return } | |
| 408 | @discardableResult | |
| 409 | func resume(id: String) -> Bool { | |
| 410 | guard let sound = sounds[id], sound.paused else { return false } | |
| 408 | 411 | sound.paused = false |
| 409 | 412 | sound.interrupted = false |
| 410 | 413 | sound.player.volume = sound.volume |
| 411 | 414 | sound.player.play() |
| 415 | return true | |
| 412 | 416 | } |
| 413 | 417 | |
| 414 | 418 | /// Changes the volume, over `fade` seconds when given. |
| 415 | func setVolume(id: String, volume: Double, fade: Double) { | |
| 416 | guard let sound = sounds[id] else { return } | |
| 419 | @discardableResult | |
| 420 | func setVolume(id: String, volume: Double, fade: Double) -> Bool { | |
| 421 | guard let sound = sounds[id] else { return false } | |
| 417 | 422 | sound.volume = Float(min(max(volume, 0), 1)) |
| 418 | 423 | if fade > 0, !sound.paused { ramp(sound, to: sound.volume, over: fade) } |
| 419 | 424 | else { sound.fade?.invalidate(); sound.player.volume = sound.volume } |
| 425 | return true | |
| 420 | 426 | } |
| 421 | 427 | |
| 422 | 428 | /// Stops the sound, fading it out over `fadeOut` seconds first. |
| 423 | func stop(id: String, fadeOut: Double) { | |
| 424 | guard let sound = sounds[id] else { return } | |
| 425 | guard fadeOut > 0, !sound.paused else { remove(id); return } | |
| 429 | @discardableResult | |
| 430 | func stop(id: String, fadeOut: Double) -> Bool { | |
| 431 | guard let sound = sounds[id] else { return false } | |
| 432 | guard fadeOut > 0, !sound.paused else { remove(id); return true } | |
| 426 | 433 | ramp(sound, to: 0, over: fadeOut) { [weak self, weak sound] in |
| 427 | 434 | guard let self, let sound, self.sounds[id] === sound else { return } |
| 428 | 435 | self.remove(id) |
| 429 | 436 | } |
| 437 | return true | |
| 430 | 438 | } |
| 431 | 439 | |
| 432 | func stopAll(fadeOut: Double) { | |
| 440 | @discardableResult | |
| 441 | func stopAll(fadeOut: Double) -> Bool { | |
| 433 | 442 | for id in Array(sounds.keys) { stop(id: id, fadeOut: fadeOut) } |
| 443 | return true | |
| 434 | 444 | } |
| 435 | 445 | |
| 436 | 446 | /// The screen closed: everything stops at once. |