From f8163de76562c32e368ca6520ec4289600312c2a Mon Sep 17 00:00:00 2001 From: Daniel Ornelas Date: Tue, 6 Mar 2018 16:02:59 -0600 Subject: [PATCH] Implement Jitsi meet presentation interface that supports custom PiP solution --- .../PiPApp.xcodeproj/project.pbxproj | 38 ++++- .../src/Base.lproj/Main.storyboard | 16 +- ios/example-pip-app/src/Info.plist | 17 ++ .../src/JitsiViewController.swift | 26 ---- ios/example-pip-app/src/ViewController.swift | 7 +- ios/sdk/sdk.xcodeproj/project.pbxproj | 28 +++- ios/sdk/src/JitsiManager.swift | 17 -- .../DragGestureController.swift | 104 +++++++++++++ .../JitsiMeetManager/JitsiMeetManager.swift | 146 ++++++++++++++++++ .../JitsiMeetViewController.swift | 16 ++ .../JitsiMeetManager/JitsiMeetWindow.swift | 45 ++++++ .../mobile/picture-in-picture/actions.js | 12 +- 12 files changed, 398 insertions(+), 74 deletions(-) delete mode 100644 ios/example-pip-app/src/JitsiViewController.swift delete mode 100644 ios/sdk/src/JitsiManager.swift create mode 100644 ios/sdk/src/JitsiMeetManager/DragGestureController.swift create mode 100644 ios/sdk/src/JitsiMeetManager/JitsiMeetManager.swift create mode 100644 ios/sdk/src/JitsiMeetManager/JitsiMeetViewController.swift create mode 100644 ios/sdk/src/JitsiMeetManager/JitsiMeetWindow.swift diff --git a/ios/example-pip-app/PiPApp.xcodeproj/project.pbxproj b/ios/example-pip-app/PiPApp.xcodeproj/project.pbxproj index 20c8c100b..162437c52 100644 --- a/ios/example-pip-app/PiPApp.xcodeproj/project.pbxproj +++ b/ios/example-pip-app/PiPApp.xcodeproj/project.pbxproj @@ -7,8 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + C6245F57205044120040BE68 /* JitsiMeet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6F99C4F204DE79F0001F710 /* JitsiMeet.framework */; }; + C6245F58205044150040BE68 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6A34247204DF18000E062DD /* WebRTC.framework */; }; C6A34249204DF18000E062DD /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C6A34247204DF18000E062DD /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C6A3424C204DF98E00E062DD /* JitsiViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3424B204DF98E00E062DD /* JitsiViewController.swift */; }; C6F99C3B204DE6BE0001F710 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F99C3A204DE6BE0001F710 /* AppDelegate.swift */; }; C6F99C3D204DE6BE0001F710 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F99C3C204DE6BE0001F710 /* ViewController.swift */; }; C6F99C40204DE6BE0001F710 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C6F99C3E204DE6BE0001F710 /* Main.storyboard */; }; @@ -34,7 +35,6 @@ /* Begin PBXFileReference section */ C6A34247204DF18000E062DD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = ""; }; - C6A3424B204DF98E00E062DD /* JitsiViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiViewController.swift; sourceTree = ""; }; C6F99C37204DE6BE0001F710 /* PiPApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PiPApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; C6F99C3A204DE6BE0001F710 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C6F99C3C204DE6BE0001F710 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -50,6 +50,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C6245F57205044120040BE68 /* JitsiMeet.framework in Frameworks */, + C6245F58205044150040BE68 /* WebRTC.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -78,7 +80,6 @@ children = ( C6F99C3A204DE6BE0001F710 /* AppDelegate.swift */, C6F99C3C204DE6BE0001F710 /* ViewController.swift */, - C6A3424B204DF98E00E062DD /* JitsiViewController.swift */, C6F99C3E204DE6BE0001F710 /* Main.storyboard */, C6F99C41204DE6BE0001F710 /* Assets.xcassets */, C6F99C43204DE6BE0001F710 /* LaunchScreen.storyboard */, @@ -103,12 +104,13 @@ isa = PBXNativeTarget; buildConfigurationList = C6F99C49204DE6BE0001F710 /* Build configuration list for PBXNativeTarget "PiPApp" */; buildPhases = ( + C6A3424A204DF91D00E062DD /* Run Adjust ATS for loading JS bundle */, + C6F99C62204DEFFE0001F710 /* Run React Packager */, C6F99C33204DE6BE0001F710 /* Sources */, C6F99C34204DE6BE0001F710 /* Frameworks */, - C6F99C61204DEDC20001F710 /* Embed Frameworks */, C6F99C35204DE6BE0001F710 /* Resources */, - C6F99C62204DEFFE0001F710 /* Run React Packager */, - C6A3424A204DF91D00E062DD /* Run Adjust ATS for loading JS bundle */, + C6F99C61204DEDC20001F710 /* Embed Frameworks */, + C6A3426E20503ECC00E062DD /* Adjust embedded framework architectures */, ); buildRules = ( ); @@ -132,6 +134,11 @@ C6F99C36204DE6BE0001F710 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; }; }; }; @@ -181,6 +188,20 @@ shellPath = /bin/sh; shellScript = "../scripts/fixup-ats.sh"; }; + C6A3426E20503ECC00E062DD /* Adjust embedded framework architectures */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Adjust embedded framework architectures"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "../scripts/fixup-frameworks.sh"; + }; C6F99C62204DEFFE0001F710 /* Run React Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -204,7 +225,6 @@ files = ( C6F99C3D204DE6BE0001F710 /* ViewController.swift in Sources */, C6F99C3B204DE6BE0001F710 /* AppDelegate.swift in Sources */, - C6A3424C204DF98E00E062DD /* JitsiViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -342,7 +362,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + FRAMEWORK_SEARCH_PATHS = "../../node_modules/react-native-webrtc/ios"; INFOPLIST_FILE = src/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.jitsi.PiPApp; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -356,7 +378,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + FRAMEWORK_SEARCH_PATHS = "../../node_modules/react-native-webrtc/ios"; INFOPLIST_FILE = src/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.jitsi.PiPApp; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/example-pip-app/src/Base.lproj/Main.storyboard b/ios/example-pip-app/src/Base.lproj/Main.storyboard index b4d8793b0..35ee2b87b 100644 --- a/ios/example-pip-app/src/Base.lproj/Main.storyboard +++ b/ios/example-pip-app/src/Base.lproj/Main.storyboard @@ -27,6 +27,7 @@ + @@ -41,20 +42,5 @@ - - - - - - - - - - - - - - - diff --git a/ios/example-pip-app/src/Info.plist b/ios/example-pip-app/src/Info.plist index 16be3b681..8a99dee66 100644 --- a/ios/example-pip-app/src/Info.plist +++ b/ios/example-pip-app/src/Info.plist @@ -20,6 +20,10 @@ 1 LSRequiresIPhoneOS + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -34,6 +38,19 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + + + NSAllowsArbitraryLoads + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/ios/example-pip-app/src/JitsiViewController.swift b/ios/example-pip-app/src/JitsiViewController.swift deleted file mode 100644 index 8b41cdb92..000000000 --- a/ios/example-pip-app/src/JitsiViewController.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// JitsiViewController.swift -// PiPApp -// -// Created by Daniel Ornelas on 3/5/18. -// Copyright © 2018 Atlassian Inc. All rights reserved. -// - -import JitsiMeet -import UIKit - -final class JitsiViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - guard let jitsiView = self.view as? JitsiMeetView else { return } - - jitsiView.welcomePageEnabled = true - jitsiView.load(nil) - - // TODO: delete me, this is only testing access to swift object in SDK - let jitsiManager = JitsiManager() - jitsiManager.testMe() - } -} diff --git a/ios/example-pip-app/src/ViewController.swift b/ios/example-pip-app/src/ViewController.swift index 969114305..64a4723ee 100644 --- a/ios/example-pip-app/src/ViewController.swift +++ b/ios/example-pip-app/src/ViewController.swift @@ -13,6 +13,8 @@ class ViewController: UIViewController { @IBOutlet weak var videoButton: UIButton? + private var jitsiMeetManager: JitsiMeetManager? + override func viewDidLoad() { super.viewDidLoad() } @@ -20,7 +22,10 @@ class ViewController: UIViewController { // MARK: - Actions @IBAction func startMeeting(sender: Any?) { - print("test") + //let url = URL(string: "") + self.jitsiMeetManager = JitsiMeetManager() + jitsiMeetManager?.welcomeScreenEnabled = true + jitsiMeetManager?.load(withUrl: nil) } } diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 7c48f91f2..ae11e1ede 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -27,7 +27,10 @@ 0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0BCA496B1EC4BBF900B793EE /* jitsi.ttf */; }; 0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; }; - C6F99C14204DB63E0001F710 /* JitsiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F99C12204DB63D0001F710 /* JitsiManager.swift */; }; + C6A3425F204EF76800E062DD /* JitsiMeetWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* JitsiMeetWindow.swift */; }; + C6A34260204EF76800E062DD /* JitsiMeetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetManager.swift */; }; + C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; }; + C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; }; C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; }; /* End PBXBuildFile section */ @@ -57,7 +60,10 @@ 0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = ""; }; 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = ""; }; - C6F99C12204DB63D0001F710 /* JitsiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiManager.swift; sourceTree = ""; }; + C6A3425C204EF76800E062DD /* JitsiMeetWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetWindow.swift; sourceTree = ""; }; + C6A3425D204EF76800E062DD /* JitsiMeetManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetManager.swift; sourceTree = ""; }; + C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = ""; }; + C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = ""; }; C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -107,13 +113,13 @@ 0BD906E71EC0C00300C8C18E /* src */ = { isa = PBXGroup; children = ( + C6A3426B204F127900E062DD /* JitsiMeetManager */, 0BCA495C1EC4B6C600B793EE /* AudioMode.m */, 0BB9AD7C1F60356D001C08DB /* AppInfo.m */, 0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */, 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */, 0BD906E91EC0C00300C8C18E /* Info.plist */, 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */, - C6F99C12204DB63D0001F710 /* JitsiManager.swift */, 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */, 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */, 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */, @@ -149,6 +155,17 @@ name = Pods; sourceTree = ""; }; + C6A3426B204F127900E062DD /* JitsiMeetManager */ = { + isa = PBXGroup; + children = ( + C6A3425C204EF76800E062DD /* JitsiMeetWindow.swift */, + C6A3425D204EF76800E062DD /* JitsiMeetManager.swift */, + C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */, + C6A3425E204EF76800E062DD /* DragGestureController.swift */, + ); + path = JitsiMeetManager; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -312,15 +329,18 @@ buildActionMask = 2147483647; files = ( 0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */, + C6A34260204EF76800E062DD /* JitsiMeetManager.swift in Sources */, 0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */, 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */, 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */, 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */, 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */, - C6F99C14204DB63E0001F710 /* JitsiManager.swift in Sources */, + C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */, + C6A3425F204EF76800E062DD /* JitsiMeetWindow.swift in Sources */, 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */, 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, + C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, 0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/sdk/src/JitsiManager.swift b/ios/sdk/src/JitsiManager.swift deleted file mode 100644 index 50668273c..000000000 --- a/ios/sdk/src/JitsiManager.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// JitsiManager.swift -// JitsiMeet -// -// Created by Daniel Ornelas on 3/5/18. -// Copyright © 2018 Jitsi. All rights reserved. -// - -import Foundation - -@objc(JitsiManager) -public class JitsiManager: NSObject { - - public func testMe() { - print("hi there") - } -} diff --git a/ios/sdk/src/JitsiMeetManager/DragGestureController.swift b/ios/sdk/src/JitsiMeetManager/DragGestureController.swift new file mode 100644 index 000000000..92f3b1b34 --- /dev/null +++ b/ios/sdk/src/JitsiMeetManager/DragGestureController.swift @@ -0,0 +1,104 @@ +// Copyright © 2018 Jitsi. All rights reserved. + +final class DragGestureController { + + var insets: UIEdgeInsets = UIEdgeInsets.zero + + private var frameBeforeDragging: CGRect = CGRect.zero + private weak var view: UIView? + private lazy var panGesture: UIPanGestureRecognizer = { + return UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:))) + }() + + func startDragListener(inView view: UIView) { + self.view = view + view.addGestureRecognizer(panGesture) + panGesture.isEnabled = true + } + + func stopDragListener() { + panGesture.isEnabled = false + view?.removeGestureRecognizer(panGesture) + view = nil + } + + @objc private func handlePan(gesture: UIPanGestureRecognizer) { + guard let view = self.view else { return } + + let translation = gesture.translation(in: view.superview) + let velocity = gesture.velocity(in: view.superview) + var frame = frameBeforeDragging + + switch gesture.state { + case .began: + frameBeforeDragging = view.frame + + case .changed: + frame.origin.x = floor(frame.origin.x + translation.x) + frame.origin.y = floor(frame.origin.y + translation.y) + view.frame = frame + + case .ended: + let currentPos = view.frame.origin + let finalPos = calculateFinalPosition() + + let distance = CGPoint(x: currentPos.x - finalPos.x, + y: currentPos.y - finalPos.y) + let distanceMagnitude = magnitude(vector: distance) + let velocityMagnitude = magnitude(vector: velocity) + let animationDuration = 0.5 + let initialSpringVelocity = velocityMagnitude / distanceMagnitude / CGFloat(animationDuration) + + frame.origin = CGPoint(x: finalPos.x, y: finalPos.y) + + UIView.animate(withDuration: animationDuration, + delay: 0, + usingSpringWithDamping: 0.9, + initialSpringVelocity: initialSpringVelocity, + options: .curveLinear, + animations: { + view.frame = frame + }, completion: nil) + + default: + break + } + } + + private func calculateFinalPosition() -> CGPoint { + guard + let view = self.view, + let bounds = view.superview?.frame + else { return CGPoint.zero } + + let currentSize = view.frame.size + let adjustedBounds = UIEdgeInsetsInsetRect(bounds, insets) + let threshold: CGFloat = 20.0 + let velocity = panGesture.velocity(in: view.superview) + let location = panGesture.location(in: view.superview) + + let goLeft: Bool + if fabs(velocity.x) > threshold { + goLeft = velocity.x < -threshold + } else { + goLeft = location.x < bounds.midX + } + + let goUp: Bool + if fabs(velocity.y) > threshold { + goUp = velocity.y < -threshold + } else { + goUp = location.y < bounds.midY + } + + let finalPosX: CGFloat = goLeft ? adjustedBounds.origin.x : bounds.size.width - insets.right - currentSize.width + let finalPosY: CGFloat = goUp ? adjustedBounds.origin.y : bounds.size.height - insets.bottom - currentSize.height + + return CGPoint(x: finalPosX, y: finalPosY) + } + + private func magnitude(vector: CGPoint) -> CGFloat { + return sqrt(pow(vector.x, 2) + pow(vector.y, 2)) + } +} + diff --git a/ios/sdk/src/JitsiMeetManager/JitsiMeetManager.swift b/ios/sdk/src/JitsiMeetManager/JitsiMeetManager.swift new file mode 100644 index 000000000..46e0d1071 --- /dev/null +++ b/ios/sdk/src/JitsiMeetManager/JitsiMeetManager.swift @@ -0,0 +1,146 @@ +// Copyright © 2018 Jitsi. All rights reserved. + +import Foundation + +/// Creates and present a JitsiMeetView inside of an external window that can be dragged +/// when minimized (if PiP mode is enabled) +open class JitsiMeetManager: NSObject { + + /// The Jitsi meet view delegate + public weak var delegate: JitsiMeetViewDelegate? = nil + /// Limits the boundries of meet view position on screen when minimized + public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25, left: 5, bottom: 5, right: 5) + /// Enables PiP mode for this jitsiMeet + public var allowPiP: Bool = true + /// The size ratio for jitsiMeetView when in PiP mode + public var pipSizeRatio: CGFloat = 0.333 + /// Defines if welcome screen should be on + public var welcomeScreenEnabled: Bool = false + + fileprivate let dragController: DragGestureController = DragGestureController() + + fileprivate lazy var meetViewController: JitsiMeetViewController = { return self.makeMeetViewController() }() + fileprivate lazy var meetWindow: JitsiMeetWindow = { return self.makeMeetWindow() }() + fileprivate var meetingInPiP: Bool = false + + /// Presents and loads a jitsi meet view + /// + /// - Parameter url: The url of the presentation + public func load(withUrl url: URL?) { + meetWindow.show() + meetViewController.jitsiMeetView.load(url) + } + + /// Presents and loads a jitsi meet view with configuration + /// + /// - Parameter urlObject: A dictionary of keys to be used for configuration + public func load(withUrlObject urlObject: [AnyHashable : Any]?) { + meetWindow.show() + meetViewController.jitsiMeetView.loadURLObject(urlObject) + } + + // MARK: - Manage PiP switching + + // update size animation + fileprivate func updateMeetViewSize(isPiP: Bool) { + UIView.animate(withDuration: 0.25) { + self.meetViewController.view.frame = self.meetViewRect(isPiP: isPiP) + self.meetViewController.view.setNeedsLayout() + } + } + + private func meetViewRect(isPiP: Bool) -> CGRect { + guard isPiP else { + return meetWindow.bounds + } + let bounds = meetWindow.bounds + + // resize to suggested ratio and position to the bottom right + let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets) + let size = CGSize(width: bounds.size.width * pipSizeRatio, + height: bounds.size.height * pipSizeRatio) + let x: CGFloat = adjustedBounds.maxX - size.width + let y: CGFloat = adjustedBounds.maxY - size.height + return CGRect(x: x, y: y, width: size.width, height: size.height) + } + + // MARK: - helpers + + fileprivate func cleanUp() { + // TODO: more clean up work on this + + dragController.stopDragListener() + meetWindow.isHidden = true + } + + private func makeMeetViewController() -> JitsiMeetViewController { + let vc = JitsiMeetViewController() + vc.jitsiMeetView.delegate = self + vc.jitsiMeetView.welcomePageEnabled = self.welcomeScreenEnabled + vc.jitsiMeetView.pictureInPictureEnabled = self.allowPiP + return vc + } + + private func makeMeetWindow() -> JitsiMeetWindow { + let window = JitsiMeetWindow(frame: UIScreen.main.bounds) + window.backgroundColor = .clear + window.windowLevel = UIWindowLevelStatusBar + 100 + window.rootViewController = self.meetViewController + return window + } +} + +extension JitsiMeetManager: JitsiMeetViewDelegate { + + public func conferenceWillJoin(_ data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.delegate?.conferenceWillJoin!(data) + } + } + + public func conferenceJoined(_ data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.delegate?.conferenceJoined!(data) + } + } + + public func conferenceWillLeave(_ data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.delegate?.conferenceWillLeave!(data) + } + } + + public func conferenceLeft(_ data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.cleanUp() + + self.delegate?.conferenceLeft!(data) + } + } + + public func conferenceFailed(_ data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.cleanUp() + + self.delegate?.conferenceFailed!(data) + } + } + + public func loadConfigError(_ data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.delegate?.loadConfigError!(data) + } + } + + public func enterPicture(inPicture data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.dragController.startDragListener(inView: self.meetViewController.view) + self.dragController.insets = self.dragBoundInsets + + self.meetingInPiP = true + self.updateMeetViewSize(isPiP: true) + + self.delegate?.enterPicture!(inPicture: data) + } + } +} diff --git a/ios/sdk/src/JitsiMeetManager/JitsiMeetViewController.swift b/ios/sdk/src/JitsiMeetManager/JitsiMeetViewController.swift new file mode 100644 index 000000000..dd9645946 --- /dev/null +++ b/ios/sdk/src/JitsiMeetManager/JitsiMeetViewController.swift @@ -0,0 +1,16 @@ +// Copyright © 2018 Jitsi. All rights reserved. + + +/// Wrapper ViewController of a JitsiMeetView +/// +/// TODO: should consider refactor and move out several logic of the JitsiMeetView to +/// this class +open class JitsiMeetViewController: UIViewController { + + private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView() + + override open func loadView() { + super.loadView() + self.view = jitsiMeetView + } +} diff --git a/ios/sdk/src/JitsiMeetManager/JitsiMeetWindow.swift b/ios/sdk/src/JitsiMeetManager/JitsiMeetWindow.swift new file mode 100644 index 000000000..0efa42e7e --- /dev/null +++ b/ios/sdk/src/JitsiMeetManager/JitsiMeetWindow.swift @@ -0,0 +1,45 @@ +// Copyright © 2018 Jitsi. All rights reserved. + +open class JitsiMeetWindow: UIWindow { + + /// Help out to bubble up the gesture detection outside of the rootVC frame + open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + guard let vc = rootViewController else { + return super.point(inside: point, with: event) + } + return vc.view.frame.contains(point) + } + + /// animate in the window + open func show() { + if self.isHidden || self.alpha < 1 { + self.isHidden = false + self.alpha = 0 + + UIView.animate( + withDuration: 0.1, + delay: 0, + options: .beginFromCurrentState, + animations: { + self.alpha = 1 + }, + completion: nil) + } + } + + /// animate out the window + open func hide() { + if !self.isHidden || self.alpha > 0 { + UIView.animate( + withDuration: 0.1, + delay: 0, + options: .beginFromCurrentState, + animations: { + self.alpha = 0 + self.isHidden = true + }, + completion: nil) + } + } +} + diff --git a/react/features/mobile/picture-in-picture/actions.js b/react/features/mobile/picture-in-picture/actions.js index d6a83b08b..3102581f9 100644 --- a/react/features/mobile/picture-in-picture/actions.js +++ b/react/features/mobile/picture-in-picture/actions.js @@ -2,6 +2,8 @@ import { NativeModules } from 'react-native'; +import { Platform } from '../../base/react'; + import { ENTER_PICTURE_IN_PICTURE, _SET_EMITTER_SUBSCRIPTIONS @@ -28,10 +30,12 @@ export function enterPictureInPicture() { && (conference || joining)) { const { PictureInPicture } = NativeModules; const p - = PictureInPicture - ? PictureInPicture.enterPictureInPicture() - : Promise.reject( - new Error('Picture-in-Picture not supported')); + = Platform.OS === 'android' + ? PictureInPicture + ? PictureInPicture.enterPictureInPicture() + : Promise.reject( + new Error('Picture-in-Picture not supported')) + : Promise.resolve(); p.then( () => dispatch({ type: ENTER_PICTURE_IN_PICTURE }),