クラウド上の Mac ではプレーヤーの自動テストがすべて通っていても、ユーザーがイヤホンを抜くとスピーカーから再生が続いてしまうことがあります。問題はデコーダーではなく、「出力経路の変更を受けて何をするか」を独立してテストしていない点にあるかもしれません。システムによる音声割り込みも同様です。割り込みが終わったからといって、ユーザーが再生を望んでいるとは限りません。システム通知と再生判断を切り分ければ、イヤホンのないビルドノードでもこのロジックを安定して検証できます。
まずイベントと期待する動作を定義する
通知を処理するコードを書く前に、製品としてのルールを決めます。この記事では保守的なルールを採用します。再生中に割り込みが始まったら一時停止し、それまで再生中だったことを記録します。割り込みの終了時には、再開が許可されている場合に限り、その記録に基づいて再開します。割り込み中にユーザーが自ら一時停止した場合、以前の再開意図は取り消さなければなりません。出力デバイスが切断された場合は直ちに一時停止し、新しい経路の名前だけから、スピーカーで聴き続けたいかどうかを推測しません。
| 入力イベント | 再生状態 | 再開意図 |
|---|---|---|
| 再生中に割り込みが始まる | 一時停止 | 直前に再生中だったことを記録 |
| 割り込みが終わり、再開が許可される | 再開意図に従って判断 | 消去 |
| ユーザーが自ら一時停止する | 一時停止 | 消去 |
| 以前の出力デバイスが利用できなくなる | 一時停止 | 消去 |
「以前の出力デバイスが利用できなくなる」はイベントの原因であり、すべての経路変更を指す言葉ではありません。たとえば、接続済みの別のデバイスへの切り替えに、製品として判断しないままイヤホン切断時のルールを適用すべきではありません。
再生判断を純粋な状態機械に切り出す
通知の解析はアダプター層に残し、コアロジックには明確なイベントだけを渡します。次の型はアプリのターゲットに配置し、テストターゲットから参照できます。isPlaying はアプリが意図する再生状態を表します。プレーヤーを実際に操作する副作用は、外側の層が状態の変化に応じて実行します。
enum AudioEvent {
case interruptionBegan
case interruptionEnded(shouldResume: Bool)
case outputDisconnected
case userPaused
}
struct PlaybackState {
var isPlaying = false
var resumeAfterInterruption = false
mutating func apply(_ event: AudioEvent) {
switch event {
case .interruptionBegan:
resumeAfterInterruption = isPlaying
isPlaying = false
case .interruptionEnded(let shouldResume):
isPlaying = resumeAfterInterruption && shouldResume
resumeAfterInterruption = false
case .outputDisconnected, .userPaused:
isPlaying = false
resumeAfterInterruption = false
}
}
}
この例が扱うのは、一度の割り込みだけです。アプリが複数の再生セッションを同時に扱う場合や、一時停止後も操作できるキューを持つ場合は、先に状態の定義を拡張し、そのうえでイベントの順序関係を決めてください。ひとつの真偽値にセッションの意味をすべて担わせてはいけません。
システム通知をイベントに変換する
アダプター層で AVAudioSession.interruptionNotification を監視し、userInfo から割り込みの種類を読み取ります。終了時には再開オプションを読み取り、その結果を状態機械に渡します。AVAudioSession.routeChangeNotification を監視するときは原因を確認し、oldDeviceUnavailable だけを outputDisconnected に変換します。フィールドが欠けている、または認識できない場合は記録して調査できるようにし、勝手に「再開が許可された」とみなさないでください。
通知のコールバックと画面上の操作は、異なる実行コンテキストから届くことがあります。イベントは同じ状態の管理者へ直列に渡し、変更前後の状態に基づいて一時停止または再生を指示します。これにより、「ユーザーによる一時停止」と「割り込みの終了」が並行して起きたとき、コールバックの到着順によってユーザーの意図が意図せず上書きされるのを防げます。
ビルドノードで境界条件をテストする
まず XCTest で、見落としやすい二つの経路を確認します。割り込み前に再生していなければ勝手に再生を始めないこと、割り込み中にユーザーが一時停止したら自動再開しないことです。テストに実際のイヤホンは不要で、システムが特定のタイミングで通知を送る必要もありません。
import XCTest
final class PlaybackStateTests: XCTestCase {
func testInterruptionDoesNotStartIdlePlayer() {
var state = PlaybackState()
state.apply(.interruptionBegan)
state.apply(.interruptionEnded(shouldResume: true))
XCTAssertFalse(state.isPlaying)
}
func testUserPauseCancelsResume() {
var state = PlaybackState(isPlaying: true)
state.apply(.interruptionBegan)
state.apply(.userPaused)
state.apply(.interruptionEnded(shouldResume: true))
XCTAssertFalse(state.isPlaying)
}
func testDisconnectedOutputStopsPlayback() {
var state = PlaybackState(isPlaying: true)
state.apply(.outputDisconnected)
XCTAssertFalse(state.isPlaying)
XCTAssertFalse(state.resumeAfterInterruption)
}
}
クラウド上の Mac では、まず xcodebuild -list -project App.xcodeproj でプロジェクトと Scheme を確認し、次にチームで実際に共有している Scheme を使って xcodebuild test -project App.xcodeproj -scheme App -destination 'platform=iOS Simulator,name=<已安装的设备名称>' を実行します。例にあるプロジェクト名、Scheme、デバイス名は置き換える必要があります。現在利用可能なデバイスは xcrun simctl list devices available で確認できます。テストターゲットから状態機械の型にアクセスでき、Scheme にそのテストターゲットが含まれていることも確認してください。
単体テストの成功が示すのは、与えたイベント列から期待どおりの判断が得られたということだけです。システム通知が必ず届くことも、実際にどのデバイスから音が出るかも証明できません。
実機で統合動作を確認する
実機確認には、少なくとも四つの手順を含めます。再生中にイヤホンを抜き、スピーカーが意図せず再生を引き継がないことを確認します。制御可能なシステム音声割り込みを一度発生させ、開始通知と終了通知を観察します。割り込み中に手動で一時停止し、終了後も一時停止したままであることを確認します。出力デバイスを再接続し、接続できたという理由だけでアプリが再生を始めないことを確認します。記録するのは受信したイベントの種類、原因、最終的な再生状態にとどめ、ユーザーのコンテンツやアクセス認証情報をログに書き込まないでください。
単体テストが通っても実機で問題が起きる場合は、まずセッションのライフサイクル中に通知の監視が有効かを確認します。次に、通知フィールドの解析、イベントの配送順序、プレーヤーの実際の状態が PlaybackState と同期しているかを調べます。特に、「状態機械が一時停止を要求した」ことと「プレーヤーが実際に一時停止を完了した」ことは区別が必要です。前者はテストで保証でき、後者にはアダプター層の確認と実機での検証が必要です。
確認項目をマージ手順に残す
マージ前に、状態機械のテストが待機中、割り込み、ユーザーによる一時停止、デバイス切断を網羅していること、通知アダプター層が未知のフィールドを保守的に扱うこと、すべてのイベントが同じ直列実行経路に入ること、対象デバイスでの検証結果をチームが少なくとも一度記録していることを確認します。クラウド上の Mac は決定的な状態テストの反復実行に適していますが、実際の音声出力経路は引き続きデバイスでの観察を基準にします。二層の検証を分けておけば、失敗時に調べるべき対象が製品ルールなのか、通知の変換なのか、音声出力そのものなのかを判断できます。
よくある質問
シミュレータの単体テストだけでイヤホン切断を検証できますか?
できません。入力したイベントへの判断は検証できますが、通知の到達と実際の出力経路は実機で確認する必要があります。
割り込みが終わったら常に再生を再開すべきですか?
いいえ。割り込み前の再生状態、再開許可、途中のユーザー操作を確認してから判断します。
次のXcodeビルドを専用物理マシンで実行
Apple Siliconのモデル、拠点、利用期間を選び、リモート接続して独自のビルド環境を実行できます。実際の提供条件は、ご注文時にコンソールでご確認ください。