oficina / meet Public
Calls for iPhone
A SwiftUI iOS 17+ app using the same Go authentication/signaling core and native WebRTC engine as Calls for Mac. No embedded webview is used. The Xcode project is generated from project.yml; edit that specification and the Swift sources.
Build
Requires Xcode, Go, and XcodeGen (brew install xcodegen). Dependencies are pinned in the shared Swift package and the isolated native Go module.
make iphone
# For a signed physical-device build:
CALLS_DEVELOPMENT_TEAM=YOUR_TEAM_ID make iphone-deviceBuild products are native/build/ios-simulator/Build/Products/Debug-iphonesimulator/CallsPhone.app and native/build/ios-device/Build/Products/Debug-iphoneos/CallsPhone.app. The device build uses automatic development signing with your selected team; it does not upload an app to App Store Connect or TestFlight.
Generate/open the Xcode project for normal device development:
xcodegen generate --spec native/ios/project.yml
open native/ios/CallsPhone.xcodeprojSelect your development team and connected iPhone, then Run. Alternatively, install a signed app using xcrun devicectl device install app --device DEVICE_ID followed by the device build's .app path. The iPhone must be connected and available to Xcode, with Developer Mode enabled.
First phone milestone
- Sign in to the Calls server (calls.oficina.works unless changed with the home screen's gear), or Join a call as a guest with a pasted invite link; sign in in Safari/the default browser.
- Open the email link normally, compare the browser's code with Calls's code, approve, then return to Calls. A member's session is kept in the Keychain (this device only) and checked with the server at the next launch; see ../README.md. Sign out forgets it.
- Join using the front camera and microphone; the local video display is mirrored. Sent video and remote video retain their original orientation.
- Native remote video, microphone/camera controls, guest admission, chat, and leave.
- Chat is always available beside the call controls. Its button shows new-message counts; opening it shows the latest messages and a composer without scrolling past the video tiles. Opening chat keeps the call active.
- Members have a Share button beside Chat, with Copy link and the system share menu.
- Camera/microphone permission refusals appear in the app. Retry after changing permission in Settings. The simulator has no physical camera.
Calls are foreground-only in this milestone. Switching away ends the call, except while you are sharing your screen; sign-in survives switching to the browser. Camera switching, CallKit/background calling, and distribution are follow-up work. Physical camera, audible audio, Bluetooth routing, rotation while capturing, and interruption recovery need a real-device acceptance pass.
Sharing the screen
Share your screen in the call is the system's broadcast button (RPSystemBroadcastPickerView, offering only Calls' own extension). The whole screen is shared, which is the only thing iOS lets any app share. The frames reach a broadcast upload extension (Broadcast/SampleHandler.swift, target CallsBroadcast), a separate process with a memory ceiling of about fifty megabytes. It links only CallsBroadcastKit and no WebRTC, which keeps it near 600 KB. It shrinks each frame to 1280 on the long side, at up to fifteen a second, and passes it to the app over a Unix socket in the App Group container (calls.sock). The app is already in the call and sends the frames on the same sendonly screen transceiver the Mac and the browser use. Only one frame is in flight at a time; any frame that arrives while one is being written is dropped, never queued.
Stopping works from either end. Stop sharing in the app closes the socket; the extension's next write fails and it ends the broadcast with "You stopped sharing your screen in Calls." Stopping from Control Center or the red status bar closes the socket from the other side, and the app stops the share. A broadcast started with no call to take it ends with "Join a call in Calls, then share your screen from there."
A call that is sharing stays up when you leave the app, because showing other apps is the point. That needs the audio background mode. Every other call still ends when the app goes to the background. If the broadcast is stopped from outside while Calls is in the background, the call ends too.
The App Group is one build setting, CALLS_APP_GROUP in project.yml (group.net.amadan.oficina.calls). It feeds both targets' entitlements and the CallsAppGroup key in both Info.plists. The extension's bundle identifier is the app's plus .broadcast. The app looks for its extension by that rule, so the two must stay in step.
Signing. The simulator build is unsigned, so it has no App Group and cannot broadcast; ReplayKit broadcasts need a device anyway. Before the first signed device build, in the Apple Developer portal (Certificates, Identifiers & Profiles):
- Identifiers → App Groups → + →
group.net.amadan.oficina.calls. - Identifiers →
net.amadan.oficina.calls.ios→ enable App Groups → choose that group. (Push stays as it is.) - Identifiers → + → App IDs → App →
net.amadan.oficina.calls.ios.broadcast, description "Calls broadcast", with App Groups enabled and the same group. CALLS_DEVELOPMENT_TEAM=<team> make iphone-device. Automatic signing makes both development profiles once the identifiers exist, and-allowProvisioningUpdatescan also create steps 1–3 for you if you would rather let Xcode do it.
A TestFlight or App Store build needs distribution profiles for both bundle identifiers, and the extension goes up inside the app.
The TestFlight release branch differs. release/calls-testflight ships under the bundle identifier works.oficina.meet, not net.amadan.oficina.calls.ios. When screen sharing reaches that branch it needs, in its project.yml:
CALLS_APP_GROUP: group.works.oficina.meet- the
CallsBroadcasttarget'sPRODUCT_BUNDLE_IDENTIFIER: works.oficina.meet.broadcast(the app finds its extension as its own identifier plus.broadcast, so this one is not a free choice)
and, in the Apple Developer portal, the App Group group.works.oficina.meet enabled on both works.oficina.meet and works.oficina.meet.broadcast, with distribution profiles for both. The development values above stay as they are on main.
Verification
xcrun simctl list devices available
# Choose an iPhone simulator UUID:
CALLS_IOS_SIMULATOR=YOUR_SIMULATOR_UUID make iphone-browserThe Go harness creates isolated databases, meetings, and captured magic-link emails. iPhone UI tests use the real Safari handoff for members and guests and compare the authorization codes before returning to the app. No production credentials are used and no real email is sent. The UI tests launch with -CallsForgetSignIn YES, which signs the app out before anything else: the simulator's Keychain outlives reinstalls, and a session kept by an earlier run against a harness server long gone would otherwise decide what the next run sees.
The native media test creates two iOS WebRTC peers with a synthetic camera, checks decoded video in both directions, continuity through three-second stream renewals, and removal of the departed peer. Hardware audio is disabled in this simulator-only test: it neither records the host microphone nor claims to prove physical audio quality. Synthetic capture is compiled only into test targets. Xcode logs and result bundles are retained under native/build/.