UNPKG

react-native-whip-whep

Version:

An implementation of WHIP and WHEP protocols functionalities for React Native.

299 lines (298 loc) 14 kB
"use strict"; 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getSbePodfileSnippet = getSbePodfileSnippet; const config_plugins_1 = require("@expo/config-plugins"); const fs = __importStar(require("promise-fs")); const path = __importStar(require("path")); /** * Gets the name of the Broadcast Upload Extension target. * This can be customized via plugin options or defaults to 'WhipWhepScreenBroadcastExtension'. */ function getSbeTargetName(props) { return (props?.ios?.broadcastExtensionTargetName || 'WhipWhepScreenBroadcastExtension'); } /** * Generates the Podfile snippet that adds the broadcast extension target. * This snippet adds the necessary WebRTC dependency to the extension. */ function getSbePodfileSnippet(props) { const targetName = getSbeTargetName(props); // The extension needs access to WebRTC for handling video frames via the MobileWhipWhepBroadcastClient return `\ntarget '${targetName}' do\n pod 'MobileWhipWhepBroadcastClient'\nend`; } const TARGETED_DEVICE_FAMILY = `"1,2"`; // 1=iPhone, 2=iPad const IPHONEOS_DEPLOYMENT_TARGET = '15.1'; // Minimum iOS version const GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; // Template placeholder for app group const BUNDLE_IDENTIFIER_TEMPLATE_REGEX = /{{BUNDLE_IDENTIFIER}}/gm; // Template placeholder for bundle ID /** * Helper function for updating template placeholders in extension files. * Replaces template variables like {{GROUP_IDENTIFIER}} with actual values. * * @param iosPath - Path to the iOS project directory * @param fileName - Name of the file to update * @param regex - Regular expression to find the placeholder * @param value - The actual value to replace the placeholder with * @param props - Plugin configuration options */ async function updateFileWithRegex(iosPath, fileName, regex, value, props) { const targetName = getSbeTargetName(props); const filePath = `${iosPath}/${targetName}/${fileName}`; let file = await fs.readFile(filePath, { encoding: 'utf-8' }); file = file.replace(regex, value); await fs.writeFile(filePath, file); } /** * Updates the Podfile to include the broadcast extension target. * This ensures the extension has access to necessary dependencies (WebRTC). * The extension runs in a separate process and needs its own dependencies. * * @param iosPath - Path to the iOS project directory * @param props - Plugin configuration options */ async function updatePodfile(iosPath, props) { const podfileSnippet = getSbePodfileSnippet(props); let matches; try { const podfile = await fs.readFile(`${iosPath}/Podfile`, { encoding: 'utf-8', }); matches = podfile.match(podfileSnippet); } catch (e) { console.error('Error reading from Podfile: ', e); } if (matches) { console.log(`${getSbeTargetName(props)} target already added to Podfile. Skipping...`); return; } try { await fs.appendFile(`${iosPath}/Podfile`, podfileSnippet); } catch (e) { console.error('Error writing to Podfile: ', e); } } /** * Adds "App Group" permission to the main app. * * The app group identifier is typically in the format: group.{bundle.identifier} */ const withAppGroupPermissions = (config, props) => { const APP_GROUP_KEY = 'com.apple.security.application-groups'; const bundleIdentifier = config.ios?.bundleIdentifier || ''; const groupIdentifier = props?.ios?.appGroupContainerId || `group.${bundleIdentifier}`; const mainTarget = props?.ios?.mainTargetName || ''; // Add to the config object for Expo config.ios ??= {}; config.ios.entitlements ??= {}; config.ios.entitlements[APP_GROUP_KEY] ??= []; const entitlementsArray = config.ios.entitlements[APP_GROUP_KEY]; if (!entitlementsArray.includes(groupIdentifier)) { entitlementsArray.push(groupIdentifier); } config = (0, config_plugins_1.withEntitlementsPlist)(config, (newConfig) => { const modResultsArray = newConfig.modResults[APP_GROUP_KEY] || []; if (!modResultsArray.includes(groupIdentifier)) { modResultsArray.push(groupIdentifier); } newConfig.modResults[APP_GROUP_KEY] = modResultsArray; return newConfig; }); config = (0, config_plugins_1.withXcodeProject)(config, (props) => { const xcodeProject = props.modResults; const targets = xcodeProject.getFirstTarget(); const project = xcodeProject.getFirstProject(); if (!targets || !project) { return props; } const targetUuid = targets.uuid; const projectUuid = project.uuid; const projectObj = xcodeProject.hash.project.objects.PBXProject[projectUuid]; projectObj.attributes ??= {}; projectObj.attributes.TargetAttributes ??= {}; projectObj.attributes.TargetAttributes[targetUuid] ??= {}; projectObj.attributes.TargetAttributes[targetUuid].SystemCapabilities ??= {}; projectObj.attributes.TargetAttributes[targetUuid].SystemCapabilities['com.apple.ApplicationGroups.iOS'] = { enabled: 1, }; const mainTargetName = mainTarget || props.modRequest.projectName; const entitlementsFilePath = `${mainTargetName}/${mainTargetName}.entitlements`; const configurations = xcodeProject.pbxXCBuildConfigurationSection(); Object.keys(configurations).forEach((key) => { const config = configurations[key]; if (config.buildSettings?.PRODUCT_NAME?.includes(mainTargetName)) { if (!config.buildSettings.CODE_SIGN_ENTITLEMENTS) { config.buildSettings.CODE_SIGN_ENTITLEMENTS = entitlementsFilePath; } } }); return props; }); return config; }; const withInfoPlistConstants = (config, props) => (0, config_plugins_1.withInfoPlist)(config, (configuration) => { const bundleIdentifier = configuration.ios?.bundleIdentifier || ''; const groupIdentifier = props?.ios?.appGroupContainerId || `group.${bundleIdentifier}`; configuration.modResults['AppGroupName'] = groupIdentifier; configuration.modResults['ScreenShareExtensionBundleId'] = `${bundleIdentifier}.${getSbeTargetName(props)}`; return configuration; }); const withWhipWhepSBE = (config, options) => (0, config_plugins_1.withXcodeProject)(config, async (props) => { const appName = props.modRequest.projectName || ''; const iosPath = props.modRequest.platformProjectRoot; const bundleIdentifier = props.ios?.bundleIdentifier; const groupIdentifier = options?.ios?.appGroupContainerId || `group.${bundleIdentifier}`; const xcodeProject = props.modResults; const targetName = getSbeTargetName(options); const pluginDir = require.resolve('react-native-whip-whep/package.json'); const extensionSourceDir = path.join(pluginDir, '../plugin/broadcastExtensionFiles/'); await updatePodfile(iosPath, options); const projPath = `${iosPath}/${appName}.xcodeproj/project.pbxproj`; const templateTargetName = 'WhipWhepScreenBroadcastExtension'; const extFiles = [ 'WhipWhepBroadcastSampleHandler.swift', `${templateTargetName}.entitlements`, `Info.plist`, ]; const destFiles = [ 'WhipWhepBroadcastSampleHandler.swift', `${targetName}.entitlements`, `Info.plist`, ]; await xcodeProject.parse(async function (err) { if (err) { console.error(`Error parsing iOS project: ${JSON.stringify(err)}`); return; } if (xcodeProject.pbxTargetByName(targetName)) { console.log(`${targetName} already exists in project. Skipping...`); return; } try { await fs.mkdir(`${iosPath}/${targetName}`, { recursive: true }); for (let i = 0; i < extFiles.length; i++) { const srcFile = `${extensionSourceDir}${extFiles[i]}`; const destFile = `${iosPath}/${targetName}/${destFiles[i]}`; await fs.copyFile(srcFile, destFile); } } catch (e) { console.error('Error copying extension files: ', e); } await updateFileWithRegex(iosPath, `${targetName}.entitlements`, GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier, options); await updateFileWithRegex(iosPath, 'WhipWhepBroadcastSampleHandler.swift', GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier, options); await updateFileWithRegex(iosPath, 'WhipWhepBroadcastSampleHandler.swift', BUNDLE_IDENTIFIER_TEMPLATE_REGEX, bundleIdentifier || '', options); const extGroup = xcodeProject.addPbxGroup(extFiles, targetName, targetName); const groups = xcodeProject.hash.project.objects['PBXGroup']; Object.keys(groups).forEach(function (key) { if (groups[key].name === undefined) { xcodeProject.addToPbxGroup(extGroup.uuid, key); } }); // WORKAROUND for xcodeProject.addTarget bug // Xcode projects don't contain these if there is only one target const projObjects = xcodeProject.hash.project.objects; projObjects['PBXTargetDependency'] = projObjects['PBXTargetDependency'] || {}; projObjects['PBXContainerItemProxy'] = projObjects['PBXContainerItemProxy'] || {}; const sbeTarget = xcodeProject.addTarget(targetName, 'app_extension', targetName, `${bundleIdentifier}.${targetName}`); xcodeProject.addBuildPhase(['WhipWhepBroadcastSampleHandler.swift'], 'PBXSourcesBuildPhase', 'Sources', sbeTarget.uuid); xcodeProject.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', sbeTarget.uuid); xcodeProject.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', sbeTarget.uuid); xcodeProject.addFramework('ReplayKit.framework', { target: sbeTarget.uuid, }); const configurations = xcodeProject.pbxXCBuildConfigurationSection(); for (const key in configurations) { if (typeof configurations[key].buildSettings !== 'undefined' && configurations[key].buildSettings.PRODUCT_NAME === `"${targetName}"`) { const buildSettingsObj = configurations[key].buildSettings; buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = options?.ios?.iphoneDeploymentTarget ?? IPHONEOS_DEPLOYMENT_TARGET; buildSettingsObj.TARGETED_DEVICE_FAMILY = TARGETED_DEVICE_FAMILY; buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `${targetName}/${targetName}.entitlements`; buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'; buildSettingsObj.INFOPLIST_FILE = `${targetName}/Info.plist`; buildSettingsObj.SWIFT_VERSION = '5.0'; buildSettingsObj.MARKETING_VERSION = '1.0.0'; buildSettingsObj.CURRENT_PROJECT_VERSION = '1'; buildSettingsObj.ENABLE_BITCODE = 'NO'; } } await fs.writeFile(projPath, xcodeProject.writeSync()); }); return props; }); /** * Adds Picture-in-Picture support by enabling audio background mode. * This allows the app to continue playing audio when in PiP mode. */ const withWhipWhepPictureInPicture = (config, props) => (0, config_plugins_1.withInfoPlist)(config, (configuration) => { if (props?.ios?.supportsPictureInPicture) { const backgroundModes = new Set(configuration.modResults.UIBackgroundModes ?? []); backgroundModes.add('audio'); configuration.modResults.UIBackgroundModes = Array.from(backgroundModes); } return configuration; }); /** * Main iOS plugin function. * Applies all iOS-specific configurations conditionally based on options. * * This orchestrates: * - Screen sharing setup (if enabled) * - Deployment target configuration * - Picture-in-Picture support */ const withWhipWhepIos = (config, props) => { if (props?.ios?.enableScreensharing) { config = withAppGroupPermissions(config, props); config = withInfoPlistConstants(config, props); config = withWhipWhepSBE(config, props); } config = (0, config_plugins_1.withPodfileProperties)(config, (configuration) => { configuration.modResults['ios.deploymentTarget'] = props?.ios?.iphoneDeploymentTarget ?? IPHONEOS_DEPLOYMENT_TARGET; return configuration; }); config = withWhipWhepPictureInPicture(config, props); return config; }; exports.default = withWhipWhepIos;