Skip to main content
Version: v7 Beta

Manual Analytics

Integrate your own analytics system using native plugins.

Overview

Most analytics systems that track player data (e.g., bitrate, errors) can be integrated directly with ExoPlayer or AVPlayer. The plugin system allows for non-intrusive analytics integration with react-native-video. It should be implemented in native languages (Kotlin/Swift) to ensure efficiency.

Architecture

┌─────────────────────────────────────┐
│ React Native App │
│ ┌───────────────────────────────┐ │
│ │ react-native-video │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Plugin System │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
│ │
│ onPlayerCreated │ onPlayerCreated
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Android Plugin │ │ iOS Plugin │
│ (Kotlin) │ │ (Swift) │
│ │ │ │
│ ExoPlayer │ │ AVPlayer │
│ Analytics │ │ KVO Observers │
│ Listener │ │ │
└─────────────────┘ └─────────────────┘

Implementation

Android (Kotlin)

import androidx.annotation.OptIn
import androidx.media3.common.Format
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.analytics.AnalyticsListener
import com.twg.video.core.plugins.NativeVideoPlayer
import com.twg.video.core.plugins.ReactNativeVideoPlugin
import java.lang.ref.WeakReference

@OptIn(UnstableApi::class)
class AnalyticsPlugin : ReactNativeVideoPlugin("MyAnalytics") {

override fun onPlayerCreated(player: WeakReference<NativeVideoPlayer>) {
val nativePlayer = player.get() ?: return
val exoPlayer = nativePlayer.player

exoPlayer.addAnalyticsListener(object : AnalyticsListener {
override fun onBandwidthEstimate(
eventTime: AnalyticsListener.EventTime,
totalLoadTimeMs: Int,
totalBytesLoaded: Long,
bitrateEstimate: Long
) {
trackMetric("bitrate", bitrateEstimate)
}

override fun onDroppedVideoFrames(
eventTime: AnalyticsListener.EventTime,
droppedFrames: Int,
elapsedMs: Long
) {
trackMetric("dropped_frames", droppedFrames)
}

override fun onVideoInputFormatChanged(
eventTime: AnalyticsListener.EventTime,
format: Format,
decoderReuseDecision: Int?
) {
trackEvent("quality_change", mapOf(
"width" to format.width,
"height" to format.height,
"bitrate" to format.bitrate
))
}

override fun onPlaybackStateChanged(
eventTime: AnalyticsListener.EventTime,
state: Int
) {
when (state) {
Player.STATE_BUFFERING -> trackEvent("buffering_start")
Player.STATE_READY -> trackEvent("buffering_end")
Player.STATE_ENDED -> trackEvent("playback_complete")
}
}

override fun onPlayerError(
eventTime: AnalyticsListener.EventTime,
error: PlaybackException
) {
trackEvent("error", mapOf(
"code" to error.errorCode,
"message" to error.message
))
}
})
}

override fun onPlayerDestroyed(player: WeakReference<NativeVideoPlayer>) {
flushAnalytics()
}

private fun trackEvent(name: String, params: Map<String, Any?> = emptyMap()) {
// Send to your analytics backend
}

private fun trackMetric(name: String, value: Number) {
// Send to your analytics backend
}

private fun flushAnalytics() {
// Flush pending analytics
}
}

iOS (Swift)

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
}
}

Available Metrics

Android (ExoPlayer AnalyticsListener)

EventMethodData
BitrateonBandwidthEstimatebitrateEstimate
Dropped framesonDroppedVideoFramesdroppedFrames, elapsedMs
Quality changeonVideoInputFormatChangedformat.width, format.height
BufferingonPlaybackStateChangedPlayer.STATE_BUFFERING
StallonStallStart / onStallEndduration
SeekonSeekStarted / onSeekProcessedposition
ErroronPlayerErrorerrorCode, message

iOS (AVPlayer)

MetricSourceProperty
BitrateaccessLogindicatedBitrate
StallsaccessLognumberOfStalls
Dropped framesaccessLognumberOfDroppedVideoFrames
BytesaccessLognumberOfBytesTransferred
Playback statusKVOplayer.status
Play/PauseKVOplayer.rate
BufferplayerItemisPlaybackLikelyToKeepUp

Registration

Plugins auto-register when instantiated. Create your plugin early in app lifecycle:

Android - MainApplication.kt:

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
AnalyticsPlugin() // Auto-registers via init block
}
}

iOS - AppDelegate.swift:

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
private var analyticsPlugin: AnalyticsPlugin?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
analyticsPlugin = AnalyticsPlugin() // Auto-registers via init
return true
}
}

See Also