To integrate custom analytics on iOS, extend ReactNativeVideoPlugin and override onPlayerCreated to set up observers for the AVPlayer. Use Key-Value Observing (KVO) for playback status and rate, and addPeriodicTimeObserver for position tracking. For detailed network and frame metrics, observe .AVPlayerItemNewAccessLogEntry notifications to access the AVPlayerItemAccessLog.
Key metrics available:
- Bitrate, Stalls, Dropped frames, Bytes: Extracted from
AVPlayerItem.accessLog(). - Playback status: Observed via
player.status (KVO). - Play/Pause: Observed via
player.rate (KVO). - Buffer status: Observed via
playerItem.isPlaybackLikelyToKeepUp.
import AVFoundation
class AnalyticsPlugin: ReactNativeVideoPlugin {
// MARK: - Properties
private weak var currentPlayer: AVPlayer?
private var rateObserver: NSKeyValueObservation?
private var statusObserver: NSKeyValueObservation?
private var timeObserver: Any?
// MARK: - Init
init() {
super.init(name: "MyAnalytics")
}
// MARK: - Plugin Lifecycle
override func onPlayerCreated(player: Weak<NativeVideoPlayer>) {
guard let nativePlayer = player.value else { return }
currentPlayer = nativePlayer.player
setupPlaybackObservers(for: nativePlayer.player)
setupQualityTracking(for: nativePlayer.playerItem)
}
override func onPlayerDestroyed(player: Weak<NativeVideoPlayer>) {
removeAllObservers()
flushAnalytics()
}
// MARK: - Setup
private func setupPlaybackObservers(for player: AVPlayer) {
rateObserver = player.observe(\.rate) { [weak self] p, _ in
self?.trackEvent(p.rate > 0 ? "play" : "pause")
}
statusObserver = player.observe(\.status) { [weak self] p, _ in
if p.status == .readyToPlay {
self?.trackEvent("ready")
} else if p.status == .failed {
self?.trackEvent("error", params: ["message": p.error?.localizedDescription ?? ""])
}
}
timeObserver = player.addPeriodicTimeObserver(
forInterval: CMTime(seconds: 10, preferredTimescale: 1),
queue: .main
) { [weak self] time in
self?.trackMetric("position", value: time.seconds)
}
}
private func setupQualityTracking(for playerItem: AVPlayerItem?) {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAccessLog),
name: .AVPlayerItemNewAccessLogEntry,
object: playerItem
)
}
@objc private func handleAccessLog(_ notification: Notification) {
guard let item = notification.object as? AVPlayerItem,
let event = item.accessLog()?.events.last else {
return
}
trackMetric("bitrate", value: event.indicatedBitrate)
trackMetric("stalls", value: Double(event.numberOfStalls))
trackMetric("dropped_frames", value: Double(event.numberOfDroppedVideoFrames))
}
private func removeAllObservers() {
if let observer = timeObserver {
currentPlayer?.removeTimeObserver(observer)
}
rateObserver?.invalidate()
statusObserver?.invalidate()
NotificationCenter.default.removeObserver(self)
currentPlayer = nil
timeObserver = nil
}
// MARK: - Analytics
private func trackEvent(_ name: String, params: [String: Any] = [:]) {
// Send to your analytics backend
}
private func trackMetric(_ name: String, value: Double) {
// Send to your analytics backend
}
private func flushAnalytics() {
// Flush pending analytics
}
}