feat(iOS): screensharing extension swift implementation
This commit is contained in:
parent
1898e4a768
commit
9bffe149d3
|
@ -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 = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DarwinNotificationCenter.h; sourceTree = "<group>"; };
|
||||
4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DarwinNotificationCenter.m; sourceTree = "<group>"; };
|
||||
4E90F93F2632D1AB001102D4 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
4EB06028260E026600F524C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
4EB06032260E08CC00F524C5 /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = "<group>"; };
|
||||
4EB06039260E09D000F524C5 /* SocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketConnection.swift; sourceTree = "<group>"; };
|
||||
4EB0603A260E09D000F524C5 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = "<group>"; };
|
||||
4EB0603B260E09D000F524C5 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
4EC49BBA25BEDAC100E76218 /* SampleHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleHandler.m; sourceTree = "<group>"; };
|
||||
4EC49BBC25BEDAC100E76218 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
4EC49BC925BEDB6400E76218 /* SocketConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocketConnection.h; sourceTree = "<group>"; };
|
||||
4EC49BCA25BEDB6400E76218 /* SocketConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocketConnection.m; sourceTree = "<group>"; };
|
||||
4EC49BCF25BF19CF00E76218 /* SampleUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleUploader.h; sourceTree = "<group>"; };
|
||||
4EC49BD025BF19CF00E76218 /* SampleUploader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleUploader.m; sourceTree = "<group>"; };
|
||||
4EC49BDB25BF280A00E76218 /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
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 = "<group>";
|
||||
|
@ -299,8 +300,8 @@
|
|||
13B07FAE1A68108700A75B9A /* src */,
|
||||
5E96ADD5E49F3B3822EF9A52 /* Pods */,
|
||||
0BEA5C261F7B8F73000D0AB4 /* Watch app */,
|
||||
4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */,
|
||||
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */,
|
||||
4EB06025260E026600F524C5 /* JitsiMeetBroadcast Extension */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
struct Atomic<Value> {
|
||||
|
||||
private var value: Value
|
||||
private let lock = NSLock()
|
||||
|
||||
init(wrappedValue value: Value) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var wrappedValue: Value {
|
||||
get { return load() }
|
||||
set { store(newValue: newValue) }
|
||||
}
|
||||
|
||||
func load() -> Value {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return value
|
||||
}
|
||||
|
||||
mutating func store(newValue: Value) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
value = newValue
|
||||
}
|
||||
}
|
|
@ -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 <Foundation/Foundation.h>
|
||||
|
||||
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
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@
|
|||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.broadcast-services-upload</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>SampleHandler</string>
|
||||
<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
|
||||
<key>RPBroadcastProcessMode</key>
|
||||
<string>RPBroadcastProcessModeSampleBuffer</string>
|
||||
</dict>
|
||||
|
|
|
@ -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 <ReplayKit/ReplayKit.h>
|
||||
|
||||
@interface SampleHandler : RPBroadcastSampleHandler
|
||||
|
||||
@end
|
|
@ -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<NSString *,NSObject *> *)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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 <Foundation/Foundation.h>
|
||||
#import <ReplayKit/ReplayKit.h>
|
||||
|
||||
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
|
|
@ -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 <MessageUI/MessageUI.h>
|
||||
#import <ReplayKit/ReplayKit.h>
|
||||
|
||||
#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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 <Foundation/Foundation.h>
|
||||
|
||||
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
|
|
@ -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 <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#import "SocketConnection.h"
|
||||
|
||||
@interface SocketConnection () <NSStreamDelegate>
|
||||
|
||||
@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
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
class SocketConnection: NSObject {
|
||||
var didOpen: (() -> Void)?
|
||||
var didClose: ((Error?) -> Void)?
|
||||
var streamHasSpaceAvailable: (() -> Void)?
|
||||
|
||||
private let filePath: String
|
||||
private var socketHandle: Int32 = -1
|
||||
private var address: sockaddr_un?
|
||||
|
||||
private var inputStream: InputStream?
|
||||
private var outputStream: OutputStream?
|
||||
|
||||
private var networkQueue: DispatchQueue?
|
||||
private var shouldKeepRunning = false
|
||||
|
||||
init?(filePath path: String) {
|
||||
filePath = path
|
||||
socketHandle = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
|
||||
|
||||
guard socketHandle != -1 else {
|
||||
print("failure: create socket")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func open() -> Bool {
|
||||
print("open socket connection")
|
||||
|
||||
guard FileManager.default.fileExists(atPath: filePath) else {
|
||||
print("failure: socket file missing")
|
||||
return false
|
||||
}
|
||||
|
||||
guard setupAddress() == true else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard connectSocket() == true else {
|
||||
return false
|
||||
}
|
||||
|
||||
setupStreams()
|
||||
|
||||
inputStream?.open()
|
||||
outputStream?.open()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func close() {
|
||||
unscheduleStreams()
|
||||
|
||||
inputStream?.delegate = nil
|
||||
outputStream?.delegate = nil
|
||||
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
inputStream = nil
|
||||
outputStream = nil
|
||||
}
|
||||
|
||||
func writeToStream(buffer: UnsafePointer<UInt8>, maxLength length: Int) -> Int {
|
||||
return outputStream?.write(buffer, maxLength: length) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
extension SocketConnection: StreamDelegate {
|
||||
|
||||
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
||||
switch eventCode {
|
||||
case .openCompleted:
|
||||
print("client stream open completed")
|
||||
if aStream == outputStream {
|
||||
didOpen?()
|
||||
}
|
||||
case .hasBytesAvailable:
|
||||
if aStream == inputStream {
|
||||
var buffer: UInt8 = 0
|
||||
let numberOfBytesRead = inputStream?.read(&buffer, maxLength: 1)
|
||||
if numberOfBytesRead == 0 && aStream.streamStatus == .atEnd {
|
||||
print("server socket closed")
|
||||
close()
|
||||
notifyDidClose(error: nil)
|
||||
}
|
||||
}
|
||||
case .hasSpaceAvailable:
|
||||
if aStream == outputStream {
|
||||
streamHasSpaceAvailable?()
|
||||
}
|
||||
case .errorOccurred:
|
||||
print("client stream error 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<sockaddr_un>.size))
|
||||
}
|
||||
}
|
||||
|
||||
guard status == noErr else {
|
||||
print("failure: \(status)")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func setupStreams() {
|
||||
var readStream: Unmanaged<CFReadStream>?
|
||||
var writeStream: Unmanaged<CFWriteStream>?
|
||||
|
||||
CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle, &readStream, &writeStream)
|
||||
|
||||
inputStream = readStream?.takeRetainedValue()
|
||||
inputStream?.delegate = self
|
||||
inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
|
||||
|
||||
outputStream = writeStream?.takeRetainedValue()
|
||||
outputStream?.delegate = self
|
||||
outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
|
||||
|
||||
scheduleStreams()
|
||||
}
|
||||
|
||||
func scheduleStreams() {
|
||||
shouldKeepRunning = true
|
||||
|
||||
networkQueue = DispatchQueue.global(qos: .userInitiated)
|
||||
networkQueue?.async { [weak self] in
|
||||
self?.inputStream?.schedule(in: .current, forMode: .common)
|
||||
self?.outputStream?.schedule(in: .current, forMode: .common)
|
||||
|
||||
var isRunning = false
|
||||
|
||||
repeat {
|
||||
isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture)
|
||||
} while (isRunning)
|
||||
}
|
||||
}
|
||||
|
||||
func unscheduleStreams() {
|
||||
networkQueue?.sync { [weak self] in
|
||||
self?.inputStream?.remove(from: .current, forMode: .common)
|
||||
self?.outputStream?.remove(from: .current, forMode: .common)
|
||||
}
|
||||
|
||||
shouldKeepRunning = false
|
||||
}
|
||||
|
||||
func notifyDidClose(error: Error?) {
|
||||
if didClose != nil {
|
||||
didClose?(error)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue