livestream-player

Migrate Arc PageBuilder from Bitmovin

An audited, staged migration from Arc's Bitmovin feature to LivePlayer.

This is not an import-only replacement. Arc's current Bitmovin feature also owns ad targeting, Nielsen, Chartbeat, GA, sticky and off-screen behavior, article overlays, and vertical-gallery coordination. Close the launch gates below before removing the Bitmovin runtime.

Review baseline

This guide is based on these revisions:

  • Arc-PageBuilder origin/dev at 7af01b4be306e5d5c176d1cf9c1aef49f41361f5
  • livestream-player at 408dfaa88c33b1fcd0f95c42fa16bc3d8ddc8e3a

The migration covers live HLS on the web. VOD stays on the EX.CO playback path; casting, DVR, native apps, and CTV are outside the Player's current scope.

What Arc uses today

The primary integration is components/features/livestream/bitmovin/default.tsx. It is both a PageBuilder feature and the shared component used by:

  • components/features/livestream/cyoa/default.tsx
  • components/features/article/story/_children/promo-object.tsx
  • components/features/article/components/ArticleBody/components/custom-embed/default.jsx
  • components/features/global/components/StoryCardV2.tsx, supplied by components/features/global/story-list-v2/default.tsx

The feature currently does much more than instantiate Bitmovin:

ResponsibilityCurrent Arc implementationMigration consequence
Playback and controlsBitmovin Player 8 and Player UI 3 loaded from CDNsReplace with package-owned Shaka and UI; delete both CDN loaders
DAIIMA DAI StreamManager, ID3 metadata forwarding, asset-key requestPass ads={{ assetKey }}; the package owns IMA DAI and fail-open loading
Pre-rollBitmovin advertising plus a VAST URL from useBuildAdTagPass preroll={{ tagUrl }}; the package owns IMA HTML5
Targeting and identitycust_params, NewsPass ID, and BlueConic ID are assembled in different placesProduce one explicit AdTargeting value and validate the outgoing DAI and VAST requests
Nielsenuse-nielsen-bitmovin consumes raw Bitmovin and IMA events and polls the Bitmovin APIRewrite against normalized events plus a stable package-owned playback snapshot
Chartbeatuse-chartbeat-bitmovin and BitmovinChartbeatStrategy consume raw events and poll duration, time, state, and bitrateReplace the strategy with a provider-neutral adapter; do not reach through getEngine()
GA / GTMEvery subscribed raw event is pushed as bitmovin_${data.type}Preserve a deliberate compatibility schema during the canary; do not accidentally rename or multiply events
Page coordinationIntersection Observer, sticky dismissal, off-screen pause, and vertical-gallery pause guardsKeep Arc-owned behavior until the package supports the same policy
PresentationPoster, custom/fill layout, article link overlay, watermark, and placement-specific controlsDecide each parity difference before rollout; several are not direct prop mappings
QoS / operationsBitmovin Analytics plus console errorsWire onTelemetry to an approved queryable sink before canarying

There are also two cleanup surfaces after cutover:

  • @grahamdigital/bitmovin-playerui and @grahamdigital/bitmovin-types in package.json / package-lock.json
  • Bitmovin globals in fusion:environment.d.ts, plus Bitmovin-only types and tests

Target integration shape

Keep Arc-specific policy in a thin compatibility adapter. The package must remain unaware of Fusion contexts, PageBuilder custom fields, cookies, consent providers, GTM, Nielsen, Chartbeat, and the vertical gallery.

Fusion custom fields and content metadata


Arc livestream compatibility adapter
  ├─ ad tag, targeting, consent, identity
  ├─ sticky/off-screen and gallery policy
  ├─ Nielsen and Chartbeat adapters
  └─ GA compatibility events and telemetry


              <LivePlayer>

                    └─ normalized PlayerEvent stream

For the first release, keep the existing components/features/livestream/bitmovin/default.tsx path and its legacy prop shape. PageBuilder compositions and several lazy imports identify that path. Rename the file, feature, and types only after stored compositions and all callers have moved, or leave a compatibility re-export. Preserve BitmovinPlayer.lazy = true (renamed internally if desired) while the file remains a Fusion feature.

Launch gates

1. Publish and pin an approved package release

Arc uses npm and React 19, which matches the package's current peer dependency. Install an approved published version and commit the npm lockfile:

npm install --save-exact @grahamdigital/livestream-player@<approved-version>

