expo-livekit-screen-share 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arkadiusz Kubaczkowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # expo-livekit-screen-share
2
+
3
+ Expo config plugin that enables screen sharing for [LiveKit](https://livekit.io) React Native apps on **iOS** and **Android**.
4
+
5
+ ## What it does
6
+
7
+ ### iOS
8
+
9
+ - Creates a **Broadcast Upload Extension** target in your Xcode project
10
+ - Downloads the [Jitsi reference implementation](https://github.com/jitsi/jitsi-meet-sdk-samples/tree/master/ios/swift-screensharing/JitsiSDKScreenSharingTest/Broadcast%20Extension) Swift files during prebuild (cached locally)
11
+ - Configures **App Groups** for inter-process communication between app and extension
12
+ - Sets up `Info.plist`, entitlements, and Xcode build settings automatically
13
+
14
+ ### Android
15
+
16
+ - Adds `FOREGROUND_SERVICE_MEDIA_PROJECTION` permission to `AndroidManifest.xml`
17
+ - Enables LiveKit's screen share foreground service
18
+
19
+ ## Installation
20
+
21
+ ```sh
22
+ npx expo install expo-livekit-screen-share
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ Add the plugin to your `app.json` or `app.config.js`:
28
+
29
+ ```json
30
+ {
31
+ "plugins": ["expo-livekit-screen-share"]
32
+ }
33
+ ```
34
+
35
+ ### Options
36
+
37
+ All options are optional with sensible defaults:
38
+
39
+ ```json
40
+ {
41
+ "plugins": [
42
+ ["expo-livekit-screen-share", {
43
+ "ios": {
44
+ "extensionName": "ScreenShareExtension",
45
+ "deploymentTarget": "16.0",
46
+ "appGroupIdentifier": "group.com.example.myapp"
47
+ },
48
+ "android": {
49
+ "enableScreenShareService": true,
50
+ "foregroundServicePermission": true
51
+ }
52
+ }]
53
+ ]
54
+ }
55
+ ```
56
+
57
+ | Option | Platform | Default | Description |
58
+ |--------|----------|---------|-------------|
59
+ | `ios.extensionName` | iOS | `"ScreenShareExtension"` | Name of the Broadcast Upload Extension target |
60
+ | `ios.deploymentTarget` | iOS | `"16.0"` | Minimum iOS deployment target for the extension |
61
+ | `ios.appGroupIdentifier` | iOS | `"group.{bundleIdentifier}"` | Custom App Group identifier (if your project uses a different naming convention) |
62
+ | `android.enableScreenShareService` | Android | `true` | Enable LiveKit's foreground service for screen sharing |
63
+ | `android.foregroundServicePermission` | Android | `true` | Add `FOREGROUND_SERVICE_MEDIA_PROJECTION` permission |
64
+
65
+ ## iOS Setup (Apple Developer Portal)
66
+
67
+ Before building, you need to register identifiers in the [Apple Developer Portal](https://developer.apple.com). Repeat these steps **for each environment** (development, staging, production):
68
+
69
+ ### 1. Register an App Group
70
+
71
+ Identifiers > **+** > App Groups:
72
+ - Identifier: `group.{your.bundle.identifier}` (e.g. `group.com.example.myapp`)
73
+
74
+ ### 2. Add App Group to your main App ID
75
+
76
+ Identifiers > find your App ID > Edit > Capabilities > **App Groups** > select the group from step 1.
77
+
78
+ ### 3. Register Extension Bundle ID
79
+
80
+ Identifiers > **+** > App IDs > type **App**:
81
+ - Bundle ID: `{your.bundle.identifier}.ScreenShareExtension`
82
+ - Enable **App Groups** capability > select the same group
83
+
84
+ ### 4. Provisioning
85
+
86
+ If using **Xcode Automatic Signing** (development builds), Xcode handles provisioning profiles automatically.
87
+
88
+ For **EAS Build**, add `appExtensions` to your config:
89
+
90
+ ```js
91
+ // app.config.js
92
+ extra: {
93
+ eas: {
94
+ build: {
95
+ experimental: {
96
+ ios: {
97
+ appExtensions: [{
98
+ targetName: 'ScreenShareExtension',
99
+ bundleIdentifier: `${bundleIdentifier}.ScreenShareExtension`,
100
+ entitlements: {
101
+ 'com.apple.security.application-groups': [
102
+ `group.${bundleIdentifier}`,
103
+ ],
104
+ },
105
+ }],
106
+ },
107
+ },
108
+ },
109
+ },
110
+ }
111
+ ```
112
+
113
+ ## Usage in your app
114
+
115
+ ### Start screen sharing
116
+
117
+ ```tsx
118
+ import { useRoomContext } from '@livekit/react-native';
119
+
120
+ // Android — call directly:
121
+ await room.localParticipant.setScreenShareEnabled(true);
122
+
123
+ // iOS — show broadcast picker first:
124
+ import { ScreenCapturePickerView } from '@livekit/react-native-webrtc';
125
+ import { findNodeHandle, NativeModules, Platform } from 'react-native';
126
+
127
+ // Mount the invisible picker view somewhere in your component tree:
128
+ <ScreenCapturePickerView ref={pickerRef} />
129
+
130
+ // Then trigger it:
131
+ if (Platform.OS === 'ios') {
132
+ const tag = findNodeHandle(pickerRef.current);
133
+ await NativeModules.ScreenCapturePickerViewManager.show(tag);
134
+ }
135
+ await room.localParticipant.setScreenShareEnabled(true);
136
+ ```
137
+
138
+ ### Check screen share state
139
+
140
+ ```tsx
141
+ import { useLocalParticipant } from '@livekit/react-native';
142
+
143
+ const { isScreenShareEnabled } = useLocalParticipant();
144
+ ```
145
+
146
+ ## How it works
147
+
148
+ The iOS Broadcast Upload Extension runs as a separate process that captures the screen via ReplayKit. It communicates with the main app through a Unix domain socket (`rtc_SSFD`) in the shared App Group container. The `@livekit/react-native-webrtc` native module (`ScreenCapturer`) listens on this socket and feeds received frames into the WebRTC pipeline.
149
+
150
+ Swift source files are downloaded from the [Jitsi reference implementation](https://github.com/jitsi/jitsi-meet-sdk-samples) during `expo prebuild` and cached in `node_modules/.cache/expo-livekit-screen-share/`. Delete this directory to force re-download.
151
+
152
+ ## Troubleshooting
153
+
154
+ ### Download fails during prebuild
155
+
156
+ If you're building offline or behind a firewall, manually download the Swift files from [Jitsi SDK samples](https://github.com/jitsi/jitsi-meet-sdk-samples/tree/master/ios/swift-screensharing/JitsiSDKScreenSharingTest/Broadcast%20Extension) and place them in `node_modules/.cache/expo-livekit-screen-share/`.
157
+
158
+ ### iOS screen share doesn't start
159
+
160
+ - Verify App Group identifiers match between the main app and extension
161
+ - Check that `RTCScreenSharingExtension` and `RTCAppGroupIdentifier` are set in the main app's `Info.plist`
162
+ - Ensure the extension's provisioning profile includes the App Group capability
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,14 @@
1
+ import { type ConfigPlugin } from "@expo/config-plugins";
2
+ type ScreenShareOptions = {
3
+ ios?: {
4
+ extensionName?: string;
5
+ deploymentTarget?: string;
6
+ appGroupIdentifier?: string;
7
+ };
8
+ android?: {
9
+ enableScreenShareService?: boolean;
10
+ foregroundServicePermission?: boolean;
11
+ };
12
+ };
13
+ declare const withScreenShare: ConfigPlugin<ScreenShareOptions | undefined>;
14
+ export default withScreenShare;
package/build/index.js ADDED
@@ -0,0 +1,244 @@
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 plist_1 = __importDefault(require("@expo/plist"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const https_1 = __importDefault(require("https"));
10
+ const path_1 = __importDefault(require("path"));
11
+ // --- Constants ---
12
+ const DEFAULT_EXTENSION_NAME = "ScreenShareExtension";
13
+ const DEFAULT_DEPLOYMENT_TARGET = "16.0";
14
+ const ANDROID_SCREEN_SHARE_SERVICE_KEY = "io.livekit.reactnative.expo.ENABLE_SCREEN_SHARE_SERVICE";
15
+ const JITSI_BASE_URL = "https://raw.githubusercontent.com/jitsi/jitsi-meet-sdk-samples/master/ios/swift-screensharing/JitsiSDKScreenSharingTest/Broadcast%20Extension";
16
+ const SWIFT_FILES = [
17
+ "SampleHandler.swift",
18
+ "SampleUploader.swift",
19
+ "SocketConnection.swift",
20
+ "DarwinNotificationCenter.swift",
21
+ "Atomic.swift",
22
+ ];
23
+ const JITSI_APP_GROUP_PLACEHOLDER = "group.com.jitsi.example-screensharing.appgroup";
24
+ const CACHE_DIR = "node_modules/.cache/expo-livekit-screen-share";
25
+ // --- Helpers ---
26
+ function getAppGroupIdentifier(bundleIdentifier) {
27
+ return `group.${bundleIdentifier}`;
28
+ }
29
+ function getExtensionBundleIdentifier(bundleIdentifier, extensionName) {
30
+ return `${bundleIdentifier}.${extensionName}`;
31
+ }
32
+ function httpsGet(url) {
33
+ return new Promise((resolve, reject) => {
34
+ https_1.default
35
+ .get(url, (res) => {
36
+ if (res.statusCode &&
37
+ res.statusCode >= 300 &&
38
+ res.statusCode < 400 &&
39
+ res.headers.location) {
40
+ httpsGet(res.headers.location).then(resolve, reject);
41
+ return;
42
+ }
43
+ if (res.statusCode !== 200) {
44
+ reject(new Error(`Failed to download ${url}: ${res.statusCode}`));
45
+ return;
46
+ }
47
+ let data = "";
48
+ res.on("data", (chunk) => (data += chunk));
49
+ res.on("end", () => resolve(data));
50
+ })
51
+ .on("error", reject);
52
+ });
53
+ }
54
+ async function downloadSwiftFiles(cacheDir) {
55
+ const allCached = SWIFT_FILES.every((file) => fs_1.default.existsSync(path_1.default.join(cacheDir, file)));
56
+ if (allCached) {
57
+ console.log("[expo-livekit-screen-share] Using cached Swift files");
58
+ return;
59
+ }
60
+ fs_1.default.mkdirSync(cacheDir, { recursive: true });
61
+ console.log("[expo-livekit-screen-share] Downloading broadcast extension files from Jitsi reference...");
62
+ for (const file of SWIFT_FILES) {
63
+ const url = `${JITSI_BASE_URL}/${file}`;
64
+ try {
65
+ const content = await httpsGet(url);
66
+ fs_1.default.writeFileSync(path_1.default.join(cacheDir, file), content);
67
+ console.log(` Downloaded ${file}`);
68
+ }
69
+ catch (error) {
70
+ throw new Error(`[expo-livekit-screen-share] Failed to download ${file}. ` +
71
+ `Ensure you have internet access or manually place the files in ${cacheDir}. ` +
72
+ `Original error: ${error}`);
73
+ }
74
+ }
75
+ }
76
+ // --- iOS Mods ---
77
+ function withScreenShareInfoPlist(config, extensionName, appGroupId) {
78
+ return (0, config_plugins_1.withInfoPlist)(config, (mod) => {
79
+ const bundleId = mod.ios?.bundleIdentifier ?? "";
80
+ const appGroup = appGroupId ?? getAppGroupIdentifier(bundleId);
81
+ mod.modResults.RTCScreenSharingExtension = getExtensionBundleIdentifier(bundleId, extensionName);
82
+ mod.modResults.RTCAppGroupIdentifier = appGroup;
83
+ return mod;
84
+ });
85
+ }
86
+ function withScreenShareEntitlements(config, appGroupId) {
87
+ return (0, config_plugins_1.withEntitlementsPlist)(config, (mod) => {
88
+ const bundleId = mod.ios?.bundleIdentifier ?? "";
89
+ const appGroup = appGroupId ?? getAppGroupIdentifier(bundleId);
90
+ const existing = mod.modResults["com.apple.security.application-groups"] ??
91
+ [];
92
+ if (!existing.includes(appGroup)) {
93
+ mod.modResults["com.apple.security.application-groups"] = [
94
+ ...existing,
95
+ appGroup,
96
+ ];
97
+ }
98
+ return mod;
99
+ });
100
+ }
101
+ function withScreenShareXcodeProject(config, extensionName, deploymentTarget) {
102
+ return (0, config_plugins_1.withXcodeProject)(config, (mod) => {
103
+ const xcodeProject = mod.modResults;
104
+ const bundleId = mod.ios?.bundleIdentifier ?? "";
105
+ const extensionBundleId = getExtensionBundleIdentifier(bundleId, extensionName);
106
+ const existingTarget = xcodeProject.pbxTargetByName(extensionName);
107
+ if (existingTarget) {
108
+ return mod;
109
+ }
110
+ const target = xcodeProject.addTarget(extensionName, "app_extension", extensionName, extensionBundleId);
111
+ // Create PBXGroup for extension files
112
+ const group = xcodeProject.addPbxGroup(SWIFT_FILES, extensionName, extensionName, '"<group>"');
113
+ // Add group to main project's root group
114
+ const mainGroupId = xcodeProject.getFirstProject().firstProject.mainGroup;
115
+ xcodeProject.getPBXGroupByKey(mainGroupId).children.push({
116
+ value: group.uuid,
117
+ comment: extensionName,
118
+ });
119
+ // Add source files to extension target's build phase
120
+ xcodeProject.addBuildPhase(SWIFT_FILES.map((f) => `${extensionName}/${f}`), "PBXSourcesBuildPhase", "Sources", target.uuid);
121
+ // Configure build settings
122
+ const configurations = xcodeProject.pbxXCBuildConfigurationSection();
123
+ const targetConfigs = xcodeProject.pbxNativeTargetSection()[target.uuid]?.buildConfigurationList;
124
+ if (targetConfigs) {
125
+ const configList = xcodeProject.pbxXCConfigurationList()[targetConfigs];
126
+ if (configList?.buildConfigurations) {
127
+ for (const buildConfig of configList.buildConfigurations) {
128
+ const configEntry = configurations[buildConfig.value];
129
+ if (configEntry?.buildSettings) {
130
+ configEntry.buildSettings.SWIFT_VERSION = "5.0";
131
+ configEntry.buildSettings.IPHONEOS_DEPLOYMENT_TARGET =
132
+ deploymentTarget;
133
+ configEntry.buildSettings.TARGETED_DEVICE_FAMILY = '"1,2"';
134
+ configEntry.buildSettings.CODE_SIGN_ENTITLEMENTS = `${extensionName}/${extensionName}.entitlements`;
135
+ configEntry.buildSettings.CODE_SIGN_STYLE = "Automatic";
136
+ configEntry.buildSettings.PRODUCT_BUNDLE_IDENTIFIER = `"${extensionBundleId}"`;
137
+ configEntry.buildSettings.GENERATE_INFOPLIST_FILE = "NO";
138
+ configEntry.buildSettings.INFOPLIST_FILE = `${extensionName}/Info.plist`;
139
+ configEntry.buildSettings.CURRENT_PROJECT_VERSION = "1";
140
+ configEntry.buildSettings.MARKETING_VERSION = "1.0";
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return mod;
146
+ });
147
+ }
148
+ function withScreenShareExtensionFiles(config, extensionName, appGroupId) {
149
+ return (0, config_plugins_1.withDangerousMod)(config, [
150
+ "ios",
151
+ async (mod) => {
152
+ const bundleId = mod.ios?.bundleIdentifier ?? "";
153
+ const appGroup = appGroupId ?? getAppGroupIdentifier(bundleId);
154
+ const iosPath = path_1.default.resolve(mod.modRequest.platformProjectRoot);
155
+ const extensionPath = path_1.default.join(iosPath, extensionName);
156
+ const projectRoot = mod.modRequest.projectRoot;
157
+ const cacheDir = path_1.default.join(projectRoot, CACHE_DIR);
158
+ // Download Swift files (with caching)
159
+ await downloadSwiftFiles(cacheDir);
160
+ fs_1.default.mkdirSync(extensionPath, { recursive: true });
161
+ // Build extension Info.plist
162
+ const extensionInfoPlist = {
163
+ CFBundleDevelopmentRegion: "$(DEVELOPMENT_LANGUAGE)",
164
+ CFBundleDisplayName: "Screen Share",
165
+ CFBundleExecutable: "$(EXECUTABLE_NAME)",
166
+ CFBundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER)",
167
+ CFBundleInfoDictionaryVersion: "6.0",
168
+ CFBundleName: "$(PRODUCT_NAME)",
169
+ CFBundlePackageType: "$(PRODUCT_BUNDLE_PACKAGE_TYPE)",
170
+ CFBundleShortVersionString: "$(MARKETING_VERSION)",
171
+ CFBundleVersion: "$(CURRENT_PROJECT_VERSION)",
172
+ NSExtension: {
173
+ NSExtensionPointIdentifier: "com.apple.broadcast-services-upload",
174
+ NSExtensionPrincipalClass: "$(PRODUCT_MODULE_NAME).SampleHandler",
175
+ },
176
+ };
177
+ fs_1.default.writeFileSync(path_1.default.join(extensionPath, "Info.plist"), plist_1.default.build(extensionInfoPlist));
178
+ // Copy Swift files, replacing app group identifier in SampleHandler
179
+ for (const file of SWIFT_FILES) {
180
+ let content = fs_1.default.readFileSync(path_1.default.join(cacheDir, file), "utf8");
181
+ if (file === "SampleHandler.swift") {
182
+ content = content.replace(JITSI_APP_GROUP_PLACEHOLDER, appGroup);
183
+ }
184
+ fs_1.default.writeFileSync(path_1.default.join(extensionPath, file), content);
185
+ }
186
+ // Write entitlements
187
+ const entitlements = {
188
+ "com.apple.security.application-groups": [appGroup],
189
+ };
190
+ fs_1.default.writeFileSync(path_1.default.join(extensionPath, `${extensionName}.entitlements`), plist_1.default.build(entitlements));
191
+ return mod;
192
+ },
193
+ ]);
194
+ }
195
+ // --- Android Mods ---
196
+ function withScreenShareAndroidPermission(config) {
197
+ return (0, config_plugins_1.withAndroidManifest)(config, (mod) => {
198
+ const manifest = mod.modResults.manifest;
199
+ if (!manifest["uses-permission"]) {
200
+ manifest["uses-permission"] = [];
201
+ }
202
+ const permission = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
203
+ const exists = manifest["uses-permission"].some((p) => p.$?.["android:name"] === permission);
204
+ if (!exists) {
205
+ manifest["uses-permission"].push({
206
+ $: { "android:name": permission },
207
+ });
208
+ }
209
+ return mod;
210
+ });
211
+ }
212
+ function withScreenShareAndroidService(config) {
213
+ return (0, config_plugins_1.withAndroidManifest)(config, (mod) => {
214
+ const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(mod.modResults);
215
+ // Remove existing entry for idempotency
216
+ if (mainApplication["meta-data"]) {
217
+ mainApplication["meta-data"] = mainApplication["meta-data"].filter((item) => item?.$?.["android:name"] !== ANDROID_SCREEN_SHARE_SERVICE_KEY);
218
+ }
219
+ config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, ANDROID_SCREEN_SHARE_SERVICE_KEY, "true");
220
+ return mod;
221
+ });
222
+ }
223
+ // --- Main Plugin ---
224
+ const withScreenShare = (config, options) => {
225
+ const extensionName = options?.ios?.extensionName ?? DEFAULT_EXTENSION_NAME;
226
+ const deploymentTarget = options?.ios?.deploymentTarget ?? DEFAULT_DEPLOYMENT_TARGET;
227
+ const appGroupId = options?.ios?.appGroupIdentifier;
228
+ const enableAndroidService = options?.android?.enableScreenShareService ?? true;
229
+ const enableAndroidPermission = options?.android?.foregroundServicePermission ?? true;
230
+ // iOS
231
+ config = withScreenShareInfoPlist(config, extensionName, appGroupId);
232
+ config = withScreenShareEntitlements(config, appGroupId);
233
+ config = withScreenShareXcodeProject(config, extensionName, deploymentTarget);
234
+ config = withScreenShareExtensionFiles(config, extensionName, appGroupId);
235
+ // Android
236
+ if (enableAndroidPermission) {
237
+ config = withScreenShareAndroidPermission(config);
238
+ }
239
+ if (enableAndroidService) {
240
+ config = withScreenShareAndroidService(config);
241
+ }
242
+ return config;
243
+ };
244
+ exports.default = withScreenShare;
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "expo-livekit-screen-share",
3
+ "version": "0.1.0",
4
+ "description": "Expo config plugin for LiveKit screen sharing on iOS (Broadcast Upload Extension) and Android (foreground service)",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/arekkubaczkowski/expo-livekit-screen-share"
11
+ },
12
+ "keywords": [
13
+ "expo",
14
+ "expo-plugin",
15
+ "livekit",
16
+ "screen-share",
17
+ "broadcast-extension",
18
+ "react-native",
19
+ "ios",
20
+ "android"
21
+ ],
22
+ "files": [
23
+ "build",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "bun run tsc",
28
+ "prepublishOnly": "bun run build",
29
+ "release": "release-it"
30
+ },
31
+ "peerDependencies": {
32
+ "@livekit/react-native": ">=2.0.0",
33
+ "expo": ">=51.0.0"
34
+ },
35
+ "dependencies": {
36
+ "@expo/config-plugins": ">=8.0.0",
37
+ "@expo/plist": ">=0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.5.0",
41
+ "release-it": "^19.2.4",
42
+ "typescript": "~5.9.3"
43
+ }
44
+ }