@videosdk.live/expo-ios-screen-share
Version:
Expo config plugin for iOS screen sharing with VideoSDK
440 lines (439 loc) • 20.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const config_plugins_1 = require("@expo/config-plugins");
const plist_1 = __importDefault(require("@expo/plist"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const utils_1 = require("./utils");
const getBundleId = (config) => {
const expoBundleId = config.ios?.bundleIdentifier;
if (expoBundleId) {
return expoBundleId;
}
// Fallback to reading from Info.plist
try {
const iosRootPath = config.modRequest?.platformProjectRoot;
if (iosRootPath) {
const projectName = config.modRequest?.projectName;
const infoPlistPath = path.join(iosRootPath, projectName, 'Info.plist');
if (fs.existsSync(infoPlistPath)) {
const infoPlistContent = fs.readFileSync(infoPlistPath, 'utf8');
const infoPlist = plist_1.default.parse(infoPlistContent);
if (infoPlist.CFBundleIdentifier) {
return infoPlist.CFBundleIdentifier;
}
}
}
}
catch (error) {
console.warn('⚠️ Could not read bundle ID from Info.plist:', error);
}
// Final fallback - try to get from Xcode project
try {
const iosRootPath = config.modRequest?.platformProjectRoot;
if (iosRootPath) {
const projectPath = path.join(iosRootPath, `${config.modRequest?.projectName}.xcodeproj`, 'project.pbxproj');
if (fs.existsSync(projectPath)) {
const projectContent = fs.readFileSync(projectPath, 'utf8');
const bundleIdMatch = projectContent.match(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/);
if (bundleIdMatch) {
return bundleIdMatch[1].trim();
}
}
}
}
catch (error) {
console.warn('⚠️ Could not read bundle ID from Xcode project:', error);
}
throw new Error('❌ Could not determine bundle ID. Please ensure ios.bundleIdentifier is set in app.json or Info.plist exists.');
};
const withIosBroadcastExtension = (config, props) => {
// Get bundle ID automatically
const bundleId = getBundleId(config);
const extensionName = props.extensionName || 'broadcast';
config = withBroadcastExtensionHandler(config, { ...props, bundleId, extensionName });
config = withBroadcastExtensionPlist(config, { ...props, bundleId, extensionName });
config = withBroadcastExtensionXcodeTarget(config, { ...props, bundleId, extensionName });
config = withVideosdkRPKFiles(config, { ...props, bundleId, extensionName });
return config;
};
const withBroadcastExtensionHandler = (config, props) => {
return (0, config_plugins_1.withDangerousMod)(config, [
'ios',
async (config) => {
const extensionRootPath = path.join(config.modRequest.platformProjectRoot, props.extensionName);
await fs.promises.mkdir(extensionRootPath, { recursive: true });
// Copy SampleHandler.swift and update app group identifier
const sampleHandlerContent = await fs.promises.readFile(path.join(__dirname, 'static', 'SampleHandler.swift'), 'utf8');
const updatedContent = sampleHandlerContent.replace('group.com.anonymous.videosdkexpoapp.appgroup', `group.${props.bundleId}.appgroup`);
await fs.promises.writeFile(path.join(extensionRootPath, 'SampleHandler.swift'), updatedContent);
// Copy other static files
const staticFiles = ['Atomic.swift', 'DarwinNotificationCenter.swift', 'SampleUploader.swift', 'SocketConnection.swift'];
for (const file of staticFiles) {
await fs.promises.copyFile(path.join(__dirname, 'static', file), path.join(extensionRootPath, file));
}
// Create broadcast extension entitlements file
const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.${props.bundleId}.appgroup</string>
</array>
</dict>
</plist>`;
await fs.promises.writeFile(path.join(extensionRootPath, `${props.extensionName}.entitlements`), entitlementsContent);
return config;
},
]);
};
const withBroadcastExtensionPlist = (config, props) => {
return (0, config_plugins_1.withDangerousMod)(config, [
'ios',
async (config) => {
const extensionRootPath = path.join(config.modRequest.platformProjectRoot, props.extensionName);
const extensionPlistPath = path.join(extensionRootPath, 'Info.plist');
const extensionPlist = {
NSExtension: {
NSExtensionPointIdentifier: 'com.apple.broadcast-services-upload',
NSExtensionPrincipalClass: '$(PRODUCT_MODULE_NAME).SampleHandler',
RPBroadcastProcessMode: 'RPBroadcastProcessModeSampleBuffer',
},
};
await fs.promises.mkdir(path.dirname(extensionPlistPath), {
recursive: true,
});
await fs.promises.writeFile(extensionPlistPath, plist_1.default.build(extensionPlist));
return config;
},
]);
};
const withBroadcastExtensionXcodeTarget = (config, props) => {
return (0, config_plugins_1.withXcodeProject)(config, async (config) => {
const appName = config.modRequest.projectName;
const extensionBundleIdentifier = `${props.bundleId}.${props.extensionName}`;
const currentProjectVersion = config.ios.buildNumber || '1';
const marketingVersion = config.version;
await addBroadcastExtensionXcodeTarget(config.modResults, {
appName,
extensionName: props.extensionName,
extensionBundleIdentifier,
currentProjectVersion,
marketingVersion,
appleTeamId: props.appleTeamId,
});
return config;
});
};
const addBroadcastExtensionXcodeTarget = async (proj, { appName, extensionName, extensionBundleIdentifier, currentProjectVersion, marketingVersion, appleTeamId, }) => {
if (proj.getFirstProject().firstProject.targets?.length > 1)
return;
const targetUuid = proj.generateUuid();
const groupName = 'Embed App Extensions';
const xCConfigurationList = addXCConfigurationList(proj, {
extensionBundleIdentifier,
currentProjectVersion,
marketingVersion,
extensionName,
appName,
appleTeamId,
});
const productFile = addProductFile(proj, extensionName, groupName);
const target = addToPbxNativeTargetSection(proj, {
extensionName,
targetUuid,
productFile,
xCConfigurationList,
});
addToPbxProjectSection(proj, target);
addTargetDependency(proj, target);
const frameworkFile = proj.addFramework('ReplayKit.framework', {
target: target.uuid,
link: false,
});
const frameworkPath = frameworkFile.path;
addBuildPhases(proj, {
groupName,
productFile,
targetUuid,
frameworkPath,
}, extensionName);
addPbxGroup(proj, productFile, extensionName);
};
const withVideosdkRPKFiles = (config, props) => {
// Step 1: Copy .m and .swift files
config = (0, config_plugins_1.withDangerousMod)(config, [
'ios',
async (config) => {
const iosRootPath = config.modRequest.platformProjectRoot;
const projectName = config.modRequest.projectName;
const appPath = path.join(iosRootPath, projectName);
await fs.promises.mkdir(appPath, { recursive: true });
const files = ['VideosdkRPK.m', 'VideosdkRPK.swift'];
for (const file of files) {
const src = path.resolve(__dirname, 'static', file);
const dest = path.join(appPath, file);
await fs.promises.copyFile(src, dest);
console.log(`✅ Copied ${file} → ${dest}`);
}
return config;
},
]);
// Step 2: Update main app's Info.plist with extension configuration
config = (0, config_plugins_1.withDangerousMod)(config, [
'ios',
async (config) => {
const iosRootPath = config.modRequest.platformProjectRoot;
const projectName = config.modRequest.projectName;
const infoPlistPath = path.join(iosRootPath, projectName, 'Info.plist');
if (fs.existsSync(infoPlistPath)) {
const infoPlistContent = fs.readFileSync(infoPlistPath, 'utf8');
const infoPlist = plist_1.default.parse(infoPlistContent);
// Update or add extension configuration
infoPlist.RTCAppGroupIdentifier = `group.${props.bundleId}.appgroup`;
infoPlist.RTCScreenSharingExtension = `${props.bundleId}.${props.extensionName}`;
infoPlist.RTCScreenSharingExtensionName = `${props.bundleId}.${props.extensionName}`;
fs.writeFileSync(infoPlistPath, plist_1.default.build(infoPlist));
}
return config;
},
]);
// Step 3: Add files to PBXGroup and PBXBuildFile sections
config = (0, config_plugins_1.withXcodeProject)(config, async (config) => {
const project = config.modResults;
const groupName = config_plugins_1.IOSConfig.XcodeUtils.getProjectName(config.modRequest.projectRoot);
const mainTarget = project.getFirstTarget();
if (!mainTarget)
throw new Error('❌ Main target not found in Xcode project');
const allGroups = project.hash.project.objects.PBXGroup;
const groupUUID = Object.entries(allGroups).find(([_, group]) => group.name === groupName || group.path === groupName)?.[0];
if (!groupUUID)
throw new Error(`❌ Could not find group "${groupName}" in Xcode project`);
const fileNames = ['VideosdkRPK.swift', 'VideosdkRPK.m'];
for (const fileName of fileNames) {
const filePath = `${groupName}/${fileName}`;
if (project.hasFile(filePath)) {
continue;
}
const file = project.addSourceFile(filePath, { target: mainTarget.uuid }, groupUUID);
if (file) {
console.log(`✅ Registered ${fileName} in PBXBuildFile and Sources build phase`);
}
else {
console.warn(`⚠️ Failed to add ${fileName} to project`);
}
}
return config;
});
// Step 4: Patch or create Objective-C bridging header
const BRIDGING_HEADER_IMPORT = '#import "React/RCTEventEmitter.h"';
config = (0, config_plugins_1.withDangerousMod)(config, ['ios', (config) => {
const projectRoot = config.modRequest.projectRoot;
const appName = config_plugins_1.IOSConfig.XcodeUtils.getProjectName(projectRoot);
const iosAppDir = path.join(projectRoot, 'ios', appName);
const bridgingHeaderFile = `${appName}-Bridging-Header.h`;
const bridgingHeaderPath = path.join(iosAppDir, bridgingHeaderFile);
if (!fs.existsSync(bridgingHeaderPath)) {
fs.writeFileSync(bridgingHeaderPath, `// ${bridgingHeaderFile}\n${BRIDGING_HEADER_IMPORT}\n`, 'utf8');
}
else {
let content = fs.readFileSync(bridgingHeaderPath, 'utf8');
if (!content.includes(BRIDGING_HEADER_IMPORT)) {
content += `\n${BRIDGING_HEADER_IMPORT}\n`;
fs.writeFileSync(bridgingHeaderPath, content, 'utf8');
}
}
return config;
}]);
// Step 5: Set bridging header path in build settings
config = (0, config_plugins_1.withXcodeProject)(config, (config) => {
const project = config.modResults;
const appName = config_plugins_1.IOSConfig.XcodeUtils.getProjectName(config.modRequest.projectRoot);
const mainTarget = project.getFirstTarget();
const bridgingHeaderRelativePath = `${appName}/${appName}-Bridging-Header.h`;
project.updateBuildProperty('SWIFT_OBJC_BRIDGING_HEADER', bridgingHeaderRelativePath, mainTarget.uuid);
return config;
});
return config;
};
const addXCConfigurationList = (proj, { extensionBundleIdentifier, currentProjectVersion, marketingVersion, extensionName, appName, appleTeamId, }) => {
const commonBuildSettings = {
CLANG_ANALYZER_NONNULL: 'YES',
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION: 'YES_AGGRESSIVE',
CLANG_CXX_LANGUAGE_STANDARD: (0, utils_1.quoted)('gnu++17'),
CLANG_ENABLE_OBJC_WEAK: 'YES',
CLANG_WARN_DOCUMENTATION_COMMENTS: 'YES',
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER: 'YES',
CLANG_WARN_UNGUARDED_AVAILABILITY: 'YES_AGGRESSIVE',
CODE_SIGN_ENTITLEMENTS: `${extensionName}/${extensionName}.entitlements`,
CODE_SIGN_STYLE: 'Automatic',
CURRENT_PROJECT_VERSION: currentProjectVersion,
DEVELOPMENT_TEAM: appleTeamId,
GCC_C_LANGUAGE_STANDARD: 'gnu11',
GENERATE_INFOPLIST_FILE: 'YES',
INFOPLIST_FILE: `${extensionName}/Info.plist`,
INFOPLIST_KEY_CFBundleDisplayName: `${extensionName}`,
INFOPLIST_KEY_NSHumanReadableCopyright: (0, utils_1.quoted)(''),
IPHONEOS_DEPLOYMENT_TARGET: '14.0',
LD_RUNPATH_SEARCH_PATHS: (0, utils_1.quoted)('$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks'),
MARKETING_VERSION: marketingVersion,
MTL_FAST_MATH: 'YES',
PRODUCT_BUNDLE_IDENTIFIER: (0, utils_1.quoted)(extensionBundleIdentifier),
PRODUCT_NAME: (0, utils_1.quoted)('$(TARGET_NAME)'),
SKIP_INSTALL: 'YES',
SWIFT_EMIT_LOC_STRINGS: 'YES',
SWIFT_VERSION: '5.0',
TARGETED_DEVICE_FAMILY: (0, utils_1.quoted)('1,2'),
};
const buildConfigurationsList = [
{
name: 'Debug',
isa: 'XCBuildConfiguration',
buildSettings: {
...commonBuildSettings,
DEBUG_INFORMATION_FORMAT: 'dwarf',
MTL_ENABLE_DEBUG_INFO: 'INCLUDE_SOURCE',
SWIFT_ACTIVE_COMPILATION_CONDITIONS: 'DEBUG',
SWIFT_OPTIMIZATION_LEVEL: (0, utils_1.quoted)('-Onone'),
},
},
{
name: 'Release',
isa: 'XCBuildConfiguration',
buildSettings: {
...commonBuildSettings,
COPY_PHASE_STRIP: 'NO',
DEBUG_INFORMATION_FORMAT: (0, utils_1.quoted)('dwarf-with-dsym'),
SWIFT_OPTIMIZATION_LEVEL: (0, utils_1.quoted)('-Owholemodule'),
},
},
];
const xCConfigurationList = proj.addXCConfigurationList(buildConfigurationsList, 'Release', `Build configuration list for PBXNativeTarget ${(0, utils_1.quoted)(extensionName)}`);
// update other build properties
proj.updateBuildProperty('ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES', 'YES', null, proj.getFirstTarget().firstTarget.name);
proj.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', '14.0');
return xCConfigurationList;
};
const addProductFile = (proj, extensionName, groupName) => {
const productFile = {
basename: `${extensionName}.appex`,
fileRef: proj.generateUuid(),
uuid: proj.generateUuid(),
group: groupName,
explicitFileType: 'wrapper.app-extension',
settings: {
ATTRIBUTES: ['RemoveHeadersOnCopy'],
},
includeInIndex: 0,
path: `${extensionName}.appex`,
sourceTree: 'BUILT_PRODUCTS_DIR',
};
proj.addToPbxFileReferenceSection(productFile);
proj.addToPbxBuildFileSection(productFile);
return productFile;
};
const addToPbxNativeTargetSection = (proj, { extensionName, targetUuid, productFile, xCConfigurationList, }) => {
const target = {
uuid: targetUuid,
pbxNativeTarget: {
isa: 'PBXNativeTarget',
buildConfigurationList: xCConfigurationList.uuid,
buildPhases: [],
buildRules: [],
dependencies: [],
name: extensionName,
productName: extensionName,
productReference: productFile.fileRef,
productType: (0, utils_1.quoted)('com.apple.product-type.app-extension'),
},
};
proj.addToPbxNativeTargetSection(target);
return target;
};
const addToPbxProjectSection = (proj, target) => {
proj.addToPbxProjectSection(target);
// Add target attributes to project section
if (!proj.pbxProjectSection()[proj.getFirstProject().uuid].attributes
.TargetAttributes) {
proj.pbxProjectSection()[proj.getFirstProject().uuid].attributes.TargetAttributes = {};
}
proj.pbxProjectSection()[proj.getFirstProject().uuid].attributes.LastSwiftUpdateCheck = 1340;
proj.pbxProjectSection()[proj.getFirstProject().uuid].attributes.TargetAttributes[target.uuid] = {
CreatedOnToolsVersion: '13.4.1',
ProvisioningStyle: 'Automatic',
};
};
const addTargetDependency = (proj, target) => {
if (!proj.hash.project.objects['PBXTargetDependency']) {
proj.hash.project.objects['PBXTargetDependency'] = {};
}
if (!proj.hash.project.objects['PBXContainerItemProxy']) {
proj.hash.project.objects['PBXContainerItemProxy'] = {};
}
proj.addTargetDependency(proj.getFirstTarget().uuid, [target.uuid]);
};
const addBuildPhases = (proj, { groupName, productFile, targetUuid, frameworkPath }, extensionName) => {
const buildPath = (0, utils_1.quoted)('');
// Sources build phase
const { uuid: sourcesBuildPhaseUuid } = proj.addBuildPhase([`SampleHandler.swift`], 'PBXSourcesBuildPhase', 'Sources', targetUuid, 'app_extension', buildPath);
// Copy files build phase
const { uuid: copyFilesBuildPhaseUuid } = proj.addBuildPhase([productFile.path], 'PBXCopyFilesBuildPhase', groupName, proj.getFirstTarget().uuid, 'app_extension', buildPath);
// Frameworks build phase
const { uuid: frameworksBuildPhaseUuid } = proj.addBuildPhase([frameworkPath], 'PBXFrameworksBuildPhase', 'Frameworks', targetUuid, 'app_extension', buildPath);
// Resources build phase
const { uuid: resourcesBuildPhaseUuid } = proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', targetUuid, 'app_extension', buildPath);
};
const addPbxGroup = (proj, productFile, extensionName) => {
// Add PBX group
const { uuid: pbxGroupUuid } = proj.addPbxGroup(['SampleHandler.swift', 'Info.plist'], extensionName, extensionName);
// Add PBXGroup to top level group
const groups = proj.hash.project.objects['PBXGroup'];
if (pbxGroupUuid) {
Object.keys(groups).forEach(function (key) {
if (groups[key].name === undefined && groups[key].path === undefined) {
proj.addToPbxGroup(pbxGroupUuid, key);
}
else if (groups[key].name === 'Products') {
proj.addToPbxGroup(productFile, key);
}
});
}
};
exports.default = withIosBroadcastExtension;