Getting Started with Publishing
Follow these steps to add the publishing capability to your application.
1. Capture audio and video
Import MillicastSDK
, get an array of available audio and video sources, and choose the preferred sources from the list. When you start capturing audio and video, the SDK will return an audio and video track.
import MillicastSDK
// Configure the audio session for capturing
let session = AVAudioSession.sharedInstance()
try session.setCategory(
.playAndRecord,
mode: .videoChat,
options: [.mixWithOthers, .allowBluetooth, .allowBluetoothA2DP]
)
try session.setActive(true)
// Create an audio track
var audioTrack : MCAudioTrack? = nil
if
let audioSources = MCMedia.getAudioSources(), // Get an array of audio sources
!audioSources.isEmpty // There is at least one audio source
{
// Choose the preferred audio source and start capturing
let audioSource = audioSources[0]
audioTrack = audioSource.startCapture() as? MCAudioTrack
}
// Create a video track
var videoTrack : MCVideoTrack? = nil
if
let videoSources = MCMedia.getVideoSources(), // Get an array of available video sources
!videoSources.isEmpty // There is at least one video source
{
// Choose the preferred video source
let videoSource = videoSources[0];
// Get capabilities of the available video sources, such as
// width, height, and frame rate of the video sources
guard let capabilities = videoSource.getCapabilities() else {
fatalError("No capability is available!") // In production replace with a throw
}
let capability = capabilities[0]; // Get the first capability
videoSource.setCapability(capability);
// Start video recording and create a video track
videoTrack = videoSource.startCapture() as? MCVideoTrack
}
2. Publish a stream
2.1 Instantiate a publisher
Create a publisher object of type MCPublisher.
let publisher = MCPublisher()
2.2 Set publisher credentials
Create a stream in your Dolby.io developer dashboard or using the Dolby.io Streaming REST API. Then, set the credentials from the dashboard. All of the MCPublisher APIs are asynchronous, so call them from asynchronous contexts.
// Get the credentials structure from your publisher instance, fill it in,
// and set the modified credentials
let credentials = MCPublisherCredentials()
credentials.streamName = "streamName"; // The name of the stream you want to publish
credentials.token = "aefea56153765316754fe"; // The publishing token
credentials.apiUrl
= "https://director.millicast.com/api/director/publish"; // The publish API URL
do {
try await publisher.setCredentials(credentials);
} catch MCGenericError.noCredentials {
// You have provided incomplete credentials
// such as an empty streamName, token, or url
}
2.3 Add video and audio tracks to the publisher
To publish media, add to the publisher the tracks that you have captured earlier.
await publisher.addTrack(with: audioTrack)
await publisher.addTrack(with: videoTrack)
2.4 Configure publishing options
Configure publishing options in the publisher, such as selecting the audio and video codecs or enabling multi-source on the publisher.
let publisherOptions = MCClientOptions()
// Get a list of supported codecs
if let audioCodecs = MCMedia.getSupportedAudioCodecs() {
// Choose the preferred audio codec
publisherOptions.audioCodec = audioCodecs[0]
} else {
print("No audio codecs available!") // In production, replace it with proper error handling
}
if let videoCodecs = MCMedia.getSupportedVideoCodecs() {
// Choose the preferred video codec
publisherOptions.videoCodec = videoCodecs[0]
} else {
print("No video codecs available!") // In production, replace it with proper error handling
}
// To use multi-source, set a source ID of the publisher and
// enable discontinuous transmission
publisherOptions.sourceId = "MySource"
publisherOptions.dtx = true
// Enable stereo
publisherOptions.stereo = true
2.5 Publish your stream
Connect to the Millicast service and publish your streams.
do {
try await publisher.connect()
// using publisherOptions from step 2.4
try await publisher.publish(with: publisherOptions)
} catch MCGenericError.restAPIError {
// Handle invalid credentials passed to the publisher
}
3. Observe state changes
Listen to state changes in the publisher via the publisher.state() method which is an AsyncStream
. It is better to start the task which is awaiting state changes before connecting the publisher so that you receive the full set of states. Every time you call the publisher.state() method you get a new AsyncStream
that you can use to consume state changes from that point onwards.
Task {
// Assuming you have a publisher instantiated before
for await state in publisher.state() {
switch(state) {
case .connected:
// Invoked when you are connected to the Millicast service
case .publishing:
// Invoked when you are currently publishing media
case .disconnected:
// Invoked when you are disconnected from the Millicast service
case .connectionError(let status: Int32, let reason: String):
// Invoked when an error occurs during a REST API call or a WebSocket establishment
// error occurs
case .signalingError(let message):
// Invoked when an error occurs during the establishment of the
// WebRTC peer connection and is returned by the Media Server
// due to a configuration error
}
}
}
You can also receive other events on the publisher, like viewer activity and viewer count:
Task {
// Assuming you have a publisher instantiated before
// Similar to state(), activity(), and all other async streams create new ones every time
for await activity in publisher.activity() {
switch(activity) {
case .active:
// Invoked when the first viewer connects to the stream
case .inactive:
// Invoked when the last viewer disconnects from the stream
}
}
}
Task {
// Assuming you have a publisher instantiated before
for await viewerCount in publisher.viewerCount() {
// do something with viewerCount
}
}
You can also use Apple's Combine AnyPublisher for a reactive programming approach. For example, for every AsyncStream
providing API shown above, there is a Combine equivalent. For state changes, you can use MCPublisher.statePublisher() to get state changes using AnyPublisher
:
publisher.statePublisher
.sink { state in
if case .connected = state {
// ...
}
}
.store(in: &cancellables)
Alternatively, you can use the old delegate-based approach, where you implement the MCPublisherDelegate protocol to receive such state changes.
class PublisherDelegate: NSObject, MCPublisherDelegate {
func onPublishing() {}
func onConnected() {}
func onDisconnected() {}
func onActive() {}
func onInactive() {}
func onViewerCount(_ count: Int32) {}
func onSignalingError(_ message: String) {}
func onConnectionError(_ status: Int32, withReason reason: String) {}
func onStatsReport(_ report: MCStatsReport) {}
func onTransformableFrame(_ data: NSMutableArray, withSsrc ssrc: Int32, withTimestamp timestamp: Int32) {}
}
When using the delegate-based approach, you can then pass this delegate during the creation of the MCPublisher. Keep the delegate alive throughout the lifetime of the publisher. The publisher does not retain the delegate.
let publisherDelegate = PublisherDelegate()
let publisher = MCPublisher(delegate: publisherDelegate)
4. Collect WebRTC statistics
Set the enableStats method to true to collect statistics.
await publisher.enableStats(true)
You can adjust the time interval of receiving statistics via MCClientOptions. To do it, set the statsDelayMs property to a value in milliseconds to adjust how frequently you receive the statistics. Remember to pass MCClientOptions to the publish method.
Then, subscribe to the statsReport AsyncStream
of the publisher. You can also use the delegate-based approach. The identifiers and way to browse the statistics are following the RTC specification. The report contains the MCStatsReport object, which is a collection of several MCStats objects. They all have a specific type, whether it is inbound, outbound, codec, or media. Inbound is the statistics of incoming transport for the viewer and outbound is a type of outgoing statistics for the publisher.
Task {
for await statsReport in publisher.statsReport() {
// Here for example we are querying the codec statistics
let codecStatsList = statsReport.getStatsOf(MCCodecsStats.get_type()) as? [MCCodecsStats]
if let codecStatsList = codecStatsList {
// Do something with the statistics
}
}
}
5. Disable automatic reconnection
By default, the publisher and subscriber attempt to reconnect automatically in case of network errors. To disable auto reconnection in connection options, use the following code:
let connectionOptions = MCConnectionOptions()
connectionOptions.autoReconnect = false
do {
try await publisher.connect(with: connectionOptions)
} catch error {
//...
}
In the case of network issues when auto reconnection is enabled, the connect method does not return as long as there is no network connection. You can abort the method at any time using the following code:
Task {
for await state in publisher.state() {
if case .connectionError(let status, let reason) = state {
try await publisher.disconnect()
}
}
}
do {
try await publisher.connect(with: connectionOptions)
} catch MCAsyncOperationCancelledError.aborted {
// The operation has been aborted by the disconnect call above
}
Updated 10 months ago