fix: suspend OTG live transfer on exit
This commit is contained in:
@ -36,6 +36,7 @@ protocol WiredCameraConnectionManaging: AnyObject {
|
|||||||
func configureLiveTransfer(albumID: Int?)
|
func configureLiveTransfer(albumID: Int?)
|
||||||
func start()
|
func start()
|
||||||
func unbindDelegate()
|
func unbindDelegate()
|
||||||
|
func suspendLiveTransfer()
|
||||||
func disconnect()
|
func disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +87,11 @@ final class ConnectionManager: NSObject, WiredCameraConnectionManaging {
|
|||||||
func configureLiveTransfer(albumID: Int?) {
|
func configureLiveTransfer(albumID: Int?) {
|
||||||
liveTransferAlbumID = albumID
|
liveTransferAlbumID = albumID
|
||||||
OTGLog.debug(.connection, "live transfer album=\(albumID.map(String.init) ?? "nil")")
|
OTGLog.debug(.connection, "live transfer album=\(albumID.map(String.init) ?? "nil")")
|
||||||
|
if albumID == nil {
|
||||||
|
stopLiveTransferServices()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resumeLiveTransferIfPossible()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Entry point when OTG UI appears: reconnect cached device or start discovery.
|
/// Entry point when OTG UI appears: reconnect cached device or start discovery.
|
||||||
@ -115,6 +121,13 @@ final class ConnectionManager: NSObject, WiredCameraConnectionManaging {
|
|||||||
cancelSearchTimers()
|
cancelSearchTimers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 页面离开时暂停边拍边传轮询,保留 USB Session 与设备缓存。
|
||||||
|
func suspendLiveTransfer() {
|
||||||
|
OTGLog.info(.connection, "suspend live transfer")
|
||||||
|
liveTransferAlbumID = nil
|
||||||
|
stopLiveTransferServices()
|
||||||
|
}
|
||||||
|
|
||||||
/// 断开 Session 并释放 Driver;缓存设备保留至 USB 拔出。
|
/// 断开 Session 并释放 Driver;缓存设备保留至 USB 拔出。
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
guard let device = activeDevice else {
|
guard let device = activeDevice else {
|
||||||
@ -131,12 +144,7 @@ final class ConnectionManager: NSObject, WiredCameraConnectionManaging {
|
|||||||
isClosingSession = true
|
isClosingSession = true
|
||||||
isContentCatalogReady = false
|
isContentCatalogReady = false
|
||||||
cancelSearchTimers()
|
cancelSearchTimers()
|
||||||
sonyRemoteCapture?.stop()
|
stopLiveTransferServices()
|
||||||
sonyRemoteCapture = nil
|
|
||||||
canonRemoteCapture?.stop()
|
|
||||||
canonRemoteCapture = nil
|
|
||||||
nikonLiveCapture?.stop()
|
|
||||||
nikonLiveCapture = nil
|
|
||||||
liveTransferAlbumID = nil
|
liveTransferAlbumID = nil
|
||||||
currentDriver?.disconnect()
|
currentDriver?.disconnect()
|
||||||
currentDriver = nil
|
currentDriver = nil
|
||||||
@ -305,12 +313,7 @@ final class ConnectionManager: NSObject, WiredCameraConnectionManaging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func clearDeviceCache() {
|
private func clearDeviceCache() {
|
||||||
sonyRemoteCapture?.stop()
|
stopLiveTransferServices()
|
||||||
sonyRemoteCapture = nil
|
|
||||||
canonRemoteCapture?.stop()
|
|
||||||
canonRemoteCapture = nil
|
|
||||||
nikonLiveCapture?.stop()
|
|
||||||
nikonLiveCapture = nil
|
|
||||||
cachedDevice = nil
|
cachedDevice = nil
|
||||||
currentDriver?.disconnect()
|
currentDriver?.disconnect()
|
||||||
currentDriver = nil
|
currentDriver = nil
|
||||||
@ -428,24 +431,7 @@ private extension ConnectionManager {
|
|||||||
await runGetDeviceInfoProbe(on: camera)
|
await runGetDeviceInfoProbe(on: camera)
|
||||||
|
|
||||||
case .sony:
|
case .sony:
|
||||||
let transactionID: UInt32 = 1
|
await startSonyLiveTransferIfNeeded(for: camera)
|
||||||
let result = await SonyPTPHelper.initializeRemoteSession(
|
|
||||||
on: camera,
|
|
||||||
startingTransactionID: transactionID
|
|
||||||
)
|
|
||||||
if result.success {
|
|
||||||
let service = SonyRemoteCaptureService()
|
|
||||||
service.onShotSaved = makeLiveShotSavedHandler()
|
|
||||||
sonyRemoteCapture = service
|
|
||||||
service.start(
|
|
||||||
camera: camera,
|
|
||||||
startingTransactionID: result.nextTransactionID,
|
|
||||||
albumID: liveTransferAlbumID
|
|
||||||
)
|
|
||||||
OTGLog.info(.sony, "Sony remote session ready")
|
|
||||||
} else {
|
|
||||||
OTGLog.error(.sony, "Sony remote session setup failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
await runGetDeviceInfoProbe(on: camera)
|
await runGetDeviceInfoProbe(on: camera)
|
||||||
@ -474,10 +460,12 @@ private extension ConnectionManager {
|
|||||||
/// catalog 就绪后再初始化 Canon 远程模式,避免 SetRemoteMode 打断 SD 卡枚举。
|
/// catalog 就绪后再初始化 Canon 远程模式,避免 SetRemoteMode 打断 SD 卡枚举。
|
||||||
private func startCanonLiveTransferIfNeeded(for camera: ICCameraDevice) {
|
private func startCanonLiveTransferIfNeeded(for camera: ICCameraDevice) {
|
||||||
guard canonRemoteCapture == nil else { return }
|
guard canonRemoteCapture == nil else { return }
|
||||||
|
guard liveTransferAlbumID != nil else { return }
|
||||||
guard PlatformDetector.detect(from: buildDeviceInfo(from: camera)) == .canon else { return }
|
guard PlatformDetector.detect(from: buildDeviceInfo(from: camera)) == .canon else { return }
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
guard self.canonRemoteCapture == nil, self.liveTransferAlbumID != nil else { return }
|
||||||
|
|
||||||
let transactionID: UInt32 = 1
|
let transactionID: UInt32 = 1
|
||||||
let result = await CanonPTPHelper.initializeRemoteSession(
|
let result = await CanonPTPHelper.initializeRemoteSession(
|
||||||
@ -504,6 +492,7 @@ private extension ConnectionManager {
|
|||||||
/// catalog 就绪后启动尼康边拍边传(didAdd + catalog 轮询,无需 PTP 远程会话)。
|
/// catalog 就绪后启动尼康边拍边传(didAdd + catalog 轮询,无需 PTP 远程会话)。
|
||||||
private func startNikonLiveTransferIfNeeded(for camera: ICCameraDevice) {
|
private func startNikonLiveTransferIfNeeded(for camera: ICCameraDevice) {
|
||||||
guard nikonLiveCapture == nil else { return }
|
guard nikonLiveCapture == nil else { return }
|
||||||
|
guard liveTransferAlbumID != nil else { return }
|
||||||
guard PlatformDetector.detect(from: buildDeviceInfo(from: camera)) == .nikon else { return }
|
guard PlatformDetector.detect(from: buildDeviceInfo(from: camera)) == .nikon else { return }
|
||||||
|
|
||||||
let service = NikonCatalogLiveCaptureService()
|
let service = NikonCatalogLiveCaptureService()
|
||||||
@ -513,6 +502,57 @@ private extension ConnectionManager {
|
|||||||
OTGLog.info(.nikon, "Nikon catalog live capture ready")
|
OTGLog.info(.nikon, "Nikon catalog live capture ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startSonyLiveTransferIfNeeded(for camera: ICCameraDevice) async {
|
||||||
|
guard sonyRemoteCapture == nil else { return }
|
||||||
|
guard liveTransferAlbumID != nil else { return }
|
||||||
|
guard PlatformDetector.detect(from: buildDeviceInfo(from: camera)) == .sony else { return }
|
||||||
|
|
||||||
|
let transactionID: UInt32 = 1
|
||||||
|
let result = await SonyPTPHelper.initializeRemoteSession(
|
||||||
|
on: camera,
|
||||||
|
startingTransactionID: transactionID
|
||||||
|
)
|
||||||
|
guard liveTransferAlbumID != nil else { return }
|
||||||
|
if result.success {
|
||||||
|
let service = SonyRemoteCaptureService()
|
||||||
|
service.onShotSaved = makeLiveShotSavedHandler()
|
||||||
|
sonyRemoteCapture = service
|
||||||
|
service.start(
|
||||||
|
camera: camera,
|
||||||
|
startingTransactionID: result.nextTransactionID,
|
||||||
|
albumID: liveTransferAlbumID
|
||||||
|
)
|
||||||
|
OTGLog.info(.sony, "Sony remote session ready")
|
||||||
|
} else {
|
||||||
|
OTGLog.error(.sony, "Sony remote session setup failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resumeLiveTransferIfPossible() {
|
||||||
|
guard let camera = cachedDevice, case .connected = state else { return }
|
||||||
|
switch PlatformDetector.detect(from: buildDeviceInfo(from: camera)) {
|
||||||
|
case .sony:
|
||||||
|
Task { await startSonyLiveTransferIfNeeded(for: camera) }
|
||||||
|
case .canon:
|
||||||
|
guard isContentCatalogReady else { return }
|
||||||
|
startCanonLiveTransferIfNeeded(for: camera)
|
||||||
|
case .nikon:
|
||||||
|
guard isContentCatalogReady else { return }
|
||||||
|
startNikonLiveTransferIfNeeded(for: camera)
|
||||||
|
case .unknown:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLiveTransferServices() {
|
||||||
|
sonyRemoteCapture?.stop()
|
||||||
|
sonyRemoteCapture = nil
|
||||||
|
canonRemoteCapture?.stop()
|
||||||
|
canonRemoteCapture = nil
|
||||||
|
nikonLiveCapture?.stop()
|
||||||
|
nikonLiveCapture = nil
|
||||||
|
}
|
||||||
|
|
||||||
private func makeLiveShotSavedHandler() -> (String) -> Void {
|
private func makeLiveShotSavedHandler() -> (String) -> Void {
|
||||||
{ [weak self] filename in
|
{ [weak self] filename in
|
||||||
guard let self, let albumID = self.liveTransferAlbumID else { return }
|
guard let self, let albumID = self.liveTransferAlbumID else { return }
|
||||||
@ -557,12 +597,7 @@ extension ConnectionManager: ICCameraDeviceDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isClosingSession = false
|
isClosingSession = false
|
||||||
sonyRemoteCapture?.stop()
|
stopLiveTransferServices()
|
||||||
sonyRemoteCapture = nil
|
|
||||||
canonRemoteCapture?.stop()
|
|
||||||
canonRemoteCapture = nil
|
|
||||||
nikonLiveCapture?.stop()
|
|
||||||
nikonLiveCapture = nil
|
|
||||||
currentDriver = nil
|
currentDriver = nil
|
||||||
isContentCatalogReady = false
|
isContentCatalogReady = false
|
||||||
if let camera = device as? ICCameraDevice {
|
if let camera = device as? ICCameraDevice {
|
||||||
|
|||||||
@ -145,9 +145,10 @@ final class WiredCameraTransferViewModel {
|
|||||||
connectionManager.start()
|
connectionManager.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页面消失时解绑 UI delegate,保留底层设备缓存。
|
/// 页面消失时解绑 UI delegate,并暂停边拍边传轮询,保留底层设备缓存。
|
||||||
func stop() {
|
func stop() {
|
||||||
connectionManager.unbindDelegate()
|
connectionManager.unbindDelegate()
|
||||||
|
connectionManager.suspendLiveTransfer()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 主动断开相机连接。
|
/// 主动断开相机连接。
|
||||||
|
|||||||
@ -51,10 +51,15 @@ final class WiredCameraTransferViewController: BaseViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
viewModel.start()
|
|
||||||
applyViewModel()
|
applyViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
viewModel.start()
|
||||||
|
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
viewModel.stop()
|
viewModel.stop()
|
||||||
|
|||||||
@ -197,6 +197,17 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(driver.downloadedObjectIds, [])
|
XCTAssertEqual(driver.downloadedObjectIds, [])
|
||||||
XCTAssertTrue(viewModel.filteredPhotos().isEmpty)
|
XCTAssertTrue(viewModel.filteredPhotos().isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testStopSuspendsLiveTransferPolling() {
|
||||||
|
let context = makeOTGTestContext()
|
||||||
|
let manager = MockWiredCameraConnectionManager(driver: MockCameraDriver(objects: []))
|
||||||
|
let viewModel = makeWiredViewModel(context: context, manager: manager)
|
||||||
|
|
||||||
|
viewModel.start()
|
||||||
|
viewModel.stop()
|
||||||
|
|
||||||
|
XCTAssertEqual(manager.suspendLiveTransferCallCount, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 旅拍相册相机历史导入 ViewModel 测试。
|
/// 旅拍相册相机历史导入 ViewModel 测试。
|
||||||
@ -437,6 +448,7 @@ private final class MockWiredCameraConnectionManager: WiredCameraConnectionManag
|
|||||||
var state: ConnectionState
|
var state: ConnectionState
|
||||||
var currentDriver: CameraDriver?
|
var currentDriver: CameraDriver?
|
||||||
var isContentCatalogReady: Bool
|
var isContentCatalogReady: Bool
|
||||||
|
private(set) var suspendLiveTransferCallCount = 0
|
||||||
|
|
||||||
init(
|
init(
|
||||||
state: ConnectionState = .connected(platform: .canon, deviceName: "Canon"),
|
state: ConnectionState = .connected(platform: .canon, deviceName: "Canon"),
|
||||||
@ -451,6 +463,9 @@ private final class MockWiredCameraConnectionManager: WiredCameraConnectionManag
|
|||||||
func configureLiveTransfer(albumID: Int?) {}
|
func configureLiveTransfer(albumID: Int?) {}
|
||||||
func start() {}
|
func start() {}
|
||||||
func unbindDelegate() {}
|
func unbindDelegate() {}
|
||||||
|
func suspendLiveTransfer() {
|
||||||
|
suspendLiveTransferCallCount += 1
|
||||||
|
}
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
state = .disconnected
|
state = .disconnected
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user