Do not remove either Bitmovin package until all Bitmovin imports and the compatibility canary have been removed.

2. Add a stable analytics read model

The normalized events are sufficient for lifecycle transitions, but the audited Arc analytics code also reads:

  • current playhead, including the absolute live playhead Nielsen expects
  • duration
  • playing, paused, and stalled state
  • selected bitrate or quality

LivePlayerHandle.getEngine() is explicitly an unstable escape hatch and returns a Shaka instance, not a Bitmovin-compatible API. Binding Arc analytics to it would violate the package seam and make the next Engine change another analytics migration.

Before porting Nielsen or Chartbeat, add a stable, engine-agnostic read API to the package—for example getSnapshot() on LivePlayerHandle—and add normalized stall transitions if the analytics contract still requires them. Define live playhead semantics explicitly and test them in both Shaka-managed HLS and Safari native-HLS paths.

This is a hard gate: Nielsen and Chartbeat parity cannot be claimed by casting the Shaka object to any or recreating Bitmovin methods around it.

3. Resolve presentation and policy differences

The following legacy inputs do not have one-to-one behavior in the current package:

Legacy behaviorCurrent package behaviorRecommended migration decision
posterNo poster propAdd package support before moving article and story placements, or explicitly approve a black pre-play frame
controls={false}Controls are always rendered outside ad/slate states; the audited legacy UI creation also appears unconditionalConfirm the field can be retired, or add a package prop if it remains supported
Creative-defined pre-roll skipThe host must supply preroll.skipOffset before the package shows its skip controlDerive skip eligibility from IMA or define a real Arc configuration; do not guess one fixed offset
Sticky only after first viewSticky docks whenever its observer reports out of viewKeep Arc's viewed-first wrapper policy or extend the package
stickyPosition="top" | "bottom"Fixed bottom-right mini-playerKeep Arc-owned positioning or approve one global position
Non-sticky player pauses off screenPackage continues playbackRetain the Arc Intersection Observer pause policy to avoid hidden playback and bandwidth
Sticky disabled in PageBuilder adminPackage has no knowledge of isAdminPass sticky={false} in admin from the Arc adapter
Full-area article link with gaTagPackage link overlay is a small, new-tab labelKeep the link and click analytics in the Arc wrapper unless product approves the new behavior
Clickable Bitmovin watermarkPackage watermark is a non-clickable imageKeep host-owned click behavior or add a documented package capability
Story-list-specific title/control UIPackage has one control barApprove the unified UI or add an explicit package variant
layout="fill" with cover videoRoot can fill a sized parent; video does not choose object-fit: coverSize the host wrapper and add scoped .lsp-player video { object-fit: cover; } styling

The old feature destructures watermarkSrc, while its PageBuilder propTypes declares the field as watermark. Treat that mismatch as legacy data compatibility: select one canonical field and temporarily accept both instead of silently dropping existing values.

4. Preserve the mute preference

Arc currently stores bitmovin_muted as "muted" or "unmuted". The package stores lsp:muted as "1" or "0". Without a bridge, returning viewers lose their previous choice.

Prefer adding a one-time fallback read in the package's persistence module: when lsp:muted is absent, read and translate bitmovin_muted, write the new key after a user action, and keep the old key untouched until rollback is no longer needed. Test SSR and hydration so the migration does not read local storage during server render.

5. Define one ad-targeting contract

Do not pass the encoded custParams string directly as a single key. Refactor the Arc builder to return both the VAST tag and structured targeting:

interface ArcLivestreamAdConfig {
  prerollTagUrl?: string;
  targeting: {
    keyValues: Record<string, string | string[]>;
    ppid?: string;
    gpp?: string;
    gppSid?: string;
    usPrivacy?: string;
  };
}

Pass that value through adConfig, or through ads.targeting for stream-specific overrides. The package serializes it separately for DAI and CSAI.

The audited Bitmovin path uses NewsPass ID for client-side ads but BlueConic session ID as DAI ppid. Decide which identifier is authoritative before changing traffic; do not silently pick one. Likewise, source GPP and US Privacy strings from the approved consent integration—an analytics-consent boolean is not a substitute for either string.

Validate the exact outgoing values in a non-production environment. cust_params encoding differs between an IMA DAI request and a VAST query string, so compare decoded key/value pairs rather than raw URL text.

6. Make telemetry queryable

