@stream-io/video-react-native-sdk 0.1.14 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/commonjs/components/Call/CallControls/ScreenShareButton.js +120 -0
  3. package/dist/commonjs/components/Call/CallControls/ScreenShareButton.js.map +1 -0
  4. package/dist/commonjs/components/Call/CallControls/index.js +11 -0
  5. package/dist/commonjs/components/Call/CallControls/index.js.map +1 -1
  6. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  7. package/dist/commonjs/components/Participant/ParticipantView/ParticipantLabel.js +5 -4
  8. package/dist/commonjs/components/Participant/ParticipantView/ParticipantLabel.js.map +1 -1
  9. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer.js +1 -1
  10. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer.js.map +1 -1
  11. package/dist/commonjs/constants/TestIds.js +1 -0
  12. package/dist/commonjs/constants/TestIds.js.map +1 -1
  13. package/dist/commonjs/hooks/index.js +11 -0
  14. package/dist/commonjs/hooks/index.js.map +1 -1
  15. package/dist/commonjs/hooks/useIsIosScreenshareBroadcastStarted.js +25 -0
  16. package/dist/commonjs/hooks/useIsIosScreenshareBroadcastStarted.js.map +1 -0
  17. package/dist/commonjs/icons/ScreenShare.js +2 -14
  18. package/dist/commonjs/icons/ScreenShare.js.map +1 -1
  19. package/dist/commonjs/icons/ScreenShareIndicator.js +38 -0
  20. package/dist/commonjs/icons/ScreenShareIndicator.js.map +1 -0
  21. package/dist/commonjs/icons/index.js +11 -0
  22. package/dist/commonjs/icons/index.js.map +1 -1
  23. package/dist/commonjs/theme/theme.js +4 -0
  24. package/dist/commonjs/theme/theme.js.map +1 -1
  25. package/dist/commonjs/translations/en.json +1 -0
  26. package/dist/commonjs/version.js +1 -1
  27. package/dist/commonjs/version.js.map +1 -1
  28. package/dist/module/components/Call/CallControls/ScreenShareButton.js +112 -0
  29. package/dist/module/components/Call/CallControls/ScreenShareButton.js.map +1 -0
  30. package/dist/module/components/Call/CallControls/index.js +1 -0
  31. package/dist/module/components/Call/CallControls/index.js.map +1 -1
  32. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  33. package/dist/module/components/Participant/ParticipantView/ParticipantLabel.js +6 -5
  34. package/dist/module/components/Participant/ParticipantView/ParticipantLabel.js.map +1 -1
  35. package/dist/module/components/Participant/ParticipantView/VideoRenderer.js +1 -1
  36. package/dist/module/components/Participant/ParticipantView/VideoRenderer.js.map +1 -1
  37. package/dist/module/constants/TestIds.js +1 -0
  38. package/dist/module/constants/TestIds.js.map +1 -1
  39. package/dist/module/hooks/index.js +1 -0
  40. package/dist/module/hooks/index.js.map +1 -1
  41. package/dist/module/hooks/useIsIosScreenshareBroadcastStarted.js +19 -0
  42. package/dist/module/hooks/useIsIosScreenshareBroadcastStarted.js.map +1 -0
  43. package/dist/module/icons/ScreenShare.js +3 -15
  44. package/dist/module/icons/ScreenShare.js.map +1 -1
  45. package/dist/module/icons/ScreenShareIndicator.js +28 -0
  46. package/dist/module/icons/ScreenShareIndicator.js.map +1 -0
  47. package/dist/module/icons/index.js +1 -0
  48. package/dist/module/icons/index.js.map +1 -1
  49. package/dist/module/theme/theme.js +4 -0
  50. package/dist/module/theme/theme.js.map +1 -1
  51. package/dist/module/translations/en.json +1 -0
  52. package/dist/module/version.js +1 -1
  53. package/dist/module/version.js.map +1 -1
  54. package/dist/typescript/components/Call/CallControls/ScreenShareButton.d.ts +22 -0
  55. package/dist/typescript/components/Call/CallControls/ScreenShareButton.d.ts.map +1 -0
  56. package/dist/typescript/components/Call/CallControls/index.d.ts +1 -0
  57. package/dist/typescript/components/Call/CallControls/index.d.ts.map +1 -1
  58. package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts +2 -2
  59. package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
  60. package/dist/typescript/components/Participant/ParticipantView/ParticipantLabel.d.ts.map +1 -1
  61. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer.d.ts.map +1 -1
  62. package/dist/typescript/constants/TestIds.d.ts +1 -0
  63. package/dist/typescript/constants/TestIds.d.ts.map +1 -1
  64. package/dist/typescript/hooks/index.d.ts +1 -0
  65. package/dist/typescript/hooks/index.d.ts.map +1 -1
  66. package/dist/typescript/hooks/useIsIosScreenshareBroadcastStarted.d.ts +2 -0
  67. package/dist/typescript/hooks/useIsIosScreenshareBroadcastStarted.d.ts.map +1 -0
  68. package/dist/typescript/icons/ScreenShare.d.ts.map +1 -1
  69. package/dist/typescript/icons/ScreenShareIndicator.d.ts +8 -0
  70. package/dist/typescript/icons/ScreenShareIndicator.d.ts.map +1 -0
  71. package/dist/typescript/icons/index.d.ts +1 -0
  72. package/dist/typescript/icons/index.d.ts.map +1 -1
  73. package/dist/typescript/theme/theme.d.ts +4 -0
  74. package/dist/typescript/theme/theme.d.ts.map +1 -1
  75. package/dist/typescript/translations/index.d.ts +1 -0
  76. package/dist/typescript/translations/index.d.ts.map +1 -1
  77. package/dist/typescript/version.d.ts +1 -1
  78. package/dist/typescript/version.d.ts.map +1 -1
  79. package/expo-config-plugin/dist/common/types.d.ts +2 -0
  80. package/expo-config-plugin/dist/index.js +2 -0
  81. package/expo-config-plugin/dist/withAndroidManifest.js +9 -5
  82. package/expo-config-plugin/dist/withBuildProperties.d.ts +2 -2
  83. package/expo-config-plugin/dist/withBuildProperties.js +2 -2
  84. package/expo-config-plugin/dist/withIosScreenCapture/addBroadcastExtensionXcodeTarget.d.ts +11 -0
  85. package/expo-config-plugin/dist/withIosScreenCapture/addBroadcastExtensionXcodeTarget.js +225 -0
  86. package/expo-config-plugin/dist/withIosScreenCapture/index.d.ts +4 -0
  87. package/expo-config-plugin/dist/withIosScreenCapture/index.js +20 -0
  88. package/expo-config-plugin/dist/withIosScreenCapture/withFilesMod.d.ts +4 -0
  89. package/expo-config-plugin/dist/withIosScreenCapture/withFilesMod.js +71 -0
  90. package/expo-config-plugin/dist/withIosScreenCapture/withPlistUpdates.d.ts +4 -0
  91. package/expo-config-plugin/dist/withIosScreenCapture/withPlistUpdates.js +33 -0
  92. package/expo-config-plugin/dist/withIosScreenCapture/withTarget.d.ts +4 -0
  93. package/expo-config-plugin/dist/withIosScreenCapture/withTarget.js +122 -0
  94. package/expo-config-plugin/static/Atomic.swift +36 -0
  95. package/expo-config-plugin/static/DarwinNotificationCenter.swift +25 -0
  96. package/expo-config-plugin/static/SampleHandler.swift +99 -0
  97. package/expo-config-plugin/static/SampleUploader.swift +143 -0
  98. package/expo-config-plugin/static/SocketConnection.swift +195 -0
  99. package/ios/StreamVideoReactNative-Bridging-Header.h +2 -1
  100. package/ios/StreamVideoReactNative.h +4 -2
  101. package/ios/StreamVideoReactNative.m +82 -1
  102. package/ios/StreamVideoReactNative.xcodeproj/project.pbxproj +3 -3
  103. package/package.json +4 -3
  104. package/src/components/Call/CallControls/ScreenShareButton.tsx +127 -0
  105. package/src/components/Call/CallControls/index.tsx +1 -0
  106. package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +2 -3
  107. package/src/components/Participant/ParticipantView/ParticipantLabel.tsx +14 -5
  108. package/src/components/Participant/ParticipantView/VideoRenderer.tsx +3 -1
  109. package/src/constants/TestIds.ts +1 -0
  110. package/src/hooks/index.ts +1 -0
  111. package/src/hooks/useIsIosScreenshareBroadcastStarted.ts +33 -0
  112. package/src/icons/ScreenShare.tsx +3 -18
  113. package/src/icons/ScreenShareIndicator.tsx +34 -0
  114. package/src/icons/index.tsx +1 -0
  115. package/src/theme/theme.ts +8 -0
  116. package/src/translations/en.json +1 -0
  117. package/src/version.ts +1 -1
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const config_plugins_1 = require("@expo/config-plugins");
7
+ const withPlistUpdates_1 = __importDefault(require("./withPlistUpdates"));
8
+ const withFilesMod_1 = __importDefault(require("./withFilesMod"));
9
+ const withTarget_1 = __importDefault(require("./withTarget"));
10
+ const withIosScreenCapture = (config, props) => {
11
+ if (!props?.enableScreenshare) {
12
+ return config;
13
+ }
14
+ return (0, config_plugins_1.withPlugins)(config, [
15
+ () => (0, withFilesMod_1.default)(config, props),
16
+ () => (0, withTarget_1.default)(config, props),
17
+ () => (0, withPlistUpdates_1.default)(config, props),
18
+ ]);
19
+ };
20
+ exports.default = withIosScreenCapture;
@@ -0,0 +1,4 @@
1
+ import { ConfigPlugin } from '@expo/config-plugins';
2
+ import { ConfigProps } from '../common/types';
3
+ declare const withFilesMod: ConfigPlugin<ConfigProps>;
4
+ export default withFilesMod;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const config_plugins_1 = require("@expo/config-plugins");
30
+ const plist_1 = __importDefault(require("@expo/plist"));
31
+ const fs = __importStar(require("fs"));
32
+ const path = __importStar(require("path"));
33
+ const withFilesMod = (config) => (0, config_plugins_1.withPlugins)(config, [
34
+ withBroadcastExtensionHandler,
35
+ withBroadcastExtensionPlist,
36
+ ]);
37
+ exports.default = withFilesMod;
38
+ // creates the extension handler directory and adds the SampleHandler.swift file
39
+ const withBroadcastExtensionHandler = (configuration) => {
40
+ return (0, config_plugins_1.withDangerousMod)(configuration, [
41
+ 'ios',
42
+ (config) => {
43
+ const extensionRootPath = path.join(config.modRequest.platformProjectRoot, 'broadcast');
44
+ fs.mkdirSync(extensionRootPath, { recursive: true });
45
+ fs.copyFileSync(path.join(__dirname, '..', '..', 'static', 'SampleHandler.swift'), path.join(extensionRootPath, 'SampleHandler.swift'));
46
+ return config;
47
+ },
48
+ ]);
49
+ };
50
+ // adds the Info.plist to the extension handler directory
51
+ const withBroadcastExtensionPlist = (configuration) => {
52
+ return (0, config_plugins_1.withDangerousMod)(configuration, [
53
+ 'ios',
54
+ (config) => {
55
+ const extensionRootPath = path.join(config.modRequest.platformProjectRoot, 'broadcast');
56
+ const extensionPlistPath = path.join(extensionRootPath, 'Info.plist');
57
+ const extensionPlist = {
58
+ NSExtension: {
59
+ NSExtensionPointIdentifier: 'com.apple.broadcast-services-upload',
60
+ NSExtensionPrincipalClass: '$(PRODUCT_MODULE_NAME).SampleHandler',
61
+ RPBroadcastProcessMode: 'RPBroadcastProcessModeSampleBuffer',
62
+ },
63
+ };
64
+ fs.mkdirSync(path.dirname(extensionPlistPath), {
65
+ recursive: true,
66
+ });
67
+ fs.writeFileSync(extensionPlistPath, plist_1.default.build(extensionPlist));
68
+ return config;
69
+ },
70
+ ]);
71
+ };
@@ -0,0 +1,4 @@
1
+ import { ConfigPlugin } from '@expo/config-plugins';
2
+ import { ConfigProps } from '../common/types';
3
+ declare const withPlistUpdates: ConfigPlugin<ConfigProps>;
4
+ export default withPlistUpdates;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_plugins_1 = require("@expo/config-plugins");
4
+ const withPlistUpdates = (config, _props) => (0, config_plugins_1.withPlugins)(config, [withAppEntitlements, withInfoPlistRTC]);
5
+ exports.default = withPlistUpdates;
6
+ // adds the app group identifier to the app's entitlements file
7
+ const withAppEntitlements = (configuration) => {
8
+ return (0, config_plugins_1.withEntitlementsPlist)(configuration, (config) => {
9
+ const appGroupIdentifier = `group.${config.ios
10
+ .bundleIdentifier}.appgroup`;
11
+ config.modResults['com.apple.security.application-groups'] = [
12
+ appGroupIdentifier,
13
+ ];
14
+ return config;
15
+ });
16
+ };
17
+ const withInfoPlistRTC = (configuration) => {
18
+ return (0, config_plugins_1.withInfoPlist)(configuration, (config) => {
19
+ const appGroupIdentifier = `group.${config.ios
20
+ .bundleIdentifier}.appgroup`;
21
+ const extensionBundleIdentifier = `${config.ios
22
+ .bundleIdentifier}.broadcast`;
23
+ config.modResults.RTCAppGroupIdentifier = appGroupIdentifier;
24
+ config.modResults.RTCScreenSharingExtension = extensionBundleIdentifier;
25
+ if (!config.modResults.UIBackgroundModes) {
26
+ config.modResults.UIBackgroundModes = [];
27
+ }
28
+ if (!config.modResults.UIBackgroundModes.includes('voip')) {
29
+ config.modResults.UIBackgroundModes.push('voip');
30
+ }
31
+ return config;
32
+ });
33
+ };
@@ -0,0 +1,4 @@
1
+ import { ConfigPlugin } from '@expo/config-plugins';
2
+ import { ConfigProps } from '../common/types';
3
+ declare const withTarget: ConfigPlugin<ConfigProps>;
4
+ export default withTarget;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const config_plugins_1 = require("@expo/config-plugins");
30
+ const plist_1 = __importDefault(require("@expo/plist"));
31
+ const fs = __importStar(require("fs"));
32
+ const path = __importStar(require("path"));
33
+ const addBroadcastExtensionXcodeTarget_1 = __importDefault(require("./addBroadcastExtensionXcodeTarget"));
34
+ // adds the extension's entitlements file
35
+ const withTarget = (configuration, props) => {
36
+ return (0, config_plugins_1.withXcodeProject)(configuration, (config) => {
37
+ const appName = config.modRequest.projectName;
38
+ const extensionName = 'broadcast';
39
+ const extensionBundleIdentifier = `${config.ios
40
+ .bundleIdentifier}.broadcast`;
41
+ const currentProjectVersion = config.ios.buildNumber || '1';
42
+ const marketingVersion = config.version;
43
+ const proj = config.modResults;
44
+ const developmentTeamId = props?.appleTeamId;
45
+ if (!developmentTeamId) {
46
+ throw new Error('No appleTeamId was provided in the Expo config. Please provide one to create the screenshare broadcast extension');
47
+ }
48
+ (0, addBroadcastExtensionXcodeTarget_1.default)(proj, {
49
+ appName,
50
+ extensionName,
51
+ extensionBundleIdentifier,
52
+ currentProjectVersion,
53
+ marketingVersion,
54
+ developmentTeamId,
55
+ });
56
+ const appGroupIdentifier = `group.${config.ios
57
+ .bundleIdentifier}.appgroup`;
58
+ const extensionRootPath = path.join(config.modRequest.platformProjectRoot, 'broadcast');
59
+ addBroadcastEntitlementsFile({
60
+ proj,
61
+ extensionRootPath,
62
+ appGroupIdentifier,
63
+ });
64
+ addBroadcastSourceFiles({
65
+ proj,
66
+ extensionRootPath,
67
+ appGroupIdentifier,
68
+ });
69
+ return config;
70
+ });
71
+ };
72
+ exports.default = withTarget;
73
+ const addBroadcastEntitlementsFile = ({ proj, extensionRootPath, appGroupIdentifier, }) => {
74
+ const entitlementsPath = path.join(extensionRootPath, 'broadcast.entitlements');
75
+ const extensionEntitlements = {
76
+ 'com.apple.security.application-groups': [appGroupIdentifier],
77
+ };
78
+ // create file
79
+ fs.mkdirSync(path.dirname(entitlementsPath), {
80
+ recursive: true,
81
+ });
82
+ fs.writeFileSync(entitlementsPath, plist_1.default.build(extensionEntitlements));
83
+ // add file to extension group
84
+ const targetUuid = proj.findTargetKey('broadcast');
85
+ const groupUuid = proj.findPBXGroupKey({ name: 'broadcast' });
86
+ proj.addFile('broadcast.entitlements', groupUuid, {
87
+ target: targetUuid,
88
+ lastKnownFileType: 'text.plist.entitlements',
89
+ });
90
+ };
91
+ const addBroadcastSourceFiles = ({ proj, extensionRootPath, appGroupIdentifier, }) => {
92
+ fs.mkdirSync(extensionRootPath, { recursive: true });
93
+ fs.copyFileSync(path.join(__dirname, '..', '..', 'static', 'Atomic.swift'), path.join(extensionRootPath, 'Atomic.swift'));
94
+ fs.copyFileSync(path.join(__dirname, '..', '..', 'static', 'DarwinNotificationCenter.swift'), path.join(extensionRootPath, 'DarwinNotificationCenter.swift'));
95
+ fs.copyFileSync(path.join(__dirname, '..', '..', 'static', 'SampleUploader.swift'), path.join(extensionRootPath, 'SampleUploader.swift'));
96
+ fs.copyFileSync(path.join(__dirname, '..', '..', 'static', 'SocketConnection.swift'), path.join(extensionRootPath, 'SocketConnection.swift'));
97
+ // Update app group bundle id in SampleHandler code
98
+ const code = fs.readFileSync(path.join(extensionRootPath, 'SampleHandler.swift'), { encoding: 'utf-8' });
99
+ fs.writeFileSync(path.join(extensionRootPath, 'SampleHandler.swift'), code.replace('group.com.example.broadcast.appgroup', appGroupIdentifier));
100
+ const targetUuid = proj.findTargetKey('broadcast');
101
+ const groupUuid = proj.findPBXGroupKey({ name: 'broadcast' });
102
+ if (!targetUuid) {
103
+ console.error('Failed to find "broadcast" target!');
104
+ return;
105
+ }
106
+ if (!groupUuid) {
107
+ console.error('Failed to find "broadcast" group!');
108
+ return;
109
+ }
110
+ proj.addSourceFile('Atomic.swift', {
111
+ target: targetUuid,
112
+ }, groupUuid);
113
+ proj.addSourceFile('DarwinNotificationCenter.swift', {
114
+ target: targetUuid,
115
+ }, groupUuid);
116
+ proj.addSourceFile('SampleUploader.swift', {
117
+ target: targetUuid,
118
+ }, groupUuid);
119
+ proj.addSourceFile('SocketConnection.swift', {
120
+ target: targetUuid,
121
+ }, groupUuid);
122
+ };
@@ -0,0 +1,36 @@
1
+ //
2
+ // Atomic.swift
3
+ // Broadcast Extension
4
+ //
5
+ // Created by Maksym Shcheglov.
6
+ // https://www.onswiftwings.com/posts/atomic-property-wrapper/
7
+ //
8
+ import Foundation
9
+
10
+ @propertyWrapper
11
+ struct Atomic<Value> {
12
+
13
+ private var value: Value
14
+ private let lock = NSLock()
15
+
16
+ init(wrappedValue value: Value) {
17
+ self.value = value
18
+ }
19
+
20
+ var wrappedValue: Value {
21
+ get { load() }
22
+ set { store(newValue: newValue) }
23
+ }
24
+
25
+ func load() -> Value {
26
+ lock.lock()
27
+ defer { lock.unlock() }
28
+ return value
29
+ }
30
+
31
+ mutating func store(newValue: Value) {
32
+ lock.lock()
33
+ defer { lock.unlock() }
34
+ value = newValue
35
+ }
36
+ }
@@ -0,0 +1,25 @@
1
+ //
2
+ // DarwinNotificationCenter.swift
3
+ // Broadcast Extension
4
+ //
5
+ import Foundation
6
+
7
+ enum DarwinNotification: String {
8
+ case broadcastStarted = "iOS_BroadcastStarted"
9
+ case broadcastStopped = "iOS_BroadcastStopped"
10
+ }
11
+
12
+ class DarwinNotificationCenter {
13
+
14
+ static let shared = DarwinNotificationCenter()
15
+
16
+ private let notificationCenter: CFNotificationCenter
17
+
18
+ init() {
19
+ notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
20
+ }
21
+
22
+ func postNotification(_ name: DarwinNotification) {
23
+ CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true)
24
+ }
25
+ }
@@ -0,0 +1,99 @@
1
+ //
2
+ // SampleHandler.swift
3
+ // Broadcast Extension
4
+ //
5
+ import ReplayKit
6
+ import OSLog
7
+
8
+ let broadcastLogger = OSLog(subsystem: "io.getstream.reactnative", category: "Broadcast")
9
+ private enum Constants {
10
+ // the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app.
11
+ static let appGroupIdentifier = "group.com.example.broadcast.appgroup"
12
+ }
13
+ class SampleHandler: RPBroadcastSampleHandler {
14
+
15
+ private var clientConnection: SocketConnection?
16
+ private var uploader: SampleUploader?
17
+
18
+ private var frameCount: Int = 0
19
+
20
+ var socketFilePath: String {
21
+ let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
22
+ return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
23
+ }
24
+
25
+ override init() {
26
+ super.init()
27
+ if let connection = SocketConnection(filePath: socketFilePath) {
28
+ clientConnection = connection
29
+ setupConnection()
30
+
31
+ uploader = SampleUploader(connection: connection)
32
+ }
33
+ os_log(.debug, log: broadcastLogger, "%{public}s", socketFilePath)
34
+ }
35
+
36
+ override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
37
+ // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
38
+ frameCount = 0
39
+
40
+ DarwinNotificationCenter.shared.postNotification(.broadcastStarted)
41
+ openConnection()
42
+ }
43
+
44
+ override func broadcastPaused() {
45
+ // User has requested to pause the broadcast. Samples will stop being delivered.
46
+ }
47
+
48
+ override func broadcastResumed() {
49
+ // User has requested to resume the broadcast. Samples delivery will resume.
50
+ }
51
+
52
+ override func broadcastFinished() {
53
+ // User has requested to finish the broadcast.
54
+ DarwinNotificationCenter.shared.postNotification(.broadcastStopped)
55
+ clientConnection?.close()
56
+ }
57
+
58
+ override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
59
+ switch sampleBufferType {
60
+ case RPSampleBufferType.video:
61
+ uploader?.send(sample: sampleBuffer)
62
+ default:
63
+ break
64
+ }
65
+ }
66
+ }
67
+
68
+ private extension SampleHandler {
69
+
70
+ func setupConnection() {
71
+ clientConnection?.didClose = { [weak self] error in
72
+ os_log(.debug, log: broadcastLogger, "client connection did close \(String(describing: error))")
73
+
74
+ if let error = error {
75
+ self?.finishBroadcastWithError(error)
76
+ } else {
77
+ // the displayed failure message is more user friendly when using NSError instead of Error
78
+ let JMScreenSharingStopped = 10001
79
+ let customError = NSError(domain: RPRecordingErrorDomain, code: JMScreenSharingStopped, userInfo: [NSLocalizedDescriptionKey: "Screen sharing stopped"])
80
+ self?.finishBroadcastWithError(customError)
81
+ }
82
+ }
83
+ }
84
+
85
+ func openConnection() {
86
+ let queue = DispatchQueue(label: "broadcast.connectTimer")
87
+ let timer = DispatchSource.makeTimerSource(queue: queue)
88
+ timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500))
89
+ timer.setEventHandler { [weak self] in
90
+ guard self?.clientConnection?.open() == true else {
91
+ return
92
+ }
93
+
94
+ timer.cancel()
95
+ }
96
+
97
+ timer.resume()
98
+ }
99
+ }
@@ -0,0 +1,143 @@
1
+ //
2
+ // SampleUploader.swift
3
+ // Broadcast Extension
4
+ //
5
+ import Foundation
6
+ import ReplayKit
7
+ import OSLog
8
+
9
+ private enum Constants {
10
+ static let bufferMaxLength = 10240
11
+ }
12
+
13
+ class SampleUploader {
14
+
15
+ private static var imageContext = CIContext(options: nil)
16
+
17
+ @Atomic private var isReady = false
18
+ private var connection: SocketConnection
19
+
20
+ private var dataToSend: Data?
21
+ private var byteIndex = 0
22
+
23
+ private let serialQueue: DispatchQueue
24
+
25
+ init(connection: SocketConnection) {
26
+ self.connection = connection
27
+ self.serialQueue = DispatchQueue(label: "org.getstream.sampleUploader")
28
+
29
+ setupConnection()
30
+ }
31
+
32
+ @discardableResult func send(sample buffer: CMSampleBuffer) -> Bool {
33
+ guard isReady else {
34
+ return false
35
+ }
36
+
37
+ isReady = false
38
+
39
+ dataToSend = prepare(sample: buffer)
40
+ byteIndex = 0
41
+
42
+ serialQueue.async { [weak self] in
43
+ self?.sendDataChunk()
44
+ }
45
+
46
+ return true
47
+ }
48
+ }
49
+
50
+ private extension SampleUploader {
51
+
52
+ func setupConnection() {
53
+ connection.didOpen = { [weak self] in
54
+ self?.isReady = true
55
+ }
56
+ connection.streamHasSpaceAvailable = { [weak self] in
57
+ self?.serialQueue.async {
58
+ if let success = self?.sendDataChunk() {
59
+ self?.isReady = !success
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ @discardableResult func sendDataChunk() -> Bool {
66
+ guard let dataToSend = dataToSend else {
67
+ return false
68
+ }
69
+
70
+ var bytesLeft = dataToSend.count - byteIndex
71
+ var length = bytesLeft > Constants.bufferMaxLength ? Constants.bufferMaxLength : bytesLeft
72
+
73
+ length = dataToSend[byteIndex..<(byteIndex + length)].withUnsafeBytes {
74
+ guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else {
75
+ return 0
76
+ }
77
+
78
+ return connection.writeToStream(buffer: ptr, maxLength: length)
79
+ }
80
+
81
+ if length > 0 {
82
+ byteIndex += length
83
+ bytesLeft -= length
84
+
85
+ if bytesLeft == 0 {
86
+ self.dataToSend = nil
87
+ byteIndex = 0
88
+ }
89
+ } else {
90
+ os_log(.debug, log: broadcastLogger, "writeBufferToStream failure")
91
+ }
92
+
93
+ return true
94
+ }
95
+
96
+ func prepare(sample buffer: CMSampleBuffer) -> Data? {
97
+ guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
98
+ os_log(.debug, log: broadcastLogger, "image buffer not available")
99
+ return nil
100
+ }
101
+
102
+ CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
103
+
104
+ let scaleFactor = 1.0
105
+ let width = CVPixelBufferGetWidth(imageBuffer)/Int(scaleFactor)
106
+ let height = CVPixelBufferGetHeight(imageBuffer)/Int(scaleFactor)
107
+ let orientation = CMGetAttachment(buffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil)?.uintValue ?? 0
108
+
109
+ let scaleTransform = CGAffineTransform(scaleX: CGFloat(1.0/scaleFactor), y: CGFloat(1.0/scaleFactor))
110
+ let bufferData = self.jpegData(from: imageBuffer, scale: scaleTransform)
111
+
112
+ CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
113
+
114
+ guard let messageData = bufferData else {
115
+ os_log(.debug, log: broadcastLogger, "corrupted image buffer")
116
+ return nil
117
+ }
118
+
119
+ let httpResponse = CFHTTPMessageCreateResponse(nil, 200, nil, kCFHTTPVersion1_1).takeRetainedValue()
120
+ CFHTTPMessageSetHeaderFieldValue(httpResponse, "Content-Length" as CFString, String(messageData.count) as CFString)
121
+ CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Width" as CFString, String(width) as CFString)
122
+ CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Height" as CFString, String(height) as CFString)
123
+ CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Orientation" as CFString, String(orientation) as CFString)
124
+
125
+ CFHTTPMessageSetBody(httpResponse, messageData as CFData)
126
+
127
+ let serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse)?.takeRetainedValue() as Data?
128
+
129
+ return serializedMessage
130
+ }
131
+
132
+ func jpegData(from buffer: CVPixelBuffer, scale scaleTransform: CGAffineTransform) -> Data? {
133
+ let image = CIImage(cvPixelBuffer: buffer).transformed(by: scaleTransform)
134
+
135
+ guard let colorSpace = image.colorSpace else {
136
+ return nil
137
+ }
138
+
139
+ let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0]
140
+
141
+ return SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options)
142
+ }
143
+ }