Skip to content

Analytics & events

OGPlayer emits events on three surfaces, mirrored across Android, iOS, web, React Native and Flutter — same names, same fields, same timing. The handful of platform-specific events are marked inline. This page is the complete inventory.

PlaybackListener — the app-facing callbacks

Section titled “PlaybackListener — the app-facing callbacks”

Drive your UI and app logic from these (all optional):

CallbackFieldsWhen
onStateChangedstateIDLE → BUFFERING → READY → ENDED
onIsPlayingChangedisPlayingactual playing/not-playing flips
onPlay—first start of the loaded item only
onPause / onResume—explicit pauses only — stalls and ad-break handoffs are silent
onProgresspositionMs, bufferedMs, durationMssteady tick, 250 ms default (configurable)
onSeekStartedfromMs, toMsseek begins
onSeekCompletedpositionMsseek lands
onLiveEdgeChangedatLiveEdgeviewer drifts behind / catches up to live
onDrmSessionRenewedreasona license quietly renewed mid-session
onPlaybackCompleted—content truly finished — waits for postrolls
onErrorOGPlayerErrorterminal errors (stable codes)
onCastStateChangedstateAndroid · Chromecast connection: NO_DEVICES / NOT_CONNECTED / CONNECTING / CONNECTED
onPipChangedisActiveWeb · the video entered or left picture-in-picture (og-pipchanged on the element); Android, iOS and the wrappers report PiP through their PiP handler and view callbacks

On live streams, onProgress reports the position inside the DVR window and the window length as the duration.

One listener, one typed event set, identical string formatting on every platform — feed them to your pipeline as-is:

EventFieldsWhen
PlaybackStartedstartupMsfirst frame of a load — startupMs measures load → visible playback
Play / Pause—transport changes (explicit; stalls are silent)
SeekfromMs, toMsuser or API seeks
BufferStart / BufferEndreason (INITIAL / SEEK / REBUFFER) / —buffering window, with why it started
BitrateChangedbitrate, width, height, frameRateABR switches — on iOS the rendition on screen: the initial selection and each completed variant switch, with the variant’s declared resolution and frame rate
DroppedFramescount, elapsedMsrender drops in the window
QualitySnapshotpositionMs, bufferedMs, bandwidthEstimateBps, droppedFramesTotal, liveLatencyMs, atLiveEdgeevery 10 s while playing (the live fields are null on VOD)
VolumeChangedvolume, mutedvolume/mute API changes
TextTrackChangedid, languagesubtitle selection (id null = off)
AudioTrackChangedid, languageaudio selection
FullscreenChangedisFullscreenfullscreen transitions
PictureInPictureChangedisActivethe PiP window opened/closed — Android, iOS, web, React Native, Flutter
OrientationChangedorientationPORTRAIT / LANDSCAPE flips (initial + changes)
MutedAutoplayFallback—Web — the browser blocked autoplay with sound and playback started muted
DrmKeysLoaded—license acquired
PlaybackRecoveredreason, positionMs (web adds detail)the player healed a fault on its own — a short rebuffer the viewer may have seen, but no Error and no onError. Count these as recoveries, never as failures. Android reasons: DRM_RENEWAL_AFTERSHOCK, CODEC_RESTART, LIVE_WINDOW_RESET; web reasons: NETWORK_RETRY, BUFFER_STALL, DRM_RETRY, MEDIA_RETRY (hls.js’s own retry/stall handling — these used to surface as non-fatal Error events). iOS never emits it by design: AVFoundation heals stalls and decoder faults internally and the SDK does no re-prepare of its own, so a silent recovery on iOS shows up as a BufferStart/BufferEnd pair — normalise against that. React Native (PlaybackRecovered) and Flutter (PlaybackRecoveredEvent, RecoveryReason) forward the Android event with its reasons; on iOS they see the same BufferStart/BufferEnd pair as the native SDK.
Errorthe OGPlayerErrorterminal errors
Complete—content finished

Every event is stamped with a sessionId (a fresh UUID per load — retries and playlist advances mint a new one) and the assetUrl it belongs to, so a pipeline can group a whole viewing session without carrying its own state. The vertical feed’s VerticalFeedImpression / ItemWatched / ItemLooped events are deliberately not session-stamped — a feed session spans many pooled loads; their index + url identify impressions.

player.addAnalyticsListener { event -> pipeline.track(event.toString()) }

Everything about ads lives here — the analytics stream carries no ad events by design, so the two feeds stay cleanly separated:

CallbackFieldsWhen
onAdBreakStartedbreakType, totalAdsa PREROLL / MIDROLL / POSTROLL break begins
onAdStartedAdInfo (adId, pod position, duration, skippability…)each ad begins
onAdProgressad, positionMs, durationMssteady tick during an ad (drives the countdown)
onAdPaused / onAdResumedadviewer pauses/resumes the ad
onAdClickedadviewer opened the clickthrough — the SDK pauses the ad and resumes it on return
onAdSkippedadviewer skipped
onAdCompletedadad finished
onAdBreakCompletedbreakTypethe break ended (its cue marker is removed)
onAdSkippableStateChangedad, isSkippable, skipOffsetMsan ad becomes skippable (slot providers)
onAdErrorOGAdError (code, phase, message)ad problems — never fatal to content

VMAP cue points are exposed separately via adCuePositionsMs and drawn as markers on the scrubber.