6
ios/.xcode.env
Normal file
@@ -0,0 +1,6 @@
|
||||
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
|
||||
|
||||
. ${THIS_DIR}/../node_modules/react-native/scripts/find-node-for-xcode.sh
|
||||
|
||||
export NODE_BINARY=$(command -v node)
|
||||
export ENTRY_FILE="${PROJECT_DIR}/../../index.ios.js"
|
||||
107
ios/Podfile
Normal file
@@ -0,0 +1,107 @@
|
||||
# Resolve react_native_pods.rb with node to allow for hoisting.
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
'require.resolve(
|
||||
"react-native/scripts/react_native_pods.rb",
|
||||
{paths: [process.argv[1]]},
|
||||
)', __dir__]).strip
|
||||
|
||||
platform :ios, '15.1'
|
||||
workspace 'jitsi-meet'
|
||||
|
||||
install! 'cocoapods', :deterministic_uuids => false
|
||||
|
||||
def cocoa_utilities
|
||||
pod 'CocoaLumberjack', '3.7.4'
|
||||
end
|
||||
|
||||
target 'JitsiMeet' do
|
||||
project 'app/app.xcodeproj'
|
||||
|
||||
pod 'Firebase/Analytics', '~> 8.0'
|
||||
pod 'Firebase/Crashlytics', '~> 8.0'
|
||||
end
|
||||
|
||||
target 'JitsiMeetSDK' do
|
||||
project 'sdk/sdk.xcodeproj'
|
||||
|
||||
# React Native and its dependencies
|
||||
#
|
||||
|
||||
config = use_native_modules!
|
||||
flags = get_default_flags()
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => true,
|
||||
:fabric_enabled => false,
|
||||
:new_arch_enabled => false,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
||||
# We use auto linking, but some dependencies are excluded from the lite build
|
||||
# (see react-native.config.js) so we add them here.
|
||||
|
||||
pod 'giphy-react-native-sdk', :path => '../node_modules/@giphy/react-native-sdk'
|
||||
pod 'RNCalendarEvents', :path => '../node_modules/react-native-calendar-events'
|
||||
pod 'RNGoogleSignin', :path => '../node_modules/@react-native-google-signin/google-signin'
|
||||
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
|
||||
|
||||
# Native pod dependencies
|
||||
#
|
||||
|
||||
cocoa_utilities
|
||||
pod 'ObjectiveDropboxOfficial', '6.2.3'
|
||||
end
|
||||
|
||||
target 'JitsiMeetSDKLite' do
|
||||
project 'sdk/sdk.xcodeproj'
|
||||
|
||||
# This is a more lightweight target, which removes some functionality.
|
||||
# Check the react-native.config.js for the list of excluded packages.
|
||||
|
||||
# React Native and its dependencies
|
||||
#
|
||||
|
||||
config = use_native_modules!
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => true,
|
||||
:fabric_enabled => false,
|
||||
:new_arch_enabled => false,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
||||
# Native pod dependencies
|
||||
#
|
||||
|
||||
cocoa_utilities
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
react_native_post_install(
|
||||
installer,
|
||||
use_native_modules![:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
# :ccache_enabled => true
|
||||
)
|
||||
installer.pods_project.targets.each do |target|
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/11402
|
||||
if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
||||
end
|
||||
end
|
||||
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['SUPPORTS_MACCATALYST'] = 'NO'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.1'
|
||||
config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -no-verify-emitted-module-interface'
|
||||
end
|
||||
end
|
||||
|
||||
# Patch SocketRocket to support TLS 1.3
|
||||
%x(patch Pods/SocketRocket/SocketRocket/SRSecurityPolicy.m -N < patches/ws-tls13.diff)
|
||||
|
||||
end
|
||||
2306
ios/Podfile.lock
Normal file
3
ios/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Jitsi Meet SDK for iOS
|
||||
|
||||
This document has been moved to [The Handbook](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-ios-sdk).
|
||||
47
ios/app/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>1C8F.1</string>
|
||||
<string>C56D.1</string>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>85F4.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
18
ios/app/app.entitlements
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:alpha.jitsi.net</string>
|
||||
<string>applinks:beta.meet.jit.si</string>
|
||||
<string>applinks:meet.jit.si</string>
|
||||
</array>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.jitsi.meet.appgroup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
1182
ios/app/app.xcodeproj/project.pbxproj
Normal file
101
ios/app/app.xcodeproj/xcshareddata/xcschemes/JitsiMeet.xcscheme
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "0BD906E41EC0C00300C8C18E"
|
||||
BuildableName = "JitsiMeetSDK.framework"
|
||||
BlueprintName = "JitsiMeetSDK"
|
||||
ReferencedContainer = "container:../sdk/sdk.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "JitsiMeet"
|
||||
ReferencedContainer = "container:app.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "JitsiMeet"
|
||||
ReferencedContainer = "container:app.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "JitsiMeet"
|
||||
ReferencedContainer = "container:app.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "JitsiMeet"
|
||||
ReferencedContainer = "container:app.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
30
ios/app/broadcast-extension/Atomic.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
struct Atomic<Value> {
|
||||
|
||||
private var value: Value
|
||||
private let lock = NSLock()
|
||||
|
||||
init(wrappedValue value: Value) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var wrappedValue: Value {
|
||||
get { return load() }
|
||||
set { store(newValue: newValue) }
|
||||
}
|
||||
|
||||
func load() -> Value {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return value
|
||||
}
|
||||
|
||||
mutating func store(newValue: Value) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
37
ios/app/broadcast-extension/DarwinNotificationCenter.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DarwinNotification: String {
|
||||
case broadcastStarted = "iOS_BroadcastStarted"
|
||||
case broadcastStopped = "iOS_BroadcastStopped"
|
||||
}
|
||||
|
||||
class DarwinNotificationCenter {
|
||||
|
||||
static var shared = DarwinNotificationCenter()
|
||||
|
||||
private var notificationCenter: CFNotificationCenter
|
||||
|
||||
init() {
|
||||
notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
}
|
||||
|
||||
func postNotification(_ name: DarwinNotification) {
|
||||
CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true)
|
||||
}
|
||||
}
|
||||
33
ios/app/broadcast-extension/Info.plist
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Jitsi Meet Broadcast Extension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.broadcast-services-upload</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
|
||||
<key>RPBroadcastProcessMode</key>
|
||||
<string>RPBroadcastProcessModeSampleBuffer</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
116
ios/app/broadcast-extension/SampleHandler.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ReplayKit
|
||||
|
||||
private enum Constants {
|
||||
// the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app.
|
||||
static let appGroupIdentifier = "group.org.jitsi.meet.appgroup"
|
||||
}
|
||||
|
||||
class SampleHandler: RPBroadcastSampleHandler {
|
||||
|
||||
private var clientConnection: SocketConnection?
|
||||
private var uploader: SampleUploader?
|
||||
|
||||
private var frameCount: Int = 0
|
||||
|
||||
var socketFilePath: String {
|
||||
let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
|
||||
|
||||
return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
if let connection = SocketConnection(filePath: socketFilePath) {
|
||||
clientConnection = connection
|
||||
setupConnection()
|
||||
|
||||
uploader = SampleUploader(connection: connection)
|
||||
}
|
||||
}
|
||||
|
||||
override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
|
||||
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
|
||||
print("broadcast started")
|
||||
|
||||
frameCount = 0
|
||||
|
||||
DarwinNotificationCenter.shared.postNotification(.broadcastStarted)
|
||||
openConnection()
|
||||
}
|
||||
|
||||
override func broadcastPaused() {
|
||||
// User has requested to pause the broadcast. Samples will stop being delivered.
|
||||
}
|
||||
|
||||
override func broadcastResumed() {
|
||||
// User has requested to resume the broadcast. Samples delivery will resume.
|
||||
}
|
||||
|
||||
override func broadcastFinished() {
|
||||
// User has requested to finish the broadcast.
|
||||
DarwinNotificationCenter.shared.postNotification(.broadcastStopped)
|
||||
clientConnection?.close()
|
||||
}
|
||||
|
||||
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
|
||||
switch sampleBufferType {
|
||||
case RPSampleBufferType.video:
|
||||
// very simple mechanism for adjusting frame rate by using every third frame
|
||||
frameCount += 1
|
||||
if frameCount % 3 == 0 {
|
||||
uploader?.send(sample: sampleBuffer)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SampleHandler {
|
||||
|
||||
func setupConnection() {
|
||||
clientConnection?.didClose = { [weak self] error in
|
||||
print("client connection did close \(String(describing: error))")
|
||||
|
||||
if let error = error {
|
||||
self?.finishBroadcastWithError(error)
|
||||
} else {
|
||||
// the displayed failure message is more user friendly when using NSError instead of Error
|
||||
let JMScreenSharingStopped = 10001
|
||||
let customError = NSError(domain: RPRecordingErrorDomain, code: JMScreenSharingStopped, userInfo: [NSLocalizedDescriptionKey: "Screen sharing stopped"])
|
||||
self?.finishBroadcastWithError(customError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openConnection() {
|
||||
let queue = DispatchQueue(label: "broadcast.connectTimer")
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500))
|
||||
timer.setEventHandler { [weak self] in
|
||||
guard self?.clientConnection?.open() == true else {
|
||||
return
|
||||
}
|
||||
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
154
ios/app/broadcast-extension/SampleUploader.swift
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import ReplayKit
|
||||
|
||||
private enum Constants {
|
||||
static let bufferMaxLength = 10240
|
||||
}
|
||||
|
||||
class SampleUploader {
|
||||
|
||||
private static var imageContext = CIContext(options: nil)
|
||||
|
||||
@Atomic private var isReady: Bool = false
|
||||
private var connection: SocketConnection
|
||||
|
||||
private var dataToSend: Data?
|
||||
private var byteIndex = 0
|
||||
|
||||
private let serialQueue: DispatchQueue
|
||||
|
||||
init(connection: SocketConnection) {
|
||||
self.connection = connection
|
||||
self.serialQueue = DispatchQueue(label: "org.jitsi.meet.broadcast.sampleUploader")
|
||||
|
||||
setupConnection()
|
||||
}
|
||||
|
||||
@discardableResult func send(sample buffer: CMSampleBuffer) -> Bool {
|
||||
guard isReady == true else {
|
||||
return false
|
||||
}
|
||||
|
||||
isReady = false
|
||||
|
||||
dataToSend = prepare(sample: buffer)
|
||||
byteIndex = 0
|
||||
|
||||
serialQueue.async { [weak self] in
|
||||
self?.sendDataChunk()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private extension SampleUploader {
|
||||
|
||||
func setupConnection() {
|
||||
connection.didOpen = { [weak self] in
|
||||
self?.isReady = true
|
||||
}
|
||||
connection.streamHasSpaceAvailable = { [weak self] in
|
||||
self?.serialQueue.async {
|
||||
self?.isReady = !(self?.sendDataChunk() ?? true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult func sendDataChunk() -> Bool {
|
||||
guard let dataToSend = dataToSend else {
|
||||
return false
|
||||
}
|
||||
|
||||
var bytesLeft = dataToSend.count - byteIndex
|
||||
var length = bytesLeft > Constants.bufferMaxLength ? Constants.bufferMaxLength : bytesLeft
|
||||
|
||||
length = dataToSend[byteIndex..<(byteIndex + length)].withUnsafeBytes {
|
||||
guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return connection.writeToStream(buffer: ptr, maxLength: length)
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
byteIndex += length
|
||||
bytesLeft -= length
|
||||
|
||||
if bytesLeft == 0 {
|
||||
self.dataToSend = nil
|
||||
byteIndex = 0
|
||||
}
|
||||
} else {
|
||||
print("writeBufferToStream failure")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func prepare(sample buffer: CMSampleBuffer) -> Data? {
|
||||
guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
|
||||
print("image buffer not available")
|
||||
return nil
|
||||
}
|
||||
|
||||
CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
|
||||
|
||||
let scaleFactor = 2.0
|
||||
let width = CVPixelBufferGetWidth(imageBuffer)/Int(scaleFactor)
|
||||
let height = CVPixelBufferGetHeight(imageBuffer)/Int(scaleFactor)
|
||||
let orientation = CMGetAttachment(buffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil)?.uintValue ?? 0
|
||||
|
||||
let scaleTransform = CGAffineTransform(scaleX: CGFloat(1.0/scaleFactor), y: CGFloat(1.0/scaleFactor))
|
||||
let bufferData = self.jpegData(from: imageBuffer, scale: scaleTransform)
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
|
||||
|
||||
guard let messageData = bufferData else {
|
||||
print("corrupted image buffer")
|
||||
return nil
|
||||
}
|
||||
|
||||
let httpResponse = CFHTTPMessageCreateResponse(nil, 200, nil, kCFHTTPVersion1_1).takeRetainedValue()
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Content-Length" as CFString, String(messageData.count) as CFString)
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Width" as CFString, String(width) as CFString)
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Height" as CFString, String(height) as CFString)
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Orientation" as CFString, String(orientation) as CFString)
|
||||
|
||||
CFHTTPMessageSetBody(httpResponse, messageData as CFData)
|
||||
|
||||
let serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse)?.takeRetainedValue() as Data?
|
||||
|
||||
return serializedMessage
|
||||
}
|
||||
|
||||
func jpegData(from buffer: CVPixelBuffer, scale scaleTransform: CGAffineTransform) -> Data? {
|
||||
var image = CIImage(cvPixelBuffer: buffer)
|
||||
image = image.transformed(by: scaleTransform)
|
||||
|
||||
guard let colorSpace = image.colorSpace else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0]
|
||||
let imageData = SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options)
|
||||
|
||||
return imageData
|
||||
}
|
||||
}
|
||||
205
ios/app/broadcast-extension/SocketConnection.swift
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
class SocketConnection: NSObject {
|
||||
var didOpen: (() -> Void)?
|
||||
var didClose: ((Error?) -> Void)?
|
||||
var streamHasSpaceAvailable: (() -> Void)?
|
||||
|
||||
private let filePath: String
|
||||
private var socketHandle: Int32 = -1
|
||||
private var address: sockaddr_un?
|
||||
|
||||
private var inputStream: InputStream?
|
||||
private var outputStream: OutputStream?
|
||||
|
||||
private var networkQueue: DispatchQueue?
|
||||
private var shouldKeepRunning = false
|
||||
|
||||
init?(filePath path: String) {
|
||||
filePath = path
|
||||
socketHandle = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
|
||||
|
||||
guard socketHandle != -1 else {
|
||||
print("failure: create socket")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func open() -> Bool {
|
||||
print("open socket connection")
|
||||
|
||||
guard FileManager.default.fileExists(atPath: filePath) else {
|
||||
print("failure: socket file missing")
|
||||
return false
|
||||
}
|
||||
|
||||
guard setupAddress() == true else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard connectSocket() == true else {
|
||||
return false
|
||||
}
|
||||
|
||||
setupStreams()
|
||||
|
||||
inputStream?.open()
|
||||
outputStream?.open()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func close() {
|
||||
unscheduleStreams()
|
||||
|
||||
inputStream?.delegate = nil
|
||||
outputStream?.delegate = nil
|
||||
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
inputStream = nil
|
||||
outputStream = nil
|
||||
}
|
||||
|
||||
func writeToStream(buffer: UnsafePointer<UInt8>, maxLength length: Int) -> Int {
|
||||
return outputStream?.write(buffer, maxLength: length) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
extension SocketConnection: StreamDelegate {
|
||||
|
||||
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
||||
switch eventCode {
|
||||
case .openCompleted:
|
||||
print("client stream open completed")
|
||||
if aStream == outputStream {
|
||||
didOpen?()
|
||||
}
|
||||
case .hasBytesAvailable:
|
||||
if aStream == inputStream {
|
||||
var buffer: UInt8 = 0
|
||||
let numberOfBytesRead = inputStream?.read(&buffer, maxLength: 1)
|
||||
if numberOfBytesRead == 0 && aStream.streamStatus == .atEnd {
|
||||
print("server socket closed")
|
||||
close()
|
||||
notifyDidClose(error: nil)
|
||||
}
|
||||
}
|
||||
case .hasSpaceAvailable:
|
||||
if aStream == outputStream {
|
||||
streamHasSpaceAvailable?()
|
||||
}
|
||||
case .errorOccurred:
|
||||
print("client stream error occurred: \(String(describing: aStream.streamError))")
|
||||
close()
|
||||
notifyDidClose(error: aStream.streamError)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SocketConnection {
|
||||
|
||||
func setupAddress() -> Bool {
|
||||
var addr = sockaddr_un()
|
||||
guard filePath.count < MemoryLayout.size(ofValue: addr.sun_path) else {
|
||||
print("failure: fd path is too long")
|
||||
return false
|
||||
}
|
||||
|
||||
_ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in
|
||||
filePath.withCString {
|
||||
strncpy(ptr, $0, filePath.count)
|
||||
}
|
||||
}
|
||||
|
||||
address = addr
|
||||
return true
|
||||
}
|
||||
|
||||
func connectSocket() -> Bool {
|
||||
guard var addr = address else {
|
||||
return false
|
||||
}
|
||||
|
||||
let status = withUnsafePointer(to: &addr) { ptr in
|
||||
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||
Darwin.connect(socketHandle, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
|
||||
}
|
||||
}
|
||||
|
||||
guard status == noErr else {
|
||||
print("failure: \(status)")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func setupStreams() {
|
||||
var readStream: Unmanaged<CFReadStream>?
|
||||
var writeStream: Unmanaged<CFWriteStream>?
|
||||
|
||||
CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle, &readStream, &writeStream)
|
||||
|
||||
inputStream = readStream?.takeRetainedValue()
|
||||
inputStream?.delegate = self
|
||||
inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
|
||||
|
||||
outputStream = writeStream?.takeRetainedValue()
|
||||
outputStream?.delegate = self
|
||||
outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
|
||||
|
||||
scheduleStreams()
|
||||
}
|
||||
|
||||
func scheduleStreams() {
|
||||
shouldKeepRunning = true
|
||||
|
||||
networkQueue = DispatchQueue.global(qos: .userInitiated)
|
||||
networkQueue?.async { [weak self] in
|
||||
self?.inputStream?.schedule(in: .current, forMode: .common)
|
||||
self?.outputStream?.schedule(in: .current, forMode: .common)
|
||||
|
||||
var isRunning = false
|
||||
|
||||
repeat {
|
||||
isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture)
|
||||
} while (isRunning)
|
||||
}
|
||||
}
|
||||
|
||||
func unscheduleStreams() {
|
||||
networkQueue?.sync { [weak self] in
|
||||
self?.inputStream?.remove(from: .current, forMode: .common)
|
||||
self?.outputStream?.remove(from: .current, forMode: .common)
|
||||
}
|
||||
|
||||
shouldKeepRunning = false
|
||||
}
|
||||
|
||||
func notifyDidClose(error: Error?) {
|
||||
if didClose != nil {
|
||||
didClose?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
ios/app/broadcast-extension/extension.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.jitsi.meet.appgroup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
78
ios/app/src/AppDelegate.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
import UIKit
|
||||
import Firebase
|
||||
import JitsiMeetSDK
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
let jitsiMeet = JitsiMeet.sharedInstance()
|
||||
|
||||
// jitsiMeet.webRtcLoggingSeverity = .verbose
|
||||
|
||||
jitsiMeet.conferenceActivityType = "org.jitsi.JitsiMeet.ios.conference" // Must match the one defined in Info.plist{}
|
||||
jitsiMeet.customUrlScheme = "org.jitsi.meet"
|
||||
jitsiMeet.universalLinkDomains = ["meet.jit.si", "alpha.jitsi.net", "beta.meet.jit.si"]
|
||||
|
||||
jitsiMeet.defaultConferenceOptions = JitsiMeetConferenceOptions.fromBuilder { builder in
|
||||
// For testing configOverrides a room needs to be set
|
||||
// builder.room = "https://meet.jit.si/test0988test"
|
||||
|
||||
builder.setFeatureFlag("welcomepage.enabled", withBoolean: true)
|
||||
builder.setFeatureFlag("ios.screensharing.enabled", withBoolean: true)
|
||||
builder.setFeatureFlag("ios.recording.enabled", withBoolean: true)
|
||||
}
|
||||
|
||||
jitsiMeet.application(application, didFinishLaunchingWithOptions: launchOptions ?? [:])
|
||||
|
||||
if self.appContainsRealServiceInfoPlist() {
|
||||
print("Enabling Firebase")
|
||||
FirebaseApp.configure()
|
||||
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(!jitsiMeet.isCrashReportingDisabled())
|
||||
}
|
||||
|
||||
let vc = ViewController()
|
||||
self.window?.rootViewController = vc
|
||||
jitsiMeet.showSplashScreen()
|
||||
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
print("Application will terminate!")
|
||||
if let rootController = self.window?.rootViewController as? ViewController {
|
||||
rootController.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Linking delegate methods
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
return JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
if url.absoluteString.contains("google/link/?dismiss=1&is_weak_match=1") {
|
||||
return false
|
||||
}
|
||||
|
||||
return JitsiMeet.sharedInstance().application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
return JitsiMeet.sharedInstance().application(application, supportedInterfaceOrientationsFor: window)
|
||||
}
|
||||
}
|
||||
|
||||
// Firebase utilities
|
||||
extension AppDelegate {
|
||||
func appContainsRealServiceInfoPlist() -> Bool {
|
||||
return InfoPlistUtil.containsRealServiceInfoPlist(in: Bundle.main)
|
||||
}
|
||||
}
|
||||
42
ios/app/src/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" userInteractionEnabled="NO" contentMode="center" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="4B8-Xf-NDE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.090196078431372548" green="0.62745098039215685" blue="0.85882352941176465" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="4B8-Xf-NDE" secondAttribute="bottom" id="aFF-BR-glX"/>
|
||||
<constraint firstItem="4B8-Xf-NDE" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="glR-YN-1GF"/>
|
||||
<constraint firstAttribute="trailing" secondItem="4B8-Xf-NDE" secondAttribute="trailing" id="tva-gl-jRX"/>
|
||||
<constraint firstItem="4B8-Xf-NDE" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="yaV-1V-oEh"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="548" y="455"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="LaunchScreen" width="480" height="480"/>
|
||||
</resources>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@2x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "AppIcon-1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@2x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "AppIcon-1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/app/src/Images.xcassets/CallKitIcon.imageset/CallKit@1x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
ios/app/src/Images.xcassets/CallKitIcon.imageset/CallKit@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
ios/app/src/Images.xcassets/CallKitIcon.imageset/CallKit@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
23
ios/app/src/Images.xcassets/CallKitIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallKit@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallKit@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CallKit@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
6
ios/app/src/Images.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
ios/app/src/Images.xcassets/LaunchScreen.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "LaunchScreen-480@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "LaunchScreen-480@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "LaunchScreen-480@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/app/src/Images.xcassets/LaunchScreen.imageset/LaunchScreen-480@1x.png
vendored
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
ios/app/src/Images.xcassets/LaunchScreen.imageset/LaunchScreen-480@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
ios/app/src/Images.xcassets/LaunchScreen.imageset/LaunchScreen-480@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 278 KiB |
107
ios/app/src/Info.plist
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Jitsi Meet</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>org.jitsi.meet</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>org.jitsi.meet</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.googleusercontent.apps</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.your-id-here</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>FirebaseCrashlyticsCollectionEnabled</key>
|
||||
<string>false</string>
|
||||
<key>FirebaseScreenReportingEnabled</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>See your scheduled meetings in the app.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Participate in meetings with video.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Local network is used for establishing Peer-to-Peer connections.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Participate in meetings with voice.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>org.jitsi.JitsiMeet.ios.conference</string>
|
||||
</array>
|
||||
<key>RTCAppGroupIdentifier</key>
|
||||
<string>group.org.jitsi.meet.appgroup</string>
|
||||
<key>RTCScreenSharingExtension</key>
|
||||
<string>org.jitsi.meet.broadcast.extension</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
94
ios/app/src/ViewController.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright @ 2025-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CoreSpotlight
|
||||
import Intents
|
||||
import MobileCoreServices
|
||||
import UIKit
|
||||
|
||||
import JitsiMeetSDK
|
||||
|
||||
@objcMembers
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func loadView() {
|
||||
let jitsiView = JitsiMeetView(frame: UIScreen.main.bounds)
|
||||
self.view = jitsiView
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let view = self.view as? JitsiMeetView else { return }
|
||||
view.delegate = self
|
||||
view.join(JitsiMeet.sharedInstance().getInitialConferenceOptions())
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
func terminate() {
|
||||
guard let view = self.view as? JitsiMeetView else { return }
|
||||
view.leave()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension ViewController: @preconcurrency JitsiMeetViewDelegate {
|
||||
|
||||
// MARK: - Private Helper Methods
|
||||
|
||||
private func onJitsiMeetViewDelegateEvent(_ name: String, withData data: [AnyHashable: Any]?) {
|
||||
NSLog("[%@:%d] JitsiMeetViewDelegate %@ %@", #file, #line, name, data ?? [:])
|
||||
|
||||
#if DEBUG
|
||||
assert(Thread.isMainThread, "JitsiMeetViewDelegate \(name) method invoked on a non-main thread")
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - JitsiMeetViewDelegate
|
||||
|
||||
func conferenceJoined(_ data: [AnyHashable: Any]) {
|
||||
onJitsiMeetViewDelegateEvent("CONFERENCE_JOINED", withData: data)
|
||||
|
||||
// Register a NSUserActivity for this conference so it can be invoked as a Siri shortcut.
|
||||
// Must match the one defined in Info.plist
|
||||
let userActivity = NSUserActivity(activityType: "org.jitsi.JitsiMeet.ios.conference")
|
||||
|
||||
if let urlStr = data["url"] as? String,
|
||||
let url = URL(string: urlStr),
|
||||
let conference = url.pathComponents.last {
|
||||
|
||||
userActivity.title = "Join \(conference)"
|
||||
userActivity.suggestedInvocationPhrase = "Join my Jitsi meeting"
|
||||
userActivity.userInfo = ["url": urlStr]
|
||||
userActivity.isEligibleForSearch = true
|
||||
userActivity.isEligibleForPrediction = true
|
||||
userActivity.persistentIdentifier = urlStr
|
||||
|
||||
// Subtitle
|
||||
let attributes = CSSearchableItemAttributeSet(contentType: UTType.item)
|
||||
attributes.contentDescription = urlStr
|
||||
userActivity.contentAttributeSet = attributes
|
||||
|
||||
self.userActivity = userActivity
|
||||
userActivity.becomeCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
func ready(toClose data: [AnyHashable: Any]) {
|
||||
onJitsiMeetViewDelegateEvent("READY_TO_CLOSE", withData: data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "24x24",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-24@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "notificationCenter",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "27.5x27.5",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-27.5@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "notificationCenter",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-29@2x.png",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-29@3x.png",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-40@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "44x44",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-88@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-100@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"size" : "86x86",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-86@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "98x98",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-98@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"size" : "108x108",
|
||||
"idiom" : "watch",
|
||||
"filename" : "Icon-216@2x.png",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "watch-marketing",
|
||||
"filename" : "Icon-1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
6
ios/app/watchos/app/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
21
ios/app/watchos/app/Assets.xcassets/hangup.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "hangup@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/app/watchos/app/Assets.xcassets/hangup.imageset/hangup@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
21
ios/app/watchos/app/Assets.xcassets/mute-off.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "mute-off@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/app/watchos/app/Assets.xcassets/mute-off.imageset/mute-off@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
21
ios/app/watchos/app/Assets.xcassets/mute-on.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "mute-on@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/app/watchos/app/Assets.xcassets/mute-on.imageset/mute-on@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
85
ios/app/watchos/app/Base.lproj/Interface.storyboard
Normal file
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="14490.70" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="AgC-eL-Hgc">
|
||||
<device id="watch38" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="watchOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="14490.21"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Meetings-->
|
||||
<scene sceneID="aou-V4-d1y">
|
||||
<objects>
|
||||
<controller title="Meetings" id="AgC-eL-Hgc" customClass="InterfaceController" customModule="JitsiMeetCompanion" customModuleProvider="target">
|
||||
<items>
|
||||
<label alignment="left" textAlignment="left" numberOfLines="0" id="OQN-sx-tDt"/>
|
||||
<table alignment="left" id="gpO-ql-Xsr">
|
||||
<items>
|
||||
<tableRow identifier="MeetingRowType" id="GGl-av-xeJ" customClass="MeetingRowController" customModule="JitsiMeetCompanion_Extension">
|
||||
<group key="rootItem" width="1" height="0.0" alignment="left" layout="vertical" id="5XE-gq-qzG">
|
||||
<items>
|
||||
<label alignment="left" text="Label" id="Sij-up-N4p"/>
|
||||
<label alignment="left" text="Label" id="V5K-sm-jEH">
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<fontDescription key="font" style="UICTFontTextStyleFootnote"/>
|
||||
</label>
|
||||
</items>
|
||||
<connections>
|
||||
<segue destination="9RD-qP-1Z0" kind="push" id="Boa-6E-eZs"/>
|
||||
</connections>
|
||||
</group>
|
||||
<connections>
|
||||
<outlet property="roomLabel" destination="Sij-up-N4p" id="PdS-SO-ylc"/>
|
||||
<outlet property="rowGroup" destination="5XE-gq-qzG" id="GZN-2c-2Gz"/>
|
||||
<outlet property="timeLabel" destination="V5K-sm-jEH" id="fWQ-kx-vE4"/>
|
||||
</connections>
|
||||
</tableRow>
|
||||
</items>
|
||||
</table>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="infoLabel" destination="OQN-sx-tDt" id="Juv-tb-SNj"/>
|
||||
<outlet property="table" destination="gpO-ql-Xsr" id="aVV-iZ-z3l"/>
|
||||
</connections>
|
||||
</controller>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-99" y="117"/>
|
||||
</scene>
|
||||
<!--Meetings-->
|
||||
<scene sceneID="ns4-Kh-qqU">
|
||||
<objects>
|
||||
<controller identifier="InCallController" title="Meetings" hidesWhenLoading="NO" id="9RD-qP-1Z0" customClass="InCallController" customModule="JitsiMeetCompanion" customModuleProvider="target">
|
||||
<items>
|
||||
<label alignment="center" text="Label" id="vFt-lL-SNY"/>
|
||||
<timer alignment="center" textAlignment="center" previewedSeconds="0" id="W8S-uZ-MPm">
|
||||
<color key="textColor" red="0.024725984125768763" green="1" blue="0.24241188365329402" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="font" style="UICTFontTextStyleHeadline"/>
|
||||
</timer>
|
||||
<group alignment="center" verticalAlignment="bottom" spacing="10" id="Hfk-a0-uWj">
|
||||
<items>
|
||||
<button width="60" height="60" alignment="left" verticalAlignment="bottom" backgroundImage="hangup" id="8jF-SI-UHz">
|
||||
<connections>
|
||||
<action selector="hangupClicked" destination="9RD-qP-1Z0" id="cXK-lw-tsd"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button width="60" height="60" alignment="right" verticalAlignment="bottom" backgroundImage="mute-off" id="LmN-FI-aQq">
|
||||
<connections>
|
||||
<action selector="muteClicked" destination="9RD-qP-1Z0" id="dJg-kV-cqH"/>
|
||||
</connections>
|
||||
</button>
|
||||
</items>
|
||||
</group>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="mutedButton" destination="LmN-FI-aQq" id="gfi-4T-gdN"/>
|
||||
<outlet property="roomLabel" destination="vFt-lL-SNY" id="cYB-Tf-Efz"/>
|
||||
<outlet property="timer" destination="W8S-uZ-MPm" id="r7T-j1-9VJ"/>
|
||||
</connections>
|
||||
</controller>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="213" y="117"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
33
ios/app/watchos/app/Info.plist
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Jitsi Meet</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>org.jitsi.meet</string>
|
||||
<key>WKWatchKitApp</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
6
ios/app/watchos/extension/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
21
ios/app/watchos/extension/Assets.xcassets/jitsi.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "jitsi@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/app/watchos/extension/Assets.xcassets/jitsi.imageset/jitsi@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
81
ios/app/watchos/extension/ComplicationController.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ClockKit
|
||||
|
||||
|
||||
class ComplicationController: NSObject, CLKComplicationDataSource {
|
||||
|
||||
// MARK: - Timeline Configuration
|
||||
|
||||
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
|
||||
handler([])
|
||||
}
|
||||
|
||||
func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
|
||||
handler(.showOnLockScreen)
|
||||
}
|
||||
|
||||
// MARK: - Timeline Population
|
||||
|
||||
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
|
||||
// Call the handler with the current timeline entry
|
||||
getLocalizableSampleTemplate(for: complication) {template in
|
||||
guard let template = template else {
|
||||
handler(nil)
|
||||
return
|
||||
}
|
||||
handler(CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template))
|
||||
}
|
||||
}
|
||||
|
||||
func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
|
||||
// Call the handler with the timeline entries prior to the given date
|
||||
handler(nil)
|
||||
}
|
||||
|
||||
func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
|
||||
// Call the handler with the timeline entries after to the given date
|
||||
handler(nil)
|
||||
}
|
||||
|
||||
// MARK: - Placeholder Templates
|
||||
|
||||
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
|
||||
// This method will be called once per supported complication, and the results will be cached
|
||||
|
||||
let imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "jitsi")!)
|
||||
if complication.family == .circularSmall {
|
||||
let small = CLKComplicationTemplateCircularSmallRingImage()
|
||||
small.imageProvider = imageProvider
|
||||
small.ringStyle = .closed
|
||||
small.fillFraction = 0
|
||||
handler(small)
|
||||
} else if complication.family == .utilitarianSmall {
|
||||
let utilitarian = CLKComplicationTemplateUtilitarianSmallSquare()
|
||||
utilitarian.imageProvider = imageProvider
|
||||
handler(utilitarian)
|
||||
} else if complication.family == .modularSmall {
|
||||
let modular = CLKComplicationTemplateModularSmallRingImage()
|
||||
modular.imageProvider = imageProvider
|
||||
modular.ringStyle = .closed
|
||||
modular.fillFraction = 0
|
||||
handler(modular)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
103
ios/app/watchos/extension/ExtensionDelegate.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import WatchConnectivity
|
||||
import WatchKit
|
||||
|
||||
class ExtensionDelegate: NSObject, WCSessionDelegate, WKExtensionDelegate {
|
||||
|
||||
var currentContext : JitsiMeetContext = JitsiMeetContext()
|
||||
|
||||
static var currentJitsiMeetContext: JitsiMeetContext {
|
||||
get {
|
||||
return (WKExtension.shared().delegate as! ExtensionDelegate).currentContext
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching() {
|
||||
// Start Watch Connectivity
|
||||
if WCSession.isSupported() {
|
||||
let session = WCSession.default
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
}
|
||||
|
||||
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
|
||||
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
|
||||
for task in backgroundTasks {
|
||||
// Use a switch statement to check the task type
|
||||
switch task {
|
||||
case let backgroundTask as WKApplicationRefreshBackgroundTask:
|
||||
// Be sure to complete the background task once you’re done.
|
||||
backgroundTask.setTaskCompletedWithSnapshot(false)
|
||||
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
|
||||
// Snapshot tasks have a unique completion call, make sure to set your expiration date
|
||||
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
|
||||
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
|
||||
// Be sure to complete the connectivity task once you’re done.
|
||||
connectivityTask.setTaskCompletedWithSnapshot(false)
|
||||
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
|
||||
// Be sure to complete the URL session task once you’re done.
|
||||
urlSessionTask.setTaskCompletedWithSnapshot(false)
|
||||
default:
|
||||
// make sure to complete unhandled task types
|
||||
task.setTaskCompletedWithSnapshot(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, activationDidCompleteWith
|
||||
activationState: WCSessionActivationState, error: Error?) {
|
||||
if let error = error {
|
||||
print("WATCH Session activation failed with error: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
print("WATCH Session activated with state: \(activationState.rawValue)")
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
||||
DispatchQueue.main.async {
|
||||
let newContext = JitsiMeetContext(context: applicationContext)
|
||||
|
||||
print("WATCH got new context: \(newContext.description)");
|
||||
|
||||
// Update context on the root controller which displays the recent list
|
||||
let controller = WKExtension.shared().rootInterfaceController as! InterfaceController
|
||||
controller.updateUI(newContext)
|
||||
|
||||
// If the current controller is not the in-call controller and we have a
|
||||
// conference URL, show the in-call controller
|
||||
if let currentController = WKExtension.shared().visibleInterfaceController as? InterfaceController {
|
||||
// Go to the in-call controller only if the conference URL has changed, because the user may have
|
||||
// clicked the back button
|
||||
if newContext.conferenceURL != nil
|
||||
&& self.currentContext.conferenceURL != newContext.conferenceURL {
|
||||
currentController.pushController(withName: "InCallController", context: newContext)
|
||||
}
|
||||
} else if let inCallController = WKExtension.shared().visibleInterfaceController as? InCallController {
|
||||
if newContext.conferenceURL == nil {
|
||||
inCallController.popToRootController()
|
||||
} else {
|
||||
inCallController.updateUI(newContext)
|
||||
}
|
||||
}
|
||||
|
||||
self.currentContext = newContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
ios/app/watchos/extension/InCallController.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import WatchConnectivity
|
||||
import WatchKit
|
||||
import Foundation
|
||||
|
||||
|
||||
class InCallController: WKInterfaceController {
|
||||
@IBOutlet var mutedButton: WKInterfaceButton!
|
||||
@IBOutlet var roomLabel: WKInterfaceLabel!
|
||||
@IBOutlet var timer: WKInterfaceTimer!
|
||||
|
||||
@IBAction func hangupClicked() {
|
||||
sendCommand(JitsiMeetCommands.CMD_HANG_UP, message: nil)
|
||||
}
|
||||
|
||||
@IBAction func muteClicked() {
|
||||
if var micMuted = ExtensionDelegate.currentJitsiMeetContext.micMuted {
|
||||
micMuted = !micMuted;
|
||||
sendCommand(
|
||||
JitsiMeetCommands.CMD_SET_MUTED,
|
||||
message: [
|
||||
"muted": micMuted ? "true" : "false"
|
||||
])
|
||||
updateMutedButton(withMuted: micMuted)
|
||||
}
|
||||
}
|
||||
|
||||
func sendCommand(_ command: JitsiMeetCommands, message: [String : Any]?) {
|
||||
if WCSession.isSupported() {
|
||||
let session = WCSession.default
|
||||
var data = [String: Any]()
|
||||
|
||||
if let sessionID = ExtensionDelegate.currentJitsiMeetContext.sessionID {
|
||||
if message != nil {
|
||||
message!.forEach { data[$0] = $1 }
|
||||
}
|
||||
|
||||
data["command"] = command.rawValue;
|
||||
data["sessionID"] = sessionID;
|
||||
|
||||
session.sendMessage(data, replyHandler: nil, errorHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI(_ newContext: JitsiMeetContext) {
|
||||
var conferenceURL = newContext.conferenceURL
|
||||
|
||||
if let joinConferenceURL = newContext.joinConferenceURL {
|
||||
sendCommand(JitsiMeetCommands.CMD_JOIN_CONFERENCE, message: [ "data" : joinConferenceURL ])
|
||||
conferenceURL = joinConferenceURL
|
||||
}
|
||||
|
||||
let newRoomName = conferenceURL != nil ? conferenceURL!.components(separatedBy: "/").last : ""
|
||||
|
||||
roomLabel.setText(newRoomName)
|
||||
|
||||
if let newTimestamp = newContext.conferenceTimestamp {
|
||||
restartTimer(newTimestamp)
|
||||
}
|
||||
if let newMuted = newContext.micMuted {
|
||||
updateMutedButton(withMuted: newMuted)
|
||||
}
|
||||
}
|
||||
|
||||
func restartTimer(_ conferenceTimestamp: Int64) {
|
||||
if (conferenceTimestamp != 0) {
|
||||
let newDate = Date(timeIntervalSince1970: TimeInterval(conferenceTimestamp / 1000))
|
||||
timer.setDate(newDate)
|
||||
timer.start();
|
||||
print("WATCH timer set date to: \(newDate) and start")
|
||||
} else {
|
||||
print("WATCH timer stop")
|
||||
timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
func updateMutedButton(withMuted isMuted: Bool) {
|
||||
if isMuted {
|
||||
mutedButton.setBackgroundImageNamed("mute-on.png")
|
||||
} else {
|
||||
mutedButton.setBackgroundImageNamed("mute-off.png")
|
||||
}
|
||||
}
|
||||
|
||||
override func awake(withContext context: Any?) {
|
||||
super.awake(withContext: context)
|
||||
|
||||
if let data = context as? JitsiMeetContext {
|
||||
updateUI(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
ios/app/watchos/extension/Info.plist
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Jitsi Meet Companion Extension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
|
||||
<key>CLKComplicationSupportedFamilies</key>
|
||||
<array>
|
||||
<string>CLKComplicationFamilyModularSmall</string>
|
||||
<string>CLKComplicationFamilyUtilitarianSmall</string>
|
||||
<string>CLKComplicationFamilyCircularSmall</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>WKAppBundleIdentifier</key>
|
||||
<string>org.jitsi.meet.watchkit</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.watchkit</string>
|
||||
</dict>
|
||||
<key>WKExtensionDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
|
||||
</dict>
|
||||
</plist>
|
||||
94
ios/app/watchos/extension/InterfaceController.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import WatchKit
|
||||
import WatchConnectivity
|
||||
import Foundation
|
||||
|
||||
|
||||
class InterfaceController: WKInterfaceController {
|
||||
|
||||
@IBOutlet var infoLabel: WKInterfaceLabel!
|
||||
@IBOutlet var table: WKInterfaceTable!
|
||||
|
||||
override func didAppear(){
|
||||
self.updateUI(ExtensionDelegate.currentJitsiMeetContext)
|
||||
}
|
||||
|
||||
func updateUI(_ newContext:JitsiMeetContext) {
|
||||
if (newContext.recentURLs == nil || newContext.recentURLs!.count == 0) {
|
||||
infoLabel.setText("There are no recent meetings. Please use the app on the phone to start a new call.")
|
||||
|
||||
table.setHidden(true)
|
||||
infoLabel.setHidden(false)
|
||||
} else {
|
||||
updateRecents(withRecents: newContext.recentURLs!, currentContext: newContext)
|
||||
|
||||
table.setHidden(false)
|
||||
infoLabel.setHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRecents(withRecents recents: NSArray, currentContext: JitsiMeetContext) {
|
||||
// Updating the # of rows only if it actually changed prevents from blinking the UI
|
||||
if (table.numberOfRows != recents.count) {
|
||||
table.setNumberOfRows(recents.count, withRowType: "MeetingRowType")
|
||||
}
|
||||
|
||||
for (index, entry) in recents.enumerated() {
|
||||
let entryDict = entry as! NSDictionary
|
||||
let roomURL = entryDict["conference"] as! NSString
|
||||
let timestamp = entryDict["date"] as! NSNumber
|
||||
|
||||
// Prepare values
|
||||
let room = roomURL.components(separatedBy: "/").last
|
||||
let date = Date(timeIntervalSince1970: timestamp.doubleValue / 1000) // timestamp is taken with Date.now() in JS, which uses milliseconds
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.timeZone = TimeZone.current
|
||||
dateFormatter.locale = NSLocale.current
|
||||
dateFormatter.dateFormat = "HH:mm yyyy-MM-dd"
|
||||
let strDate = dateFormatter.string(from: date)
|
||||
|
||||
// Update row controller
|
||||
let controller = table.rowController(at: index) as! MeetingRowController
|
||||
controller.room = room
|
||||
controller.roomUrl = roomURL as String
|
||||
controller.roomLabel.setText(room)
|
||||
controller.timeLabel.setText(strDate)
|
||||
|
||||
// Change the background for the active meeting
|
||||
if (controller.roomUrl == currentContext.conferenceURL) {
|
||||
controller.rowGroup.setBackgroundColor(UIColor(red: 0.125, green: 0.58, blue: 0.98, alpha: 1))
|
||||
} else {
|
||||
controller.rowGroup.setBackgroundColor(UIColor(red: 0.949, green: 0.956, blue: 1, alpha: 0.14))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
|
||||
let controller = table.rowController(at: rowIndex) as! MeetingRowController
|
||||
let currentContext = ExtensionDelegate.currentJitsiMeetContext
|
||||
|
||||
// Copy the current context and add the joinConferenceURL to trigger the command when the in-call screen is displayed
|
||||
let actionContext = JitsiMeetContext(jmContext: currentContext)
|
||||
actionContext.joinConferenceURL = controller.roomUrl
|
||||
|
||||
print("WATCH contextForSegue: \(actionContext.description)");
|
||||
|
||||
return actionContext;
|
||||
}
|
||||
}
|
||||
27
ios/app/watchos/extension/JitsiMeetCommands.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This needs to be in sync with features/mobile/watchos/constants.js
|
||||
enum JitsiMeetCommands : String {
|
||||
typealias RawValue = String
|
||||
|
||||
case CMD_HANG_UP = "hangup";
|
||||
|
||||
case CMD_JOIN_CONFERENCE = "joinConference";
|
||||
|
||||
case CMD_SET_MUTED = "setMuted";
|
||||
}
|
||||
71
ios/app/watchos/extension/JitsiMeetContext.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
class JitsiMeetContext {
|
||||
private var dictionary : [String : Any]
|
||||
|
||||
var joinConferenceURL : String? = nil;
|
||||
|
||||
init() {
|
||||
dictionary = [:]
|
||||
}
|
||||
|
||||
init(context: [String : Any]) {
|
||||
dictionary = context
|
||||
}
|
||||
|
||||
init(jmContext: JitsiMeetContext) {
|
||||
dictionary = jmContext.dictionary
|
||||
joinConferenceURL = jmContext.joinConferenceURL
|
||||
}
|
||||
|
||||
var conferenceURL : String? {
|
||||
get {
|
||||
return dictionary["conferenceURL"] as? String
|
||||
}
|
||||
}
|
||||
|
||||
var conferenceTimestamp : Int64? {
|
||||
get {
|
||||
return dictionary["conferenceTimestamp"] as? Int64;
|
||||
}
|
||||
}
|
||||
|
||||
var sessionID : Int64? {
|
||||
get {
|
||||
return dictionary["sessionID"] as? Int64;
|
||||
}
|
||||
}
|
||||
|
||||
var recentURLs : NSArray? {
|
||||
get {
|
||||
return dictionary["recentURLs"] as? NSArray
|
||||
}
|
||||
}
|
||||
|
||||
var micMuted : Bool? {
|
||||
get {
|
||||
return (dictionary["micMuted"] as? NSNumber)?.boolValue ?? nil;
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "JitsiMeetContext[conferenceURL: \(String(describing: conferenceURL)), conferenceTimestamp: \(String(describing:conferenceTimestamp)), sessionID: \(String(describing:sessionID)), recentURLs: \(String(describing:recentURLs)), joinConferenceURL: \(String(describing:joinConferenceURL)) "
|
||||
}
|
||||
}
|
||||
27
ios/app/watchos/extension/MeetingRowController.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import WatchKit
|
||||
|
||||
class MeetingRowController: NSObject {
|
||||
@IBOutlet var roomLabel: WKInterfaceLabel!
|
||||
@IBOutlet var timeLabel: WKInterfaceLabel!
|
||||
@IBOutlet var rowGroup: WKInterfaceGroup!
|
||||
|
||||
var room: String!
|
||||
var roomUrl: String!
|
||||
}
|
||||
15
ios/apple-app-site-association
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"applinks": {
|
||||
"apps": [],
|
||||
"details": [
|
||||
{
|
||||
"appID": "BQNXB4G3KQ.org.jitsi.JitsiMeet.ios",
|
||||
"paths": [ "*" ]
|
||||
},
|
||||
{
|
||||
"appID": "UPXU4CQZ5P.com.atlassian.JitsiMeet.ios",
|
||||
"paths": [ "*" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
4
ios/fastlane/Appfile
Normal file
@@ -0,0 +1,4 @@
|
||||
app_identifier("com.atlassian.JitsiMeet.ios") # The bundle identifier of your app
|
||||
itc_team_id("304592") # App Store Connect Team ID
|
||||
team_id("FC967L3QRG") # Developer Portal Team ID
|
||||
|
||||
111
ios/fastlane/Fastfile
Normal file
@@ -0,0 +1,111 @@
|
||||
ENV["FASTLANE_SKIP_UPDATE_CHECK"] = "1"
|
||||
opt_out_usage
|
||||
|
||||
default_platform(:ios)
|
||||
|
||||
platform :ios do
|
||||
desc "Push a new beta build to TestFlight"
|
||||
lane :deploy do
|
||||
# Make sure we are on a clean tree
|
||||
ensure_git_status_clean
|
||||
|
||||
# Connect to Apple Store Connect
|
||||
app_store_connect_api_key(
|
||||
key_id: ENV["ASC_KEY_ID"],
|
||||
issuer_id: ENV["ASC_ISSUER_ID"],
|
||||
key_content: ENV["ASC_KEY_CONTENT"],
|
||||
duration: 1200,
|
||||
in_house: false
|
||||
)
|
||||
|
||||
# Set the app identifier
|
||||
update_app_identifier(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "src/Info.plist",
|
||||
app_identifier: "com.atlassian.JitsiMeet.ios"
|
||||
)
|
||||
|
||||
# Set the broadcast extension identifier
|
||||
update_app_identifier(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "broadcast-extension/Info.plist",
|
||||
app_identifier: "com.atlassian.JitsiMeet.ios.broadcast"
|
||||
)
|
||||
update_info_plist(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "src/Info.plist",
|
||||
block: proc do |plist|
|
||||
plist["RTCScreenSharingExtension"] = "com.atlassian.JitsiMeet.ios.broadcast"
|
||||
end
|
||||
)
|
||||
|
||||
# Set the (watch) app identifier
|
||||
update_app_identifier(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "watchos/app/Info.plist",
|
||||
app_identifier: "com.atlassian.JitsiMeet.ios.watchkit"
|
||||
)
|
||||
|
||||
# Set the (watch) extension identifier
|
||||
update_app_identifier(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "watchos/extension/Info.plist",
|
||||
app_identifier: "com.atlassian.JitsiMeet.ios.watchkit.extension"
|
||||
)
|
||||
|
||||
update_info_plist(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "watchos/app/Info.plist",
|
||||
block: proc do |plist|
|
||||
plist["WKCompanionAppBundleIdentifier"] = "com.atlassian.JitsiMeet.ios"
|
||||
end
|
||||
)
|
||||
|
||||
update_info_plist(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "watchos/extension/Info.plist",
|
||||
block: proc do |plist|
|
||||
plist["NSExtension"]["NSExtensionAttributes"]["WKAppBundleIdentifier"] = "com.atlassian.JitsiMeet.ios.watchkit"
|
||||
end
|
||||
)
|
||||
|
||||
# Increment the build number by 1
|
||||
increment_build_number(
|
||||
build_number: Time.now.to_i,
|
||||
xcodeproj: "app/app.xcodeproj"
|
||||
)
|
||||
|
||||
# Actually build the app
|
||||
build_app(
|
||||
scheme: "JitsiMeet",
|
||||
include_symbols: true,
|
||||
export_xcargs: "-allowProvisioningUpdates",
|
||||
xcodebuild_formatter: ""
|
||||
)
|
||||
|
||||
# Upload the build to TestFlight
|
||||
upload_to_testflight(
|
||||
beta_app_description: ENV["JITSI_CHANGELOG"],
|
||||
beta_app_feedback_email: ENV["JITSI_REVIEW_EMAIL"],
|
||||
beta_app_review_info: {
|
||||
contact_email: ENV["JITSI_REVIEW_EMAIL"],
|
||||
contact_first_name: ENV["JITSI_REVIEW_NAME"],
|
||||
contact_last_name: ENV["JITSI_REVIEW_SURNAME"],
|
||||
contact_phone: ENV["JITSI_REVIEW_PHONE"],
|
||||
demo_account_name: ENV["JITSI_DEMO_ACCOUNT"],
|
||||
demo_account_password: ENV["JITSI_DEMO_PASSWORD"],
|
||||
},
|
||||
changelog: ENV["JITSI_CHANGELOG"],
|
||||
demo_account_required: false,
|
||||
distribute_external: true,
|
||||
groups: ENV["JITSI_BETA_TESTING_GROUPS"],
|
||||
uses_non_exempt_encryption: false
|
||||
)
|
||||
|
||||
upload_symbols_to_crashlytics
|
||||
|
||||
# Cleanup
|
||||
clean_build_artifacts
|
||||
reset_git_repo(skip_clean: true)
|
||||
end
|
||||
end
|
||||
29
ios/fastlane/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
fastlane documentation
|
||||
================
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
Install _fastlane_ using
|
||||
```
|
||||
[sudo] gem install fastlane -NV
|
||||
```
|
||||
or alternatively using `brew cask install fastlane`
|
||||
|
||||
# Available Actions
|
||||
## iOS
|
||||
### ios deploy
|
||||
```
|
||||
fastlane ios deploy
|
||||
```
|
||||
Push a new beta build to TestFlight
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
|
||||
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
|
||||
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
BIN
ios/fastlane/screenshots/Feature-Graphic-1024x500-1-1.png
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
ios/fastlane/screenshots/Feature-Graphic-1024x500-1.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
ios/fastlane/screenshots/Feature-Graphic-1024x500-2.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
ios/fastlane/screenshots/Feature-Graphic-1024x500-3.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
ios/fastlane/screenshots/GroupCallIphone8Plus.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
ios/fastlane/screenshots/GroupCallIphone8PlusDevice.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
ios/fastlane/screenshots/MoreMenu8Plus.png
Normal file
|
After Width: | Height: | Size: 779 KiB |
BIN
ios/fastlane/screenshots/MoreMenu8PlusDevice.png
Normal file
|
After Width: | Height: | Size: 888 KiB |
BIN
ios/fastlane/screenshots/WelcomePage8Plus.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
ios/fastlane/screenshots/WelcomePage8PlusDevice.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
ios/fastlane/screenshots/iPad-9.7-inch-In-Call-1.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
ios/fastlane/screenshots/iPad-9.7-inch-In-Call-2.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
ios/fastlane/screenshots/iPad-9.7-inch-Welcome.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
ios/fastlane/screenshots/iPadCall.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
ios/fastlane/screenshots/iPadCallDevice.png
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
BIN
ios/fastlane/screenshots/iPhone 8 Plus Calendar.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |