Skip to content

Commit 53c9ce2

Browse files
authored
[Fix]Picture-in-Picture trigger and lifecycle (#796)
1 parent 92c7658 commit 53c9ce2

File tree

10 files changed

+61
-26
lines changed

10 files changed

+61
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99

1010
### 🐞 Fixed
1111
- Fix ringing flow issues. [#792](https://github.com/GetStream/stream-video-swift/pull/792)
12+
- Fix a few points that were negatively affecting Picture-in-Picture lifecycle. [#796](https://github.com/GetStream/stream-video-swift/pull/796)
1213

1314
# [1.21.1](https://github.com/GetStream/stream-video-swift/releases/tag/1.21.1)
1415
_April 25, 2025_

DemoApp/Sources/Views/CallView/DemoSnapshotViewModel.swift

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright © 2025 Stream.io Inc. All rights reserved.
33
//
44

5+
import Combine
56
import Foundation
67
import StreamVideo
78
import StreamVideoSwiftUI
@@ -11,7 +12,7 @@ import SwiftUI
1112
final class DemoSnapshotViewModel: ObservableObject {
1213

1314
private let viewModel: CallViewModel
14-
private var snapshotEventsTask: Task<Void, Never>?
15+
private var cancellable: AnyCancellable?
1516

1617
@Published var toast: Toast?
1718

@@ -21,27 +22,31 @@ final class DemoSnapshotViewModel: ObservableObject {
2122
}
2223

2324
private func subscribeForSnapshotEvents() {
25+
cancellable?.cancel()
26+
cancellable = nil
27+
2428
guard let call = viewModel.call else {
25-
snapshotEventsTask?.cancel()
26-
snapshotEventsTask = nil
2729
return
2830
}
2931

30-
snapshotEventsTask = Task {
31-
for await event in call.subscribe(for: CustomVideoEvent.self) {
32+
cancellable = call
33+
.eventPublisher(for: CustomVideoEvent.self)
34+
.compactMap {
3235
guard
33-
let imageBase64Data = event.custom["snapshot"]?.stringValue,
36+
let imageBase64Data = $0.custom["snapshot"]?.stringValue,
3437
let imageData = Data(base64Encoded: imageBase64Data),
3538
let image = UIImage(data: imageData)
3639
else {
37-
return
40+
return nil
3841
}
39-
40-
toast = .init(
42+
return image
43+
}
44+
.map {
45+
Toast(
4146
style: .custom(
4247
baseStyle: .success,
4348
icon: AnyView(
44-
Image(uiImage: image)
49+
Image(uiImage: $0)
4550
.resizable()
4651
.frame(maxWidth: 30, maxHeight: 30)
4752
.aspectRatio(contentMode: .fit)
@@ -51,6 +56,6 @@ final class DemoSnapshotViewModel: ObservableObject {
5156
message: "Snapshot captured!"
5257
)
5358
}
54-
}
59+
.assign(to: \.toast, on: self)
5560
}
5661
}

Sources/StreamVideoSwiftUI/CallView/CallControls/Stateless/StatelessAudioOutputIconView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public struct StatelessAudioOutputIconView: View {
1515
@Injected(\.images) private var images
1616

1717
/// The associated call for the audio output icon.
18-
public var call: Call?
18+
public weak var call: Call?
1919

2020
/// The size of the audio output icon.
2121
public var size: CGFloat

Sources/StreamVideoSwiftUI/CallView/CallControls/Stateless/StatelessMicrophoneIconView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public struct StatelessMicrophoneIconView: View {
1515
@Injected(\.images) private var images
1616

1717
/// The associated call for the microphone icon.
18-
public var call: Call?
18+
public weak var call: Call?
1919

2020
/// The size of the microphone icon.
2121
public var size: CGFloat

Sources/StreamVideoSwiftUI/CallView/CallControls/Stateless/StatelessSpeakerIconView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public struct StatelessSpeakerIconView: View {
1515
@Injected(\.images) private var images
1616

1717
/// The associated call for the speaker icon.
18-
public var call: Call?
18+
public weak var call: Call?
1919

2020
/// The size of the speaker icon.
2121
public var size: CGFloat

Sources/StreamVideoSwiftUI/CallView/CallControls/Stateless/StatelessToggleCameraIconView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public struct StatelessToggleCameraIconView: View {
1515
@Injected(\.images) private var images
1616

1717
/// The associated call for the toggle camera icon.
18-
public var call: Call?
18+
public weak var call: Call?
1919

2020
/// The size of the toggle camera icon.
2121
public var size: CGFloat

Sources/StreamVideoSwiftUI/CallView/CallControls/Stateless/StatelessVideoIconView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public struct StatelessVideoIconView: View {
1515
@Injected(\.images) private var images
1616

1717
/// The associated call for the video icon.
18-
public var call: Call?
18+
public weak var call: Call?
1919

2020
/// The size of the video icon.
2121
public var size: CGFloat

Sources/StreamVideoSwiftUI/CallViewModel.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,6 @@ open class CallViewModel: ObservableObject {
616616

617617
pictureInPictureAdapter.call = nil
618618
pictureInPictureAdapter.sourceView = nil
619-
isPictureInPictureEnabled = false
620619

621620
call = nil
622621
callParticipants = [:]

Sources/StreamVideoSwiftUI/Utils/PictureInPicture/PictureInPictureController.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ final class PictureInPictureController: @unchecked Sendable {
6565
.sinkTask { @MainActor [weak self] in self?.didUpdate($0) }
6666
.store(in: disposableBag)
6767

68+
proxyDelegate
69+
.publisher
70+
.compactMap {
71+
switch $0 {
72+
case let .failedToStart(_, error):
73+
return error
74+
default:
75+
return nil
76+
}
77+
}
78+
.log(.error, subsystems: .pictureInPicture) { "Picture-in-Picture failed to start: \($0)." }
79+
.sink { _ in }
80+
.store(in: disposableBag)
81+
6882
// Add delay to prevent premature cancellation
6983
applicationStateAdapter
7084
.statePublisher
@@ -78,8 +92,11 @@ final class PictureInPictureController: @unchecked Sendable {
7892
/// Updates the Picture-in-Picture controller when the source view changes.
7993
@MainActor
8094
private func didUpdate(_ sourceView: UIView?) {
81-
guard let sourceView else {
95+
guard let sourceView, store.state.call != nil else {
96+
/// We ensure to cleanUp every Picture-in-Picture interacting component so that the next
97+
/// Call will start with clean state.
8298
pictureInPictureController?.contentSource = nil
99+
contentViewController = nil
83100
pictureInPictureController = nil
84101
disposableBag.remove(DisposableKey.isPossible.rawValue)
85102
disposableBag.remove(DisposableKey.isActive.rawValue)
@@ -96,16 +113,27 @@ final class PictureInPictureController: @unchecked Sendable {
96113
guard let contentViewController else {
97114
return
98115
}
116+
let contentSource = AVPictureInPictureController.ContentSource(
117+
activeVideoCallSourceView: sourceView,
118+
contentViewController: contentViewController
119+
)
99120

100-
pictureInPictureController = .init(
101-
contentSource: .init(
102-
activeVideoCallSourceView: sourceView,
103-
contentViewController: contentViewController
121+
if pictureInPictureController == nil {
122+
pictureInPictureController = .init(
123+
contentSource: contentSource
104124
)
125+
pictureInPictureController?.canStartPictureInPictureAutomaticallyFromInline = store
126+
.state
127+
.canStartPictureInPictureAutomaticallyFromInline
128+
pictureInPictureController?.delegate = proxyDelegate
129+
} else {
130+
pictureInPictureController?.contentSource = contentSource
131+
}
132+
133+
log.debug(
134+
"SourceView updated and contentViewController preferredContentSize:\(contentViewController.preferredContentSize)",
135+
subsystems: .pictureInPicture
105136
)
106-
pictureInPictureController?.canStartPictureInPictureAutomaticallyFromInline = store.state
107-
.canStartPictureInPictureAutomaticallyFromInline
108-
pictureInPictureController?.delegate = proxyDelegate
109137
}
110138

111139
/// Updates the content view controller when the view factory changes.

Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ public final class StreamPictureInPictureAdapter: @unchecked Sendable {
7474
store
7575
.publisher(for: \.sourceView)
7676
.removeDuplicates()
77-
.log(.debug, subsystems: .pictureInPicture) { "SourceView updated: \($0?.description ?? "-")." }
77+
.log(.debug, subsystems: .pictureInPicture) {
78+
"SourceView updated frame:\($0?.frame ?? .zero) hasWindow:\($0?.window != nil)."
79+
}
7880
.sink { _ in }
7981
.store(in: disposableBag)
8082
}

0 commit comments

Comments
 (0)