The app and its extensions default to iOS 12. SDK-conditional build settings raise the
minimum to iOS 15 when using iOS 27 SDKs, and the Podfile applies the same minimum to Pods.
Use a toolchain supporting iOS 12 deployment for packages intended for iOS 12 devices.
On iOS 12, AppDelegate creates the
window and loads the initial controller from Main.storyboard. On iOS 13 and later,
Info.plist declares a single application scene: UIKit loads Main.storyboard and
assigns its window to SceneDelegate. Scene declarations and window APIs are guarded
by iOS availability checks; the window helper retains its iOS 12 fallback.
Do not rebuild the root controller on scene activation or end RTC sessions merely because
the scene enters the background. Use a build toolchain that supports the deployment target;
adopting the scene lifecycle does not require raising the minimum iOS version.
ViewController owns a LogFloatingButton defined in Common/LogViewController.swift.
It is attached to the navigation container after appearance so it remains available in
example pages on both iOS 12 and later. Its safe-area placement and share sheet use its
own view/window hierarchy; do not use Floaty.global, which creates a window without a scene.
| Case | Path | Key APIs | Description |
|---|---|---|---|
| JoinChannelVideo | Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift |
joinChannel(), setupLocalVideo(), setupRemoteVideo() |
Basic video call — join channel and render local/remote video |
| JoinChannelVideo(Token) | Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift |
joinChannel(byToken:), setupLocalVideo(), setupRemoteVideo() |
Video call with token authentication |
| JoinChannelVideo(Recorder) | Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift |
createMediaRecorder(), joinChannel(), setupLocalVideo() |
Local and remote stream recording |
| JoinChannelAudio | Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift |
joinChannel(), setAudioProfile(), enableAudioVolumeIndication(), adjustRecordingSignalVolume() |
Basic audio call with relay, scenario, volume and in-ear monitoring controls; participant area adapts to available height |
| LiveStreaming | Examples/Advanced/LiveStreaming/LiveStreaming.swift |
setClientRole(), setVideoScenario(), preloadChannel(), enableCameraCenterStage() |
Interactive live streaming with role switching and camera features |
| RTMPStreaming | Examples/Advanced/RTMPStreaming/RTMPStreaming.swift |
startRtmpStreamWithoutTranscoding(), startRtmpStream(withTranscoding:), updateRtmpTranscoding(), stopRtmpStream() |
Push stream to CDN with optional transcoding |
| VideoMetadata | Examples/Advanced/VideoMetadata/VideoMetadata.swift |
setMediaMetadataDataSource(), setMediaMetadataDelegate() |
Send and receive metadata attached to video stream |
| VoiceChanger | Examples/Advanced/VoiceChanger/VoiceChanger.swift |
setVoiceBeautifierPreset(), setAudioEffectPreset(), setVoiceConversionPreset(), setAINSMode() |
Voice beautifier, effects, conversion presets, and AI noise suppression |
| CustomPcmAudioSource | Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift |
createCustomAudioTrack(), enableCustomAudioLocalPlayback(), pushExternalAudioFrameRawData() |
Push custom PCM audio frames as external audio source |
| CustomAudioRender | Examples/Advanced/CustomAudioRender/CustomAudioRender.swift |
enableExternalAudioSink(), pullPlaybackAudioFrameRawData() |
Pull audio frames for custom rendering |
| CustomAudioSource | Examples/Advanced/CustomAudioSource/CustomAudioSource.swift |
createCustomAudioTrack() |
Push custom audio via mixable audio track |
| CustomVideoSourcePush | Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift |
setExternalVideoSource(), pushExternalVideoFrame() |
Push external video frames as custom video source |
| CustomVideoSourcePushMulti | Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift |
createCustomVideoTrack(), createCustomEncodedVideoTrack(), pushExternalEncodedVideoFrame() |
Multi-track custom video source with encoded frame push |
| CustomVideoRender | Examples/Advanced/CustomVideoRender/CustomVideoRender.swift |
setVideoFrameDelegate() |
Custom rendering of remote video frames via delegate |
| RawAudioData | Examples/Advanced/RawAudioData/RawAudioData.swift |
setAudioFrameDelegate(), sendAudioMetadata() |
Capture raw audio PCM data via delegate |
| RawVideoData | Examples/Advanced/RawVideoData/RawVideoData.swift |
setVideoFrameDelegate() |
Capture raw video frames via delegate |
| RawMediaData | Examples/Advanced/RawMediaData/RawMediaData.swift |
setVideoFrameDelegate(), setAudioFrameDelegate(), setRecordingAudioFrameParametersWithSampleRate(), startAudioRecording() |
Capture both raw audio and video data simultaneously |
| PictureInPicture | Examples/Advanced/PictureInPicture/ |
AVPictureInPictureController, joinChannel(), setVideoFrameDelegate() |
Picture-in-Picture using AVKit (iOS 15+) |
| SimpleFilter | Examples/Advanced/SimpleFilter/SimpleFilter.swift |
enableExtension(), setExtensionPropertyWithVendor() |
Apply audio/video filter via Agora Extension API |
| QuickSwitchChannel | Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift |
joinChannel(), leaveChannel() |
Quickly switch between channels as audience |
| JoinMultiChannel | Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift |
joinChannelEx(), takeSnapshotEx() |
Join multiple channels simultaneously via ex connection |
| StreamEncryption | Examples/Advanced/StreamEncryption/StreamEncryption.swift |
enableEncryption() |
Built-in and custom stream encryption |
| AudioMixing | Examples/Advanced/AudioMixing/AudioMixing.swift |
startAudioMixing(), stopAudioMixing(), adjustAudioMixingVolume(), setEffectsVolume() |
Mix local audio file with microphone input |
| PrecallTest | Examples/Advanced/PrecallTest/PrecallTest.swift |
startEchoTest(), stopEchoTest(), startLastmileProbeTest(), stopLastmileProbeTest() |
Pre-call echo test and last-mile network probe |
| MediaPlayer | Examples/Advanced/MediaPlayer/MediaPlayer.swift |
createMediaPlayer(), updateChannelEx() |
Play media files and publish to channel via media player |
| ScreenShare | Examples/Advanced/ScreenShare/ScreenShare.swift |
startScreenCapture(), updateScreenCapture(), stopScreenCapture(), setScreenCaptureScenario() |
Screen capture and sharing via ReplayKit extension |
| LocalCompositeGraph | Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.swift |
startLocalVideoTranscoder(), startCameraCapture(), startScreenCapture(), enableVirtualBackground() |
Composite multiple video sources locally before publishing |
| VideoProcess | Examples/Advanced/VideoProcess/VideoProcess.swift |
setBeautyEffectOptions(), enableVirtualBackground(), enableExtension() |
Built-in beauty, virtual background, and video enhancement |
| AgoraBeauty | Examples/Advanced/AgoraBeauty/AgoraBeauty.swift |
createVideoEffectObject(), setVideoEffectStringParam(), enableVirtualBackground() |
Agora beauty extension with virtual background |
| RhythmPlayer (hidden) | Examples/Advanced/RhythmPlayer/RhythmPlayer.swift |
startRhythmPlayer(), stopRhythmPlayer() |
Source retained for reference; hidden from the menu because the Rhythm Player APIs are deprecated since RTC SDK 4.6.0 |
| CreateDataStream | Examples/Advanced/CreateDataStream/CreateDataStream.swift |
createDataStream(), sendStreamMessage() |
Create and send data stream messages between users |
| MediaChannelRelay | Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift |
startOrUpdateChannelMediaRelay(), stopChannelMediaRelay(), pauseAllChannelMediaRelay(), resumeAllChannelMediaRelay() |
Relay media stream to multiple destination channels |
| SpatialAudio | Examples/Advanced/SpatialAudio/SpatialAudio.swift |
createMediaPlayer(), updateChannel() |
3D spatial audio with media player integration |
| ContentInspect | Examples/Advanced/ContentInspect/ContentInspect.swift |
enableContentInspect(), switchCamera() |
Moderate content in video stream |
| MutliCamera | Examples/Advanced/MutliCamera/MutliCamera.swift |
enableMultiCamera(), startCameraCapture(), stopCameraCapture() |
Capture from front and back cameras simultaneously (iOS 13+) |
| KtvCopyrightMusic | Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.swift |
— | Links to KTV copyright music documentation |
| ThirdBeautify | Examples/Advanced/ThirdBeautify/ThirdBeautify.swift |
enableExtension() |
Third-party beauty SDK integration (FaceUnity / SenseTime) |
| ARKit | Examples/Advanced/ARKit/ARKit.swift |
setVideoFrameDelegate(), enableInstantMediaRendering(), startMediaRenderingTracing() |
Push ARKit face tracking frames as custom video source |
| AudioRouterPlayer | Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift |
setEnableSpeakerphone() |
Control audio output routing with third-party player |
| AudioWaveform | Examples/Advanced/AudioWaveform/AudioWaveform.swift |
setAudioProfile(), enableAudioVolumeIndication() |
Visualize audio waveform from volume callbacks |
| TransparentRender | Examples/Advanced/TransparentRender/TransparentRender.swift |
createMediaPlayer(), setExternalVideoSource(), pushExternalVideoFrame() |
Render video with transparent background |
| RtePlayer | Examples/Advanced/RtePlayer/RtePlayer.swift |
AgoraRte, AgoraRtePlayer, AgoraRteCanvas |
URL-based stream playback via RTE Player API |
| Simulcast | Examples/Advanced/Simulcast/Simulcast.swift |
setSimulcastConfig(), setRemoteVideoStream() |
Publish multiple video quality layers simultaneously |
| Multipath | Examples/Advanced/Multipath/Multipath.swift |
updateChannel() |
Multi-path network transmission configuration |
APIExample/
├── Podfile # CocoaPods dependencies (AgoraRtcEngine_iOS, Floaty, AGEVideoLayout, etc.)
├── SimpleFilter/ # Optional C++ audio/video extension module
├── Agora-ScreenShare-Extension/ # ReplayKit broadcast extension for screen sharing
├── FULib/ # Optional FaceUnity beauty SDK resources
├── SenseLib/ # Optional SenseTime beauty SDK resources
├── libs/ # Local SDK frameworks (when not using CocoaPods)
└── APIExample/
├── AppDelegate.swift
├── SceneDelegate.swift # Window owned by the application scene
├── ViewController.swift # Root menu controller — MenuItem registration lives here
├── Info.plist
├── APIExample.entitlements
├── APIExample-Bridging-Header.h
│
├── Common/
│ ├── KeyCenter.swift # App ID and Certificate
│ ├── GlobalSettings.swift # Shared runtime config (resolution, fps, orientation, role)
│ ├── BaseViewController.swift # Base class all Main VCs must extend
│ ├── EntryViewController.swift # Generic Entry VC for storyboard == "Main" cases
│ ├── LogViewController.swift # Log viewer
│ ├── AlertManager.swift
│ ├── AgoraExtension.swift
│ ├── PickerView.swift
│ ├── StatisticsInfo.swift
│ ├── UITypeAlias.swift
│ ├── VideoView.swift / .xib # Reusable video rendering view
│ ├── Settings/ # Settings UI components
│ ├── Utils/ # LogUtils, Util (privatization config)
│ ├── NetworkManager/ # Token request helper
│ ├── ExternalAudio/ # External audio source helpers
│ ├── ExternalVideo/ # External video source helpers
│ ├── CustomEncryption/ # Custom stream encryption helpers
│ └── ARKit/ # ARKit integration helpers
│
├── Examples/
│ ├── Basic/
│ │ ├── JoinChannelVideo/ # "Join a channel (Video)"
│ │ ├── JoinChannelVideo(Token)/ # "Join a channel (Token)"
│ │ ├── JoinChannelVideo(Recorder)/ # "Local or remote recording"
│ │ └── JoinChannelAudio/ # "Join a channel (Audio)"
│ └── Advanced/
│ ├── LiveStreaming/ # "Live Streaming" — setClientRole
│ ├── RTMPStreaming/ # "RTMP Streaming" — push to CDN
│ ├── VideoMetadata/ # "Media Metadata" — send/receive metadata
│ ├── VoiceChanger/ # "Voice Effects" — voice beautifier/effects
│ ├── CustomPcmAudioSource/ # "Custom Audio Source (PCM)" — push PCM audio
│ ├── CustomAudioRender/ # "Custom Audio Render" — pull audio rendering
│ ├── CustomAudioSource/ # (legacy custom audio source)
│ ├── CustomVideoSourcePush/ # "Custom Video Source" — push external video
│ ├── CustomVideoSourcePushMulti/ # "Multiple Video Sources" — multi-track push
│ ├── CustomVideoRender/ # "Custom Video Render"
│ ├── RawAudioData/ # "Raw Audio Data"
│ ├── RawVideoData/ # "Raw Video Data"
│ ├── RawMediaData/ # (legacy raw media data)
│ ├── PictureInPicture/ # "Picture In Picture"
│ ├── SimpleFilter/ # "Simple Filter Extension"
│ ├── QuickSwitchChannel/ # "Quick Switch Channel"
│ ├── JoinMultiChannel/ # "Join Multiple Channels"
│ ├── StreamEncryption/ # "Stream Encryption"
│ ├── AudioMixing/ # "Audio Mixing"
│ ├── PrecallTest/ # "Pre-call Test"
│ ├── MediaPlayer/ # "Media Player"
│ ├── ScreenShare/ # "Screen Share"
│ ├── LocalCompositeGraph/ # "Local Composite Graph"
│ ├── VideoProcess/ # "Video Process"
│ ├── AgoraBeauty/ # "Beauty 2.0"
│ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0
│ ├── CreateDataStream/ # "Send Data Stream"
│ ├── MediaChannelRelay/ # "Media Channel Relay"
│ ├── SpatialAudio/ # "Spatial Audio"
│ ├── ContentInspect/ # "Content Inspect"
│ ├── MutliCamera/ # "Multi Camera"
│ ├── KtvCopyrightMusic/ # "KTV Copyright Music"
│ ├── ThirdBeautify/ # "Third-party Beauty" — third-party beauty SDK
│ ├── ARKit/ # "ARKit"
│ ├── AudioRouterPlayer/ # "Audio Router (Third-party Player)"
│ ├── AudioWaveform/ # "Audio Waveform"
│ ├── TransparentRender/ # "Transparent Rendering"
│ ├── RtePlayer/ # "URL Streaming (RTE Player)"
│ ├── Simulcast/ # "Simulcast"
│ ├── Multipath/ # "Multipath"
│ └── VideoChat/ # (disabled) Group Video Chat
│
├── Resources/ # Audio/video sample files, beauty resources
├── Assets.xcassets/
├── Base.lproj/ # Main.storyboard, LaunchScreen.storyboard
└── zh-Hans.lproj/ # Chinese localization
Registration is manual via the menus array in ViewController.swift. No reflection or annotation scanning.
MenuItem struct:
struct MenuItem {
var name: String // display name in the list
var entry: String // storyboard ID of the entry VC (default: "EntryViewController")
var storyboard: String // storyboard file name (default: "Main")
var controller: String // storyboard ID of the main VC
var note: String // optional description
}Two navigation paths exist depending on storyboard:
storyboard == "Main"— uses the sharedMain.storyboard. The genericEntryViewControlleris instantiated, andnextVCIdentifieris set tocontrollerto load the Main VC.storyboard != "Main"— each example has its own.storyboardfile. The VC with identifierentry(default"EntryViewController") is instantiated directly from that storyboard.
Most examples use path 2 (their own storyboard).
A registered case connects the MenuItem, its Swift controller(s), and its storyboard.
New source files must belong to the Xcode target's Sources build phase, and storyboards,
localized files, and media assets must belong to Resources. Follow
upsert-case for the complete change procedure and
update the Case Index when the case changes.
Every example is split into two view controller roles:
Entry (<ExampleName>Entry : UIViewController)
- Collects user configuration before entering the example
- Passes configuration to Main via a
configsdictionary
Main (<ExampleName>Main : BaseViewController)
- Owns the
AgoraRtcEngineKitlifecycle for the duration of the example - Implements
AgoraRtcEngineDelegate - Receives configuration exclusively through
configs
viewDidLoad → AgoraRtcEngineKit.sharedEngine(withAppId:delegate:)
→ engine.setVideoEncoderConfiguration / setChannelProfile
→ engine.joinChannel() (after permission granted)
↓
[AgoraRtcEngineDelegate callbacks — may be on background thread]
↓
willMove(toParent:) when parent == nil
→ engine.leaveChannel()
→ AgoraRtcEngineKit.destroy()
For navigation-based cases, cleanup runs when the controller is removed from its parent. Temporary disappearance alone must not destroy the engine; follow the guarded cleanup in the upsert and review skills.
Use the guarded permission → Token → join implementation in upsert-case. Snapshot channel, UID, request generation and engine identity before asynchronous work. Recheck them on main before each continuation; leave/destroy invalidates pending requests and destroy clears engine ownership. Weak capture alone does not protect a still-alive controller whose RTC session has ended.
A nil/empty Token is allowed only when no App Certificate is configured. Reject a missing required Token and check the SDK join return code without logging credentials.