Wire onTelemetry to the Arc-approved operational sink with station, stream ID, DAI asset key presence (not secrets), placement, URL path, error category/code, fatality, and reload attempt. Apply the repository's analytics-disable and consent policy as required by that sink.

The canary must be observable for startup_failure and fatal_error; console logging alone does not replace the Bitmovin Analytics visibility being removed.

Prop mapping in the Arc adapter

Keep the legacy customFields contract initially and map it in one place:

Arc inputLivePlayer mapping
hlssrc and DAI fail-open fallback
daiAssetKeyads={{ assetKey: daiAssetKey }} when present
resolved adVastTagpreroll={{ tagUrl: adVastTag }} when pre-roll is enabled
structured targeting / consent / PPIDadConfig
autoPlayautoPlay
mutedInitial muted; the migrated persisted preference wins on the client
autoPlay={false}, muted={false}startMode="click-to-play" for a user gesture and audio
autoPlay={true}, muted={false}Not a reliable browser mode; choose muted autoplay or click-to-play rather than assuming audible autoplay succeeds
title, id, daiAssetKey, poster, isLiveHost analytics metadata; poster also needs the parity decision above
setAdPlayingtrue on adbreakstart; false on adbreakend and aderror cleanup
canonicalWebsiteInput to Arc's ad-tag builder only
url, enableLinkOverlay, gaTagArc-owned article overlay and click analytics
className, sx, layout, placeholderArc wrapper styling; only pass plain CSSProperties through style
stickyEnabled, stickyPosition, initialInViewArc-owned compatibility policy until the parity gate is resolved

Session-start props are captured by the package. Give changing-stream surfaces such as CYOA a key that changes with all session-defining inputs—not just the HLS URL:

const sessionKey = [id, hls, daiAssetKey, adVastTag].filter(Boolean).join(":");

<LivePlayer
  key={sessionKey}
  src={hls}
  ads={daiAssetKey ? { assetKey: daiAssetKey } : undefined}
  preroll={adVastTag ? { tagUrl: adVastTag } : undefined}
  adConfig={targeting}
  autoPlay={autoPlay}
  muted={muted}
  startMode={!autoPlay && !muted ? "click-to-play" : "muted-autoplay"}
/>

The final adapter should also forward a LivePlayerHandle ref, event callback, telemetry callback, and Arc-owned wrapper behavior; they are omitted here to keep the prop mapping focused.

Event and analytics migration

One event fan-out

Create one memoized onEvent(event: PlayerEvent) callback in the Arc adapter and fan out from it only when the relevant consent/configuration allows:

  1. Re-assert the vertical-gallery pause on play and playing.
  2. Drive setAdPlaying from ad-break boundaries.
  3. Forward the event and stable playback snapshot to Nielsen.
  4. Forward them to Chartbeat.
  5. Emit the intentional GA compatibility event.

The existing usePauseOnVerticalVideoGalleryOpen helper already accepts any ref whose current value has pause(), so LivePlayerHandle fits it without a Bitmovin adapter. Keep the point-in-time mount guard and the play / playing re-check: async startup, DAI fail-open, or a player recreation can otherwise resume beneath the gallery.

The Player has no visibility-hide pause, which matches Arc's current decision to let background audio continue. Do not add a general visibilitychange pause while porting the gallery guard.

GA compatibility window

The checked-in GTM container listens for the bitmovin_ prefix, reads the action from data.type, and hard-codes video_player=Bitmovin. Renaming the runtime event and GTM container in the same canary makes analytics regressions harder to isolate.

During a bounded compatibility window, emit the legacy envelope with an explicit event translation:

Normalized eventLegacy data.type
ready, play, playing, paused, error, adquartile, aderrorunchanged
adbreakstartadbreakstarted
adbreakendadbreakfinished
adstartadstarted
adskipadskipped
adfinishadfinished
texttrackschange, cuechangeno legacy GA event unless reporting explicitly requires one

Keep the established videoTitle, videoUrl, videoId, bitmovin_context, and isLive fields during this window. Mark the alias for removal and migrate GTM and dashboards to a provider-neutral livestream_* schema after parity is proven. Do not emit both schemas into the same GA tag unless deduplication is designed and tested.

Nielsen

Replace use-nielsen-bitmovin with a provider-neutral livestream hook, but retain its metadata rules, per-player session isolation, consent teardown, and before-unload cleanup. Preserve the current play-based content start unless the measurement owner approves switching to the stricter playing signal; map stop/error and ad transitions from paused / error and adbreakstart / adbreakend / adskip / aderror.

