This article details how to integrate CDN Mesh Delivery into your mobile app using Apple's AVPlayer on iOS.
Not into tutorials? |
Project setup
CocoaPods
Cocoapods 1.6.0 + is recommended
To integrate Lumen Mesh SDK into your Xcode project using CocoaPods, specify this in your Podfile
:
use_frameworks!
target ' <Your Target Name >'
pod 'LumenMeshSDK', '~> 22.03.4'
end
Then, run the following command:
$ pod update
Finally, open the generated Workspace.
Carthage
Add the dependencies to the Cartfile:
binary "https://sdk.streamroot.io/ios/LumenMeshSDK.json" ~> 22.03.4
Link the frameworks to your project:
Then, run the following command:
$ carthage update --use-xcframeworks
Project setup
CocoaPods
Cocoapods 1.9.0+ is recommended
To integrate Streamroot SDK into your Xcode project using CocoaPods, specify this in your Podfile
:
use_frameworks!
target '<Your Target Name> '
pod 'LumenMeshSDK', '~> 22.03.1'
end
Carthage
Add the dependencies to the Cartfile:
binary "https://sdk.streamroot.io/ios/LumenOrchestratorSDK.json"
Integrate Lumen SDK
1. Set the DeliveryClientKey
In the Project Navigator, right click on the main target "Info.plist", and "Open as" → "Source Code".
Add the following lines with the values of the right parameters:
<key>DeliveryClient</key>
<dict>
<key>Key</key>
<string>customerkey</string>
</dict>
Here, the key
element contained in DeliveryClient
refers to the deliveryClientKey
that you will find in the Account section of the Streamroot Dashboard. If you don't have a DeliveryClientKey, you can register for a free trial account on our website.
2. Allow loading of local resources
In the Project Navigator, right click on "Info.plist", and "Open as" → "Source Code". Add the following lines with the right parameters values.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
And
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
3. Importing the SDK
First, import the SDK.
#import LumenMeshSDK
Integrating the Code:
1. Initialize the Delivery SDK from the App delegate
Initialize the Delivery SDK from the App delegate application(_:didFinishLaunchingWithOptions:) function
LMDeliveryClient.initializeApp()
2. Integrate the Player Interactor
This Player Interactor will allow you to link AVPlayer to our SDK in order to monitors Quality of Service (QoS) metrics and allows the SDK to behave accordingly. See an implementation example
import AVKit
class PlayerInteractor: LMPlayerInteractorBase {
fileprivate var player: AVPlayer?
fileprivate var playbackState: LMPlaybackState
fileprivate var observer: Any?
override init() {
self.playbackState = .idle
super.init()
}
func linkPlayer(_ player: AVPlayer) {
self.player = player
guard let playerItem = player.currentItem else { return }
NotificationCenter.default.addObserver(self, selector: #selector(handlePlayedToEndFail),
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(handlePlayToEndSucceded),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(handleAccesLogEntry),
name: NSNotification.Name.AVPlayerItemNewAccessLogEntry,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(handleErrorLogEntry),
name: NSNotification.Name.AVPlayerItemNewErrorLogEntry,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(handleItemPlayBackJumped),
name: NSNotification.Name.AVPlayerItemTimeJumped,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(handleItemPlayBackStall),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: playerItem)
player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.new, context: nil)
observePlayback()
}
deinit {
if let observer = self.observer {
player?.removeTimeObserver(observer)
self.observer = nil
}
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: player?.currentItem)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player?.currentItem)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemNewAccessLogEntry,
object: player?.currentItem)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemNewErrorLogEntry,
object: player?.currentItem)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemTimeJumped,
object: player?.currentItem)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: player?.currentItem)
player?.removeObserver(self, forKeyPath: "rate")
}
fileprivate func updateState(_ state: LMPlaybackState) {
if playbackState != state {
super.playerStateDidChange(state)
playbackState = state
}
}
public override func observeValue(forKeyPath keyPath: String?,
of _: Any?,
change _: [NSKeyValueChangeKey: Any]?,
context _: UnsafeMutableRawPointer?) {
if keyPath == "rate", player?.rate == 0.0 {
updateState(.paused)
}
}
fileprivate func observePlayback() {
if let observer = self.observer {
player?.removeTimeObserver(observer)
self.observer = nil
}
// Invoke callback every half second
let interval = CMTime(seconds: 1,
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
observer = player?.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) { [weak self] _ in
guard let self = self else { return }
let playbackLikelyToKeepUp: Bool = self.player?.currentItem?.isPlaybackLikelyToKeepUp ?? false
if !playbackLikelyToKeepUp {
// rebuffering
self.updateState(.buffering)
} else {
// playing
let rate = self.player?.rate
if rate == 1.0, self.player?.error == nil {
self.updateState(.playing)
}
}
}
}
}
// MARK: - Handler
extension PlayerInteractor {
@objc private func handlePlayedToEndFail(_: Notification) {
super.playbackErrorOccurred()
}
@objc private func handlePlayToEndSucceded(_: Notification) {
updateState(.ended)
}
@objc private func handleAccesLogEntry(_: Notification) {
guard let playerEvents = player?.currentItem?.accessLog()?.events.first else {
return
}
// trackswitch
if playerEvents.switchBitrate > 0 {
super.trackSwitchOccurred()
}
// dropframe
if playerEvents.numberOfDroppedVideoFrames > 0 {
super.updateDroppedFrameCount(playerEvents.numberOfDroppedVideoFrames)
}
}
@objc private func handleErrorLogEntry(_: Notification) {
super.playbackErrorOccurred()
}
@objc private func handleItemPlayBackJumped(_: Notification) {
updateState(.seeking)
}
@objc private func handleItemPlayBackStall(_: Notification) {
updateState(.buffering)
}
}
3. Build and start the DeliveryClient
Declare the deliveryClient as an Instance Variable
var deliveryClient: LMDeliveryClient?
Build the Lumen delivery client with the mandatory fields which are the playerInteractor
, meshProperty
, and the manifestUrl
.
deliveryClient = LMDeliveryClientBuilder.clientBuilder()
.playerInteractor(<#playerInteractor#>)
.contentId(<#string#>)
.meshProperty(<#string#>)
.build(<#manifestUrl#>)
.latency(<#latency#>)
.logLevel(<#level#>)
.build(<#manifestUrl#>)
Parameter name | Mandatory | Description |
---|---|---|
playerInteractor | Yes | The playerInteractor is a component in charge of the interactions with the player. This is essential to monitor the Quality of Service of the current playback session. In this example, the PlayerInteractor implements LMPlayerInteractorBase and serves as a Helper class of the sample app project, with a reference to the player. More info here. |
contentID | No | A parameter to identify the content and make our Dashboard more user friendly. The default value is the stream URL. |
manifestUrl | Yes | The HLS .m3u8 master playlist URL needed to be passed to the deliveryClient |
meshProperty | No | The property to be used for this content. If none is set, we will use "default". . More info here. |
latency | No | A method that works on live content. If it's set for a VoD content, this parameter will be ignored. The value must be strictly positive and in seconds. We recommend setting the value to 30s. |
logLevel | No | Set Mesh’s log level : Trace | Critical | Error | Warning | Info | Debug | Off. Default is set to Off. |
The player Interactor is a class which implements LMPlayerInteractorBase
and raises events with the associated super methods.
Example, when the playback starts, the .playing event is raised as following:
super.playerStateDidChange(.playing)
Then Start the SDK instance and get the final url.
var deliveryClient = createDeliveryClient()
deliveryClient.start();
4. Play the Stream
Once you have an up and running instance of the SDK, you must retrieve the final URL and input it to AVPlayer instead of the original one.
Then, start the player with the new url provided by the delivery client and link it with the player interactor:
guard let deliveryUrl = deliveryClient?.localManifestURL else {
print("Local Url manifets could not be generated")
return
}
let playerItem = AVPlayerItem(asset: AVURLAsset(url: deliveryUrl))
player = AVPlayer(playerItem: playerItem)
/*
*
* We are calling here linkPlayer to start the playerInteractor
*
*/
playerInteractor.linkPlayer(player!)
// Call the player play() method
player?.play()
localManifestURL
refers to the HLS .m3u8
master playlist URL that will be passed to the player.
5. Stop the Lumen Delivery Client.
In order to stop the delivery client, we recommend to put it in the viewDidDisappear(:bool)
or any callback closing the player.
self.deliveryClient?.stop()
6. Display Stats
A helper method is provided to the client to display the stats on a defined UIView
. The stats view will let you visualize different parameters of the Orchestrator such as number of bytes downloaded, the bandwidth, Global Score, Business Score, QoS Score and the Error Count.
self.deliveryClient?.displayStatView(someView!)
Note: In the sample app project, we are using contentOverlayView
a subview of AVPlayerViewController
.