27c582bceb
Lets a native-call user share their iPhone screen (whole device, works backgrounded). LiveKit 2.15.3 ships the broadcast stack (BroadcastManager + LKSampleHandler + IPC), so: - New Broadcast Upload Extension target "BroadcastExtension" (com.bizgaze.connect.broadcast): SampleHandler.swift subclasses LKSampleHandler; injected by mobile/scripts/add-broadcast-extension.rb which also LINKS the LiveKit SPM product into the extension + sets the App Group. Sources in mobile/ios-broadcast/. - Plugin: Room created with ScreenShareCaptureOptions(useBroadcastExtension: true); startScreenShare -> BroadcastManager.requestActivation() (system picker); stopScreenShare -> requestStop(); BroadcastManagerDelegate -> fires screenShareState to the web. LiveKit auto-publishes the track. - ios-patch.sh: RTCScreenSharingExtension + RTCAppGroupIdentifier keys. - codemagic.yaml: run the injector + sign the 3rd bundle id (.broadcast). - home.html: toggleScreen native -> start/stop; screenShareState listener reflects state + broadcasts meeting-screen so peers' stage shows it. REQUIRES a one-time manual Apple portal step: enable the App Group on the com.bizgaze.connect.broadcast App ID (see mobile/IOS_SETUP.md) or the archive fails code-signing. SPM-linked extension is new on our CI — expect build iteration. Web deployed (no-ops on builds without startScreenShare). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
6.4 KiB
Ruby
123 lines
6.4 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
#
|
|
# Inject the Broadcast Upload Extension (ReplayKit screen sharing) into the Capacitor-generated Xcode project.
|
|
# Mirrors add-share-extension.rb, with the extra step that this extension LINKS the LiveKit Swift package
|
|
# (LKSampleHandler lives in the LiveKit product), so it adds a package product dependency to the new target.
|
|
#
|
|
# WHAT IT WIRES:
|
|
# * a new app-extension target "BroadcastExtension" (bundle id <app>.broadcast) whose source is our
|
|
# SampleHandler.swift (subclass of LiveKit's LKSampleHandler) + Info.plist + entitlements, copied from
|
|
# mobile/ios-broadcast/
|
|
# * the App Group entitlement (group.com.bizgaze.connect) on the extension (LiveKit's IPC socket lives there)
|
|
# * a Swift Package product dependency on LiveKit (github.com/livekit/client-sdk-swift 2.15.3) so the
|
|
# extension can subclass LKSampleHandler
|
|
# * the extension embedded into the app ("Embed App Extensions") + set as a build dependency
|
|
#
|
|
# Idempotent: if the target already exists it is removed and rebuilt.
|
|
|
|
require 'xcodeproj'
|
|
require 'fileutils'
|
|
|
|
ROOT = File.expand_path('..', __dir__)
|
|
PROJECT = File.join(ROOT, 'ios', 'App', 'App.xcodeproj')
|
|
SRC_DIR = File.join(ROOT, 'ios-broadcast')
|
|
APP_DIR = File.join(ROOT, 'ios', 'App')
|
|
EXT_NAME = 'BroadcastExtension'
|
|
EXT_DIR = File.join(APP_DIR, EXT_NAME)
|
|
APP_TARGET = 'App'
|
|
APP_BUNDLE = ENV.fetch('BUNDLE_ID', 'com.bizgaze.connect')
|
|
EXT_BUNDLE = "#{APP_BUNDLE}.broadcast"
|
|
APP_GROUP = 'group.com.bizgaze.connect'
|
|
LK_URL = 'https://github.com/livekit/client-sdk-swift.git'
|
|
LK_VERSION = '2.15.3'
|
|
|
|
abort "xcodeproj not found at #{PROJECT}" unless File.directory?(PROJECT)
|
|
|
|
project = Xcodeproj::Project.open(PROJECT)
|
|
app = project.targets.find { |t| t.name == APP_TARGET }
|
|
abort "App target not found" unless app
|
|
|
|
# ── Clean any previous injection so this is idempotent ──────────────────────────────────────────────
|
|
project.targets.select { |t| t.name == EXT_NAME }.each do |t|
|
|
t.build_configuration_list&.build_configurations&.each { |c| c.remove_from_project }
|
|
t.remove_from_project
|
|
end
|
|
if (grp = project.main_group.children.find { |g| g.respond_to?(:display_name) && g.display_name == EXT_NAME })
|
|
grp.remove_from_project
|
|
end
|
|
|
|
# ── Copy our sources into the app project so paths inside the .xcodeproj are stable ──────────────────
|
|
FileUtils.mkdir_p(EXT_DIR)
|
|
%w[SampleHandler.swift Info.plist BroadcastExtension.entitlements].each do |f|
|
|
FileUtils.cp(File.join(SRC_DIR, f), File.join(EXT_DIR, f))
|
|
end
|
|
|
|
# ── Create the extension target ──────────────────────────────────────────────────────────────────────
|
|
deployment = app.deployment_target || project.build_configurations.first&.build_settings&.[]('IPHONEOS_DEPLOYMENT_TARGET') || '15.0'
|
|
ext = project.new_target(:app_extension, EXT_NAME, :ios, deployment, project.products_group, :swift)
|
|
|
|
group = project.main_group.new_group(EXT_NAME, EXT_NAME.to_s)
|
|
swift_ref = group.new_reference(File.join(EXT_DIR, 'SampleHandler.swift'))
|
|
ext.add_file_references([swift_ref])
|
|
|
|
ext.build_configurations.each do |cfg|
|
|
s = cfg.build_settings
|
|
s['PRODUCT_BUNDLE_IDENTIFIER'] = EXT_BUNDLE
|
|
s['PRODUCT_NAME'] = '$(TARGET_NAME)'
|
|
s['INFOPLIST_FILE'] = "#{EXT_NAME}/Info.plist"
|
|
s['CODE_SIGN_ENTITLEMENTS'] = "#{EXT_NAME}/BroadcastExtension.entitlements"
|
|
s['IPHONEOS_DEPLOYMENT_TARGET'] = deployment
|
|
s['SWIFT_VERSION'] = '5.0'
|
|
s['TARGETED_DEVICE_FAMILY'] = '1,2'
|
|
s['GENERATE_INFOPLIST_FILE'] = 'NO'
|
|
s['SKIP_INSTALL'] = 'YES'
|
|
s['CODE_SIGN_STYLE'] = 'Manual'
|
|
s['MARKETING_VERSION'] = '1.0'
|
|
s['CURRENT_PROJECT_VERSION'] = ENV.fetch('BUILD_NUMBER', '1')
|
|
s['LD_RUNPATH_SEARCH_PATHS'] = '$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks'
|
|
end
|
|
|
|
# ── Link LiveKit (Swift Package product) so the extension can subclass LKSampleHandler ────────────────
|
|
# The app already resolves client-sdk-swift 2.15.3 (via the native-call plugin's Package.swift). Add a
|
|
# project-level remote package reference to the SAME repo+version (SPM dedupes it) and attach the "LiveKit"
|
|
# product to this extension target.
|
|
root = project.root_object
|
|
pkg = root.package_references.find { |r| r.respond_to?(:repositoryURL) && r.repositoryURL == LK_URL }
|
|
unless pkg
|
|
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
|
|
pkg.repositoryURL = LK_URL
|
|
pkg.requirement = { 'kind' => 'exactVersion', 'version' => LK_VERSION }
|
|
root.package_references << pkg
|
|
end
|
|
prod = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
|
|
prod.package = pkg
|
|
prod.product_name = 'LiveKit'
|
|
ext.package_product_dependencies << prod
|
|
bf = project.new(Xcodeproj::Project::Object::PBXBuildFile)
|
|
bf.product_ref = prod
|
|
ext.frameworks_build_phase.files << bf
|
|
|
|
# ── App Group entitlement on the MAIN app target too (merge — keep aps-environment etc.) ──────────────
|
|
app_ent_path = File.join(APP_DIR, 'App', 'App.entitlements')
|
|
app_ent = File.exist?(app_ent_path) ? (Xcodeproj::Plist.read_from_path(app_ent_path) || {}) : {}
|
|
groups = app_ent['com.apple.security.application-groups'] || []
|
|
groups << APP_GROUP unless groups.include?(APP_GROUP)
|
|
app_ent['com.apple.security.application-groups'] = groups
|
|
Xcodeproj::Plist.write_to_path(app_ent, app_ent_path)
|
|
app.build_configurations.each { |cfg| cfg.build_settings['CODE_SIGN_ENTITLEMENTS'] = 'App/App.entitlements' }
|
|
|
|
# ── Embed the extension into the app + depend on it ──────────────────────────────────────────────────
|
|
app.add_dependency(ext)
|
|
embed = app.build_phases.find { |p| p.respond_to?(:symbol_dst_subfolder_spec) && p.symbol_dst_subfolder_spec == :plug_ins }
|
|
embed ||= begin
|
|
phase = app.new_copy_files_build_phase('Embed App Extensions')
|
|
phase.symbol_dst_subfolder_spec = :plug_ins
|
|
phase
|
|
end
|
|
build_file = embed.add_file_reference(ext.product_reference)
|
|
build_file.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] }
|
|
|
|
project.save
|
|
puts "Broadcast Extension injected: #{EXT_NAME} (#{EXT_BUNDLE}) linked to LiveKit #{LK_VERSION}, embedded in #{APP_TARGET}"
|