Add a screen-sharing video preview to the call view

This commit is contained in:
Benoit Martins 2026-03-24 17:24:00 +01:00
parent 127e12b384
commit 9524c6f2f3
6 changed files with 140 additions and 66 deletions

View file

@ -1,7 +1,7 @@
import Foundation
public enum AppGitInfo {
public static let branch = "master"
public static let commit = "b84bd1faf"
public static let branch = "feature/screen_sharing"
public static let commit = "1dc5dec22"
public static let tag = "6.1.0-alpha"
}

View file

@ -167,22 +167,24 @@ class TelecomManager: ObservableObject {
}
func doCallOrJoinConf(address: Address, isVideo: Bool = false, isConference: Bool = false) {
if address.asStringUriOnly().hasPrefix("sip:conference-focus@sip.linphone.org") {
do {
let meetingAddress = try Factory.Instance.createAddress(addr: address.asStringUriOnly())
DispatchQueue.main.async {
withAnimation {
self.meetingWaitingRoomDisplayed = true
self.meetingWaitingRoomSelected = meetingAddress
}
}
} catch {}
} else {
doCallWithCore(
addr: address, isVideo: isVideo, isConference: isConference
)
}
CoreContext.shared.doOnCoreQueue { core in
if let _ = core.findConferenceInformationFromUri(uri: address) {
do {
let meetingAddress = try Factory.Instance.createAddress(addr: address.asStringUriOnly())
DispatchQueue.main.async {
withAnimation {
self.meetingWaitingRoomDisplayed = true
self.meetingWaitingRoomSelected = meetingAddress
}
}
} catch {}
} else {
self.doCallWithCore(
addr: address, isVideo: isVideo, isConference: isConference
)
}
}
}
func doCallWithCore(addr: Address, isVideo: Bool, isConference: Bool) {

View file

@ -607,9 +607,9 @@ struct CallView: View {
}
} else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil {
let heightValue = (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom)
if optionsChangeLayout == 1 && callViewModel.participantList.count <= 5 {
if optionsChangeLayout == 1 && callViewModel.participantList.count <= 5 && callViewModel.activeSpeakerParticipant?.isScreenSharing == false {
mosaicMode(geometry: geometry, height: heightValue)
} else if optionsChangeLayout == 3 {
} else if optionsChangeLayout == 3 && callViewModel.activeSpeakerParticipant?.isScreenSharing == false {
audioOnlyMode(geometry: geometry, height: heightValue)
} else {
activeSpeakerMode(geometry: geometry)
@ -990,7 +990,7 @@ struct CallView: View {
.cornerRadius(20)
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
if callViewModel.activeSpeakerParticipant != nil && !callViewModel.participantList[index].address.equal(address2: callViewModel.activeSpeakerParticipant!.address) {
if callViewModel.activeSpeakerParticipant != nil && (!callViewModel.participantList[index].address.weakEqual(address2: callViewModel.activeSpeakerParticipant!.address) || callViewModel.activeSpeakerParticipant!.isScreenSharing) {
ZStack {
if callViewModel.participantList[index].isJoining {
VStack {
@ -1040,7 +1040,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1160,7 +1160,7 @@ struct CallView: View {
.cornerRadius(20)
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
if callViewModel.activeSpeakerParticipant != nil && !callViewModel.participantList[index].address.equal(address2: callViewModel.activeSpeakerParticipant!.address) {
if callViewModel.activeSpeakerParticipant != nil && (!callViewModel.participantList[index].address.weakEqual(address2: callViewModel.activeSpeakerParticipant!.address) || callViewModel.activeSpeakerParticipant!.isScreenSharing) {
ZStack {
if callViewModel.participantList[index].isJoining {
VStack {
@ -1210,7 +1210,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1466,7 +1466,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1707,7 +1707,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -2218,28 +2218,37 @@ struct CallView: View {
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
} else {
VStack {
Button {
changeLayoutSheet = true
} label: {
HStack {
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
Text("call_action_change_layout")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
ZStack {
VStack {
Button {
changeLayoutSheet = true
} label: {
HStack {
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.activeSpeakerParticipant?.isScreenSharing == true)
Text("call_action_change_layout")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
if callViewModel.activeSpeakerParticipant?.isScreenSharing == true {
Color.gray600.opacity(0.8)
.allowsHitTesting(false)
}
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
}
}
.frame(height: geo.size.height * 0.15)

View file

@ -34,8 +34,9 @@ class ParticipantModel: ObservableObject {
@Published var isMuted: Bool
@Published var isAdmin: Bool
@Published var isSpeaking: Bool
@Published var isScreenSharing: Bool
init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false, isSpeaking: Bool = false) {
init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false, isSpeaking: Bool = false, isScreenSharing: Bool = false) {
self.address = address
self.sipUri = address.asStringUriOnly()
@ -49,6 +50,7 @@ class ParticipantModel: ObservableObject {
self.isMuted = isMuted
self.isAdmin = isAdmin
self.isSpeaking = isSpeaking
self.isScreenSharing = isScreenSharing
ContactsManager.shared.getFriendWithAddressInCoreQueue(address: self.address) { friendResult in
if let addressFriend = friendResult {

View file

@ -458,9 +458,9 @@ class CallViewModel: ObservableObject {
var myParticipantModelTmp: ParticipantModel?
if conf.me?.address != nil {
myParticipantModelTmp = ParticipantModel(address: conf.me!.address!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin)
myParticipantModelTmp = ParticipantModel(address: conf.me!.address!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin, isScreenSharing: false)
} else if self.currentCall?.callLog?.localAddress != nil {
myParticipantModelTmp = ParticipantModel(address: self.currentCall!.callLog!.localAddress!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin)
myParticipantModelTmp = ParticipantModel(address: self.currentCall!.callLog!.localAddress!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin, isScreenSharing: false)
}
var activeSpeakerParticipantTmp: ParticipantModel?
@ -469,21 +469,24 @@ class CallViewModel: ObservableObject {
address: conf.activeSpeakerParticipantDevice!.address!,
isJoining: conf.activeSpeakerParticipantDevice!.state == .Joining || conf.activeSpeakerParticipantDevice!.state == .Alerting,
onPause: conf.activeSpeakerParticipantDevice!.state == .OnHold,
isMuted: conf.activeSpeakerParticipantDevice!.isMuted
isMuted: conf.activeSpeakerParticipantDevice!.isMuted,
isScreenSharing: conf.activeSpeakerParticipantDevice!.screenSharingEnabled
)
} else if conf.participantList.first?.address != nil && conf.participantList.first!.address!.clone()!.equal(address2: (conf.me?.address)!) {
activeSpeakerParticipantTmp = ParticipantModel(
address: conf.participantDeviceList.first!.address!,
isJoining: conf.participantDeviceList.first!.state == .Joining || conf.participantDeviceList.first!.state == .Alerting,
onPause: conf.participantDeviceList.first!.state == .OnHold,
isMuted: conf.participantDeviceList.first!.isMuted
isMuted: conf.participantDeviceList.first!.isMuted,
isScreenSharing: conf.participantDeviceList.first!.screenSharingEnabled
)
} else if conf.participantList.last?.address != nil {
activeSpeakerParticipantTmp = ParticipantModel(
address: conf.participantDeviceList.last!.address!,
isJoining: conf.participantDeviceList.last!.state == .Joining || conf.participantDeviceList.last!.state == .Alerting,
onPause: conf.participantDeviceList.last!.state == .OnHold,
isMuted: conf.participantDeviceList.last!.isMuted
isMuted: conf.participantDeviceList.last!.isMuted,
isScreenSharing: conf.participantDeviceList.last!.screenSharingEnabled
)
}
@ -514,7 +517,8 @@ class CallViewModel: ObservableObject {
isJoining: participantDevice.state == .Joining || participantDevice.state == .Alerting,
onPause: participantDevice.state == .OnHold,
isMuted: participantDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: participantDevice.screenSharingEnabled
)
)
}
@ -572,7 +576,8 @@ class CallViewModel: ObservableObject {
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
@ -588,21 +593,24 @@ class CallViewModel: ObservableObject {
address: conference.activeSpeakerParticipantDevice!.address!,
isJoining: conference.activeSpeakerParticipantDevice!.state == .Joining || conference.activeSpeakerParticipantDevice!.state == .Alerting,
onPause: conference.activeSpeakerParticipantDevice!.state == .OnHold,
isMuted: conference.activeSpeakerParticipantDevice!.isMuted
isMuted: conference.activeSpeakerParticipantDevice!.isMuted,
isScreenSharing: conference.activeSpeakerParticipantDevice!.screenSharingEnabled
)
} else if conference.participantList.first?.address != nil && conference.participantList.first!.address!.clone()!.equal(address2: (conference.me?.address)!) {
activeSpeakerParticipantTmp = ParticipantModel(
address: conference.participantDeviceList.first!.address!,
isJoining: conference.participantDeviceList.first!.state == .Joining || conference.participantDeviceList.first!.state == .Alerting,
onPause: conference.participantDeviceList.first!.state == .OnHold,
isMuted: conference.participantDeviceList.first!.isMuted
isMuted: conference.participantDeviceList.first!.isMuted,
isScreenSharing: conference.participantDeviceList.first!.screenSharingEnabled
)
} else if conference.participantList.last?.address != nil {
activeSpeakerParticipantTmp = ParticipantModel(
address: conference.participantDeviceList.last!.address!,
isJoining: conference.participantDeviceList.last!.state == .Joining || conference.participantDeviceList.last!.state == .Alerting,
onPause: conference.participantDeviceList.last!.state == .OnHold,
isMuted: conference.participantDeviceList.last!.isMuted
isMuted: conference.participantDeviceList.last!.isMuted,
isScreenSharing: conference.participantDeviceList.last!.screenSharingEnabled
)
}
@ -648,7 +656,8 @@ class CallViewModel: ObservableObject {
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
@ -697,7 +706,57 @@ class CallViewModel: ObservableObject {
}
}
})
}, onParticipantDeviceIsSpeakingChanged: { (_: Conference, device: ParticipantDevice, isSpeaking: Bool) in
}, onParticipantDeviceScreenSharingChanged: { (_: Conference, device: ParticipantDevice, enabled: Bool) in
self.toggleVideoMode(isAudioOnlyMode: false)
let activeSpeakerParticipantTmp = ParticipantModel(
address: device.address!,
isJoining: device.state == .Joining || device.state == .Alerting,
onPause: device.state == .OnHold,
isMuted: device.isMuted,
isScreenSharing: device.screenSharingEnabled
)
var activeSpeakerNameTmp = ""
let friend = ContactsManager.shared.getFriendWithAddress(address: activeSpeakerParticipantTmp.address)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
activeSpeakerNameTmp = friend!.address!.displayName!
} else {
if activeSpeakerParticipantTmp.address.displayName != nil {
activeSpeakerNameTmp = activeSpeakerParticipantTmp.address.displayName!
} else if activeSpeakerParticipantTmp.address.username != nil {
activeSpeakerNameTmp = activeSpeakerParticipantTmp.address.username!
} else {
activeSpeakerNameTmp = String(activeSpeakerParticipantTmp.address.asStringUriOnly().dropFirst(4))
}
}
var participantListTmp: [ParticipantModel] = []
conference.participantDeviceList.forEach({ pDevice in
if pDevice.address != nil && !conference.isMe(uri: pDevice.address!.clone()!) {
if !conference.isMe(uri: pDevice.address!.clone()!) {
let isAdmin = conference.participantList.first(where: {$0.address!.equal(address2: pDevice.address!.clone()!)})?.isAdmin
participantListTmp.append(
ParticipantModel(
address: pDevice.address!,
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
}
})
DispatchQueue.main.async {
self.activeSpeakerParticipant = activeSpeakerParticipantTmp
self.activeSpeakerName = activeSpeakerNameTmp
self.participantList = participantListTmp
}
} , onParticipantDeviceIsSpeakingChanged: { (_: Conference, device: ParticipantDevice, isSpeaking: Bool) in
let isSpeaking = device.isSpeaking
if self.myParticipantModel != nil && self.myParticipantModel!.address.clone()!.equal(address2: device.address!) {
DispatchQueue.main.async {
@ -732,7 +791,8 @@ class CallViewModel: ObservableObject {
address: participantDevice!.address!,
isJoining: participantDevice!.state == .Joining || participantDevice!.state == .Alerting,
onPause: participantDevice!.state == .OnHold,
isMuted: participantDevice!.isMuted
isMuted: participantDevice!.isMuted,
isScreenSharing: participantDevice!.screenSharingEnabled
)
var activeSpeakerNameTmp = ""
@ -763,7 +823,8 @@ class CallViewModel: ObservableObject {
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}

View file

@ -123,7 +123,7 @@
"location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git",
"state" : {
"branch" : "beta",
"revision" : "5c2b2f448fbbd0326d01116227c7f5bda8e277c6"
"revision" : "2444acf4b34629fc229435895750f3784e52b5d9"
}
},
{
@ -149,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "5596511fce902e649c403cd4d6d5da1254f142b7",
"version" : "1.35.1"
"revision" : "a008af1a102ff3dd6cc3764bb69bf63226d0f5f6",
"version" : "1.36.1"
}
}
],