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):
| Callback | Fields | When |
|---|---|---|
onStateChanged | state | IDLE → BUFFERING → READY → ENDED |
onIsPlayingChanged | isPlaying | actual playing/not-playing flips |
onPlay | — | first start of the loaded item only |
onPause / onResume | — | explicit pauses only — stalls and ad-break handoffs are silent |
onProgress | positionMs, bufferedMs, durationMs | steady tick, 250 ms default (configurable) |
onSeekStarted | fromMs, toMs | seek begins |
onSeekCompleted | positionMs | seek lands |
onLiveEdgeChanged | atLiveEdge | viewer drifts behind / catches up to live |
onDrmSessionRenewed | reason | a license quietly renewed mid-session |
onPlaybackCompleted | — | content truly finished — waits for postrolls |
onError | OGPlayerError | terminal errors (stable codes) |
onCastStateChanged | state | Android · Chromecast connection: NO_DEVICES / NOT_CONNECTED / CONNECTING / CONNECTED |
onPipChanged | isActive | Web · 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.
AnalyticsEvent — the measurement stream
Section titled “AnalyticsEvent — the measurement stream”One listener, one typed event set, identical string formatting on every platform — feed them to your pipeline as-is:
| Event | Fields | When |
|---|---|---|
PlaybackStarted | startupMs | first frame of a load — startupMs measures load → visible playback |
Play / Pause | — | transport changes (explicit; stalls are silent) |
Seek | fromMs, toMs | user or API seeks |
BufferStart / BufferEnd | reason (INITIAL / SEEK / REBUFFER) / — | buffering window, with why it started |
BitrateChanged | bitrate, width, height, frameRate | ABR switches — on iOS the rendition on screen: the initial selection and each completed variant switch, with the variant’s declared resolution and frame rate |
DroppedFrames | count, elapsedMs | render drops in the window |
QualitySnapshot | positionMs, bufferedMs, bandwidthEstimateBps, droppedFramesTotal, liveLatencyMs, atLiveEdge | every 10 s while playing (the live fields are null on VOD) |
VolumeChanged | volume, muted | volume/mute API changes |
TextTrackChanged | id, language | subtitle selection (id null = off) |
AudioTrackChanged | id, language | audio selection |
FullscreenChanged | isFullscreen | fullscreen transitions |
PictureInPictureChanged | isActive | the PiP window opened/closed — Android, iOS, web, React Native, Flutter |
OrientationChanged | orientation | PORTRAIT / LANDSCAPE flips (initial + changes) |
MutedAutoplayFallback | — | Web — the browser blocked autoplay with sound and playback started muted |
DrmKeysLoaded | — | license acquired |
PlaybackRecovered | reason, 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. |
Error | the OGPlayerError | terminal 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()) }player.addAnalyticsListener(MyAnalytics()) // onEvent(_ event: AnalyticsEvent)player.addAnalyticsListener((event) => pipeline.track(event));<OGPlayerView style={{ flex: 1 }} source={{ url }} onAnalyticsEvent={(event) => pipeline.track(event)}/>OGPlayerView( source: OGMediaItem(url: url), onAnalyticsEvent: (event) => pipeline.track(event),)AdListener — the full ad lifecycle
Section titled “AdListener — the full ad lifecycle”Everything about ads lives here — the analytics stream carries no ad events by design, so the two feeds stay cleanly separated:
| Callback | Fields | When |
|---|---|---|
onAdBreakStarted | breakType, totalAds | a PREROLL / MIDROLL / POSTROLL break begins |
onAdStarted | AdInfo (adId, pod position, duration, skippability…) | each ad begins |
onAdProgress | ad, positionMs, durationMs | steady tick during an ad (drives the countdown) |
onAdPaused / onAdResumed | ad | viewer pauses/resumes the ad |
onAdClicked | ad | viewer opened the clickthrough — the SDK pauses the ad and resumes it on return |
onAdSkipped | ad | viewer skipped |
onAdCompleted | ad | ad finished |
onAdBreakCompleted | breakType | the break ended (its cue marker is removed) |
onAdSkippableStateChanged | ad, isSkippable, skipOffsetMs | an ad becomes skippable (slot providers) |
onAdError | OGAdError (code, phase, message) | ad problems — never fatal to content |
VMAP cue points are exposed separately via adCuePositionsMs and drawn as
markers on the scrubber.