Extracting Video Title Metadata
To display the video title, the application must parse the metadata embedded within the media resource. This requires fetching the commonMetadata property from the AVAsset. When initializing the AVPlayerItem, the asset keys must be pre-loaded to ensure the data is ready when the player reaches the .readyToPlay state.
Defining the Delegate Protocol
First, define the necessary methods in the player control delegate to handle UI updates for titles and subtitle lists.
protocol VideoControlDelegate: AnyObject {
var playerDelegate: VideoPlayerDelegate? { get set }
func beginPlayback()
func updatePlaybackProgress(current: TimeInterval, total: TimeInterval)
func configureVideoTitle(_ title: String)
func configureSubtitleOptions(_ options: [String])
func handlePlaybackCompletion()
}
In the view controller or control view, implement the title configuration method to update the label text.
func configureVideoTitle(_ title: String) {
videoTitleLabel.text = title
}
Preparing the Asset with Metadata Keys
When preparing the player, specify commonMetadata in the array of asset keys to load asynchronously.
private func initializePlayer() {
let requiredAssetKeys = [
"tracks",
"duration",
"commonMetadata"
]
guard let mediaAsset = mediaAsset else { return }
playerItem = AVPlayerItem(asset: mediaAsset,
automaticallyLoadedAssetKeys: requiredAssetKeys)
guard let item = playerItem else { return }
player = AVPlayer(playerItem: item)
playerLayer = AVPlayerLayer(player: player)
controlDelegate = overlayView?.controls
controlDelegate?.playerDelegate = self
item.addObserver(self, forKeyPath: "status",
options: [.new, .initial],
context: &playerItemContext)
}
Observing Status and Retrieving Title
Once the player item status changes to .readyToPlay, extract the title string from the metadata and pass it to the delegate.
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if context == &playerItemContext {
guard let item = playerItem else { return }
guard let player = player else { return }
if item.status == .readyToPlay {
item.removeObserver(self, forKeyPath: "status")
player.play()
let totalDuration = item.duration
let seconds = CMTimeGetSeconds(totalDuration)
controlDelegate?.beginPlayback()
controlDelegate?.updatePlaybackProgress(current: 0.0, total: seconds)
let extractedTitle = retrieveTitleFromAsset()
controlDelegate?.configureVideoTitle(extractedTitle)
setupTimeObserver()
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
private func retrieveTitleFromAsset() -> String {
guard let asset = mediaAsset else { return "" }
let titleKey = AVMetadataCommonKeyTitle
let titleItems = AVMetadataItem.metadataItems(from: asset.commonMetadata,
withKey: titleKey,
keySpace: AVMetadataKeySpaceCommon)
if let titleItem = titleItems.first,
let titleString = titleItem.stringValue {
return titleString
}
return ""
}
Implementing Subtitle Selection
AVFoundation provides robust support for closed captions and subtitles using AVMediaSelectionGroup and AVMediaSelectionOption. The AVPlayerLayer renders legible media automatically if the correct option is selected within the AVPlayerItem.
Loading Media Selection Options
Modify the asset loading phase to include availableMediaCharacteristicsWithMediaSelectionOptions. This allows the inspection of audio, video, and legible (subtitle) tracks.
private func initializePlayer() {
let requiredAssetKeys = [
"tracks",
"duration",
"commonMetadata",
"availableMediaCharacteristicsWithMediaSelectionOptions"
]
guard let mediaAsset = mediaAsset else { return }
playerItem = AVPlayerItem(asset: mediaAsset,
automaticallyLoadedAssetKeys: requiredAssetKeys)
// ... (player setup code)
}
In the observation handler, call a method to fetch the available subtitle options.
// Inside observeValue... readyToPlay block
let subtitleList = fetchAvailableSubtitles()
controlDelegate?.configureSubtitleOptions(subtitleList)
private func fetchAvailableSubtitles() -> [String] {
var subtitleTitles = [String]()
guard let asset = mediaAsset else { return subtitleTitles }
let legibleCharacteristic = AVMediaCharacteristic.legible
asset.loadMediaSelectionGroup(for: legibleCharacteristic) { [weak self] group, error in
guard let self = self, let selectionGroup = group else { return }
let titles = selectionGroup.options.map { $0.displayName }
self.controlDelegate?.configureSubtitleOptions(titles)
}
return subtitleTitles
}
User Interface for Subtitle Selection
Implement the delegate method in the control view to store the options and create a button interaction that presents the list.
func configureSubtitleOptions(_ options: [String]) {
availableSubtitles = options
}
@objc func showSubtitleSelectionMenu() {
guard let subtitles = availableSubtitles, !subtitles.isEmpty else { return }
guard let presenter = window?.rootViewController else { return }
let sheet = UIAlertController(title: "Select Subtitle",
message: nil,
preferredStyle: .actionSheet)
for subtitleName in subtitles {
let action = UIAlertAction(title: subtitleName, style: .default) { [weak self] _ in
self?.playerDelegate?.chooseSubtitle(subtitleName)
}
sheet.addAction(action)
}
let disableAction = UIAlertAction(title: "Off", style: .destructive) { [weak self] _ in
self?.playerDelegate?.chooseSubtitle("")
}
sheet.addAction(disableAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
sheet.addAction(cancelAction)
presenter.present(sheet, animated: true)
}
Applying the Subtitle Selection
When the user selects an option, the player controller must load the media selection group and apply the chosen option to the AVPlayerItem.
func chooseSubtitle(_ subtitleName: String) {
guard let asset = mediaAsset else { return }
guard let item = playerItem else { return }
let characteristic = AVMediaCharacteristic.legible
asset.loadMediaSelectionGroup(for: characteristic) { group, error in
guard let group = group else { return }
if subtitleName.isEmpty {
// Turn off subtitles
item.select(nil, in: group)
return
}
// Find matching option
if let matchedOption = group.options.first(where: { $0.displayName == subtitleName }) {
item.select(matchedOption, in: group)
}
}
}