@scrippsproduct/shadowstream
v0.0.36
Published
<p align="center"> <img src="public/images/shadowstream-player-screenshot.png" alt="ShadowStream player screenshot" width="800" /> </p>
Downloads
426
Keywords
Readme
ShadowStream Player
ShadowStream is a React + TypeScript video player library and embed surface for HLS playback, ad delivery, playlist management, and external site control.
What this repository contains
- A standalone Vite app used for local development, testing, and deployment builds.
- A distributable library package published as
@scrippsproduct/shadowstream. - A global browser API exposed as
window.shadowstreamfor playlist control, player control, and event subscriptions.
Current versions and runtime expectations
These versions were pulled from the repository at the time of this README update.
| Item | Current version |
| --- | --- |
| App package (package.json) | 0.0.10 |
| Library package (package.lib.json) | 0.0.33 |
| Node runtime used in current setup | v20.17.0 |
| React / React DOM | 19.1.2 |
| Vite | 6.2.1 |
| TypeScript | 5.5.3 |
| Storybook | 9.1.5 |
| Vitest | 3.1.3 |
| ESLint | 9.9.0 |
| Playwright | 1.56.0 |
NVM / Node Version
This repo currently uses Node v20.17.0
Recommended setup:
nvm install 20.17.0
nvm use 20.17.0
npm installAny current NVM release that can install Node 20.17.0 is sufficient.
Getting started
npm installPrimary development commands
| Command | Purpose |
| --- | --- |
| npm run storybook | Main local development environment at http://localhost:6006 |
| npm run dev | Vite dev server |
| npm run build | Production build |
| npm run watch:full | TypeScript watch + Vite watch |
| npm run lint | Lint the repository |
| npm run lint:fix | Auto-fix lintable issues |
| npm run lint:report | Write eslint-report.json |
| npm run test | Run Vitest once |
| npm run test:watch | Run Vitest in watch mode |
| npm run test:coverage | Run Vitest with coverage |
| npm run build-storybook | Static Storybook build |
Production build and release notes
App build output
The production and stage builds output compiled assets into dist/.
Typical generated assets:
dist/shadowstream-v<version>.umd.js
Versioning
The repo currently uses package version bumps during CI with:
npm version patch --no-git-tag-versionLibrary packaging
To build the library package for distribution:
npm run build to test locally
but you can just push to stage to test qa
-or- push to main for production
GitLab workflow and deployment map
This repository currently contains GitLab CI in .gitlab-ci.yml.
Branch behavior
Push to stage
-> build_npm_stage
-> npm install
-> npm version patch --no-git-tag-version
-> npm run build-stage
-> upload dist/ to s3://$AWS_S3_BUCKET/shadowstream/
-> deploy happens automatically
Push to main
-> build_npm
-> npm install
-> npm version patch --no-git-tag-version
-> npm run build
-> deploy_prod job targets the same S3 path
-> deploy is manualGitLab environments
| Branch | Build job | Deploy job | Environment | Deploy behavior |
| --- | --- | --- | --- | --- |
| stage | build_npm_stage | deploy_stage | stage | Automatic |
| main | build_npm | deploy_prod | prod | Manual |
Deploy target
Both deploy jobs publish dist/ to:
s3://$AWS_S3_BUCKET/shadowstream/stage -> dev-cdn.scrippscloud.com bucket
-example-> https://dev-cdn.scrippscloud.com/shadowstream/shadowstream-v0.0.11.umd.js
main -> cdn.scrippscloud.com bucket
-example-> https://cdn.scrippscloud.com/shadowstream/shadowstream-v0.0.11.umd.js
Architecture overview
Main entry points
| File | Responsibility |
| --- | --- |
| src/App.tsx | Normalizes embed/incoming props, resolves privacy values, builds the initial playlist, registers early playlist controls, and swaps preview into the real player |
| src/components/preview/Preview.tsx | Main Poster image with big play button to "start" the app
| src/components/player/HLSPlayer.tsx | Core player orchestration: playback, HLS lifecycle, ads, playlist progression, external events, and player registration |
| src/hooks/usePlaylistManager.ts | Playlist state, next/previous navigation, viewed tracking, preload/load-more behavior, replay handling |
| src/hooks/useAdManager.ts | Ad request coordination for each video transition |
| src/hooks/useUserSync.ts | Resolves SpringServe user sync data used for ad targeting |
| src/lib/externalApi.ts | Builds and maintains the window.shadowstream API |
| src/contexts/UserActivityContext.tsx | User activity/inactivity tracking for controls and interaction state |
Component and state flow maps
1. Embed and initialization flow
Incoming embed props
-> src/App.tsx normalizes values
-> privacy strings are merged into ads config
-> initial video is converted into a PlaylistItem
-> incoming playlist is normalized and combined with the initial video
-> Preview renders first unless autoplay/live-autoplay activates the player
-> HLSPlayer mounts with normalized PlayerProps2. External API registration flow
App.tsx
-> creates an instanceId from verizonId or a generated shadowstream_* fallback
-> registers early playlist controls immediately
-> allows outside code to queue playlist actions before HLSPlayer is mounted
HLSPlayer.tsx
-> useExternalPlaylistControls registers the live playlist manager controls
-> registerPlayer(instanceId, playerControls) exposes the full player instance
-> shadowStreamAPI updates window.shadowstream3. Playlist and continuous-play flow
App.tsx
-> buildCombinedPlaylist(initial item, incoming playlist)
HLSPlayer.tsx
-> usePlaylistManager({ initialPlaylist, callLetters, continuousPlay, onEnd, ... })
-> currentItem/currentIndex become the active source of truth
-> next/previous/jump update currentIndex
-> preload checks run near the end of the available queue
-> additional videos can be fetched from station data when callLetters is available
-> handleVideoEnd decides whether to advance, replay, or fire the end callback4. Ad flow
HLSPlayer currentItem changes
-> useAdManager evaluates adsEnabled + player readiness
-> ad options are built from player opts + user sync data
-> bids.getBids(...) resolves bid payloads
-> showAds is enabled
-> AdPlayer / IMA playback runs
-> adStart and adEnd events are emitted through window.shadowstream
-> content playback resumes5. User sync and activity flow
useUserSync
-> src/lib/ssUserSync.ts fetches https://sync.springserve.com/usersync/json
-> returned ID is attached to ad request config
UserActivityContext
-> tracks activity/inactivity across pointer, keyboard, and touch events
-> drives control visibility and idle behavior inside the player UIPublic parameter map
There are two practical ways to think about the public API:
- Embed/incoming props handled by
src/App.tsx - Library player props consumed by
src/components/player/HLSPlayer.tsx
Incoming props handled by src/App.tsx
These are defined primarily in src/App.types.ts and normalized in src/App.tsx.
| Param | Accepted forms | Internal behavior |
| --- | --- | --- |
| m3u8 / streamUrl | string | Normalized into options.streamUrl and used as the primary HLS source |
| mp4 | string | Normalized into options.mp4Url as fallback playback/source metadata |
| video-title | string | Normalized into videoTitle, displayed in player state and carried into playlist items/events |
| verizonId | string | Used as the stable instance ID for window.shadowstream[instanceId]; if omitted a generated shadowstream_* ID is used |
| thumbnailUrl | string | Used by Preview before player activation and stored on playlist items |
| vertical | boolean | Passed through to the player and playlist items for vertical-video layout behavior |
| autoplay | boolean | Causes App to skip preview and activate HLSPlayer automatically |
| disable-continuous-play | 'true' | Inverted into continuousPlay; when disabled the playlist manager will not auto-advance continuously |
| disable-ads / disableAds | 'true' or boolean | Inverted into adsEnabled; controls whether useAdManager participates in the playback flow |
| is-live / isLive / live | string or boolean | Coerced into isLive; affects load timing, autoplay logic, and live handling in player state |
| live-autoplay / liveAutoplay | string or boolean | Allows live streams to activate immediately when isLive is true |
| muteOnLoad | boolean | Passed to player startup state to begin muted |
| width / height | number | Passed into player sizing and layout |
| duration | number or numeric string | Normalized to a non-negative number and stored on the current item/player metadata |
| callLetters | string | Used by playlist-loading logic to fetch additional videos for continuous play |
| playlist | PlaylistItem[] | Normalized item-by-item, merged with the initial video, then handed to usePlaylistManager |
| cbs.onVideoEnd | () => void | Wrapped by App as the end callback and fired when playback completes |
| fname | string | Copied into options.ads.fname for ad configuration |
| enableConvivaDebug | boolean | Passed through to HLSPlayer, but only enabled when environment debug is on |
| enableConvivaTouchstone | boolean | Passed through to HLSPlayer, but only enabled when environment debug is on |
| convivaCustomerKey | string | Passed to analytics/Conviva integration |
| section | string | Forwarded into PlayerProps; useful for downstream analytics/context |
| channel | string | Forwarded into PlayerProps; useful for downstream analytics/context |
Ad config fields
props.ads is merged with privacy strings resolved at runtime. These fields end up in options.ads and are used during bid creation and ad playback.
| Field | Internal behavior |
| --- | --- |
| iu | Ad unit identifier used in ad configuration |
| sz | Creative size string passed into ad config |
| fname | Ad metadata field; also backfilled from top-level fname when supplied |
| pxconfig | Passed through for ad/provider configuration |
| categories | Passed through for targeting/categories |
| refdomain | Passed through for referrer/targeting context |
| tfcd | Passed through for targeting/compliance |
| url | Passed through into ad request configuration |
| description_url | Passed through into ad request configuration |
| us_privacy | Auto-populated from privacy helpers unless already provided |
| gpp | Auto-populated from privacy helpers unless already provided |
| gpp_sid | Auto-populated from privacy helpers unless already provided |
| inv | Optional targeting field |
| adUnit | Optional ad unit metadata |
| preroll | Optional pre-roll behavior/config flag |
| cust_params | Custom targeting object forwarded to ad request config |
Library-facing player props
The library exports HLSPlayer from src/index.ts, with the public declaration living in src/index.d.ts.
| Prop | What it does internally |
| --- | --- |
| verizonId | Defines the external API instance key when provided |
| streamUrl | Primary content source for the current item/player load |
| mp4Url | Fallback content URL metadata |
| thumbnailUrl | Poster/preview metadata |
| vttUrl | Caption/subtitle metadata attached to the current item |
| vertical | Layout mode for portrait video |
| autoplay | Starts playback automatically when the player is initialized |
| continuousPlay | Enables continuous playlist progression in usePlaylistManager |
| adsEnabled | Turns the ad manager flow on or off |
| videoTitle | Used in UI metadata, playlist items, and emitted event payloads |
| muteOnLoad | Initial mute state |
| liveAutoplay | Live-stream startup behavior |
| start | Gate for initial player loading in HLSPlayer |
| end | Final end-of-playback callback |
| width / height | Player layout dimensions |
| duration | Metadata for the current item and external state |
| callLetters | Enables dynamic playlist loading for station-driven continuous play |
| ads | Ad request and targeting payload |
| isLive | Live-stream handling and initial load timing |
| playlist | Seed data for usePlaylistManager |
| onPlaylistUpdate | Callback slot for playlist changes |
| onItemChange | Callback slot for item transitions |
External API map
The player exposes a global API through window.shadowstream.
Instance lookup
const player = window.shadowstream['your-instance-id'];Global helpers
window.shadowstream.getAllPlayers();
window.shadowstream.getPlayer('your-instance-id');
window.shadowstream.pauseAll();
window.shadowstream.playAll();Playlist control methods
await player.playlist.jumpToVideoById('video-123');
await player.playlist.jumpToVideoByIndex(2);
await player.playlist.nextVideo();
await player.playlist.previousVideo();
await player.playlist.getCurrentPlaylist();
await player.playlist.getCurrentVideo();
await player.playlist.getCurrentIndex();Player control methods
player.play();
player.pause();
player.togglePlayPause();
player.seek(30);
player.setVolume(0.8);
player.toggleMute();
player.toggleFullscreen();
player.nextVideo();
player.skipVideo();Events emitted through the API
The current event types defined in src/types/externalApi.ts are:
playpauseadStartadEndvideoChangedplaylistUpdatedvideoEndedvideoStarted
For a longer API walkthrough, see EXTERNAL_API.md.
Key implementation notes before production
App.tsxcurrently acts as the compatibility/normalization layer for embed-style props.HLSPlayer.tsxis the real orchestration center and is the best place to trace runtime behavior.- Privacy strings are merged at runtime before ads initialize.
- User sync currently comes from SpringServe via
https://sync.springserve.com/usersync/json. - There is no checked-in
.nvmrc, so Node version discipline currently depends on documentation and local environment setup. - The repository currently uses GitLab CI/CD, not GitHub Actions, for build/deploy automation.
Suggested pre-production checklist
- Use Node
20.17.0via NVM. - Run
npm install. - Run
npm run lint. - Run
npm run test. - Run
npm run buildfor a production validation build. - Confirm the intended versions in
package.jsonandpackage.lib.json. - Confirm whether the release should go to
stagefirst or straight tomainfor the manual production deploy job.
