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 property 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
  }
}

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
}