How to track program changes on a live stream?
Use the programChange API to start a new analytics session when the content of a continuous stream changes, without reloading the source.
Live streams often carry more than one piece of content. A 24/7 channel moves from one EPG program to the next, a linear feed switches from a show to the news, or your app switches channels inside the same manifest. In all of these cases the stream keeps running, but from an analytics point of view the content is no longer the same.
The programChange API lets you tell the Bitmovin Analytics collector about exactly that: the content changed logically, while the underlying stream and the player attachment stay untouched.
Calling it closes the current analytics session and starts a new one — with a new impression ID and the metadata of the new program — without interrupting playback.
Not sure whether this is the right API for your use case?See Choosing the right API at the end of this page for a comparison with source changes and the custom data APIs.
Availability
| Platform | Collector version | Supported players |
|---|---|---|
| Web | 2.56.0+ | Bitmovin Player v8, THEOplayer, Chromecast CAF v3, HTML5 native video, HLS.js, Shaka Player, Video.js |
| Android | 3.23.0+ | Bitmovin Player (standalone collector and bundled Analytics), Media3 ExoPlayer, THEOplayer |
| iOS, tvOS, visionOS | 3.22.0+ | Bitmovin Player (standalone collector and bundled Analytics), AVPlayer, THEOplayer |
| Roku | 2.14.0+ | THEOplayer collector only |
Not yet supported on Web with the Bitmovin Player Web X (PWX) analytics packageThe PWX integration does not expose an analytics adapter to application code, so
programChangecannot be called there.
What happens when you call programChange
programChangeWhen a session is already running (playback has started):
- The current impression is closed. A final sample is sent with the metadata of the old program and the playback time accumulated so far. This sample carries the
isProgramChangeflag. - A new impression ID is generated and the sequence number is reset.
- The new metadata is applied and a startup sample with the state
programchangeis sent for the new session. It reports a syntheticvideoStartupTimeof1 ms, because no actual loading happened — the stream was already playing. - Playback continues uninterrupted, and every following sample belongs to the new impression.
When the call happens before or during startup (no session has been established yet):
- No sample is sent and no new impression is created. The metadata is simply applied, and the upcoming natural startup is tracked with the new values. This makes the call safe to use in racy situations such as a channel switch right after loading a source.
Each program is a separate impressionBecause a program change starts a new session, it also counts as a new impression in your Analytics dashboard and license usage — the same as a regular source change does.
Metadata handling
The metadata you pass to programChange replaces the metadata of the currently active source. Fields you do not set are not carried over from the previous program, so always pass the complete set of source-level fields for the new program.
Metadata configured on a session-independent level (DefaultMetadata on Android/iOS, the analytics config on Web/Roku) keeps applying as usual, and customData set on the new program takes precedence over the default values.
SourceMetadata accepts the following fields:
| Field | Type | Description |
|---|---|---|
title | String | Human-readable title of the new program |
videoId | String | Identifier of the new program |
cdnProvider | String | CDN provider serving the content |
path | String | Breadcrumb within your app |
isLive | Boolean | Whether the content is a live stream |
customData | Object | Free-form fields customData1–customData100 and experimentName |
Web
Bitmovin Player v8
With the Bitmovin Player Web SDK, the analytics API is available on the player instance:
const player = new bitmovin.player.Player(container, {
key: '<YOUR PLAYER KEY>',
analytics: {
key: '<YOUR ANALYTICS KEY>',
videoId: 'channel-1',
title: 'Channel 1',
isLive: true,
},
});
// Later, when the next program starts on the same stream
player.analytics.programChange({
title: 'Evening News',
videoId: 'program-news-2100',
cdnProvider: 'akamai',
path: '/live/channel-1',
isLive: true,
customData: {
customData1: 'news',
experimentName: 'epg-tracking',
},
});Other players
For all other web players, call programChange on the analytics adapter instance you created for the player. The following example uses HLS.js — the same applies to ShakaAdapter, VideojsAdapter, HTMLVideoElementAdapter, THEOplayerAdapter and CAFv3Adapter:
const analyticsConfig = {
key: '<YOUR ANALYTICS KEY>',
videoId: 'channel-1',
title: 'Channel 1',
isLive: true,
};
const player = new Hls();
const analytics = new bitmovin.analytics.HlsAdapter(analyticsConfig, player);
// Later, when the next program starts on the same stream
analytics.programChange({
title: 'Evening News',
videoId: 'program-news-2100',
isLive: true,
customData: {
customData1: 'news',
},
});
Invalid metadata is ignoredThe collector validates the object you pass in. If it is not a plain object, the call is ignored and an error is logged to the console. Individual fields with an unexpected type are dropped, while the rest of the metadata is still applied.
Android
Standalone collector
val analyticsConfig = AnalyticsConfig(licenseKey = "<YOUR ANALYTICS KEY>")
val collector = IBitmovinPlayerCollector.create(context, analyticsConfig)
collector.setSourceMetadata(source, SourceMetadata(videoId = "channel-1", title = "Channel 1", isLive = true))
collector.attachPlayer(player)
player.load(source)
// Later, when the next program starts on the same stream
collector.programChange(
SourceMetadata(
title = "Evening News",
videoId = "program-news-2100",
cdnProvider = "akamai",
isLive = true,
customData = CustomData(customData1 = "news"),
),
)Bundled Analytics in the Bitmovin Player
When you use the Analytics that ships with the Bitmovin Player for Android, the collector is available through the player:
player.analytics?.programChange(
SourceMetadata(
title = "Evening News",
videoId = "program-news-2100",
isLive = true,
customData = CustomData(customData1 = "news"),
),
)Java
SourceMetadata sourceMetadata = new SourceMetadata.Builder()
.setTitle("Evening News")
.setVideoId("program-news-2100")
.setIsLive(true)
.setCustomData(new CustomData.Builder().setCustomData1("news").build())
.build();
collector.programChange(sourceMetadata);
A player must be attached
programChangeis ignored when no player is attached to the collector. Call it while the collector is attached and tracking.
iOS, tvOS and visionOS
Standalone collector
programChange(newSourceMetadata:) is part of the shared collector API, so it is available on every collector — BitmovinPlayerCollector, AVPlayerCollector and THEOplayerCollector. The following example uses AVPlayer:
let analyticsConfig = AnalyticsConfig(licenseKey: "<YOUR ANALYTICS KEY>")
let collector = AVPlayerCollectorFactory.create(config: analyticsConfig)
collector.sourceMetadata = SourceMetadata(videoId: "channel-1", title: "Channel 1", isLive: true)
collector.attach(to: player)
// Later, when the next program starts on the same stream
collector.programChange(
newSourceMetadata: SourceMetadata(
videoId: "program-news-2100",
title: "Evening News",
isLive: true,
cdnProvider: "akamai",
customData: CustomData(customData1: "news")
)
)Bundled Analytics in the Bitmovin Player
When you use the Analytics that ships with the Bitmovin Player for iOS, tvOS and visionOS, the analytics API is available on the player instance:
let player = PlayerFactory.createPlayer(
playerConfig: playerConfig,
analytics: .enabled(analyticsConfig: AnalyticsConfig(licenseKey: "<YOUR ANALYTICS KEY>"))
)
// Later, when the next program starts on the same stream
player.analytics?.programChange(
newSourceMetadata: SourceMetadata(
videoId: "program-news-2100",
title: "Evening News",
isLive: true,
customData: CustomData(customData1: "news")
)
)Objective-C
BMASourceMetadata *sourceMetadata = [[[[BMASourceMetadataBuilder new]
withVideoId:@"program-news-2100"]
withTitle:@"Evening News"]
build];
[collector programChangeWithNewSourceMetadata:sourceMetadata];Roku
On Roku, programChange is available on the THEOplayer collector and is called through the collector node's function interface. In addition to the standard metadata fields it accepts the stream URL of the new program, which is used to report the stream format:
m.collector.callFunc("programChange", {
title: "Evening News",
videoId: "program-news-2100",
isLive: true,
customData1: "news",
m3u8Url: "https://example.com/news.m3u8",
})| Field | Type | Description |
|---|---|---|
mpdUrl | String | DASH manifest URL, sets streamFormat to dash |
m3u8Url | String | HLS manifest URL, sets streamFormat to hls |
progUrl | String | Progressive video URL, sets streamFormat to progressive |
path | String | Path reported for the stream |
Verifying the result in the dashboard
After a program change you will see two separate sessions in the Analytics dashboard:
- the session of the previous program, ending at the moment of the call, and
- the session of the new program, starting with a
programchangestate instead of the usualstartupstate.
Both sessions have their own impression ID, so all metrics — playtime, rebuffering, quality changes, errors — are attributed to the program they happened in.
Choosing the right API
Use programChange when the content changes but the stream does not. If you load a new source into the player, use the regular source change flow instead. If you only want to correct or enrich the metadata of the current session, use the custom data APIs.
| Your situation | API to use |
|---|---|
| The player loads a different source / stream URL | sourceChange() (Web) or detachPlayer() + attachPlayer() (Android, iOS) |
| The same stream continues, but a new program/channel starts | programChange() |
| The current session should keep running, only metadata values change | setCustomData() / setNewMetadata() |
| You want to mark a one-off event on the current session | sendCustomDataEvent() / setCustomDataOnce() |
Updated 11 days ago