mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 15:22:39 +00:00
Merge pull request #7566 from vector-im/mauroromito/broadcast_extension
Broadcast Upload Extension
This commit is contained in:
commit
c87b8e25e1
23 changed files with 767 additions and 0 deletions
30
BroadcastUploadExtension/Common.xcconfig
Normal file
30
BroadcastUploadExtension/Common.xcconfig
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Copyright 2023 Vector Creations 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.
|
||||
//
|
||||
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
#include "Config/AppIdentifiers.xcconfig"
|
||||
#include "Config/AppVersion.xcconfig"
|
||||
|
||||
PRODUCT_NAME = BroadcastUploadExtension
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(BROADCAST_UPLOAD_EXTENSION_BUNDLE_IDENTIFIER)
|
||||
|
||||
INFOPLIST_FILE = BroadcastUploadExtension/SupportingFiles/Info.plist
|
||||
|
||||
CODE_SIGN_ENTITLEMENTS = BroadcastUploadExtension/SupportingFiles/BroadcastUploadExtension.entitlements
|
||||
|
||||
SKIP_INSTALL = YES
|
21
BroadcastUploadExtension/Debug.xcconfig
Normal file
21
BroadcastUploadExtension/Debug.xcconfig
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Copyright 2020 Vector Creations 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.
|
||||
//
|
||||
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
#include "Common.xcconfig"
|
||||
#include "Pods/Target Support Files/Pods-RiotPods-BroadcastUploadExtension/Pods-RiotPods-BroadcastUploadExtension.debug.xcconfig"
|
26
BroadcastUploadExtension/Release.xcconfig
Normal file
26
BroadcastUploadExtension/Release.xcconfig
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright 2020 Vector Creations 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.
|
||||
//
|
||||
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
#include "Common.xcconfig"
|
||||
#include "Pods/Target Support Files/Pods-RiotPods-BroadcastUploadExtension/Pods-RiotPods-BroadcastUploadExtension.release.xcconfig"
|
||||
|
||||
PROVISIONING_PROFILE = $(BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE)
|
||||
PROVISIONING_PROFILE_SPECIFIER = $(BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER)
|
||||
|
||||
COPY_PHASE_STRIP = NO
|
41
BroadcastUploadExtension/Sources/Atomic.swift
Normal file
41
BroadcastUploadExtension/Sources/Atomic.swift
Normal file
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// License from the original repository:
|
||||
// https://github.com/jitsi/jitsi-meet-sdk-samples/blob/master/LICENSE
|
||||
|
||||
//
|
||||
// Atomic.swift
|
||||
// Broadcast Extension
|
||||
//
|
||||
// Created by Maksym Shcheglov.
|
||||
// https://www.onswiftwings.com/posts/atomic-property-wrapper/
|
||||
//
|
||||
|
||||
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 { 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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// License from the original repository:
|
||||
// https://github.com/jitsi/jitsi-meet-sdk-samples/blob/master/LICENSE
|
||||
|
||||
//
|
||||
// DarwinNotificationCenter.swift
|
||||
// Broadcast Extension
|
||||
//
|
||||
// Created by Alex-Dan Bumbu on 23/03/2021.
|
||||
// Copyright © 2021 8x8, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DarwinNotification: String {
|
||||
case broadcastStarted = "iOS_BroadcastStarted"
|
||||
case broadcastStopped = "iOS_BroadcastStopped"
|
||||
}
|
||||
|
||||
class DarwinNotificationCenter {
|
||||
|
||||
static let shared = DarwinNotificationCenter()
|
||||
|
||||
private let notificationCenter: CFNotificationCenter
|
||||
|
||||
init() {
|
||||
notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
}
|
||||
|
||||
func postNotification(_ name: DarwinNotification) {
|
||||
CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true)
|
||||
}
|
||||
}
|
125
BroadcastUploadExtension/Sources/SampleHandler.swift
Normal file
125
BroadcastUploadExtension/Sources/SampleHandler.swift
Normal file
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// License from the original repository:
|
||||
// https://github.com/jitsi/jitsi-meet-sdk-samples/blob/master/LICENSE
|
||||
//
|
||||
// SampleHandler.swift
|
||||
// Broadcast Extension
|
||||
//
|
||||
// Created by Alex-Dan Bumbu on 04.06.2021.
|
||||
//
|
||||
|
||||
import ReplayKit
|
||||
|
||||
import MatrixSDK
|
||||
|
||||
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 = BuildSettings.applicationGroupIdentifier
|
||||
}
|
||||
|
||||
class SampleHandler: RPBroadcastSampleHandler {
|
||||
|
||||
private var clientConnection: SocketConnection?
|
||||
private var uploader: SampleUploader?
|
||||
|
||||
private var frameCount: Int = 0
|
||||
|
||||
private var socketFilePath: String {
|
||||
let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
|
||||
return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
setupLogger()
|
||||
|
||||
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.
|
||||
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
|
||||
MXLog.error("client connection did close", context: 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()
|
||||
}
|
||||
|
||||
func setupLogger() {
|
||||
let configuration = MXLogConfiguration()
|
||||
configuration.logLevel = .verbose
|
||||
configuration.maxLogFilesCount = 100
|
||||
configuration.logFilesSizeLimit = 10 * 1024 * 1024; // 10MB
|
||||
configuration.subLogName = "broadcastUploadExtension"
|
||||
|
||||
if isatty(STDERR_FILENO) == 0 {
|
||||
configuration.redirectLogsToFiles = true
|
||||
}
|
||||
|
||||
MXLog.configure(configuration)
|
||||
}
|
||||
}
|
151
BroadcastUploadExtension/Sources/SampleUploader.swift
Normal file
151
BroadcastUploadExtension/Sources/SampleUploader.swift
Normal file
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// License from the original repository:
|
||||
// https://github.com/jitsi/jitsi-meet-sdk-samples/blob/master/LICENSE
|
||||
//
|
||||
// SampleUploader.swift
|
||||
// Broadcast Extension
|
||||
//
|
||||
// Created by Alex-Dan Bumbu on 22/03/2021.
|
||||
// Copyright © 2021 8x8, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ReplayKit
|
||||
|
||||
import MatrixSDK
|
||||
|
||||
private enum Constants {
|
||||
static let bufferMaxLength = 10240
|
||||
}
|
||||
|
||||
class SampleUploader {
|
||||
|
||||
private static var imageContext = CIContext(options: nil)
|
||||
|
||||
@Atomic private var isReady = 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 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 {
|
||||
if let success = self?.sendDataChunk() {
|
||||
self?.isReady = !success
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
MXLog.error("writeBufferToStream failure")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func prepare(sample buffer: CMSampleBuffer) -> Data? {
|
||||
guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
|
||||
MXLog.error("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 {
|
||||
MXLog.error("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? {
|
||||
let image = CIImage(cvPixelBuffer: buffer).transformed(by: scaleTransform)
|
||||
|
||||
guard let colorSpace = image.colorSpace else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0]
|
||||
|
||||
return SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options)
|
||||
}
|
||||
}
|
203
BroadcastUploadExtension/Sources/SocketConnection.swift
Normal file
203
BroadcastUploadExtension/Sources/SocketConnection.swift
Normal file
|
@ -0,0 +1,203 @@
|
|||
//
|
||||
// License from the original repository:
|
||||
// https://github.com/jitsi/jitsi-meet-sdk-samples/blob/master/LICENSE
|
||||
//
|
||||
// SocketConnection.swift
|
||||
// Broadcast Extension
|
||||
//
|
||||
// Created by Alex-Dan Bumbu on 22/03/2021.
|
||||
// Copyright © 2021 Atlassian Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import MatrixSDK
|
||||
|
||||
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 {
|
||||
MXLog.error("failure: create socket")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func open() -> Bool {
|
||||
MXLog.info("open socket connection")
|
||||
|
||||
guard FileManager.default.fileExists(atPath: filePath) else {
|
||||
MXLog.error("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 {
|
||||
outputStream?.write(buffer, maxLength: length) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
extension SocketConnection: StreamDelegate {
|
||||
|
||||
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
||||
switch eventCode {
|
||||
case .openCompleted:
|
||||
MXLog.info("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 {
|
||||
MXLog.info("server socket closed")
|
||||
close()
|
||||
notifyDidClose(error: nil)
|
||||
}
|
||||
}
|
||||
case .hasSpaceAvailable:
|
||||
if aStream == outputStream {
|
||||
streamHasSpaceAvailable?()
|
||||
}
|
||||
case .errorOccurred:
|
||||
MXLog.error("client stream error occured", context: 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 {
|
||||
MXLog.error("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 {
|
||||
MXLog.error("connect socket failure", context: 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)
|
||||
RunLoop.current.run()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>$(APPLICATION_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
33
BroadcastUploadExtension/SupportingFiles/Info.plist
Normal file
33
BroadcastUploadExtension/SupportingFiles/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>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(BUNDLE_DISPLAY_NAME)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</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>
|
42
BroadcastUploadExtension/target.yml
Normal file
42
BroadcastUploadExtension/target.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: BroadcastUploadExtension
|
||||
|
||||
schemes:
|
||||
BroadcastUploadExtension:
|
||||
analyze:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
build:
|
||||
targets:
|
||||
BroadcastUploadExtension:
|
||||
- running
|
||||
- testing
|
||||
- profiling
|
||||
- analyzing
|
||||
- archiving
|
||||
profile:
|
||||
config: Release
|
||||
run:
|
||||
askForAppToLaunch: true
|
||||
config: Debug
|
||||
debugEnabled: false
|
||||
disableMainThreadChecker: true
|
||||
launchAutomaticallySubstyle: 2
|
||||
test:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
|
||||
targets:
|
||||
BroadcastUploadExtension:
|
||||
platform: iOS
|
||||
type: app-extension
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
Release: Release.xcconfig
|
||||
|
||||
sources:
|
||||
- path: .
|
||||
- path: ../Config/BuildSettings.swift
|
||||
- path: ../Riot/Categories/Bundle.swift
|
||||
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
|
|
@ -37,3 +37,6 @@ SHARE_EXTENSION_PROVISIONING_PROFILE = 8c797ca0-0440-49bd-be8d-11d761152995
|
|||
|
||||
SIRI_INTENTS_PROVISIONING_PROFILE_SPECIFIER = "Vector Siri Intents: App Store"
|
||||
SIRI_INTENTS_PROVISIONING_PROFILE = 1690e81a-5ad3-4d99-b578-02693579be71
|
||||
|
||||
BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER = "Vector Broadcast Upload Extension: App Store"
|
||||
BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE = c86239f4-0d3a-47f4-a5f2-9f4763c42b5d
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
// Application constants
|
||||
KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared
|
||||
BROADCAST_UPLOAD_EXTENSION_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).broadcastUploadExtension
|
||||
|
||||
// Build settings
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0
|
||||
|
|
4
Podfile
4
Podfile
|
@ -127,6 +127,10 @@ abstract_target 'RiotPods' do
|
|||
import_MatrixKit_pods
|
||||
end
|
||||
|
||||
target "BroadcastUploadExtension" do
|
||||
import_MatrixSDK
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
|
|
@ -35,6 +35,7 @@ static NSString * _Nonnull kRCTTextViewClassName = @"RCTTextView";
|
|||
Some feature flags defined in https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
|
||||
*/
|
||||
static NSString * _Nonnull kJitsiFeatureFlagChatEnabled = @"chat.enabled";
|
||||
static NSString * _Nonnull kJitsiFeatureFlagScreenSharingEnabled = @"ios.screensharing.enabled";
|
||||
|
||||
@interface JitsiViewController () <PictureInPicturable, JitsiMeetViewDelegate>
|
||||
|
||||
|
@ -278,6 +279,7 @@ static NSString * _Nonnull kJitsiFeatureFlagChatEnabled = @"chat.enabled";
|
|||
andAvatar:avatarUrl];
|
||||
builder.token = self.jwtToken;
|
||||
[builder setFeatureFlag:kJitsiFeatureFlagChatEnabled withBoolean:NO];
|
||||
[builder setFeatureFlag:kJitsiFeatureFlagScreenSharingEnabled withBoolean: YES];
|
||||
}];
|
||||
|
||||
[self.jitsiMeetView join:jitsiMeetConferenceOptions];
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>RTCAppGroupIdentifier</key>
|
||||
<string>$(APPLICATION_GROUP_IDENTIFIER)</string>
|
||||
<key>RTCScreenSharingExtension</key>
|
||||
<string>$(BROADCAST_UPLOAD_EXTENSION_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(BUNDLE_DISPLAY_NAME)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
|
|
|
@ -37,6 +37,7 @@ targets:
|
|||
- target: RiotShareExtension
|
||||
- target: SiriIntents
|
||||
- target: RiotNSE
|
||||
- target: BroadcastUploadExtension
|
||||
- target: DesignKit
|
||||
- target: CommonKit
|
||||
- package: AnalyticsEvents
|
||||
|
|
|
@ -38,3 +38,6 @@ SHARE_EXTENSION_PROVISIONING_PROFILE = b47f96e0-647b-4274-b2bb-8103b9a97146
|
|||
|
||||
SIRI_INTENTS_PROVISIONING_PROFILE_SPECIFIER = "Alpha Vector Siri Intents Ad hoc"
|
||||
SIRI_INTENTS_PROVISIONING_PROFILE = a9fdb684-a68c-4207-afc2-810f3287b1f5
|
||||
|
||||
BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER = "Alpha Vector Broadcast Upload Extension Ad hoc"
|
||||
BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE = 3616761e-f7f6-4f38-bf74-13bc6b0a1000
|
||||
|
|
|
@ -7,11 +7,13 @@ MAIN_TARGET=Riot
|
|||
SHARE_EXTENSION_TARGET=RiotShareExtension
|
||||
SIRI_INTENTS_EXTENSION_TARGET=SiriIntents
|
||||
NSE_TARGET=RiotNSE
|
||||
BROADCAST_UPLOAD_EXTENSION_TARGET=BroadcastUploadExtension
|
||||
|
||||
MAIN_BUNDLE_ID=im.vector.app.alpha
|
||||
SHARE_EXTENSION_BUNDLE_ID=im.vector.app.alpha.shareExtension
|
||||
SIRI_INTENTS_EXTENSION_BUNDLE_ID=im.vector.app.alpha.SiriIntents
|
||||
NSE_BUNDLE_ID=im.vector.app.alpha.nse
|
||||
BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID=im.vector.app.alpha.broadcastUploadExtension
|
||||
|
||||
## Build configuration
|
||||
|
||||
|
@ -29,6 +31,7 @@ MAIN_PROVISIONING_PROFILE_FILENAME=main.mobileprovision
|
|||
SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME=share_extension.mobileprovision
|
||||
SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_FILENAME=siri_intents.mobileprovision
|
||||
NSE_PROVISIONING_PROFILE_FILENAME=nse.mobileprovision
|
||||
BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME=broadcast_upload_extension.mobileprovision
|
||||
|
||||
## App Store code signing
|
||||
|
||||
|
@ -39,6 +42,7 @@ APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER="Vector App Store"
|
|||
APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Share Extension: App Store"
|
||||
APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Siri Intents: App Store"
|
||||
APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER="Vector NSE: App Store"
|
||||
APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Broadcast Upload Extension: App Store"
|
||||
|
||||
## Ad-Hoc code signing
|
||||
|
||||
|
@ -46,6 +50,7 @@ ADHOC_MAIN_PROVISIONING_PROFILE_SPECIFIER="Alpha Vector App Ad hoc"
|
|||
ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Alpha Vector Share Extension Ad hoc"
|
||||
ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Alpha Vector Siri Intents Ad hoc"
|
||||
ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER="Alpha Vector NSE Ad hoc"
|
||||
ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Alpha Vector Broadcast Upload Extension Ad hoc"
|
||||
|
||||
## Account information
|
||||
|
||||
|
|
1
changelog.d/7566.feature
Normal file
1
changelog.d/7566.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Broadcast Upload Extension added to the app targets to allow Jitsi screen sharing feature.
|
|
@ -7,11 +7,13 @@ MAIN_TARGET=Riot
|
|||
SHARE_EXTENSION_TARGET=RiotShareExtension
|
||||
SIRI_INTENTS_EXTENSION_TARGET=SiriIntents
|
||||
NSE_TARGET=RiotNSE
|
||||
BROADCAST_UPLOAD_EXTENSION_TARGET=BroadcastUploadExtension
|
||||
|
||||
MAIN_BUNDLE_ID=im.vector.app
|
||||
SHARE_EXTENSION_BUNDLE_ID=im.vector.app.shareExtension
|
||||
SIRI_INTENTS_EXTENSION_BUNDLE_ID=im.vector.app.SiriIntents
|
||||
NSE_BUNDLE_ID=im.vector.app.nse
|
||||
BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID=im.vector.app.broadcastUploadExtension
|
||||
|
||||
## Build configuration
|
||||
|
||||
|
@ -29,6 +31,7 @@ MAIN_PROVISIONING_PROFILE_FILENAME=main.mobileprovision
|
|||
SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME=share_extension.mobileprovision
|
||||
SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_FILENAME=siri_intents.mobileprovision
|
||||
NSE_PROVISIONING_PROFILE_FILENAME=nse.mobileprovision
|
||||
BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME=broadcast_upload_extension.mobileprovision
|
||||
|
||||
## App Store code signing
|
||||
|
||||
|
@ -39,6 +42,7 @@ APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER="Vector App Store"
|
|||
APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Share Extension: App Store"
|
||||
APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Siri Intents: App Store"
|
||||
APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER="Vector NSE: App Store"
|
||||
APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Broadcast Upload Extension: App Store"
|
||||
|
||||
## Ad-Hoc code signing
|
||||
|
||||
|
@ -46,6 +50,7 @@ ADHOC_MAIN_PROVISIONING_PROFILE_SPECIFIER="Vector Ad Hoc"
|
|||
ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Share Extension: Ad Hoc"
|
||||
ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Siri Intents: Ad Hoc"
|
||||
ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER="Vector NSE: Ad Hoc"
|
||||
ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="Vector Broadcast Upload Extension: Ad Hoc"
|
||||
|
||||
## Account information
|
||||
|
||||
|
|
|
@ -290,12 +290,14 @@ platform :ios do
|
|||
share_extension_provisioning_profile = ENV["ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
siri_intents_provisioning_profile = ENV["ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
nse_provisioning_profile = ENV["ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
broadcast_upload_extension_provisioning_profile = ENV["ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
else
|
||||
export_method = "app-store"
|
||||
main_provisioning_profile = ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
share_extension_provisioning_profile = ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
siri_intents_provisioning_profile = ENV["APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
nse_provisioning_profile = ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
broadcast_upload_extension_provisioning_profile = ENV["APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
end
|
||||
|
||||
# Build app and create ipa
|
||||
|
@ -321,6 +323,7 @@ platform :ios do
|
|||
ENV["SHARE_EXTENSION_BUNDLE_ID"] => share_extension_provisioning_profile,
|
||||
ENV["NSE_BUNDLE_ID"] => nse_provisioning_profile,
|
||||
ENV["SIRI_INTENTS_EXTENSION_BUNDLE_ID"] => siri_intents_provisioning_profile,
|
||||
ENV["BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID"] => broadcast_upload_extension_provisioning_profile,
|
||||
},
|
||||
iCloudContainerEnvironment: "Production",
|
||||
},
|
||||
|
@ -340,6 +343,7 @@ platform :ios do
|
|||
share_extension_provisioning_name = adhoc ? ENV["ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
siri_intents_provisioning_name = adhoc ? ENV["ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
notification_service_extension_provisioning_name = adhoc ? ENV["ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
broadcast_upload_extension_provisioning_name = adhoc ? ENV["ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
||||
|
||||
# Main application
|
||||
get_provisioning_profile(
|
||||
|
@ -385,6 +389,17 @@ platform :ios do
|
|||
filename: ENV["NSE_PROVISIONING_PROFILE_FILENAME"],
|
||||
readonly: true,
|
||||
)
|
||||
# Broadcast Upload Extension
|
||||
get_provisioning_profile(
|
||||
app_identifier: ENV["BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID"],
|
||||
provisioning_name: broadcast_upload_extension_provisioning_name,
|
||||
ignore_profiles_with_different_name: true,
|
||||
adhoc: adhoc,
|
||||
skip_certificate_verification: skip_certificate_verification,
|
||||
output_path: output_path,
|
||||
filename: ENV["BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
|
||||
readonly: true,
|
||||
)
|
||||
end
|
||||
|
||||
desc "Update provisioning profiles for each target"
|
||||
|
@ -421,6 +436,13 @@ platform :ios do
|
|||
target_filter: ENV["NSE_TARGET"],
|
||||
build_configuration: build_configuration,
|
||||
)
|
||||
# Broadcast Upload Extension
|
||||
update_project_provisioning(
|
||||
xcodeproj: xcodeproj,
|
||||
profile: "#{provisioning_profiles_path}#{ENV["BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
|
||||
target_filter: ENV["BROADCAST_UPLOAD_EXTENSION_TARGET"],
|
||||
build_configuration: build_configuration,
|
||||
)
|
||||
end
|
||||
|
||||
desc "Update application build number for all targets"
|
||||
|
|
|
@ -32,6 +32,7 @@ include:
|
|||
- path: RiotShareExtension/target.yml
|
||||
- path: SiriIntents/target.yml
|
||||
- path: RiotNSE/target.yml
|
||||
- path: BroadcastUploadExtension/target.yml
|
||||
- path: DesignKit/target.yml
|
||||
- path: RiotSwiftUI/target.yml
|
||||
- path: RiotSwiftUI/targetUnitTests.yml
|
||||
|
|
Loading…
Reference in a new issue