@streamiq/core
v1.1.0
Published
Headless streaming playback SDK — Player, FSM, events, and adapters.
Maintainers
Readme
@streamiq/core
Headless browser playback SDK — Player, playback FSM, typed event bus, native + progressive playback, plugins, headless subtitles, qualities, audio tracks, live helpers, source recovery, and a generic DRM type surface.
Stable
1.0.x. Pair with@streamiq/hlsor@streamiq/dashfor adaptive streaming,@streamiq/reactfor React bindings, and@streamiq/playerfor a drop-in cinematic preset.
Install
npm install @streamiq/core
# Adaptive streaming
npm install @streamiq/hls
npm install @streamiq/dashcore has exactly one runtime dependency (mitt) and zero @streamiq/* dependencies. It is enforced as a leaf package by the boundary checker.
Quick start
Progressive / MP4
import { Player } from '@streamiq/core';
const player = new Player({ target: videoElement });
await player.load('https://example.com/video.mp4');
await player.play();HLS
import { Player } from '@streamiq/core';
import { createHlsPlaybackAdapter } from '@streamiq/hls';
const player = new Player({
target: videoElement,
createPlaybackAdapter: createHlsPlaybackAdapter,
});
await player.load('https://example.com/master.m3u8');DASH
import { Player } from '@streamiq/core';
import { createDashPlaybackAdapter } from '@streamiq/dash';
const player = new Player({
target: videoElement,
createPlaybackAdapter: createDashPlaybackAdapter,
});
await player.load('https://example.com/manifest.mpd');Source with CDN fallbacks
await player.load({
src: 'https://cdn-a.example.com/stream.m3u8',
fallbacks: [{ src: 'https://cdn-b.example.com/stream.m3u8' }],
});Architecture
core is internally module-sliced. Every domain has a folder, types live next to the runtime, and the public surface is curated through src/index.ts.
@streamiq/core/src
├── player/ Lifecycle owner
│ ├── Player.ts Public class
│ ├── PlayerInstance.ts Per-mount state container
│ ├── PlayerLifecycle.ts load/destroy orchestration
│ ├── PlayerConfig.ts Resolves PlayerOptions → ResolvedPlayerConfig
│ ├── playerDestroy.ts Tear-down sequence
│ ├── defaultRecoveryPolicy.ts
│ └── loadRecovery/ Abortable retry pipeline
│ ├── runLoadRecovery.ts
│ ├── recoveryDecision.ts
│ ├── normalizeLoadError.ts
│ ├── createAttemptAbortSignal.ts
│ └── teardownAdapterForRecovery.ts
│
├── state/ Single source of truth for playback state
│ ├── StateMachine.ts
│ ├── transitions.ts
│ ├── PlayerState.ts
│ └── state.types.ts
│
├── events/ Typed event bus over mitt
│ ├── EventBus.ts
│ ├── createEmitter.ts
│ ├── PlayerEvents.ts
│ ├── events.types.ts
│ └── playbackStateChange.types.ts
│
├── source/ Source resolution + validation
│ ├── Source.ts
│ ├── source.types.ts
│ ├── detectSourceType.ts
│ ├── validateSource.ts
│ ├── flattenSourceChain.ts
│ └── SourceResolver.ts
│
├── adapters/ Playback engine abstraction
│ ├── PlaybackAdapter.ts Public interface (AdapterContext, options)
│ ├── BaseAdapter.ts Shared base
│ ├── AdapterFactory.ts createPlaybackAdapter
│ ├── NativeAdapter.ts Built-in <video src> adapter
│ ├── bindMediaElementListeners.ts
│ ├── formatMediaElementErrorMessage.ts
│ └── seekMediaElement.ts
│
├── playback/ Domain types + element helpers
│ ├── qualities.types.ts QualitiesSnapshot, EMPTY_QUALITIES, STREAMQ_AUTO_QUALITY_ID
│ ├── audio-tracks.types.ts
│ ├── live.types.ts
│ ├── liveMediaElement.ts Native live-edge math
│ ├── pictureInPicture.ts readPiPCapability + enter/exit + WEBKIT_MODE_EVENT
│ ├── nativeAudioTracks.ts
│ └── buildProgressiveQualities.ts
│
├── tracks/text/ Headless subtitles MVP
│ ├── TextTrackManager.ts Single owner of registered tracks
│ ├── CueSynchronizer.ts Active-cue clock owned by manager
│ ├── WebVTTParser.ts
│ ├── parseTimestamp.ts
│ ├── mergeTextCues.ts
│ ├── pruneTextCues.ts
│ ├── disableNativeTextTracks.ts (+ guardNativeTextTracks)
│ ├── SubtitleStyle.ts DOM-overlay styling tokens
│ ├── TextCue.ts / TextTrack.ts
│ ├── textTrackSourcePolicy.ts (precedence)
│ ├── textTrackRegistry.types.ts
│ └── textTrackRegistry.helpers.ts
│
├── drm/ Generic DRM (format-agnostic)
│ ├── drmTypes.ts DrmConfig, key system filters
│ ├── drmMediaRegistry.ts Per-element registry
│ └── keySystems.ts WIDEVINE_KEY_SYSTEM, …
│
├── plugins/ Extension surface
│ ├── Plugin.ts
│ ├── PluginManager.ts Install / hooks / error isolation
│ └── plugins.types.ts
│
├── errors/ Typed error hierarchy
│ ├── StreamqError.ts (base)
│ ├── PlaybackError.ts
│ ├── NetworkError.ts
│ ├── DRMError.ts
│ ├── SubtitleError.ts
│ └── errors.types.ts Code enums
│
├── performance/
│ └── PerformanceMonitor.ts Runtime telemetry hooks
│
├── internal/ Maintainer surface (separate ./internal entry)
├── utils/ assert, invariant, clamp, delayWithAbortSignal, noop
└── types/ config / player / recovery / sharedPrinciples
| Principle | Detail |
|-----------|--------|
| Headless | No React, no DOM beyond <video>/<audio> |
| Event-driven | Typed mitt bus — modules communicate via PlayerEvents |
| Adapter pattern | HLS/DASH live in separate packages; core ships NativeAdapter only |
| Single FSM | One playback state machine; no duplicated boolean flags |
| Plugins | Optional setup(player) / destroy() extensions, error-isolated |
| Source precedence for subtitles | adapter > sidecar > renderer |
Lifecycle flow
new Player(options)
└─ PlayerConfig.resolve(options)
└─ PluginManager.install(plugins)
└─ EventBus → mitt
player.load(input)
└─ SourceResolver.resolveSource(input) (validate + detect)
└─ AdapterFactory.createPlaybackAdapter(ctx) (default: NativeAdapter)
└─ runLoadRecovery({ retryPolicy, signal }) (abortable attempts)
└─ adapter.load(resolvedSource)
└─ adapter binds media listeners → PlayerEvents
└─ StateMachine.transition('loading' → 'ready' → 'playing' …)
player.destroy()
└─ AbortController.abort() on every in-flight signal
└─ teardownAdapterForRecovery()
└─ PluginManager.destroyAll()
└─ EventBus.allOff()
└─ release media elementPlayer API
Lifecycle
| Method | Description |
|--------|-------------|
| load(source) | Load URL string, Source, or SourceInput with fallbacks |
| play() | Start playback (Promise<void>) |
| pause() | Pause playback |
| seek(seconds) | Seek to time in seconds |
| destroy() | Tear down adapter, listeners, plugins, FSM |
State
| Property / method | Description |
|-------------------|-------------|
| state | Current PlaybackState |
| on(event, handler) | Subscribe to typed events |
| off(event, handler) | Unsubscribe |
Playback states
idle → loading → ready → playing | paused | buffering → ended | error | destroyed
Features
Sources & recovery
| Feature | Description |
|---------|-------------|
| Format detection | .m3u8 → HLS, .mpd → DASH, .mp4 / progressive → native |
| Typed sources | Source, SourceInput, SourceKind, SourceFormat, ResolvedSource |
| CDN fallbacks | Ordered fallback URLs on load failure |
| Load recovery | Abortable retry / failover via defaultShouldRetryLoad |
| Validation | validateSourceInput, resolveSource, flattenSourceChain, toPublicSource |
Events (PlayerEvents)
| Event | When |
|-------|------|
| playback:state-change | FSM transition (from → to) |
| timeupdate | Current playback time |
| durationchange | Media duration known/changed |
| buffering | Buffering start/stop |
| volumechange | Volume or mute changed |
| qualitieschange | ABR levels updated |
| audiotrackschange | Audio renditions updated |
| texttrackdiscovery | Text tracks discovered |
| texttrackcuesupdate | Cues hydrated for a track |
| texttrackcuesprune | Stale cues removed |
| cuechange | Active subtitle cue changed |
| sourcechange | New source loaded |
| error | Playback / network / DRM / subtitle error |
| ended | Playback reached end |
| ready | Media ready to play |
Qualities (ABR)
| API | Description |
|-----|-------------|
| getQualities() | QualitiesSnapshot with levels + active id |
| selectQuality(id) | Manual rendition or STREAMQ_AUTO_QUALITY_ID |
| EMPTY_QUALITIES | Identity snapshot |
Capabilities vary by adapter (Safari native HLS exposes one observed level — adapters may set capabilities.manualSelection: false).
Audio tracks
| API | Description |
|-----|-------------|
| getAudioTracks() | Available audio renditions |
| selectAudioTrack(id) | Switch language / commentary track |
Headless subtitles
| API / module | Description |
|--------------|-------------|
| TextTrackManager | Single owner — registry + cue stream |
| CueSynchronizer | Active-cue clock (singleton, owned by manager) |
| parseWebVTT, parseTimestamp | Parsers |
| mergeTextCues, findActiveCue, isSameCueIdentity | Cue utilities |
| disableNativeTextTracks (+ guardNativeTextTracks) | Prevent UA captions racing the SDK |
| SubtitleStyle (+ EMPTY_SUBTITLE_STYLE, mergeSubtitleStyle) | DOM overlay styling tokens |
| TEXT_TRACK_SOURCE_* constants | Source precedence (adapter > sidecar > renderer) |
Subtitle rendering is owned by the consumer — use @streamiq/react + @streamiq/ui (SubtitleLayer) or your own renderer.
Live streaming
| API | Description |
|-----|-------------|
| isLive() | Presentation is live |
| getLiveLatency() | Seconds behind live edge |
| getLiveSeekableRange() | DVR window { start, end } |
| seekToLive() | Jump to live edge |
| Native helpers | isNativePresentationLive, getNativeLiveLatencySeconds, seekNativeMediaToLiveEdge |
Picture-in-Picture
| API | Description |
|-----|-------------|
| readPiPCapability() | Browser PiP support |
| enterPictureInPicture() / exitPictureInPicture() | PiP control |
| isPictureInPictureActive() | Current PiP state |
| WEBKIT_MODE_EVENT | Safari WebKit legacy PiP event name |
DRM (generic surface)
| Export | Purpose |
|--------|---------|
| DrmConfig, DrmKeySystemConfig | Format-agnostic configuration |
| DrmRequestFilter, DrmResponseFilter | License flow hooks |
| setDrmConfigForMedia / getDrmConfigForMedia | Per-element registry |
| setDrmCertPending / awaitDrmCertPending / clearDrmCertPending | Coordinate cert preflight |
| WIDEVINE_KEY_SYSTEM, PLAYREADY_KEY_SYSTEM, FAIRPLAY_KEY_SYSTEM | Key system constants |
Adapters and @streamiq/plugin-drm bridge into this registry instead of inventing parallel state.
Errors
| Class | Codes |
|-------|-------|
| StreamqError | Base typed error |
| PlaybackError | PlaybackErrorCode |
| NetworkError | NetworkErrorCode |
| DRMError | DRMErrorCode |
| SubtitleError | SubtitleErrorCode |
Plugins
import type { StreamqPlugin } from '@streamiq/core';
class MyPlugin implements StreamqPlugin {
setup(player) { /* subscribe to PlayerEvents */ }
destroy() { /* cleanup */ }
}
new Player({ target, plugins: [new MyPlugin()] });PluginManager isolates errors via PluginErrorHandler so a misbehaving plugin never breaks playback.
Adapters (extensibility)
| Export | Role |
|--------|------|
| PlaybackAdapter | Interface for format engines |
| NativeAdapter | Progressive / <video src> |
| createPlaybackAdapter | Default factory (native only) |
| BaseAdapter | Shared adapter base |
| AdapterContext, AdapterLoadOptions, NativeAdapterDelegate | Injection context |
HLS/DASH adapters in their own packages implement PlaybackAdapter and normalize engine events → PlayerEvents.
Package exports
| Entry | Contents |
|-------|----------|
| @streamiq/core | Full public API (see src/index.ts) |
| @streamiq/core/internal | Maintainer metrics — not semver-stable |
Bundle size
| Budget (.size-limit.json) | Limit |
|-----------------------------|-------|
| Full bundle | 5 kB gzipped |
| Tree-shake single export | 450 B |
Engines (hls.js, shaka-player) are never bundled in core.
Dependencies
| | |
|-|-|
| Runtime | mitt |
| Peer | none |
| Workspace | none (core must not depend on other @streamiq/*) |
Related packages
| Package | Role |
|---------|------|
| @streamiq/react | React hooks, stores, StreamqProvider, headless <Video>/<Audio> |
| @streamiq/ui | Render-only React media primitives |
| @streamiq/player | Cinematic preset + UX orchestration |
| @streamiq/hls | HLS adapter (lazy hls.js) |
| @streamiq/dash | DASH adapter (lazy shaka-player) |
| @streamiq/shared | Low-latency utilities (adapter-internal) |
License
MIT
