Server-side ad insertion is a method where advert setup is inserted into the OTT stream manifest during or after the encoding process in conjunction with an ad server.
The serverside-ad-insertion (or Server side Ad Insertion in UnifiedExampleCode) demonstrates how an application can:
This example only shows how to extract the advert information but does not perform the ad insertion, as there is no specific ad server against it. The application inserts advertisements into the OTVAVPlayer
using metadata in the M3U8 using the metadata property on the OTVAVPlayerItem
class.
Prerequisites
A content server configured to inject advert tags into an HLS M3U8 manifest.
Example code
The Swift source code contains the VDGMetadataObserver
and the VDGMetadataParser
classes that show how to parse the metadata to filter out and pass the custom tags. The following instructions show you how to create and attach the OTVPlayer
and OTVAVPlayerItem
to receive the metadata update. The following instructions show you how to create and attach the OTVPlayer
and OTVAVPlayerItem
to receive the metadata update.
Click here to view the example code.
CODE
// initialise the player by passing in the asset url.
otvPlayer = OTVAVPlayer(url: assetURL)
//initialise the parser
metadataParser = VDGMetadataParser()
func setupMetadataObserver() {
//set ViewController as delegate for metadataObserver protocol
metadataParser.setMetadataObserver(self)
//add observer to the playerItem's metadata property in order to be notified when it changes
if let playerItem = otvPlayer.currentItem as? OTVAVPlayerItem {
metadataObserver = playerItem.observe(\.metadata, options: [.new, .old]) { (_, change) in
if let newMetadata = change.newValue {
print(newMetadata)
//parse received metadata for Ad tags
self.metadataParser.parse(newMetadata)
}
}
}
}
Click here for an example of how to use the VDGMetadataObserver to create a protocol to listen to new metadata updates.
CODE
import Foundation
/**
`VDGMetadataObserver` is a protocol used by `VDGMetadataParser` to update the metadata from player
*/
protocol VDGMetadataObserver: NSObject {
// If this metadata does not work, go back to original format and have parameter as a note
/// Callback when receiving metedata update
/// - Parameter periods: list of period which contains a XML fragment in following format:
/// '''
/// <Period id="AD_$period_sequence$" duration="PT$duration$S" start="PT$startTime$S">
/// <AssetIdentifier schemeIdUri="urn:com:vodafone:vtv:ssai:2019" value="ad"/>
/// <EventStream schemeIdUri="urn:scte:scte35:2013:xml" timescale="1000">
/// <Event id="$ad_sequence_number$" xmlns:VTV-SSAI="urn:vodafone:vtv:ssai:event">
/// <VTV-SSAI:beaconurls>
/// <TrackingEvents>
/// <Tracking event="$ad_event$">$http_beacon_UDL$</Tracking>
/// </TrackingEvents>
/// </VTV-SSAI:beaconurls>
/// </Event>
/// </EventStream>
/// </Period>
/// '''
func newHLSAdUpdate(_ periods: [String])
}
/// Default implemetation of VDGMetadataObserver
extension VDGMetadataObserver {
func newHLSAdUpdate(_ periods: [String]) {
//Do something here with the received data
print(periods)
}
}
Click here for an example of how to use the VDGMetadataParser to parse the metadata received from OTVAVPlayerItem and abstract the adverts tags/custom tags from the M3U8 playlist.
CODE
import Foundation
/**
`VDGMetadataParser` is class used to parse playlist from player and call `VDGMetadataObserver` to expose the list of period
*/
class VDGMetadataParser {
weak var vdgMetaDataObserver: VDGMetadataObserver?
let hlsStreamInfoTag = "#EXTINF"
let hlsDiscontinuityTag = "#EXT-X-DISCONTINUITY"
let vdgSsaiBeaconTag = "#EXT-X-BEACON"
let vdgSsaiIndentifier = "#EXT-X-BEACON:AD_LENGTH"
let beaconAdLengthPrefix = ":AD_LENGTH"
let beanconEventPrefix = ":EVENT"
// The template used to generate the Event node
// `1$` is the string of sequence number
// `2$` is the string of event name
// `3$` is the string of url
let eventNodeTemplate = """
\t\t<Event id="%1$@" xmlns:VTV-SSAI="urn:vodafone:vtv:ssai:event">
\t\t\t<VTV-SSAI:beaconurls>
\t\t\t\t<TrackingEvents>
\t\t\t\t\t<Tracking event="%2$@">%3$@</Tracking>
\t\t\t\t</TrackingEvents>
\t\t\t</VTV-SSAI:beaconurls>
\t\t</Event>
"""
// The template used to generate the Period node
// `1$` is the string of period id number
// `2$` is the string of duration number in second
// `3$` is the string of start time number in second
// `4$` is the string of event node list
let periodNodeTemplate = """
<Period id="AD_%1$@" duration="PT%2$@S" start="PT%3$@S">
\t<AssetIdentifier schemeIdUri="urn:com:vodafone:vtv:ssai:2019" value="ad"/>
\t<EventStream schemeIdUri="urn:scte:scte35:2013:xml" timescale="1000">
%4$@
\t</EventStream>
</Period>
"""
func setMetadataObserver(_ observer: VDGMetadataObserver) {
vdgMetaDataObserver = observer
}
func parse(_ metadata: String) {
if !metadata.contains(vdgSsaiIndentifier) {
print("Info: cannot find \(vdgSsaiIndentifier). L\(#line)")
return
}
var periodNodes = [String](), periodSequence = 0
var eventNodes = [String](), eventSequence = 0
var periodStartTime = Float(0.0), periodDuration = Float(0.0)
let lines = metadata.split { line in line.isNewline }
lines.forEach { line in
let lineWithoutSpace = line.removeAllSpaces()
switch self.getHlsTag(from: lineWithoutSpace) {
case hlsStreamInfoTag: // #EXTINF
let durationString = lineWithoutSpace.getSubString(after: hlsStreamInfoTag + ":", before: ",")
if let durationNumber = Float(durationString) {
periodDuration += durationNumber
} else {
print("Warning: wrong duration format. L\(#line)")
}
case hlsDiscontinuityTag: // #EXT-X-DISCONTINUITY
print("Info: find \(hlsDiscontinuityTag). L\(#line)")
if eventNodes.isEmpty {
print("Info: start of period. L\(#line)")
periodStartTime += periodDuration
periodDuration = 0
} else {
print("Info: end of period. L\(#line)")
let events = eventNodes.joined(separator: "\n")
let periodNode = self.createPeriodNode(sequence: periodSequence, duration: periodDuration, startTime: periodStartTime, events: events)
periodNodes.append(periodNode)
periodSequence += 1
eventNodes.removeAll()
}
case vdgSsaiBeaconTag: // #EXT-X-BEACON
let beancon = lineWithoutSpace.getSubString(after: vdgSsaiBeaconTag)
if beancon.starts(with: beanconEventPrefix) {
eventNodes.append(self.createEventNode(from: lineWithoutSpace, sequence: eventSequence))
eventSequence += 1
print("Info: create a new ad event. L\(#line)")
} else if beancon.starts(with: beaconAdLengthPrefix) {
eventSequence = 0
print("Info: end of ad event. L\(#line)")
}
default:
print("Info: Ignore this line: \(lineWithoutSpace). L\(#line)")
}
}
vdgMetaDataObserver?.newHLSAdUpdate(periodNodes)
}
// Helper method to create a event node from input string by using the teamplate
private func createEventNode(from string: String, sequence: Int) -> String {
let name = string.getSubString(after: "EVENT=", before: ",URL")
let url = string.getSubString(after: "URL=")
return String(format: eventNodeTemplate, arguments: [String(sequence), name, url])
}
// Helper method to create a period node from input string by using the teamplate
private func createPeriodNode(sequence: Int, duration: Float, startTime: Float, events: String) -> String {
return String(format: periodNodeTemplate, arguments: [String(sequence), String(duration), String(startTime), events])
}
// Helper method to get HLS tag from one line of playlist
private func getHlsTag(from string: String) -> String {
let tag = string.getSubString(before: ":")
if tag.isEmpty {
return string
} else {
return tag
}
}
}
fileprivate extension String {
// Helper method to get substring from a string located after and before a specific string
func getSubString(after: String, before: String = "") -> String {
if let startIndex = self.range(of: after)?.upperBound,
let endIndex = before.isEmpty ? self.endIndex : self.range(of: before)?.lowerBound {
return String(self[startIndex..<endIndex])
} else {
return ""
}
}
// Helper method to get substring from a string located before a specific string
func getSubString(before: String) -> String {
if let endIndex = self.range(of: before)?.lowerBound {
return String(self[startIndex..<endIndex])
} else {
return ""
}
}
}
fileprivate extension Substring {
// Helper method to remove all space from the string
func removeAllSpaces() -> String {
return self.replacingOccurrences(of: " ", with: "")
}
}