Skip to main content
Skip table of contents

Server-side ad insertion

To test this feature and view the example code, please see the Apple (FPS) SDK 5 Example Code Quick Start guide.

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 (Server side Ad Insertion in UnifiedExampleCode) demonstrates how an application can:

  • Extract HLS manifest (metadata) whenever a new HLS M3U8 file is present.

  • Parse the metadata and look for tags and patterns that indicate when to insert the adverts.

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.

VDGMetadataParser

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: "")
  }
}
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.