Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature]Reconnection #503

Merged
merged 33 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
32a1722
Feature/reconnection v2 attempt 1 (#480)
ipavlidakis Sep 3, 2024
1de8ad2
[Fix]Failing tests (#509)
ipavlidakis Sep 4, 2024
6c1eb03
[Fix]Ringing call screen closing immediately (#511)
ipavlidakis Sep 6, 2024
8f28dfb
[Fix]Restore active VideoFilter after reconnection (#512)
ipavlidakis Sep 6, 2024
3673cfc
Add comments and test for SFU components (#510)
ipavlidakis Sep 6, 2024
6f6fd73
Fix peerConnection having the same type
ipavlidakis Sep 6, 2024
ef0c6be
[Fix]Missing tracks (#507)
ipavlidakis Sep 9, 2024
0684b32
Peerconnection tests and docs (#516)
ipavlidakis Sep 11, 2024
d7ac012
[Fix]Disconnect rather than leaving call on connection lost (#517)
ipavlidakis Sep 11, 2024
7796f6e
[Fix]Retain camera position after reconnection (#520)
ipavlidakis Sep 11, 2024
5c1604e
[Fix]Reconnection when socket disconnects before connection is lost (…
ipavlidakis Sep 11, 2024
5de8964
Add tests for WebRTCStateMachine stages (#522)
ipavlidakis Sep 13, 2024
6678c08
Add docs and tests for the WebRTC components (#523)
ipavlidakis Sep 18, 2024
0be2618
Update tests for RTCPeerConnectionCoordinator on handling negotiation…
ipavlidakis Sep 18, 2024
6a52f95
[Fix]Pass all available parameters to connect (#528)
ipavlidakis Sep 18, 2024
014647c
[Fix]Migration failing due to migrationCompleted event not received (…
ipavlidakis Sep 18, 2024
2cd87fe
Point app to Pronto
ipavlidakis Sep 18, 2024
df11faa
Reenable Callcontroller tests (#529)
ipavlidakis Sep 19, 2024
3c85174
Fix memory leaks
ipavlidakis Sep 19, 2024
e216e0a
Reenable testReconnectingMessage (#533)
ipavlidakis Sep 19, 2024
cbdbbfa
Add more tests for ICEAdapter (#531)
ipavlidakis Sep 19, 2024
7c6a2b3
Reenable CRDU Test (#532)
ipavlidakis Sep 19, 2024
881b101
[Fix]Track orientation issue (#534)
ipavlidakis Sep 19, 2024
b4bc676
[Fix]Orientation issue on older devices
ipavlidakis Sep 20, 2024
999e11b
[Fix]Close transceiver before closing the PeerConnection (#538)
ipavlidakis Sep 23, 2024
6247428
[Fix]Cancel running tasks on stages when transitioning away (#536)
ipavlidakis Sep 23, 2024
75f6773
[Fix]Stop SFU event observation during reconnection (#537)
ipavlidakis Sep 23, 2024
f636094
[Fix]UI freezing
ipavlidakis Sep 24, 2024
846b4f8
Streamline tracks updates on a userInteractive queue
ipavlidakis Sep 24, 2024
7a27a2e
Remove record: true
ipavlidakis Sep 26, 2024
f5bc6f4
Fix snapshot tests
ipavlidakis Sep 26, 2024
be42073
Update participants state as possible
ipavlidakis Sep 26, 2024
27b921a
[Fix]UI Freeze (#546)
ipavlidakis Sep 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### 🔄 Changed
- Updated the default sorting for Participants during a call to minimize the movement of already visible tiles [#515](https://github.com/GetStream/stream-video-swift/pull/515)
- **Breaking** The `StreamDeviceOrientation` values now are `.portrait(isUpsideDown: Bool)` & `.landscape(isLeft: Bool)`. [#534](https://github.com/GetStream/stream-video-swift/pull/534)

### 🐞 Fixed
- An `MissingPermissions` error was thrown when creating a `StreamVideo` with anonymous user type. [#525](https://github.com/GetStream/stream-video-swift/pull/525)
Expand All @@ -20,6 +21,15 @@ _August 29, 2024_
- Participants (regular and anonymous) count, can be accessed - before or after joining a call - from the `Call.state.participantCount` & `Call.state.anonymousParticipantCount` respectively. [#496](https://github.com/GetStream/stream-video-swift/pull/496)
- You can now provide the `CallSettings` when you start a ringing call [#497](https://github.com/GetStream/stream-video-swift/pull/497)

### 🔄 Changed
- The following `Call` APIs have been now marked as async to provide better observability.
- `func focus(at point: CGPoint)`
- `func addCapturePhotoOutput(_ capturePhotoOutput: AVCapturePhotoOutput)`
- `func removeCapturePhotoOutput(_ capturePhotoOutput: AVCapturePhotoOutput)`
- `func addVideoOutput(_ videoOutput: AVCaptureVideoDataOutput)`
- `func removeVideoOutput(_ videoOutput: AVCaptureVideoDataOutput)`
- `func zoom(by factor: CGFloat)`

# [1.0.9](https://github.com/GetStream/stream-video-swift/releases/tag/1.0.9)
_July 19, 2024_

Expand Down
2 changes: 1 addition & 1 deletion DemoApp/Sources/Components/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ extension AppEnvironment {
static var tokenExpiration: TokenExpiration = {
switch configuration {
case .debug:
return .oneMinute
return .never
case .test:
return .oneMinute
case .release:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,51 @@ import UIKit

final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDelegate,
AVCaptureVideoDataOutputSampleBufferDelegate {

private actor State {
private(set) var isCapturingVideoFrame = false
private(set) var zoomFactor: Float = 1

func setIsCapturingVideoFrame(_ value: Bool) {
isCapturingVideoFrame = value
}

func setZoomFactor(_ value: Float) {
zoomFactor = value
}
}

private lazy var photoOutput: AVCapturePhotoOutput = .init()
private lazy var videoOutput: AVCaptureVideoDataOutput = .init()
private var state = State()

weak var call: Call? {
didSet {
guard call?.cId != oldValue?.cId else { return }
do {
#if !targetEnvironment(simulator)
if #available(iOS 16.0, *) {
try call?.addVideoOutput(videoOutput)
/// Following Apple guidelines for videoOutputs from here:
/// https://developer.apple.com/library/archive/technotes/tn2445/_index.html
videoOutput.alwaysDiscardsLateVideoFrames = true
} else {
try call?.addCapturePhotoOutput(photoOutput)
Task {
do {
#if !targetEnvironment(simulator)
if #available(iOS 16.0, *) {
try await call?.addVideoOutput(videoOutput)
/// Following Apple guidelines for videoOutputs from here:
/// https://developer.apple.com/library/archive/technotes/tn2445/_index.html
videoOutput.alwaysDiscardsLateVideoFrames = true
} else {
try await call?.addCapturePhotoOutput(photoOutput)
}
#endif
} catch {
log.error("Failed to setup for localParticipant snapshot", error: error)
}
#endif
} catch {
log.error("Failed to setup for localParticipant snapshot", error: error)
}
}
}

func capturePhoto() {
guard !photoOutput.connections.isEmpty else { return }
photoOutput.capturePhoto(with: .init(), delegate: self)
}

func captureVideoFrame() {
guard !videoOutput.connections.isEmpty else { return }
videoOutput.setSampleBufferDelegate(
Expand All @@ -61,25 +63,25 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe
)
Task { await state.setIsCapturingVideoFrame(true) }
}

func zoom() {
Task {
do {
if await state.zoomFactor > 1 {
await state.setZoomFactor(1)
try call?.zoom(by: 1)
try await call?.zoom(by: 1)
} else {
await state.setZoomFactor(1.5)
try call?.zoom(by: 1.5)
try await call?.zoom(by: 1.5)
}
} catch {
log.error(error)
}
}
}

// MARK: - Private Helpers

private func sendImageData(_ data: Data) async {
defer { videoOutput.setSampleBufferDelegate(nil, queue: nil) }
guard
Expand All @@ -89,7 +91,7 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe
else {
return
}

do {
try await call?.sendCustomEvent([
"snapshot": .string(snapshotData.base64EncodedString())
Expand All @@ -98,7 +100,7 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe
log.error("Failed to send image.", error: error)
}
}

private func resize(
image: UIImage,
to targetSize: CGSize
Expand All @@ -108,13 +110,13 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe
else {
return image
}

let widthRatio = targetSize.width / image.size.width
let heightRatio = targetSize.height / image.size.height

// Determine the scale factor that preserves aspect ratio
let scaleFactor = min(widthRatio, heightRatio)

let scaledWidth = image.size.width * scaleFactor
let scaledHeight = image.size.height * scaleFactor
let targetRect = CGRect(
Expand All @@ -123,19 +125,19 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe
width: scaledWidth,
height: scaledHeight
)

// Create a new image context
UIGraphicsBeginImageContextWithOptions(targetSize, false, 0)
image.draw(in: targetRect)

let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

return newImage
}

// MARK: - AVCapturePhotoCaptureDelegate

func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
Expand All @@ -149,24 +151,24 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe
}
}
}

// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate

func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
Task {
guard await state.isCapturingVideoFrame else { return }

if let imageBuffer = sampleBuffer.imageBuffer {
let ciImage = CIImage(cvPixelBuffer: imageBuffer)
if let data = UIImage(ciImage: ciImage).jpegData(compressionQuality: 1) {
await sendImageData(data)
}
}

await state.setIsCapturingVideoFrame(false)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import StreamVideo
import StreamVideoSwiftUI
import SwiftUI

private struct DemoMoreControlsViewModifier: ViewModifier {
struct DemoMoreControlsViewModifier: ViewModifier {

@ObservedObject var appState: AppState = .shared
@ObservedObject var viewModel: CallViewModel
Expand All @@ -21,7 +21,7 @@ private struct DemoMoreControlsViewModifier: ViewModifier {
localParticipantSnapshotViewModel.call = viewModel.call
}

fileprivate func body(content: Content) -> some View {
func body(content: Content) -> some View {
content
.halfSheet(isPresented: $viewModel.moreControlsShown) {
ScrollView(showsIndicators: false) {
Expand Down Expand Up @@ -97,6 +97,12 @@ private struct DemoMoreControlsViewModifier: ViewModifier {
action: { isStatsPresented = true },
label: "Stats"
) { Image(systemName: "chart.xyaxis.line") }

#if OBSERVE_RECONNECTION_NOTIFICATIONS
if AppEnvironment.configuration != .release {
DemoReconnectionButtonView { viewModel.moreControlsShown = false }
}
#endif
}
}
}
Expand All @@ -110,115 +116,3 @@ private struct DemoMoreControlsViewModifier: ViewModifier {
}
}
}

struct DemoTranscriptionButtonView: View {

@ObservedObject var viewModel: CallViewModel
@State private var isTranscriptionAvailable = false
@State private var isTranscribing = false

init(viewModel: CallViewModel) {
self.viewModel = viewModel
isTranscriptionAvailable = (viewModel.call?.state.settings?.transcription.mode ?? .disabled) != .disabled
isTranscribing = viewModel.call?.state.transcribing == true
}

var body: some View {
Group {
if isTranscriptionAvailable {
DemoMoreControlListButtonView(
action: {
Task {
do {
if isTranscribing {
try await viewModel.call?.stopTranscription()
} else {
try await viewModel.call?.startTranscription()
}
} catch {
log.error(error)
}
}
},
label: isTranscribing ? "Disable Transcription" : "Transcription"
) {
Image(
systemName: isTranscribing
? "captions.bubble.fill"
: "captions.bubble"
)
}
.onReceive(viewModel.call?.state.$transcribing) { isTranscribing = $0 }
}
}
.onReceive(viewModel.call?.state.$settings) {
guard let mode = $0?.transcription.mode else {
isTranscriptionAvailable = false
return
}
isTranscriptionAvailable = mode != .disabled
}
}
}

struct DemoNoiseCancellationButtonView: View {

@Injected(\.streamVideo) var streamVideo

@ObservedObject var viewModel: CallViewModel
@State var isNoiseCancellationAvailable = false
@State var isActive: Bool = false

init(viewModel: CallViewModel) {
self.viewModel = viewModel
if let mode = viewModel.call?.state.settings?.audio.noiseCancellation?.mode {
isNoiseCancellationAvailable = mode != .disabled
} else {
isNoiseCancellationAvailable = false
}
isActive = streamVideo.videoConfig.noiseCancellationFilter?.id == streamVideo.videoConfig.audioProcessingModule
.activeAudioFilter?.id
}

var body: some View {
if let call = viewModel.call, let noiseCancellationAudioFilter = streamVideo.videoConfig.noiseCancellationFilter {
Group {
if isNoiseCancellationAvailable {
DemoMoreControlListButtonView(
action: {
if isActive {
call.setAudioFilter(nil)
isActive = false
} else {
call.setAudioFilter(noiseCancellationAudioFilter)
isActive = true
}
},
label: isActive ? "Disable Noise Cancellation" : "Noise Cancellation"
) {
Image(
systemName: isActive
? "waveform.path.ecg"
: "waveform.path"
)
}
}
}
.onReceive(call.state.$settings.map(\.?.audio.noiseCancellation)) {
if let mode = $0?.mode {
isNoiseCancellationAvailable = mode != .disabled
} else {
isNoiseCancellationAvailable = false
}
}
}
}
}

extension View {

@ViewBuilder
func presentsMoreControls(viewModel: CallViewModel) -> some View {
modifier(DemoMoreControlsViewModifier(viewModel: viewModel))
}
}
Loading
Loading