David Cordero

HLS Timed Metadata with AVPlayer

Published on 01 Feb 2021

As documented by Apple, HTTP Live Streaming (HLS) supports the inclusion of timed metadata in ID3 format.

In order to get this in-stream timed metadata from the client, we can make use of AVPlayerItemMetadataOutputPushDelegate.

Please notice that even though getting access to timed metadata used to be straightforward by observing the property timedMetadata of AVPlayerItem. Recently, with the release of iOS 13.0, Apple marked this property as deprecated.

You can find here below an example of a simple player catching and logging timed metadata to the console using AVPlayerItemMetadataOutputPushDelegate.

The example uses one of the example streams from Apple which contains timed metadata (time code every 5 seconds).

And here is the code

import UIKit
import AVFoundation

class PlaygroundPlayerViewController: UIViewController, AVPlayerItemMetadataOutputPushDelegate {
    
    // MARK: - UIViewController
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpPlayerLayer()
        
        let stream = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8")!
        play(url: stream)
    }
    
    // MARK: - AVPlayerItemMetadataOutputPushDelegate
    
    func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
        if let item = groups.first?.items.first
        {
            item.value(forKeyPath: #keyPath(AVMetadataItem.value))
            let metadataValue = (item.value(forKeyPath: #keyPath(AVMetadataItem.value))!)
            print("Metadata value: \n \(metadataValue)")
        } else {
            print("MetaData Error")
        }
    }
    
    // MARK: - Private
    
    private var playerLayer: AVPlayerLayer!
    private var player: AVPlayer!
    private var playerItem: AVPlayerItem!
    
    private func play(url: URL?) {
        guard let url = url else { return }
        
        let asset = AVAsset(url: url)
        
        playerItem = AVPlayerItem(asset: asset)
        player = AVPlayer(playerItem: playerItem)
        
        let metadataOutput = AVPlayerItemMetadataOutput(identifiers: nil)
        metadataOutput.setDelegate(self, queue: DispatchQueue.main)
        playerItem.add(metadataOutput)
        
        playerLayer.player = player
        player.play()
    }
    
    private func setUpPlayerLayer() {
        playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        view.layer.addSublayer(playerLayer)
    }
}

The output in the console looks like this:

Metadata value: 
  *** THIS IS Timed MetaData @ -- 00:00:00.0 *** 
Metadata value: 
  *** THIS IS Timed MetaData @ -- 00:00:05.0 *** 
Metadata value: 
  *** THIS IS Timed MetaData @ -- 00:00:10.0 *** 
Metadata value: 
  *** THIS IS Timed MetaData @ -- 00:00:15.0 *** 
Metadata value: 
  *** THIS IS Timed MetaData @ -- 00:00:20.0 ***