You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
71 lines
3.3 KiB
71 lines
3.3 KiB
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)
|
|
}
|
|
}
|
|
|