Use the package's new stable snapshot for playhead polling. Do not infer the absolute live playhead from an arbitrary Shaka or media-element time without a documented and tested conversion. Decide whether Nielsen debug playerKind remains "bitmovin" during the compatibility window or becomes a new provider-neutral value, then update the discriminated union and validation manifest together.

Chartbeat

Replace the Bitmovin-specific strategy and registry with a strategy whose verify, state, duration, time, and bitrate methods consume the stable package contract. Preserve the corrected registration flow at the reviewed Arc revision: Chartbeat constructs the strategy for the player object pushed to _cbv, and Arc removes that player during cleanup.

Keep existing video paths and player-type labels during the compatibility window unless the reporting owner approves new values. The provider rename is a reporting migration, not an incidental code cleanup.

  1. Add and test the missing package capabilities: analytics snapshot/stall semantics, required poster/control behavior, and legacy mute preference fallback. Publish an approved version.
  2. In Arc, add provider-neutral livestream types, analytics adapters, structured ad targeting, telemetry, and their focused unit tests without changing the active Bitmovin feature.
  3. Rewrite the internals of the existing livestream/bitmovin feature as the compatibility adapter. Keep its path, custom fields, lazy flag, and callers stable.
  4. Replace tests that mock Bitmovin, IMA globals, and CDN hooks with FakeEngine / FakeAds from @grahamdigital/livestream-player/testing. Retain focused host tests for analytics fan-out, gallery re-pause, off-screen behavior, sticky policy, and CYOA ad locking.
  5. Canary one station and one placement at a time behind the existing Arc deployment controls. Validate plain HLS, DAI, CSAI, DAI + CSAI, fail-open, captions, mobile, analytics, and telemetry.
  6. After every caller and stored PageBuilder composition is safe, remove CDN loaders, manual IMA DAI types, Bitmovin globals, Bitmovin analytics hooks/strategies, the two Bitmovin packages, and finally the compatibility analytics aliases.

Verification matrix

Automated tests must use injected fakes and must never call production GAM, a production DAI asset key, or Nielsen with synthetic playback.

SurfaceRequired verification
Package seamSSR import/render, hydration, Strict Mode teardown, source switch, fatal recovery, mute-key fallback
Plain HLSready → play → playing → paused; captions; unmuted click-to-play
DAIrequest carries decoded targeting/identity/consent; break events lock controls; failure loads src
CSAIjoin pre-roll completes, skips, or errors before content; CYOA cannot switch streams during the break
DAI + CSAIpre-roll runs once, then the DAI stream starts; session-key changes do not reuse stale config
Arc page policygallery pauses on open and re-pauses on play/playing; hidden page keeps audio; off-screen non-sticky pauses
Stickynever docks before first view, respects top/bottom choice, disables in admin, dismisses without layout jump
Nielsenone session per player; content/ad metadata order; live absolute playhead; pause, consent, unload, and unmount cleanup
Chartbeatstrategy registration and removal; state/time/duration/bitrate; content/ad transitions; placement label
GA / GTMone event per intended transition; translated action names; stable ID/title/URL/context; no duplicate tag firing
Operationsstartup and fatal failures are queryable by station, stream, placement, and reload attempt
Browsersdesktop Chrome/Firefox/Edge/Safari and mobile Safari/Chrome, using non-production streams and sample ads

Run Arc's focused Jest tests, npm run typecheck, npm run lint, and a production build. Run the package's unit/type checks and the on-demand browser smoke suite described in Testing. Do not make live-ad smoke tests CI-gating.

Rollback and deletion gate

Keep the Bitmovin implementation recoverable until the canary has passed revenue/fill, Nielsen, Chartbeat, GA, and startup/fatal-error checks. A deployment rollback must not depend on a deleted CDN integration or a removed npm type package.

Delete the legacy runtime only when:

  • all Arc livestream surfaces use the compatibility adapter backed by LivePlayer
  • stored PageBuilder compositions still resolve their feature path
  • the analytics and operational gates pass for the agreed observation window
  • no browser loads Bitmovin Player, Bitmovin UI, or the old hard-coded license and analytics configuration
  • package and global-type searches contain no active Bitmovin imports; intentional migration docs and historical ADRs may remain

On this page