forked from mirrors/linphone-iphone
Implement PiP for video calls
This commit is contained in:
parent
b3d83c1580
commit
511c6e4093
5 changed files with 180 additions and 6 deletions
|
|
@ -22,6 +22,7 @@
|
|||
667E5D812B8E444E00EBCFC4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 667E5D802B8E444D00EBCFC4 /* GoogleService-Info.plist */; };
|
||||
6691CA7E2B839C2D00B2A7B8 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691CA7D2B839C2D00B2A7B8 /* NotificationService.swift */; };
|
||||
66A3E5B72CAE8E5C00FCB7FA /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */; };
|
||||
66C468FB2D2BE54800A836F7 /* PIPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C468FA2D2BE54300A836F7 /* PIPViewModel.swift */; };
|
||||
66C491F92B24D25B00CEA16D /* ConfigExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */; };
|
||||
66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FA2B24D32600CEA16D /* CoreExtension.swift */; };
|
||||
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */; };
|
||||
|
|
@ -227,6 +228,7 @@
|
|||
6646A7A22BB2E224006B842A /* ScheduleMeetingFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleMeetingFragment.swift; sourceTree = "<group>"; };
|
||||
667E5D802B8E444D00EBCFC4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
6691CA7D2B839C2D00B2A7B8 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
66C468FA2D2BE54300A836F7 /* PIPViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PIPViewModel.swift; sourceTree = "<group>"; };
|
||||
66C491F82B24D25A00CEA16D /* ConfigExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigExtension.swift; sourceTree = "<group>"; };
|
||||
66C491FA2B24D32600CEA16D /* CoreExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreExtension.swift; sourceTree = "<group>"; };
|
||||
66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteUtils.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -858,6 +860,7 @@
|
|||
children = (
|
||||
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */,
|
||||
D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */,
|
||||
66C468FA2D2BE54300A836F7 /* PIPViewModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1307,6 +1310,7 @@
|
|||
D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */,
|
||||
C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */,
|
||||
D714DE622C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift in Sources */,
|
||||
66C468FB2D2BE54800A836F7 /* PIPViewModel.swift in Sources */,
|
||||
D7CEE0352B7A210300FD79B7 /* ConversationsView.swift in Sources */,
|
||||
D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */,
|
||||
D71968922B86369D00DF4459 /* ChatBubbleView.swift in Sources */,
|
||||
|
|
@ -1557,8 +1561,11 @@
|
|||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Linphone/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Linphone;
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Deprecated - Prior to iOS 17 full calendar access is required";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "Make calls with your friends";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "App requires access to the local network to establish VoIP connections";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone usage is required for VOIP calls";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Share photos with your friends and customize avatars";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
|
|
@ -1610,8 +1617,10 @@
|
|||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Linphone/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Linphone;
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Deprecated - Prior to iOS 17 full calendar access is required";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "Make calls with your friends";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "App requires access to the local network to establish VoIP connections";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone usage is required for VOIP calls";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Share photos with your friends and customize avatars";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ final class CoreContext: ObservableObject {
|
|||
|
||||
static let shared = CoreContext()
|
||||
private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
var pipViewModel = PIPViewModel()
|
||||
|
||||
var coreVersion: String = Core.getVersion
|
||||
@Published var loggedIn: Bool = false
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>App requires access to the local network to establish VoIP connections</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
|
@ -111,6 +109,8 @@
|
|||
<true/>
|
||||
<key>ITSEncryptionExportComplianceCode</key>
|
||||
<string>b5cb085f-772a-4a4f-8c77-5d1332b1f93f</string>
|
||||
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSSupportsSuddenTermination</key>
|
||||
<false/>
|
||||
<key>UIAppFonts</key>
|
||||
|
|
@ -126,15 +126,12 @@
|
|||
<array>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UIImageName</key>
|
||||
<string>linphone</string>
|
||||
</dict>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>Deprecated - Prior to iOS 17 full calendar access is required</string>
|
||||
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -534,6 +534,10 @@ struct CallView: View {
|
|||
LinphoneVideoViewHolder { view in
|
||||
coreContext.doOnCoreQueue { core in
|
||||
core.nativeVideoWindow = view
|
||||
Log.info("debugtrace -- LinphoneVideoViewHolder set view 1")
|
||||
DispatchQueue.main.async {
|
||||
CoreContext.shared.pipViewModel.setupPiPViewController(remoteView: view)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
|
|
@ -549,6 +553,9 @@ struct CallView: View {
|
|||
}
|
||||
.onAppear {
|
||||
if callViewModel.videoDisplayed {
|
||||
if coreContext.pipViewModel.pipController?.isPictureInPictureActive ?? false {
|
||||
coreContext.pipViewModel.pipController?.stopPictureInPicture()
|
||||
}
|
||||
callViewModel.videoDisplayed = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
callViewModel.videoDisplayed = true
|
||||
|
|
@ -557,6 +564,9 @@ struct CallView: View {
|
|||
}
|
||||
.onDisappear {
|
||||
if callViewModel.videoDisplayed {
|
||||
if !(coreContext.pipViewModel.pipController?.isPictureInPictureActive ?? false){
|
||||
coreContext.pipViewModel.pipController?.startPictureInPicture()
|
||||
}
|
||||
callViewModel.videoDisplayed = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
callViewModel.videoDisplayed = true
|
||||
|
|
@ -893,6 +903,10 @@ struct CallView: View {
|
|||
LinphoneVideoViewHolder { view in
|
||||
coreContext.doOnCoreQueue { core in
|
||||
core.nativeVideoWindow = view
|
||||
Log.info("debugtrace -- LinphoneVideoViewHolder set view 2")
|
||||
DispatchQueue.main.async {
|
||||
CoreContext.shared.pipViewModel.setupPiPViewController(remoteView: view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import linphonesw
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
class PIPViewModel: NSObject, AVPictureInPictureControllerDelegate {
|
||||
|
||||
var pipController: AVPictureInPictureController?
|
||||
var pipRemoteVideoView = SampleBufferVideoCallView()
|
||||
var videoCallView = UIView()
|
||||
|
||||
var callStateChangedDelegate: CallDelegate?
|
||||
|
||||
func setupPiPViewController(remoteView: UIView) {
|
||||
Log.info("debugtrace setupPiPViewController")
|
||||
videoCallView = remoteView
|
||||
let pipVideoCallController = PictureInPictureVideoCallViewController()
|
||||
pipRemoteVideoView = pipVideoCallController.pipRemoteVideoView
|
||||
let pipContentSource = AVPictureInPictureController.ContentSource(
|
||||
activeVideoCallSourceView: videoCallView,
|
||||
contentViewController: pipVideoCallController)
|
||||
pipController = AVPictureInPictureController(contentSource: pipContentSource)
|
||||
pipController?.delegate = self
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
if let call = core.currentCall {
|
||||
self.callStateChangedDelegate = CallDelegateStub(onStateChanged: { (_: Call, cstate: Call.State, _: String) in
|
||||
if cstate != .StreamsRunning && CoreContext.shared.pipViewModel.pipController?.isPictureInPictureActive ?? false {
|
||||
Log.info("debugtrace -- callstate changed stop pip")
|
||||
CoreContext.shared.pipViewModel.pipController?.stopPictureInPicture()
|
||||
if cstate == .End || cstate == .Error {
|
||||
self.callStateChangedDelegate = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
call.addDelegate(delegate: self.callStateChangedDelegate!)
|
||||
Log.info("debugtrace -- added callstatechanged delegate")
|
||||
} else {
|
||||
Log.info("debugtrace -- no current call")
|
||||
}
|
||||
}
|
||||
/*
|
||||
ControlsViewModel.shared.isVideoEnabled.readCurrentAndObserve{ (video) in
|
||||
pipVideoCallController.matchVideoDimension()
|
||||
self.pipController?.canStartPictureInPictureAutomaticallyFromInline = video == true
|
||||
}
|
||||
|
||||
CallsViewModel.shared.currentCallData.observe(onChange: { callData in
|
||||
if (callData??.call.state != .StreamsRunning && self.pipController?.isPictureInPictureActive) {
|
||||
self.pipController?.stopPictureInPicture()
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
Log.info("debugtrace pictureInPictureControllerWillStartPictureInPicture")
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
core.nativeVideoWindow = self.pipRemoteVideoView
|
||||
}
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
Log.info("debugtrace pictureInPictureControllerDidStopPictureInPicture")
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
core.nativeVideoWindow = self.videoCallView
|
||||
}
|
||||
}
|
||||
|
||||
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
core.nativeVideoWindow = self.videoCallView
|
||||
}
|
||||
Log.error("Start Picture in Picture video call error : \(error)")
|
||||
// DispatchQueue.main.async { self.configurationPiPViewController() }
|
||||
}
|
||||
|
||||
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
Log.info("debugtrace restoreUserInterfaceForPictureInPictureStopWithCompletionHandler")
|
||||
|
||||
|
||||
TelecomManager.shared.callDisplayed = true
|
||||
/* a
|
||||
if (CallsViewModel.shared.currentCallData.value??.call.state == .StreamsRunning && PhoneMainView.instance().currentView != self.compositeViewDescription()) {
|
||||
PhoneMainView.instance().changeCurrentView(self.compositeViewDescription())
|
||||
//Core.get().nativeVideoWindow = pipRemoteVideoView // let the video on the pip view during the stop animation
|
||||
}
|
||||
//pictureInPictureController.contentSource?.activeVideoCallContentViewController.view.layer.cornerRadius = ActiveCallView.center_view_corner_radius
|
||||
*/
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
|
||||
class PictureInPictureVideoCallViewController: AVPictureInPictureVideoCallViewController {
|
||||
|
||||
var pipRemoteVideoView = SampleBufferVideoCallView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .black
|
||||
view.clipsToBounds = true
|
||||
view.addSubview(pipRemoteVideoView)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
view.layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
matchVideoDimension()
|
||||
super.viewDidLayoutSubviews()
|
||||
}
|
||||
|
||||
func matchVideoDimension() {
|
||||
Log.info("debugtrace - matchVideoDimension")
|
||||
self.preferredContentSize = CGSize(width: Double(720), height: Double(480))
|
||||
pipRemoteVideoView.frame = view.bounds
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable force_cast
|
||||
class SampleBufferVideoCallView: UIView {
|
||||
override class var layerClass: AnyClass {
|
||||
AVSampleBufferDisplayLayer.self
|
||||
}
|
||||
|
||||
var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer {
|
||||
layer as! AVSampleBufferDisplayLayer
|
||||
}
|
||||
}
|
||||
// swiftlint:enable force_cast
|
||||
Loading…
Add table
Reference in a new issue