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 +21 -0
- package/README.md +166 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +244 -0
- package/package.json +44 -0
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
|
package/build/index.d.ts
ADDED
|
@@ -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
|
+
}
|