import Testing import Foundation import AVFoundation @testable import Music // Reproduces the "CoreMediaErrorDomain error -16044 after streaming a few tracks" // bug (plus the accompanying HALC_ProxyIOContext overload / out-of-order log spew). // // Root cause: setting `player = nil` does NOT release an AVPlayer's decode/render // pipeline — it is the *association* of an AVPlayerItem with an AVPlayer that owns // the pipeline, and ARC tears it down asynchronously. The teardown path paused and // nilled the player but never called `replaceCurrentItem(with: nil)`, so each track // switch leaked a still-associated pipeline. After a handful of tracks they exceed // CoreMedia's small concurrent-pipeline limit and a new player can't acquire a // decode session, failing with -16044. // // The invariant proven here: tearing down the player must dissociate its item so // the pipeline is released immediately. @MainActor struct PlaybackPipelineTeardownTests { // Verifies the streaming provider releases the previous player's pipeline on teardown. // Steps: // 1. Create a StreamingPlaybackProvider (host/key unused — we drive AVPlayer // directly with a local file URL to bypass the network pre-flight). // 2. Start an AVPlayer on a real local audio fixture and capture a strong // reference to it, then confirm the pipeline is established (currentItem set). // 3. Tear down via stop() — the same cleanup path a queue advance runs. // 4. The captured player must have been dissociated from its item (currentItem // == nil). Before the fix it is still set, so pipelines accumulate. @Test func streamingProviderReleasesPipelineOnTeardown() throws { // 1. Provider with throwaway connection details. let provider = StreamingPlaybackProvider(hostURL: "http://unused.invalid", apiKey: "unused") // 2. Start playback on a real local file and grab the AVPlayer it created. let fixture = try TestFixtures.shortMP3URL() provider.startAVPlayer(url: fixture) let firstPlayer = try #require(provider.player) #expect(firstPlayer.currentItem != nil) // 3. Tear down (queue advance / stop runs this path). provider.stop() // 4. The pipeline must be released: the item is dissociated from the player. #expect(firstPlayer.currentItem == nil) } // Verifies the local-playback provider (AudioService) has the same fix. // Steps: // 1. Create an AudioService. // 2. Play a real local audio fixture and capture the AVPlayer; confirm the // pipeline is established (currentItem set). // 3. Tear down via stop(). // 4. The captured player must have been dissociated from its item. @Test func audioServiceReleasesPipelineOnTeardown() throws { // 1. Local playback provider. let provider = AudioService() // 2. Play a real local file and grab the AVPlayer it created. let fixture = try TestFixtures.shortMP3URL() provider.play(url: fixture) let firstPlayer = try #require(provider.player) #expect(firstPlayer.currentItem != nil) // 3. Tear down. provider.stop() // 4. The pipeline must be released: the item is dissociated from the player. #expect(firstPlayer.currentItem == nil) } }