iOS Examples
GitHub Examples
You can find our iOS examples on GitHub:
Override Playout Delay
Example Code for overriding the playout delay via a PhenixMediaStream object
import PhenixSdk
class SomeClass {
private var currentRendererPlayoutDelayOverride: PhenixDisposable?
private var currentRenderer: PhenixRenderer?
private func setPlayoutDelayOverrideFor10Seconds() {
// Previously obtained
let mediaStream: PhenixMediaStream = ...
self.currentRenderer = mediaStream.createRenderer()
// Override playout delay to 900ms for 10 seconds
let playoutDelay: TimeInterval = 0.9
self.currentRendererPlayoutDelayOverride = self.currentRenderer?.overridePlayoutDelay(playoutDelay)
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
// Dropping the disposable will undo the playout delay override
self.currentRendererPlayoutDelayOverride = nil
}
}
}
Example Code for limiting video bandwidth with a PhenixExpressSubscriber object
import PhenixSdk
class SomeClass {
private var currentRendererPlayoutDelayOverride: PhenixDisposable?
private var currentRenderer: PhenixRenderer?
private func setPlayoutDelayOverrideFor10Seconds() {
// Previously obtained
let subscriber: PhenixExpressSubscriber = ...
self.currentRenderer = subscriber.createRenderer()
// Override playout delay to 900ms for 10 seconds
let playoutDelay: TimeInterval = 0.9
self.currentRendererPlayoutDelayOverride = self.currentRenderer?.overridePlayoutDelay(playoutDelay)
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
// Dropping the disposable will undo the playout delay override
self.currentRendererPlayoutDelayOverride = nil
}
}
}
The playout delay represents the amount of time by which audio and video are delayed when content is rendered by a subscriber; i.e. it works as a buffer. The delay adds to the overall end-to-end latency experienced by the user (on top of capture, encoding, and network latencies). It is necessary to handle network-related fluctuations (such as jitter or data loss). By default, the playout delay for real-time streams is set to 230ms. The following API allows app developers to override this default value.
In order to access the API, a reference to a PhenixRenderer
is needed.
It can be obtained either by creating it from PhenixMediaStream
or PhenixExpressSubscriber
,
or via several of the Express APIs, which can return renderers
(joinChannel,
subscribeToMemberStream,
subscribe).
The returned disposable allows control over how long the override should stay in effect; it therefore needs to be held onto via a strong reference. If overridePlayoutDelay
is called multiple times before any of the previous disposables are released, then only the most recent override will remain in effect until its disposable is released. Releasing any of the disposables from earlier overridePlayoutDelay
calls will have no effect.
Notes:
- The override represents an absolute value, not a delta.
- If an override increases the playout delay, it will result in content being paused. Example: changing a delay of 1 second to 5 seconds will cause the content to be paused for roughly 4 seconds.
- If an override decreases the playout delay, it will cause a jump where some of the content will be skipped.
- Very large override values will increase the amount of memory consumed. It is generally recommended to stay below 10 seconds.
Parameters
Name | Type | Description |
---|---|---|
desiredPlayoutDelay (required) | TimeInterval | Desired playout delay |
Returns
Type | Description |
---|---|
PhenixDisposable | Ensures override is kept in effect for as long as a strong reference is held |
Picture-in-picture
Once subscribed to a stream, the renderer may provide an AVPictureInPictureControllerContentSource
that can be used to initialize an AVPictureInPictureController
that will show the content of the stream in a picture-in-picture overlay when the application is minimized.
The PhenixRenderer has a setPictureInPictureContentSourceChangedCallback
function to register
for the availability of a ContentSource
object to use in Picture-in-Picture
controllers.
Note: setPictureInPictureContentSourceChangedCallback
must be called on the main thread.
The setPictureInPictureContentSourceChangedCallback
method of the renderer provides the AVPictureInPictureControllerContentSource
object when it is available. If the current renderer does not support picture-in-picture (for example, if it is an audio-only stream), the callback is triggered with a nil
object.
The example code below configures an AVPictureInPictureController
to automatically show a picture-in-picture overlay when the application is minimized.
class SomeClass {
private var pipController: AVPictureInPictureController?
private var renderer: PhenixRenderer?
public func subscribeToStream() {
// Subscribe to a stream.
...
// After subscription, save the renderer.
renderer = ...
// The picture-in-picture controller must be initialized on the main dispatch queue.
DispatchQueue.main.async { [weak self] in
self?.setupPictureInPictureOverlay()
}
}
public func setupPictureInPictureOverlay() {
// Ensure this method is called from the main dispatch queue.
precondition(Thread.isMainThread, "Picture-in-picture must be initialized from the main thread")
// Showing a Phenix stream in picture-in-picture is supported only starting iOS 15.
guard #available(iOS 15, *) else {
return
}
// Register a callback to update the controller whenever the content source changes.
renderer?.setPictureInPictureContentSourceChangedCallback { [weak self] pipContentSource in
guard let self = self else {
return
}
// If the current stream does not support picture-in-picture, the content source is nil.
guard let pipContentSource = pipContentSource else {
return
}
// When subscribing to a new stream, the controller of the previous stream can be used by just
// replacing the existing content source.
// This allows a seamless transition between two streams in the picture-in-picture overlay.
if let pipController = pipController {
pipController.contentSource = pipContentSource
} else {
pipController = AVPictureInPictureController(contentSource: pipContentSource)
pipController?.delegate = ... // Optional: use a delegate to catch picture-in-picture events.
}
}
}
}
Set Camera Focus Position
See also Focus Modes and Focus Target.
When publishing from the device camera, an application can configure the focal length.
To set an area for the camera to focus on, a focus target is configured. A focus target is expressed in video source frame coordinates.
The video source coordinates can be obtained by calling
PhenixRenderer.convertRenderPreviewCoordinatesToSourceCoordinates
with the renderer
currently showing the video preview.
A focus target is specified in relative frame coordinates, which range from 0 to 1 on both the X and Y axis. For example, a (x, y) coordinate focus target of (0.5, 0.5) would specify the camera to focus in the middle of the captured source frame.

If the focus mode is Continuous
and a focus target has been set, the focus will be adjusted automatically if the device is moved. Note that Continuous
is the default focus mode, it is not necessary to set it explicitly.
If the focus mode is AutoThenLocked
and a focus target has been set, the focus distance will not change. If the device is moved, the focus subject will go out of focus and the application will need to adjust the focus again.
If the focus mode is Locked
, the focus is configured automatically by the device and cannot be changed.
Set the focus target by updating the DeviceCapabilities of the UserMedia used in the application.
// X and Y must be the coordinates in pixels relative to the surface being rendered.
let locationInView = // CGPoint obtained from touch event handler
// Make relative coordinates using the current renderer.
let relativeCoordinates = renderer.convertRenderPreviewCoordinatesToSourceCoordinates(locationInView)
// Update the current user media options
// It is recommended to make a copy of the options. This way, the focus can be reset easily by restoring the previous options.
let userMediaOptions = // Copy of the currently active options
userMediaOptions.video.capabilityConstraints[PhenixDeviceCapability.autoFocusTarget.rawValue] = [PhenixDeviceConstraint.initWith(relativeCoordinates)]
// Apply the options
let status = userMediaStream.applyOptions(userMediaOptions)
// Verify the status
// Setting a focus target can fail for example when:
// - The current focus mode does not support setting a focus target.
// - The current video capture device does not support setting a focus target (front cameras usually do not support it).
// - The coordinates are invalid (e.g. outside of the render surface).
if (status == .ok) {
// Show a visual indication of where the focus was set.
}