fix(tvos): answer the Atmos probe start call exactly once

Hold the pending result in a one-shot latch fired by every terminal path,
including cancellation and deallocation. Stopping a probe mid-download
released the only strong reference and left the method call unanswered.
This commit is contained in:
edde746
2026-07-27 17:44:52 +02:00
parent ad3af474e9
commit 7e00de3ae9
3 changed files with 104 additions and 7 deletions
+4
View File
@@ -11,6 +11,7 @@
1C6B234E223CDFFE5BF3FC0B /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25B7925EFD8A2C1C7EB667D5 /* TVServices.framework */; };
28DB4404B17342F46BC2B0A1 /* TopShelfProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F829B3F190657106F66379 /* TopShelfProvider.swift */; };
2A7C0B1D9E5F4A6381027C12 /* MpvPlayerContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7C0B1D9E5F4A6381027C11 /* MpvPlayerContractTests.swift */; };
A7A0C001A7A0C001A7A0C002 /* AtmosProbeContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7A0C001A7A0C001A7A0C001 /* AtmosProbeContractTests.swift */; };
35DB0C8FEF635A3BCA0B722A /* PackageInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9426EFA282CDA8E0E98EEE9 /* PackageInfoPlusPlugin.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */; };
@@ -121,6 +122,7 @@
9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPlugin.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerPlugin.swift; sourceTree = "<source_root>"; };
A12B8610AE5D580077264851 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCore.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerCore.swift; sourceTree = "<source_root>"; };
2A7C0B1D9E5F4A6381027C11 /* MpvPlayerContractTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MpvPlayerContractTests.swift; sourceTree = "<group>"; };
A7A0C001A7A0C001A7A0C001 /* AtmosProbeContractTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AtmosProbeContractTests.swift; sourceTree = "<group>"; };
A2484B9C94406BF0A99EB64A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
A2635E12EB9322B151EE5127 /* MpvPipController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPipController.swift; path = ../ios/Runner/MpvPlayer/MpvPipController.swift; sourceTree = "<source_root>"; };
A5E1F001234567890ABCDE01 /* SystemShelfPluginTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SystemShelfPluginTests.swift; sourceTree = "<group>"; };
@@ -341,6 +343,7 @@
children = (
4F1000014F1000014F100001 /* FlutterNativeTextInputTests.mm */,
2A7C0B1D9E5F4A6381027C11 /* MpvPlayerContractTests.swift */,
A7A0C001A7A0C001A7A0C001 /* AtmosProbeContractTests.swift */,
FE6C8125B201A8BC3261AE2B /* TvosEventDeliveryCoordinatorTests.swift */,
934BC4E316D2AC788C954766 /* ConnectivityPlusPluginTests.swift */,
A5E1F001234567890ABCDE01 /* SystemShelfPluginTests.swift */,
@@ -595,6 +598,7 @@
files = (
4F1000024F1000024F100002 /* FlutterNativeTextInputTests.mm in Sources */,
2A7C0B1D9E5F4A6381027C12 /* MpvPlayerContractTests.swift in Sources */,
A7A0C001A7A0C001A7A0C002 /* AtmosProbeContractTests.swift in Sources */,
D2004D7BB4A40340AB7A01E0 /* TvosEventDeliveryCoordinatorTests.swift in Sources */,
65AC2C222043B3E6723E2076 /* ConnectivityPlusPluginTests.swift in Sources */,
A5E1F001234567890ABCDE02 /* SystemShelfPluginTests.swift in Sources */,
@@ -0,0 +1,61 @@
import Foundation
import XCTest
@testable import Runner
final class AtmosProbeContractTests: XCTestCase {
func testCancelCompletesStartExactlyOnce() {
let completionDelivered = expectation(description: "start completion delivered")
let duplicateCompletion = expectation(description: "start completion delivered twice")
duplicateCompletion.isInverted = true
var invocationCount = 0
var probe: AsbarProbe? = makeProbe()
probe?.start { message in
invocationCount += 1
if invocationCount == 1 {
XCTAssertEqual(message, "cancelled")
completionDelivered.fulfill()
} else {
duplicateCompletion.fulfill()
}
}
probe?.cancel()
probe = nil
wait(for: [completionDelivered], timeout: 1)
wait(for: [duplicateCompletion], timeout: 1)
XCTAssertEqual(invocationCount, 1)
}
func testDeinitCompletesStartExactlyOnce() {
let completionDelivered = expectation(description: "start completion delivered")
let duplicateCompletion = expectation(description: "start completion delivered twice")
duplicateCompletion.isInverted = true
var invocationCount = 0
var probe: AsbarProbe? = makeProbe()
probe?.start { message in
invocationCount += 1
if invocationCount == 1 {
XCTAssertEqual(message, "cancelled")
completionDelivered.fulfill()
} else {
duplicateCompletion.fulfill()
}
}
probe = nil
wait(for: [completionDelivered], timeout: 1)
wait(for: [duplicateCompletion], timeout: 1)
XCTAssertEqual(invocationCount, 1)
}
private func makeProbe() -> AsbarProbe {
AsbarProbe(
source: URL(string: "http://127.0.0.1:1/probe.eac3")!,
regenerateFormatDescription: false,
sessionMode: .moviePlayback
)
}
}