diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index ba66c65b3..c0d5d9be2 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -23,12 +23,13 @@ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 4E51B75E25E4115F0038575A /* DarwinNotificationCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */; }; - 4EC49BB725BEDAC100E76218 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EC49B8625BED71300E76218 /* ReplayKit.framework */; }; - 4EC49BBB25BEDAC100E76218 /* SampleHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */; }; - 4EC49BBF25BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 4EC49BCB25BEDB6400E76218 /* SocketConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */; }; - 4EC49BD125BF19CF00E76218 /* SampleUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BD025BF19CF00E76218 /* SampleUploader.m */; }; + 4E90F9402632D1AB001102D4 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F93F2632D1AB001102D4 /* Atomic.swift */; }; + 4EB06024260E026600F524C5 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EC49B8625BED71300E76218 /* ReplayKit.framework */; }; + 4EB06027260E026600F524C5 /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB06026260E026600F524C5 /* SampleHandler.swift */; }; + 4EB0602B260E026600F524C5 /* JitsiMeetBroadcastExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4EB06023260E026600F524C5 /* JitsiMeetBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4EB0603C260E09D000F524C5 /* SocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB06039260E09D000F524C5 /* SocketConnection.swift */; }; + 4EB0603D260E09D000F524C5 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0603A260E09D000F524C5 /* DarwinNotificationCenter.swift */; }; + 4EB0603E260E09D000F524C5 /* SampleUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0603B260E09D000F524C5 /* SampleUploader.swift */; }; 55BEDABDA92D47D399A70A5E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */; }; DE050389256E904600DEE3A5 /* WebRTC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE050388256E904600DEE3A5 /* WebRTC.xcframework */; }; DE05038A256E904600DEE3A5 /* WebRTC.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE050388256E904600DEE3A5 /* WebRTC.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -54,11 +55,11 @@ remoteGlobalIDString = 0BEA5C241F7B8F73000D0AB4; remoteInfo = JitsiMeetCompanion; }; - 4EC49BBD25BEDAC100E76218 /* PBXContainerItemProxy */ = { + 4EB06029260E026600F524C5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; proxyType = 1; - remoteGlobalIDString = 4EC49BB525BEDAC100E76218; + remoteGlobalIDString = 4EB06022260E026600F524C5; remoteInfo = "JitsiMeetBroadcast Extension"; }; /* End PBXContainerItemProxy section */ @@ -104,7 +105,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 4EC49BBF25BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex in Embed App Extensions */, + 4EB0602B260E026600F524C5 /* JitsiMeetBroadcastExtension.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -139,19 +140,18 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4670A512A688E2DC34528282 /* Pods-jitsi-meet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-jitsi-meet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-jitsi-meet/Pods-jitsi-meet.debug.xcconfig"; sourceTree = ""; }; - 4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DarwinNotificationCenter.h; sourceTree = ""; }; - 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DarwinNotificationCenter.m; sourceTree = ""; }; + 4E90F93F2632D1AB001102D4 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + 4EB06023260E026600F524C5 /* JitsiMeetBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = JitsiMeetBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 4EB06026260E026600F524C5 /* SampleHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = ""; }; + 4EB06028260E026600F524C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4EB06032260E08CC00F524C5 /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = ""; }; + 4EB06039260E09D000F524C5 /* SocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketConnection.swift; sourceTree = ""; }; + 4EB0603A260E09D000F524C5 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = ""; }; + 4EB0603B260E09D000F524C5 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = ""; }; 4EC49B8625BED71300E76218 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; - 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "JitsiMeetBroadcast Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4EC49BB925BEDAC100E76218 /* SampleHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleHandler.h; sourceTree = ""; }; - 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleHandler.m; sourceTree = ""; }; - 4EC49BBC25BEDAC100E76218 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4EC49BC925BEDB6400E76218 /* SocketConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocketConnection.h; sourceTree = ""; }; - 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocketConnection.m; sourceTree = ""; }; - 4EC49BCF25BF19CF00E76218 /* SampleUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleUploader.h; sourceTree = ""; }; - 4EC49BD025BF19CF00E76218 /* SampleUploader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleUploader.m; sourceTree = ""; }; - 4EC49BDB25BF280A00E76218 /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = ""; }; + 5FEF9D87A4D2A38AD7193308 /* Pods-JitsiMeet-JitsiMeetBroadcastExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet-JitsiMeetBroadcastExtension.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet-JitsiMeetBroadcastExtension/Pods-JitsiMeet-JitsiMeetBroadcastExtension.release.xcconfig"; sourceTree = ""; }; 609CB2080B75F75A89923F3D /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = ""; }; + A7B2827E068A0E05260054AC /* Pods-JitsiMeet-JitsiMeetBroadcastExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet-JitsiMeetBroadcastExtension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet-JitsiMeetBroadcastExtension/Pods-JitsiMeet-JitsiMeetBroadcastExtension.debug.xcconfig"; sourceTree = ""; }; B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = ""; }; D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JitsiMeet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; DE050388256E904600DEE3A5 /* WebRTC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WebRTC.xcframework; path = "../../node_modules/react-native-webrtc/apple/WebRTC.xcframework"; sourceTree = ""; }; @@ -189,11 +189,11 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4EC49BB325BEDAC100E76218 /* Frameworks */ = { + 4EB06020260E026600F524C5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4EC49BB725BEDAC100E76218 /* ReplayKit.framework in Frameworks */, + 4EB06024260E026600F524C5 /* ReplayKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -261,23 +261,22 @@ path = src; sourceTree = ""; }; - 4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */ = { + 4EB06025260E026600F524C5 /* JitsiMeetBroadcast Extension */ = { isa = PBXGroup; children = ( - 4EC49BDB25BF280A00E76218 /* extension.entitlements */, - 4EC49BB925BEDAC100E76218 /* SampleHandler.h */, - 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */, - 4EC49BC925BEDB6400E76218 /* SocketConnection.h */, - 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */, - 4EC49BCF25BF19CF00E76218 /* SampleUploader.h */, - 4EC49BD025BF19CF00E76218 /* SampleUploader.m */, - 4EC49BBC25BEDAC100E76218 /* Info.plist */, - 4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */, - 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */, + 4EB06032260E08CC00F524C5 /* extension.entitlements */, + 4EB06026260E026600F524C5 /* SampleHandler.swift */, + 4EB0603B260E09D000F524C5 /* SampleUploader.swift */, + 4EB06039260E09D000F524C5 /* SocketConnection.swift */, + 4EB0603A260E09D000F524C5 /* DarwinNotificationCenter.swift */, + 4E90F93F2632D1AB001102D4 /* Atomic.swift */, + 4EB06028260E026600F524C5 /* Info.plist */, ); + indentWidth = 4; name = "JitsiMeetBroadcast Extension"; path = "broadcast-extension"; sourceTree = ""; + tabWidth = 4; }; 5E96ADD5E49F3B3822EF9A52 /* Pods */ = { isa = PBXGroup; @@ -286,6 +285,8 @@ 09AA3B93E4CC62D84B424690 /* Pods-jitsi-meet.release.xcconfig */, 609CB2080B75F75A89923F3D /* Pods-JitsiMeet.debug.xcconfig */, FC040BBED70876444D89E91C /* Pods-JitsiMeet.release.xcconfig */, + A7B2827E068A0E05260054AC /* Pods-JitsiMeet-JitsiMeetBroadcastExtension.debug.xcconfig */, + 5FEF9D87A4D2A38AD7193308 /* Pods-JitsiMeet-JitsiMeetBroadcastExtension.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -299,8 +300,8 @@ 13B07FAE1A68108700A75B9A /* src */, 5E96ADD5E49F3B3822EF9A52 /* Pods */, 0BEA5C261F7B8F73000D0AB4 /* Watch app */, - 4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */, 0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */, + 4EB06025260E026600F524C5 /* JitsiMeetBroadcast Extension */, ); indentWidth = 2; sourceTree = ""; @@ -312,7 +313,7 @@ 13B07F961A680F5B00A75B9A /* jitsi-meet.app */, 0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */, 0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */, - 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */, + 4EB06023260E026600F524C5 /* JitsiMeetBroadcastExtension.appex */, ); name = Products; sourceTree = ""; @@ -376,28 +377,28 @@ ); dependencies = ( 0BEA5C401F7B8F73000D0AB4 /* PBXTargetDependency */, - 4EC49BBE25BEDAC100E76218 /* PBXTargetDependency */, + 4EB0602A260E026600F524C5 /* PBXTargetDependency */, ); name = JitsiMeet; productName = "Jitsi Meet"; productReference = 13B07F961A680F5B00A75B9A /* jitsi-meet.app */; productType = "com.apple.product-type.application"; }; - 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */ = { + 4EB06022260E026600F524C5 /* JitsiMeetBroadcastExtension */ = { isa = PBXNativeTarget; - buildConfigurationList = 4EC49BC025BEDAC100E76218 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcast Extension" */; + buildConfigurationList = 4EB0602C260E026700F524C5 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcastExtension" */; buildPhases = ( - 4EC49BB225BEDAC100E76218 /* Sources */, - 4EC49BB325BEDAC100E76218 /* Frameworks */, - 4EC49BB425BEDAC100E76218 /* Resources */, + 4EB0601F260E026600F524C5 /* Sources */, + 4EB06020260E026600F524C5 /* Frameworks */, + 4EB06021260E026600F524C5 /* Resources */, ); buildRules = ( ); dependencies = ( ); - name = "JitsiMeetBroadcast Extension"; + name = JitsiMeetBroadcastExtension; productName = "JitsiMeetBroadcast Extension"; - productReference = 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */; + productReference = 4EB06023260E026600F524C5 /* JitsiMeetBroadcastExtension.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ @@ -406,6 +407,7 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1020; ORGANIZATIONNAME = Facebook; TargetAttributes = { @@ -429,8 +431,8 @@ }; }; }; - 4EC49BB525BEDAC100E76218 = { - CreatedOnToolsVersion = 12.2; + 4EB06022260E026600F524C5 = { + CreatedOnToolsVersion = 12.4; }; }; }; @@ -450,7 +452,7 @@ 13B07F861A680F5B00A75B9A /* JitsiMeet */, 0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */, 0BEA5C301F7B8F73000D0AB4 /* JitsiMeetCompanion Extension */, - 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */, + 4EB06022260E026600F524C5 /* JitsiMeetBroadcastExtension */, ); }; /* End PBXProject section */ @@ -483,7 +485,7 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4EC49BB425BEDAC100E76218 /* Resources */ = { + 4EB06021260E026600F524C5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -625,14 +627,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4EC49BB225BEDAC100E76218 /* Sources */ = { + 4EB0601F260E026600F524C5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4EC49BCB25BEDB6400E76218 /* SocketConnection.m in Sources */, - 4EC49BBB25BEDAC100E76218 /* SampleHandler.m in Sources */, - 4E51B75E25E4115F0038575A /* DarwinNotificationCenter.m in Sources */, - 4EC49BD125BF19CF00E76218 /* SampleUploader.m in Sources */, + 4EB0603C260E09D000F524C5 /* SocketConnection.swift in Sources */, + 4EB0603E260E09D000F524C5 /* SampleUploader.swift in Sources */, + 4EB0603D260E09D000F524C5 /* DarwinNotificationCenter.swift in Sources */, + 4EB06027260E026600F524C5 /* SampleHandler.swift in Sources */, + 4E90F9402632D1AB001102D4 /* Atomic.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -649,10 +652,10 @@ target = 0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */; targetProxy = 0BEA5C3F1F7B8F73000D0AB4 /* PBXContainerItemProxy */; }; - 4EC49BBE25BEDAC100E76218 /* PBXTargetDependency */ = { + 4EB0602A260E026600F524C5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */; - targetProxy = 4EC49BBD25BEDAC100E76218 /* PBXContainerItemProxy */; + target = 4EB06022260E026600F524C5 /* JitsiMeetBroadcastExtension */; + targetProxy = 4EB06029260E026600F524C5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -879,7 +882,7 @@ }; name = Release; }; - 4EC49BC125BEDAC100E76218 /* Debug */ = { + 4EB0602D260E026700F524C5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; @@ -890,13 +893,13 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "broadcast-extension/extension.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FC967L3QRG; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "broadcast-extension/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -907,11 +910,14 @@ PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 4EC49BC225BEDAC100E76218 /* Release */ = { + 4EB0602E260E026700F524C5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; @@ -922,14 +928,14 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "broadcast-extension/extension.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = FC967L3QRG; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "broadcast-extension/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -939,6 +945,8 @@ PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1087,11 +1095,11 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4EC49BC025BEDAC100E76218 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcast Extension" */ = { + 4EB0602C260E026700F524C5 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcastExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( - 4EC49BC125BEDAC100E76218 /* Debug */, - 4EC49BC225BEDAC100E76218 /* Release */, + 4EB0602D260E026700F524C5 /* Debug */, + 4EB0602E260E026700F524C5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/ios/app/broadcast-extension/Atomic.swift b/ios/app/broadcast-extension/Atomic.swift new file mode 100644 index 000000000..159dd9534 --- /dev/null +++ b/ios/app/broadcast-extension/Atomic.swift @@ -0,0 +1,30 @@ + +import Foundation + +@propertyWrapper +struct Atomic { + + 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 + } +} diff --git a/ios/app/broadcast-extension/DarwinNotificationCenter.h b/ios/app/broadcast-extension/DarwinNotificationCenter.h deleted file mode 100644 index 145f0d911..000000000 --- a/ios/app/broadcast-extension/DarwinNotificationCenter.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 - -NS_ASSUME_NONNULL_BEGIN - -extern NSNotificationName const kBroadcastStartedNotification; -extern NSNotificationName const kBroadcastStoppedNotification; - -@interface DarwinNotificationCenter: NSObject - -+ (instancetype)sharedInstance; -- (void)postNotificationWithName:(NSNotificationName)name; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/app/broadcast-extension/DarwinNotificationCenter.m b/ios/app/broadcast-extension/DarwinNotificationCenter.m deleted file mode 100644 index 65221515d..000000000 --- a/ios/app/broadcast-extension/DarwinNotificationCenter.m +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 "DarwinNotificationCenter.h" - -NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted"; -NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped"; - -@implementation DarwinNotificationCenter { - CFNotificationCenterRef _notificationCenter; -} - -+ (instancetype)sharedInstance { - static DarwinNotificationCenter *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _notificationCenter = CFNotificationCenterGetDarwinNotifyCenter(); - } - - return self; -} - -- (void)postNotificationWithName:(NSString*)name { - CFNotificationCenterPostNotification(_notificationCenter, (__bridge CFStringRef)name, NULL, NULL, true); -} - -@end - diff --git a/ios/app/broadcast-extension/DarwinNotificationCenter.swift b/ios/app/broadcast-extension/DarwinNotificationCenter.swift new file mode 100644 index 000000000..eb97ec152 --- /dev/null +++ b/ios/app/broadcast-extension/DarwinNotificationCenter.swift @@ -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) + } +} diff --git a/ios/app/broadcast-extension/Info.plist b/ios/app/broadcast-extension/Info.plist index 976b51d79..0fe45caca 100644 --- a/ios/app/broadcast-extension/Info.plist +++ b/ios/app/broadcast-extension/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - JitsiMeet Broadcast Extension + Jitsi Meet Broadcast Extension CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -25,7 +25,7 @@ NSExtensionPointIdentifier com.apple.broadcast-services-upload NSExtensionPrincipalClass - SampleHandler + $(PRODUCT_MODULE_NAME).SampleHandler RPBroadcastProcessMode RPBroadcastProcessModeSampleBuffer diff --git a/ios/app/broadcast-extension/SampleHandler.h b/ios/app/broadcast-extension/SampleHandler.h deleted file mode 100644 index f063375ec..000000000 --- a/ios/app/broadcast-extension/SampleHandler.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 - -@interface SampleHandler : RPBroadcastSampleHandler - -@end diff --git a/ios/app/broadcast-extension/SampleHandler.m b/ios/app/broadcast-extension/SampleHandler.m deleted file mode 100644 index e48140865..000000000 --- a/ios/app/broadcast-extension/SampleHandler.m +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 "SampleHandler.h" -#import "SocketConnection.h" -#import "SampleUploader.h" -#import "DarwinNotificationCenter.h" - -@interface SampleHandler () - -@property (nonatomic, retain) SocketConnection *clientConnection; -@property (nonatomic, retain) SampleUploader *uploader; - -@end - -@implementation SampleHandler - -- (instancetype)init { - self = [super init]; - if (self) { - self.clientConnection = [[SocketConnection alloc] initWithFilePath:self.socketFilePath]; - [self setupConnection]; - - self.uploader = [[SampleUploader alloc] initWithConnection:self.clientConnection]; - } - - return self; -} - -- (void)broadcastStartedWithSetupInfo:(NSDictionary *)setupInfo { - // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. - NSLog(@"broadcast started"); - - [[DarwinNotificationCenter sharedInstance] postNotificationWithName:kBroadcastStartedNotification]; - [self openConnection]; -} - -- (void)broadcastPaused { - // User has requested to pause the broadcast. Samples will stop being delivered. -} - -- (void)broadcastResumed { - // User has requested to resume the broadcast. Samples delivery will resume. -} - -- (void)broadcastFinished { - // User has requested to finish the broadcast. - [[DarwinNotificationCenter sharedInstance] postNotificationWithName:kBroadcastStoppedNotification]; - [self.clientConnection close]; -} - -- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { - static NSUInteger frameCount = 0; - switch (sampleBufferType) { - case RPSampleBufferTypeVideo: - // adjust frame rate by using every third frame - if (++frameCount%3 == 0 && self.uploader.isReady) { - [self.uploader sendSample:sampleBuffer]; - } - break; - - default: - break; - } -} - -// MARK: Private Methods - -- (NSString *)socketFilePath { - // the appGroupIdentifier must match the value provided in the app's info.plist for the RTCAppGroupIdentifier key - NSString *appGroupIdentifier = @"group.org.jitsi.meet.appgroup"; - NSURL *sharedContainer = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupIdentifier]; - NSString *socketFilePath = [[sharedContainer URLByAppendingPathComponent:@"rtc_SSFD"] path]; - - return socketFilePath; -} - -- (void)setupConnection { - __weak __typeof(self) weakSelf = self; - self.clientConnection.didClose = ^(NSError *error) { - NSLog(@"client connection did close: %@", error); - if (error) { - [weakSelf finishBroadcastWithError:error]; - } - else { - NSInteger JMScreenSharingStopped = 10001; - NSError *customError = [NSError errorWithDomain:RPRecordingErrorDomain - code:JMScreenSharingStopped - userInfo:@{NSLocalizedDescriptionKey: @"Screen sharing stopped"}]; - [weakSelf finishBroadcastWithError:customError]; - } - }; -} - -- (void)openConnection { - dispatch_queue_t queue = dispatch_queue_create("org.jitsi.meet.broadcast.connectTimer", 0); - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 0.1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); - - dispatch_source_set_event_handler(timer, ^{ - BOOL success = [self.clientConnection open]; - if (success) { - dispatch_source_cancel(timer); - } - }); - - dispatch_resume(timer); -} - -@end diff --git a/ios/app/broadcast-extension/SampleHandler.swift b/ios/app/broadcast-extension/SampleHandler.swift new file mode 100644 index 000000000..b92f43bf6 --- /dev/null +++ b/ios/app/broadcast-extension/SampleHandler.swift @@ -0,0 +1,117 @@ +/* + * 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 +import JitsiMeetSDK + +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() + } +} diff --git a/ios/app/broadcast-extension/SampleUploader.h b/ios/app/broadcast-extension/SampleUploader.h deleted file mode 100644 index 5685cdd75..000000000 --- a/ios/app/broadcast-extension/SampleUploader.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 -#import - -NS_ASSUME_NONNULL_BEGIN - -@class SocketConnection; - -@interface SampleUploader : NSObject - -@property (nonatomic, assign, readonly) BOOL isReady; - -- (instancetype)initWithConnection:(SocketConnection *)connection; -- (void)sendSample:(CMSampleBufferRef)sampleBuffer; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/app/broadcast-extension/SampleUploader.m b/ios/app/broadcast-extension/SampleUploader.m deleted file mode 100644 index 594954814..000000000 --- a/ios/app/broadcast-extension/SampleUploader.m +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 -#import - -#import "SampleUploader.h" -#import "SocketConnection.h" - -static const NSInteger kBufferMaxLenght = 10 * 1024; - -@interface SampleUploader () - -@property (nonatomic, assign) BOOL isReady; - -@property (nonatomic, strong) dispatch_queue_t serialQueue; -@property (nonatomic, strong) SocketConnection *connection; -@property (nonatomic, strong) CIContext *imageContext; - -@property (nonatomic, strong) NSData *dataToSend; -@property (nonatomic, assign) NSUInteger byteIndex; - -@end - -@implementation SampleUploader - -- (instancetype)initWithConnection:(SocketConnection *)connection { - self = [super init]; - if (self) { - self.serialQueue = dispatch_queue_create("org.jitsi.meet.broadcast.sampleUploader", DISPATCH_QUEUE_SERIAL); - - self.connection = connection; - [self setupConnection]; - - self.imageContext = [[CIContext alloc] initWithOptions:nil]; - self.isReady = false; - } - - return self; -} - -- (void)sendSample:(CMSampleBufferRef)sampleBuffer { - self.isReady = false; - - self.dataToSend = [self prepareSample:sampleBuffer]; - self.byteIndex = 0; - - dispatch_async(self.serialQueue, ^{ - [self sendData]; - }); -} - -// MARK: Private Methods - -- (void)setupConnection { - __weak __typeof(self) weakSelf = self; - self.connection.didOpen = ^{ - weakSelf.isReady = true; - }; - self.connection.streamHasSpaceAvailable = ^{ - dispatch_async(weakSelf.serialQueue, ^{ - weakSelf.isReady = ![weakSelf sendData]; - }); - }; -} - -/** - This function downscales and converts to jpeg the provided sample buffer, then wraps the resulted image data into a CFHTTPMessageRef. Returns the serialized CFHTTPMessageRef. - */ -- (NSData *)prepareSample:(CMSampleBufferRef)sampleBuffer { - CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - - CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); - - CGFloat scaleFactor = 2; - size_t width = CVPixelBufferGetWidth(imageBuffer)/scaleFactor; - size_t height = CVPixelBufferGetHeight(imageBuffer)/scaleFactor; - CGImagePropertyOrientation orientation = ((__bridge NSNumber*)CMGetAttachment(sampleBuffer, (__bridge CFStringRef)RPVideoSampleOrientationKey , NULL)).unsignedIntValue; - - CGAffineTransform scaleTransform = CGAffineTransformMakeScale(1/scaleFactor, 1/scaleFactor); - NSData *bufferData = [self jpegDataFromPixelBuffer:imageBuffer withScaling:scaleTransform]; - - CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); - - if (bufferData) { - CFHTTPMessageRef httpResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1); - CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Content-Length", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", bufferData.length]); - CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Width", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", width]); - CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Height", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", height]); - CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Orientation", (__bridge CFStringRef)[NSString stringWithFormat:@"%u", orientation]); - - CFHTTPMessageSetBody(httpResponse, (__bridge CFDataRef)bufferData); - - CFDataRef serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse); - CFRelease(httpResponse); - - return CFBridgingRelease(serializedMessage); - } - - return nil; -} - -- (BOOL)sendData { - if (!self.dataToSend) { - NSLog(@"no data to send"); - return false; - } - - NSUInteger bytesLeft = self.dataToSend.length - self.byteIndex; - - NSInteger length = bytesLeft > kBufferMaxLenght ? kBufferMaxLenght : bytesLeft; - uint8_t buffer[length]; - [self.dataToSend getBytes:&buffer range:NSMakeRange(self.byteIndex, length)]; - - length = [self.connection writeBufferToStream:buffer maxLength:length]; - if (length > 0) { - self.byteIndex += length; - bytesLeft -= length; - - if (bytesLeft == 0) { - NSLog(@"video sample processed successfully"); - self.dataToSend = nil; - self.byteIndex = 0; - } - } - else { - NSLog(@"writeBufferToStream failure"); - } - - return true; -} - -- (NSData *)jpegDataFromPixelBuffer:(CVPixelBufferRef)pixelBuffer withScaling:(CGAffineTransform)scaleTransform { - CIImage *image = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer]; - image = [image imageByApplyingTransform:scaleTransform]; - - NSDictionary *options = @{(NSString *)kCGImageDestinationLossyCompressionQuality: [NSNumber numberWithFloat:1.0]}; - NSData *imageData = [self.imageContext JPEGRepresentationOfImage:image - colorSpace:image.colorSpace - options:options]; - return imageData; -} - -@end diff --git a/ios/app/broadcast-extension/SampleUploader.swift b/ios/app/broadcast-extension/SampleUploader.swift new file mode 100644 index 000000000..8c545b90f --- /dev/null +++ b/ios/app/broadcast-extension/SampleUploader.swift @@ -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 + } +} diff --git a/ios/app/broadcast-extension/SocketConnection.h b/ios/app/broadcast-extension/SocketConnection.h deleted file mode 100644 index 7ba38943a..000000000 --- a/ios/app/broadcast-extension/SocketConnection.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 - -NS_ASSUME_NONNULL_BEGIN - -@interface SocketConnection : NSObject - -@property (nonatomic, copy, nullable) void (^didOpen)(void); -@property (nonatomic, copy, nullable) void (^didClose)(NSError*); -@property (nonatomic, copy, nullable) void (^streamHasSpaceAvailable)(void); - -- (instancetype)initWithFilePath:(nonnull NSString *)filePath; -- (BOOL)open; -- (void)close; -- (NSInteger)writeBufferToStream:(const uint8_t*)buffer maxLength:(NSInteger)length; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/app/broadcast-extension/SocketConnection.m b/ios/app/broadcast-extension/SocketConnection.m deleted file mode 100644 index 7e92d9543..000000000 --- a/ios/app/broadcast-extension/SocketConnection.m +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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. - */ - -#include -#include - -#import "SocketConnection.h" - -@interface SocketConnection () - -@property (nonatomic, copy) NSString *filePath; - -@property (nonatomic, strong) NSInputStream *inputStream; -@property (nonatomic, strong) NSOutputStream *outputStream; - -@property (nonatomic, strong) NSThread *networkThread; - -@end - -@implementation SocketConnection { - int _socket; - struct sockaddr_un _socketAddr; -} - -- (instancetype)initWithFilePath:(NSString *)path { - self = [super init]; - if (self) { - self.filePath = path; - - [self setupSocketWithFilePath:path]; - [self setupNetworkThread]; - } - - return self; -} - -- (BOOL)open { - NSLog(@"Open socket connection"); - - if (![[NSFileManager defaultManager] fileExistsAtPath:self.filePath]) { - NSLog(@"failure: socket file missing"); - return false; - } - - int status = connect(_socket, (struct sockaddr *)&_socketAddr, sizeof(_socketAddr)); - if (status < 0) { - NSLog(@"failure: socket connect (%d)", status); - return false; - } - - [self.networkThread start]; - - CFReadStreamRef readStream; - CFWriteStreamRef writeStream; - - CFStreamCreatePairWithSocket(kCFAllocatorDefault, _socket, &readStream, &writeStream); - - self.inputStream = (__bridge_transfer NSInputStream *)readStream; - self.inputStream.delegate = self; - [self.inputStream setProperty:@"kCFBooleanTrue" forKey:@"kCFStreamPropertyShouldCloseNativeSocket"]; - - self.outputStream = (__bridge_transfer NSOutputStream *)writeStream; - self.outputStream.delegate = self; - [self.outputStream setProperty:@"kCFBooleanTrue" forKey:@"kCFStreamPropertyShouldCloseNativeSocket"]; - - [self performSelector:@selector(scheduleStreams) onThread:self.networkThread withObject:nil waitUntilDone:true]; - - [self.inputStream open]; - [self.outputStream open]; - - NSLog(@"read stream status: %ld", CFReadStreamGetStatus(readStream)); - NSLog(@"write stream status: %ld", CFWriteStreamGetStatus(writeStream)); - - return true; -} - -- (void)close { - [self performSelector:@selector(unscheduleStreams) onThread:self.networkThread withObject:nil waitUntilDone:true]; - - self.inputStream.delegate = nil; - self.outputStream.delegate = nil; - - [self.inputStream close]; - [self.outputStream close]; - - [self.networkThread cancel]; -} - -- (NSInteger)writeBufferToStream:(const uint8_t*)buffer maxLength:(NSInteger)length { - return [self.outputStream write:buffer maxLength:length]; -} - -// MARK: Private Methods - -- (BOOL)isOpen { - return self.inputStream.streamStatus == NSStreamStatusOpen && self.outputStream.streamStatus == NSStreamStatusOpen; -} - -- (void)setupSocketWithFilePath:(NSString*)path { - _socket = socket(AF_UNIX, SOCK_STREAM, 0); - - memset(&_socketAddr, 0, sizeof(_socketAddr)); - _socketAddr.sun_family = AF_UNIX; - strncpy(_socketAddr.sun_path, path.UTF8String, sizeof(_socketAddr.sun_path) - 1); -} - -- (void)setupNetworkThread { - self.networkThread = [[NSThread alloc] initWithBlock:^{ - do { - @autoreleasepool { - [[NSRunLoop currentRunLoop] run]; - } - } while (![NSThread currentThread].isCancelled); - }]; - self.networkThread.qualityOfService = NSQualityOfServiceUserInitiated; -} - -- (void)scheduleStreams { - [self.inputStream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; - [self.outputStream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; -} - -- (void)unscheduleStreams { - [self.inputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; - [self.outputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; -} - -- (void)notifyDidClose:(NSError *)error { - if (self.didClose) { - self.didClose(error); - } -} - -@end - -#pragma mark - NSStreamDelegate - -@implementation SocketConnection (NSStreamDelegate) - -- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { - switch (eventCode) { - case NSStreamEventOpenCompleted: - NSLog(@"client stream open completed"); - if (aStream == self.outputStream && self.didOpen) { - self.didOpen(); - } - break; - case NSStreamEventHasBytesAvailable: - if (aStream == self.inputStream) { - uint8_t buffer; - NSInteger numberOfBytesRead = [(NSInputStream *)aStream read:&buffer maxLength:sizeof(buffer)]; - if (!numberOfBytesRead && aStream.streamStatus == NSStreamStatusAtEnd) { - NSLog(@"server socket closed"); - [self close]; - [self notifyDidClose:nil]; - } - } - break; - case NSStreamEventHasSpaceAvailable: - if (aStream == self.outputStream && self.streamHasSpaceAvailable) { - NSLog(@"client stream has space available"); - self.streamHasSpaceAvailable(); - } - break; - case NSStreamEventErrorOccurred: - NSLog(@"client stream error occurred: %@", aStream.streamError); - [self close]; - [self notifyDidClose:aStream.streamError]; - break; - - default: - break; - } -} - -@end diff --git a/ios/app/broadcast-extension/SocketConnection.swift b/ios/app/broadcast-extension/SocketConnection.swift new file mode 100644 index 000000000..b0a1f5c7c --- /dev/null +++ b/ios/app/broadcast-extension/SocketConnection.swift @@ -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, 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 occured: \(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.size)) + } + } + + guard status == noErr else { + print("failure: \(status)") + return false + } + + return true + } + + func setupStreams() { + var readStream: Unmanaged? + var writeStream: Unmanaged? + + 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) + } + } +}