Nami is an iOS live-streaming app written in Swift/SwiftUI. The RTMP stack, RTSP/RTP stack, MPEG-TS muxer, and the encoding pipeline are all implemented from scratch in this repo. Two protocols bring in native libraries: SRT uses the libsrt xcframework (vendored SPM binary package, checksum-pinned), and WebRTC-WHIP uses Google's libwebrtc via the stasel/WebRTC Swift package.
Streaming
- Publish live camera + microphone to RTMP / RTMPS (FLV tags, AMF0, full chunk I/O, enhanced-RTMP HEVC) and RTSP / RTSPS (ANNOUNCE → SETUP → RECORD, RTP over interleaved TCP per RFC 6184/7798/3640, periodic RTCP sender reports, Basic + Digest auth).
- SRT — MPEG-TS over SRT (libsrt), MediaMTX
streamidpublish convention (srt://host:8890/pathor?streamid=publish:path), with?passphrase=and?latency=query support. - WebRTC-WHIP (
whip:///whips://) — HTTP POST/DELETE signaling. WebRTC encodes internally, so custom encoder settings and adaptive bitrate apply to the other protocols only; audio uses WebRTC's own microphone capture — the in-app gain slider does not affect WHIP audio, mute does. - H.264 or HEVC video (VideoToolbox), AAC-LC audio (AVAudioConverter).
- Multiple simultaneous connections — every enabled connection streams at once.
- Adaptive bitrate: Off / Logarithmic Descend / Ladder Ascend / Hybrid.
- Automatic reconnect with backoff while streaming.
Main screen
- Live preview (fill/fit), rotate camera, audio mute toggle, settings, and a
4-tab options popup:
- Camera — torch, exposure compensation, lens (0.5×/1×/tele), zoom, white balance auto/fixed (Kelvin slider), stabilization (Off/Standard/Cine/Cine Ext/Cine++/Auto).
- Display — preview fill/fit, stream orientation (rotates the app UI and the published stream together; locked while live), rule-of-thirds grid, real-time horizon level (CoreMotion), VU meter.
- Audio — digital input gain, active microphone picker, preferred built-in mic position (bottom/front/back) on devices that support it.
- Stats — per-connection state, bytes sent, measured bandwidth, queue, dropped frames, reconnects; overall fps/bitrate/duration.
Settings
- Connections — manage RTMP(S)/RTSP(S)/SRT/WHIP(S) publish URLs with optional credentials.
- Camera — initial camera, per-side lens, resolution (480p–4K), frame rate (24–60), stream orientation, initial zoom, stabilization, multi-cam mode (off / picture-in-picture / custom layout with an interactive drag-resize canvas).
- Video encoding — bitrate (match-resolution or custom), keyframe interval, CBR/VBR, H.264/HEVC, HDR (HLG 10-bit best-effort), adaptive bitrate mode.
- Audio — input kind (auto/built-in/headset/Bluetooth), mic position, mono/stereo, bitrate, sample rate, input gain.
xcodebuild -project Nami.xcodeproj -scheme Nami \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' buildOr open Nami.xcodeproj in Xcode 16+ and run. iOS 17+ deployment target.
The project uses synchronized folder groups — new Swift files are picked up
automatically without pbxproj edits.
On the simulator there is no camera, so the app switches to a synthetic animated test pattern (which still reflects zoom/torch/EV/camera-switch), and uses the Mac microphone via AVAudioEngine when available (440 Hz tone fallback).
With MediaMTX running locally (RTSP :8554, RTMP :1935, SRT :8890, WHIP :8889):
- Settings → Connections → add
rtsp://127.0.0.1:8554/mystream,rtmp://127.0.0.1:1935/mystream,srt://127.0.0.1:8890/mystream, orwhip://127.0.0.1:8889/mystream(on a real device, replace127.0.0.1with your Mac's LAN IP). - Tap GO LIVE.
- Verify:
ffprobe rtsp://127.0.0.1:8554/mystreamor play in VLC/ffplay.
xcrun simctl launch <UDID> me.stageguard.Nami \
-autoURL "rtsp://127.0.0.1:8554/test,rtmp://127.0.0.1:1935/test" \
-autoStart 1 -videoCodec hevc -uiScreen options-stats-autoURL url[,url2]— inject enabled AutoTest connections-autoStart 1— go live ~2 s after the preview starts-videoCodec h264|hevc— override the codec-orientation portrait|landscape— override the stream/app orientation-uiScreen …— deep-link a screen (options-camera|display|audio|stats,settings[-connections|-camera|-video|-audio])
CaptureService (camera / synthetic) AudioSessionManager
│ CMSampleBuffers (video, PCM audio)
▼
BroadcastController ── PreviewFeed ─► AVSampleBufferDisplayLayer (UI)
│ gain/mute/VU → AAC encoder; frames → VideoToolbox encoder
▼ EncodedVideoFrame / EncodedAudioFrame (host-clock pts, rebased to epoch)
StreamTransport fan-out ──► RTMPTransport (FLV/AMF0/chunks over NWConnection)
│ ├──► RTSPTransport (SDP/RTSP + RTP packetizers)
│ ├──► SRTTransport (MPEG-TS mux over libsrt)
│ └──► WHIPTransport (raw frames → libwebrtc)
└── stats loop: per-connection stats, ABR, reconnect
Key contracts live in Core/Pipeline/MediaTypes.swift,
Core/Pipeline/EncoderContracts.swift, and Core/Capture/CaptureContracts.swift.
- RIST is not implemented — there is no maintained iOS librist package; it would require custom cross-compilation.
- WHIP over plain
http://works for loopback (simulator); on-device use against a LAN server needs an ATS exception in the Info.plist, or usewhips://. - WHIP video is encoded by WebRTC itself: the bitrate is capped at the configured video bitrate, but codec, keyframe-interval, and adaptive-bitrate settings do not apply.
- WHIP audio uses WebRTC's internal microphone capture (Opus). On a headless simulator there is no audio input device, so WHIP publishes video-only there; on real devices audio flows normally. The in-app gain slider does not affect WHIP audio (mute does).
- HDR capture is best-effort: it prefers 10-bit
x420formats + HEVC Main10 and falls back silently when unsupported. - Multi-cam capture requires a multi-cam capable device; on the simulator only the layout editor is available.
- RTMPS/RTSPS use default TLS validation (no custom certificate pinning).
- Camera-affecting settings changed mid-stream apply after the stream stops.
Released under the MIT License.