Files
react-native-video/ios/core/VideoPlayerObserver.swift
2025-10-13 22:23:17 +02:00

295 lines
11 KiB
Swift

//
// VideoPlayerObserver.swift
// ReactNativeVideo
//
// Created by Krzysztof Moch on 15/04/2025.
//
import Foundation
import AVFoundation
protocol VideoPlayerObserverDelegate: AnyObject {
func onPlayedToEnd(player: AVPlayer)
func onPlayerItemChange(player: AVPlayer, playerItem: AVPlayerItem?)
func onPlayerItemWillChange(hasNewPlayerItem: Bool)
func onTextTrackDataChanged(texts: [NSAttributedString])
func onTimedMetadataChanged(timedMetadata: [AVMetadataItem])
func onRateChanged(rate: Float)
func onPlaybackBufferEmpty()
func onPlaybackLikelyToKeepUp()
func onVolumeChanged(volume: Float)
func onExternalPlaybackActiveChanged(isActive: Bool)
func onTimeControlStatusChanged(status: AVPlayer.TimeControlStatus)
func onPlayerStatusChanged(status: AVPlayer.Status)
func onPlayerItemStatusChanged(status: AVPlayerItem.Status)
func onBandwidthUpdate(bitrate: Double)
func onProgressUpdate(currentTime: Double, bufferDuration: Double)
}
extension VideoPlayerObserverDelegate {
func onPlayedToEnd(player: AVPlayer) {}
func onPlayerItemChange(player: AVPlayer, playerItem: AVPlayerItem?) {}
func onPlayerItemWillChange(hasNewPlayerItem: Bool) {}
func onTextTrackDataChanged(texts: [NSAttributedString]) {}
func onTimedMetadataChanged(timedMetadata: [AVMetadataItem]) {}
func onRateChanged(rate: Float) {}
func onPlaybackBufferEmpty() {}
func onPlaybackLikelyToKeepUp() {}
func onVolumeChanged(volume: Float) {}
func onExternalPlaybackActiveChanged(isActive: Bool) {}
func onTimeControlStatusChanged(status: AVPlayer.TimeControlStatus) {}
func onPlayerStatusChanged(status: AVPlayer.Status) {}
func onPlayerItemStatusChanged(status: AVPlayerItem.Status) {}
func onBandwidthUpdate(bitrate: Double) {}
func onProgressUpdate(currentTime: Double, bufferDuration: Double) {}
}
class VideoPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate, AVPlayerItemLegibleOutputPushDelegate {
private weak var delegate: HybridVideoPlayer?
var player: AVPlayer? {
delegate?.player
}
// Player observers
var playerCurrentItemObserver: NSKeyValueObservation?
var playerRateObserver: NSKeyValueObservation?
var playerTimeControlStatusObserver: NSKeyValueObservation?
var playerExternalPlaybackActiveObserver: NSKeyValueObservation?
var playerVolumeObserver: NSKeyValueObservation?
var playerTimedMetadataObserver: NSKeyValueObservation?
var playerStatusObserver: NSKeyValueObservation?
var playerProgressPeriodicObserver: Any?
// Player item observers
var playbackEndedObserver: NSObjectProtocol?
var playbackBufferEmptyObserver: NSKeyValueObservation?
var playbackLikelyToKeepUpObserver: NSKeyValueObservation?
var playbackBufferFullObserver: NSKeyValueObservation?
var playerItemStatusObserver: NSKeyValueObservation?
var playerItemAccessLogObserver: NSObjectProtocol?
var metadataOutput: AVPlayerItemMetadataOutput?
var legibleOutput: AVPlayerItemLegibleOutput?
init(delegate: HybridVideoPlayer) {
self.delegate = delegate
}
deinit {
invalidatePlayerObservers()
invalidatePlayerItemObservers()
}
public func updatePlayerObservers() {
invalidatePlayerItemObservers()
invalidatePlayerObservers()
initializePlayerObservers()
}
func initializePlayerObservers() {
guard let player else {
return
}
playerCurrentItemObserver = player.observe(\.currentItem, options: [.new, .old]) { [weak self] _, change in
self?.onPlayerCurrentItemChanged(player: player, change: change)
}
playerRateObserver = player.observe(\.rate, options: [.new]) { [weak self] _, change in
guard let rate = change.newValue else { return }
self?.delegate?.onRateChanged(rate: rate)
}
playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new]) { [weak self] _, change in
guard let status = change.newValue else { return }
self?.delegate?.onTimeControlStatusChanged(status: status)
}
playerExternalPlaybackActiveObserver = player.observe(\.isExternalPlaybackActive, options: [.new]) { [weak self] _, change in
guard let isActive = change.newValue else { return }
self?.delegate?.onExternalPlaybackActiveChanged(isActive: isActive)
}
playerVolumeObserver = player.observe(\.volume, options: [.new]) { [weak self] _, change in
guard let volume = change.newValue else { return }
self?.delegate?.onVolumeChanged(volume: volume)
}
playerStatusObserver = player.observe(\.status, options: [.new]) { [weak self] _, change in
guard let status = change.newValue else { return }
self?.delegate?.onPlayerStatusChanged(status: status)
}
// 500ms interval TODO: Make this configurable
let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
playerProgressPeriodicObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
guard let self, let player = self.player, let delegate = self.delegate else { return }
delegate.onProgressUpdate(currentTime: player.currentTime().seconds, bufferDuration: player.currentItem?.getbufferDuration() ?? 0)
}
}
private func initializePlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
playbackEndedObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: playerItem,
queue: nil
) { [weak self] notification in
self?.delegate?.onPlayedToEnd(player: player)
}
playerItemAccessLogObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemNewAccessLogEntry,
object: playerItem,
queue: nil
) { [weak self] notification in
self?.onPlayerAccessLog(playerItem: playerItem)
}
setupBufferObservers(for: playerItem)
playerItemStatusObserver = playerItem.observe(\.status, options: [.new]) { [weak self] _, change in
self?.delegate?.onPlayerItemStatusChanged(status: playerItem.status)
}
let metadataOutput = AVPlayerItemMetadataOutput()
playerItem.add(metadataOutput)
metadataOutput.setDelegate(self, queue: .global(qos: .userInteractive))
let legibleOutput = AVPlayerItemLegibleOutput()
playerItem.add(legibleOutput)
metadataOutput.setDelegate(self, queue: .global(qos: .userInteractive))
}
private func invalidatePlayerItemObservers() {
// Remove NotificationCenter observers
if let playbackEndedObserver = playbackEndedObserver {
NotificationCenter.default.removeObserver(playbackEndedObserver)
self.playbackEndedObserver = nil
}
if let playerItemAccessLogObserver = playerItemAccessLogObserver {
NotificationCenter.default.removeObserver(playerItemAccessLogObserver)
self.playerItemAccessLogObserver = nil
}
// Invalidate KVO observers
clearBufferObservers()
playerItemStatusObserver?.invalidate()
playerItemStatusObserver = nil
// Remove outputs if needed
// (Assumes outputs are not shared between items)
if let playerItem = player?.currentItem {
if let metadataOutput = metadataOutput {
playerItem.remove(metadataOutput)
}
if let legibleOutput = legibleOutput {
playerItem.remove(legibleOutput)
}
}
metadataOutput = nil
legibleOutput = nil
}
func invalidatePlayerObservers() {
// Invalidate KVO observers
playerCurrentItemObserver?.invalidate()
playerCurrentItemObserver = nil
playerRateObserver?.invalidate()
playerRateObserver = nil
playerTimeControlStatusObserver?.invalidate()
playerTimeControlStatusObserver = nil
playerExternalPlaybackActiveObserver?.invalidate()
playerExternalPlaybackActiveObserver = nil
playerVolumeObserver?.invalidate()
playerVolumeObserver = nil
playerStatusObserver?.invalidate()
playerStatusObserver = nil
// Remove periodic time observer from player
if let player = player, let periodicObserver = playerProgressPeriodicObserver {
player.removeTimeObserver(periodicObserver)
playerProgressPeriodicObserver = nil
}
}
// MARK: - AVPlayerItemLegibleOutputPushDelegate
public func legibleOutput(_: AVPlayerItemLegibleOutput,
didOutputAttributedStrings strings: [NSAttributedString],
nativeSampleBuffers _: [Any],
forItemTime _: CMTime) {
delegate?.onTextTrackDataChanged(texts: strings)
}
// MARK: - AVPlayerItemMetadataOutputPushDelegate
public func metadataOutput(_: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from _: AVPlayerItemTrack?) {
for metadataGroup in groups {
delegate?.onTimedMetadataChanged(timedMetadata: metadataGroup.items)
}
}
// MARK: - AVPlayer Observers
func onPlayerCurrentItemChanged(player: AVPlayer, change: NSKeyValueObservedChange<AVPlayerItem?>) {
let newPlayerItem = change.newValue?.flatMap { $0 }
// Remove observers for old player item
invalidatePlayerItemObservers()
// Notify delegate about player item state change
delegate?.onPlayerItemWillChange(hasNewPlayerItem: newPlayerItem != nil)
if let playerItem = newPlayerItem {
// Initialize observers for new player item
initializePlayerItemObservers(player: player, playerItem: playerItem)
delegate?.onPlayerItemChange(player: player, playerItem: playerItem)
}
}
// MARK: - AVPlayerItem Observers
func onPlayerAccessLog(playerItem: AVPlayerItem) {
guard let accessLog = playerItem.accessLog() else { return }
guard let lastEvent = accessLog.events.last else { return }
delegate?.onBandwidthUpdate(bitrate: lastEvent.indicatedBitrate)
}
// MARK: - Buffer State Management
func setupBufferObservers(for playerItem: AVPlayerItem) {
clearBufferObservers()
// Observe buffer empty - this indicates definite buffering
playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .initial]) { [weak self] playerItem, change in
let isEmpty = change.newValue ?? playerItem.isPlaybackBufferEmpty
if isEmpty {
self?.delegate?.onPlaybackBufferEmpty()
}
}
// Observe likely to keep up - this indicates that buffering has finished
playbackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .initial]) { [weak self] playerItem, change in
let isLikelyToKeepUp = change.newValue ?? playerItem.isPlaybackLikelyToKeepUp
if isLikelyToKeepUp {
self?.delegate?.onPlaybackLikelyToKeepUp()
}
}
// Observe buffer full as an additional signal
playbackBufferFullObserver = playerItem.observe(\.isPlaybackBufferFull, options: [.new, .initial]) { [weak self] playerItem, change in
let isFull = change.newValue ?? playerItem.isPlaybackBufferFull
if isFull {
self?.delegate?.onPlaybackLikelyToKeepUp()
}
}
}
func clearBufferObservers() {
playbackBufferEmptyObserver?.invalidate()
playbackBufferFullObserver?.invalidate()
playbackLikelyToKeepUpObserver?.invalidate()
playbackBufferEmptyObserver = nil
playbackBufferFullObserver = nil
playbackLikelyToKeepUpObserver = nil
}
}