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-PageBuilderorigin/devat7af01b4be306e5d5c176d1cf9c1aef49f41361f5livestream-playerat408dfaa88c33b1fcd0f95c42fa16bc3d8ddc8e3a
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.tsxcomponents/features/article/story/_children/promo-object.tsxcomponents/features/article/components/ArticleBody/components/custom-embed/default.jsxcomponents/features/global/components/StoryCardV2.tsx, supplied bycomponents/features/global/story-list-v2/default.tsx
The feature currently does much more than instantiate Bitmovin:
| Responsibility | Current Arc implementation | Migration consequence |
|---|---|---|
| Playback and controls | Bitmovin Player 8 and Player UI 3 loaded from CDNs | Replace with package-owned Shaka and UI; delete both CDN loaders |
| DAI | IMA DAI StreamManager, ID3 metadata forwarding, asset-key request | Pass ads={{ assetKey }}; the package owns IMA DAI and fail-open loading |
| Pre-roll | Bitmovin advertising plus a VAST URL from useBuildAdTag | Pass preroll={{ tagUrl }}; the package owns IMA HTML5 |
| Targeting and identity | cust_params, NewsPass ID, and BlueConic ID are assembled in different places | Produce one explicit AdTargeting value and validate the outgoing DAI and VAST requests |
| Nielsen | use-nielsen-bitmovin consumes raw Bitmovin and IMA events and polls the Bitmovin API | Rewrite against normalized events plus a stable package-owned playback snapshot |
| Chartbeat | use-chartbeat-bitmovin and BitmovinChartbeatStrategy consume raw events and poll duration, time, state, and bitrate | Replace the strategy with a provider-neutral adapter; do not reach through getEngine() |
| GA / GTM | Every 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 coordination | Intersection Observer, sticky dismissal, off-screen pause, and vertical-gallery pause guards | Keep Arc-owned behavior until the package supports the same policy |
| Presentation | Poster, custom/fill layout, article link overlay, watermark, and placement-specific controls | Decide each parity difference before rollout; several are not direct prop mappings |
| QoS / operations | Bitmovin Analytics plus console errors | Wire onTelemetry to an approved queryable sink before canarying |
There are also two cleanup surfaces after cutover:
@grahamdigital/bitmovin-playeruiand@grahamdigital/bitmovin-typesinpackage.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 streamFor 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 behavior | Current package behavior | Recommended migration decision |
|---|---|---|
poster | No poster prop | Add 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 unconditional | Confirm the field can be retired, or add a package prop if it remains supported |
| Creative-defined pre-roll skip | The host must supply preroll.skipOffset before the package shows its skip control | Derive skip eligibility from IMA or define a real Arc configuration; do not guess one fixed offset |
| Sticky only after first view | Sticky docks whenever its observer reports out of view | Keep Arc's viewed-first wrapper policy or extend the package |
stickyPosition="top" | "bottom" | Fixed bottom-right mini-player | Keep Arc-owned positioning or approve one global position |
| Non-sticky player pauses off screen | Package continues playback | Retain the Arc Intersection Observer pause policy to avoid hidden playback and bandwidth |
| Sticky disabled in PageBuilder admin | Package has no knowledge of isAdmin | Pass sticky={false} in admin from the Arc adapter |
Full-area article link with gaTag | Package link overlay is a small, new-tab label | Keep the link and click analytics in the Arc wrapper unless product approves the new behavior |
| Clickable Bitmovin watermark | Package watermark is a non-clickable image | Keep host-owned click behavior or add a documented package capability |
| Story-list-specific title/control UI | Package has one control bar | Approve the unified UI or add an explicit package variant |
layout="fill" with cover video | Root can fill a sized parent; video does not choose object-fit: cover | Size 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 input | LivePlayer mapping |
|---|---|
hls | src and DAI fail-open fallback |
daiAssetKey | ads={{ assetKey: daiAssetKey }} when present |
resolved adVastTag | preroll={{ tagUrl: adVastTag }} when pre-roll is enabled |
| structured targeting / consent / PPID | adConfig |
autoPlay | autoPlay |
muted | Initial 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, isLive | Host analytics metadata; poster also needs the parity decision above |
setAdPlaying | true on adbreakstart; false on adbreakend and aderror cleanup |
canonicalWebsite | Input to Arc's ad-tag builder only |
url, enableLinkOverlay, gaTag | Arc-owned article overlay and click analytics |
className, sx, layout, placeholder | Arc wrapper styling; only pass plain CSSProperties through style |
stickyEnabled, stickyPosition, initialInView | Arc-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:
- Re-assert the vertical-gallery pause on
playandplaying. - Drive
setAdPlayingfrom ad-break boundaries. - Forward the event and stable playback snapshot to Nielsen.
- Forward them to Chartbeat.
- 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 event | Legacy data.type |
|---|---|
ready, play, playing, paused, error, adquartile, aderror | unchanged |
adbreakstart | adbreakstarted |
adbreakend | adbreakfinished |
adstart | adstarted |
adskip | adskipped |
adfinish | adfinished |
texttrackschange, cuechange | no 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.
Recommended implementation sequence
- Add and test the missing package capabilities: analytics snapshot/stall semantics, required poster/control behavior, and legacy mute preference fallback. Publish an approved version.
- In Arc, add provider-neutral livestream types, analytics adapters, structured ad targeting, telemetry, and their focused unit tests without changing the active Bitmovin feature.
- Rewrite the internals of the existing
livestream/bitmovinfeature as the compatibility adapter. Keep its path, custom fields, lazy flag, and callers stable. - Replace tests that mock Bitmovin, IMA globals, and CDN hooks with
FakeEngine/FakeAdsfrom@grahamdigital/livestream-player/testing. Retain focused host tests for analytics fan-out, gallery re-pause, off-screen behavior, sticky policy, and CYOA ad locking. - 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.
- 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.
| Surface | Required verification |
|---|---|
| Package seam | SSR import/render, hydration, Strict Mode teardown, source switch, fatal recovery, mute-key fallback |
| Plain HLS | ready → play → playing → paused; captions; unmuted click-to-play |
| DAI | request carries decoded targeting/identity/consent; break events lock controls; failure loads src |
| CSAI | join pre-roll completes, skips, or errors before content; CYOA cannot switch streams during the break |
| DAI + CSAI | pre-roll runs once, then the DAI stream starts; session-key changes do not reuse stale config |
| Arc page policy | gallery pauses on open and re-pauses on play/playing; hidden page keeps audio; off-screen non-sticky pauses |
| Sticky | never docks before first view, respects top/bottom choice, disables in admin, dismisses without layout jump |
| Nielsen | one session per player; content/ad metadata order; live absolute playhead; pause, consent, unload, and unmount cleanup |
| Chartbeat | strategy registration and removal; state/time/duration/bitrate; content/ad transitions; placement label |
| GA / GTM | one event per intended transition; translated action names; stable ID/title/URL/context; no duplicate tag firing |
| Operations | startup and fatal failures are queryable by station, stream, placement, and reload attempt |
| Browsers | desktop 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