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

PlatformCollector versionSupported players
Web2.56.0+Bitmovin Player v8, THEOplayer, Chromecast CAF v3, HTML5 native video, HLS.js, Shaka Player, Video.js
Android3.23.0+Bitmovin Player (standalone collector and bundled Analytics), Media3 ExoPlayer, THEOplayer
iOS, tvOS, visionOS3.22.0+Bitmovin Player (standalone collector and bundled Analytics), AVPlayer, THEOplayer
Roku2.14.0+THEOplayer collector only
🚧

Not yet supported on Web with the Bitmovin Player Web X (PWX) analytics package

The PWX integration does not expose an analytics adapter to application code, so programChange cannot be called there.

What happens when you call programChange

When a session is already running (playback has started):

  1. 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 isProgramChange flag.
  2. A new impression ID is generated and the sequence number is reset.
  3. The new metadata is applied and a startup sample with the state programchange is sent for the new session. It reports a synthetic videoStartupTime of 1 ms, because no actual loading happened — the stream was already playing.
  4. 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 impression

Because 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:

FieldTypeDescription
titleStringHuman-readable title of the new program
videoIdStringIdentifier of the new program
cdnProviderStringCDN provider serving the content
pathStringBreadcrumb within your app
isLiveBooleanWhether the content is a live stream
customDataObjectFree-form fields customData1customData100 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 ignored

The 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

programChange is 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",
})
FieldTypeDescription
mpdUrlStringDASH manifest URL, sets streamFormat to dash
m3u8UrlStringHLS manifest URL, sets streamFormat to hls
progUrlStringProgressive video URL, sets streamFormat to progressive
pathStringPath 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 programchange state instead of the usual startup state.

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 situationAPI to use
The player loads a different source / stream URLsourceChange() (Web) or detachPlayer() + attachPlayer() (Android, iOS)
The same stream continues, but a new program/channel startsprogramChange()
The current session should keep running, only metadata values changesetCustomData() / setNewMetadata()
You want to mark a one-off event on the current sessionsendCustomDataEvent() / setCustomDataOnce()


Did this page help you?