@osiris-smarttv/beacon
v1.2.1
Published
Remote observability SDK for Smart TV web apps — PS5, Tizen, WebOS
Maintainers
Readme
@osiris-smarttv/beacon
Remote observability SDK for Smart TV web apps — PS5 browser, Tizen, webOS, and desktop lab builds.
Ship structured logs, network traces, performance gauges, Shaka/SSAI playback telemetry, and dual-stack Player Filmstrip observability to your telemetry gateway (POST /ingest), then visualize in Grafana (Loki · Tempo · Prometheus).
What's new in 1.2.0 — Shaka v4 + v5 dual support: pass
shakaVersion: 'v4' | 'v5'(default'v4') so one SDK works with Shaka ≥4.14 and ≥5.0; telemetry unchanged. See Shaka version.
What's new in 1.1.0 — Player Filmstrip:
monitorShakanow fuses Shaka events +<video>events into a multi-lane per-playback_idsession timeline (startup waterfall, stall classification, adaptation divergence, session-analytics rollup) with zero app instrumentation. Enabled by default. See Player Filmstrip.
npm package: published as
@osiris-smarttv/beacon— ships compileddist/only (no TypeScript source).
Integration playbook for humans & AI: seeRULE.mdbundled in the package (node_modules/@osiris-smarttv/beacon/RULE.md).
Your app (browser / PS5 webview)
logger.* / capture() / plugins
│
▼
FetchTransport → POST {endpoint}/ingest
│
▼
Your telemetry gateway → Loki / Tempo / Prometheus → GrafanaInstall
npm install @osiris-smarttv/beaconInstall optional peers only for features you use:
# Shaka QoE + SSAI
npm install shaka-player
# Session replay
npm install rrweb
# Faro bridge (legacy)
npm install @grafana/faro-web-sdk| Peer | Import subpath | Purpose |
|------|----------------|---------|
| shaka-player | @osiris-smarttv/beacon/plugins/shaka | QoE, session milestones, SSAI |
| rrweb | @osiris-smarttv/beacon/plugins/rrweb | Session replay chunks |
| @grafana/faro-web-sdk | @osiris-smarttv/beacon/plugins/faro | Faro bridge |
Quick start
import { init, destroy, logger } from '@osiris-smarttv/beacon'
import {
monitorNetwork,
monitorConsole,
monitorPerformance,
} from '@osiris-smarttv/beacon'
const ENDPOINT = 'https://telemetry.example.com' // your gateway base URL
const API_KEY = 'your-api-key' // sent as X-App-Key header
init({
endpoint: ENDPOINT,
apiKey: API_KEY,
app: {
name: 'my-streaming-app', // → Loki app_id label on gateway
version: '1.2.0',
build: 'ci-4821',
env: 'uat',
},
plugins: [
monitorNetwork({
urlDenyList: [ENDPOINT, '/ingest', '/health'],
}),
monitorConsole(),
monitorPerformance({ reportIntervalMs: 10_000 }),
],
})
logger.info('app.startup', 'Application ready', { route: '/home' })
// SPA unmount / teardown
await logger.flush()
destroy()Environment variables (typical Vite / bundler setup)
| Variable | Purpose |
|----------|---------|
| VITE_BEACON_ENDPOINT | Gateway base URL |
| VITE_BEACON_API_KEY | API key registered on gateway |
| VITE_BEACON_APP_NAME | App id label |
| VITE_BEACON_DEVICE_ID | Stable device identifier |
| VITE_BEACON_APP_VERSION | App version label |
| VITE_BEACON_APP_BUILD | Build / CI id label |
Gateway auth: every ingest request sends X-App-Key: <apiKey>. Your gateway operator maps keys to app_id (e.g. APP_KEYS=app_id:api_key).
CORS: the gateway must respond to browser preflight with Access-Control-Allow-Origin: * (or your app origin). The SDK uses standard fetch — no credentials mode.
What's in the npm package
| Path | Description |
|------|-------------|
| dist/index.{js,cjs,d.ts} | Core SDK |
| dist/plugins/shaka.* | Shaka / SSAI plugin |
| dist/plugins/rrweb.* | rrweb replay plugin |
| dist/plugins/faro.* | Faro bridge |
| README.md | This file |
| RULE.md | Integration conventions + debug-case / Grafana playbook |
TypeScript types are included. Source maps are not published.
Three ways to emit telemetry
| API | Pipeline | Dedup / sample | Best for |
|-----|----------|----------------|----------|
| logger.* | Main | Yes | App logs, business events |
| logger.track() | Breadcrumbs only | — | Context on next flush |
| capture() | Monitor lane | No | Dense traces, plugin internals |
Logger
import { logger } from '@osiris-smarttv/beacon'
logger.setContext({ feature: 'checkout', contentId: 'movie-42' })
logger.debug('ui.click', 'Button pressed', { id: 'play' })
logger.info ('playback.user_action', 'User pressed play')
logger.warn ('network.slow', 'Manifest > 2s', { latencyMs: 2400 })
logger.error('player.error', 'Load failed', { code: 1001 })
const flow = logger.child({ component: 'ad-break', podIndex: 2 })
flow.info('ads.pod_start', 'Pod starting')
flow.end()
await logger.flush()Capture (inside custom plugins)
client.capture({
event: 'debug.my-case.step_3',
message: 'Before seek',
level: 'info',
monitor: { kind: 'media' },
context: { targetSec: 120, playhead: 45.2 },
})Built-in plugins
| Plugin | Import | Emits |
|--------|--------|-------|
| monitorNetwork | @osiris-smarttv/beacon | monitor.network + traces |
| monitorConsole | main | monitor.console |
| monitorStorage | main | monitor.storage |
| monitorPerformance | main | monitor.perf + device metrics |
| monitorShaka | @osiris-smarttv/beacon/plugins/shaka | media.*, media.ssai.*, media.filmstrip.* |
Configuration reference
See TypeScript types in BeaconConfig (dist/index.d.ts). Highlights:
endpoint,apiKey,app— requiredsampleRate(0–1),dedupWindowMs— setdedupWindowMs: 0when debuggingbatch— transport batching (batchSize,flushIntervalMs,monitorRealtime)beforeSend,piiScrubbing,blockedFields— privacy hooksplugins[]— monitor + custom plugins
Shaka Player plugin
import { monitorShaka, getSsaiTracker } from '@osiris-smarttv/beacon/plugins/shaka'
monitorShaka({
player,
videoElement: video,
content: {
id: 'asset-123',
playbackId: crypto.randomUUID(),
streamType: 'vod',
},
heartbeatMs: 10_000,
session: { enabled: true },
ads: { mode: 'custom' },
})
const ssai = getSsaiTracker(player)
ssai?.podStart({ podIndex: 0, availId: 'avail-1' })
ssai?.adStart({ adId: 'ad-1', creativeId: 'cr-1', durationMs: 15_000 })
ssai?.adComplete({ adId: 'ad-1' })
ssai?.podEnd({ podIndex: 0 })Requires shaka-player peer dependency (>=4.14.0 for v4, >=5.0.0 for v5).
Shaka version (v1.2.0)
Set shakaVersion explicitly to match the Shaka build your app ships — there is no auto-detect. Default is 'v4' (existing integrations unchanged).
monitorShaka({
shakaVersion: 'v5', // or 'v4' (default)
player,
videoElement: video,
})| Version | Min Shaka | Notes |
|---------|-----------|-------|
| 'v4' (default) | 4.14.0 | Uses setTextTrackVisibility, texttrackvisibility event |
| 'v5' | 5.0.0 | Uses selectAudioTrack / selectTextTrack; no setTextTrackVisibility |
configSchema validates the player API fingerprint at init — mismatches throw before listeners attach (e.g. declaring 'v5' on a v4 player).
Content context (v1.0.2+, optional)
Pass human-readable metadata on content — all fields optional except what you already use (id, playbackId). Old apps without these fields behave exactly as before; Grafana falls back to content_id.
import { monitorShaka, updateActiveProgram } from '@osiris-smarttv/beacon/plugins/shaka'
// VOD
monitorShaka({
player, videoElement,
content: {
id: 'bb-s01e03',
playbackId: crypto.randomUUID(),
streamType: 'vod',
title: 'Full Measure',
series: { title: 'Breaking Bad', season: 1, episode: 3 },
},
})
// Live + EPG refresh (same playbackId)
monitorShaka({
player, videoElement,
content: {
id: 'vtv1-hd',
playbackId: crypto.randomUUID(),
streamType: 'live',
channel: { id: 'vtv1-hd', name: 'VTV1' },
program: { title: 'Thời sự 19h' },
},
})
updateActiveProgram({ title: 'Phim tài liệu' }) // → media.content.updateGateway promotes low-cardinality labels: stream_type, channel_id, content_kind. Titles stay in log body (displayName, program name).
Player Filmstrip (v1.1.0)
Reconstruct the full playback story for one playback_id — playback, buffering,
quality, ABR, audio/subtitle, DRM, ads, config — with zero app-side instrumentation.
monitorShaka attaches two synchronized listener grids (Shaka events + HTMLMediaElement
events), fuses them, and reconciles state on each heartbeat. The app only passes
player + videoElement (+ optional content.playbackId).
Enabled by default — no config needed. Tune or disable via filmstrip:
monitorShaka({
player, videoElement,
content: { id: 'asset-123', playbackId: crypto.randomUUID() },
filmstrip: {
enabled: true, // default true — set false to opt out entirely
dedupeMs: 100, // cross-stack (Shaka↔video) dedupe window
reconcileMs: 2000, // heartbeat snapshot-diff quiet window
proxyFallback: false, // wrap player.configure/track setters for Shaka
// builds that don't emit configurationchanged
},
})What it emits (all additive — existing media.* events unchanged):
| Signal | Where | Notes |
|--------|-------|-------|
| Normalized lane changes | Loki | media.filmstrip.*, extended media.quality / media.track.* / media.config.changed, media.video.* — carry lane, source (shaka_event | html_media | fused | reconcile | proxy) |
| media.filmstrip.startup_waterfall | Loki | ms per milestone (loadstart → canplay → playing → shaka.loaded) |
| media.filmstrip.stall_classified | Loki | type: initial_buffer | rebuffer | seek_buffer | decoder_stall | network_stall |
| media.filmstrip.divergence | Loki | Shaka vs <video> mismatch (e.g. adaptation up but frame static) |
| media.session.analytics | Loki | end-of-session rollup: time-in-grade, rebuffer score, ABR efficiency, engagement, stall breakdown, completion, divergence count |
| Session-scoped gauges | Prometheus | beacon_media_playback_state, shaka_buffering, video_ready_state, video_network_state, abr_enabled, text_visible, drm_state, config_hash, playback_rate_x100, … labelled by playback_id |
High-frequency Shaka events (segmentappended, …) are metrics-only; whitelisted
config keys only (never license URLs / tokens).
Gateway requirement: the filmstrip lanes are scoped by playback_id, so the gateway
must stamp playback_id on media metric data points (already handled by the Osiris
gateway toOtlpMetrics).
Grafana: the Player Filmstrip board (/d/osiris-player-filmstrip, Admin / Official
only) renders these as synchronized state-timeline lanes + enterprise KPI row + causality log.
Manifest audits (v1.0.1+)
Opt-in HLS playlist audits run on every MANIFEST response 200 and emit
media.manifest_audit (warn on violation, info on clean playlists with
discontinuities, skipped on pure content playlists). Results are
deduplicated per audit kind by manifest hash — a live playlist refreshed
every ~4 s with identical content emits at most once.
monitorShaka({
player, videoElement,
manifest: {
// Built-in rules — see below.
audits: ['ssai-xmap-missing'],
// Optional app-specific rule invoked alongside built-ins.
onAudit: (uri, text) => { /* custom check */ },
},
})Built-in audits:
| Name | Rule | Fires when |
|------|------|------------|
| ssai-xmap-missing | Every #EXT-X-DISCONTINUITY must be followed by an #EXT-X-MAP within 6 non-empty lines | SSAI stitcher (MediaTailor / CDN splicer) forgets to restate the init segment after an ad-in/ad-out boundary — fMP4 playback stalls |
Event payload (under monitor.* — unwrap body first in LogQL):
{
"auditKind": "ssai-xmap-missing",
"uri": "https://cdn/live.m3u8",
"hash": "a1b2c3d4",
"bytes": 84210,
"mediaSequence": 1200,
"discontinuityCount": 28,
"extXMapCount": 27,
"violationCount": 1,
"ok": false,
"violations": [{ "discontinuityLine": 142, "windowLines": 6, "windowPreview": "…", "mediaSequenceHint": 1201 }]
}The pure audit function is also exported for standalone use (e.g. gateway
analyzer over media.manifest_raw):
import { auditHlsDiscontinuityMap } from '@osiris-smarttv/beacon/plugins/shaka'
const report = auditHlsDiscontinuityMap(playlistText)
if (!report.ok) console.warn('missing X-MAP', report.violations)Custom plugins
import { createPlugin } from '@osiris-smarttv/beacon'
export const traceMyFeature = createPlugin<{ thresholdMs: number }>({
name: 'my-app:feature-x',
version: '1.0.0',
setup(client, config) {
// … hooks using client.capture() or logger via setup context …
return () => { /* restore patches */ }
},
})Register in init({ plugins: [traceMyFeature({ thresholdMs: 500 })] }).
React / SPA lifecycle
import { init, destroy } from '@osiris-smarttv/beacon'
useEffect(() => {
init({ /* config */ })
return () => { void destroy() }
}, [])init() resets any prior instance — safe for HMR / Strict Mode.
Event naming
| Prefix | Use | Affects shared SLO dashboards? |
|--------|-----|--------------------------------|
| app.* | Stable product events | Only if dashboards query them |
| debug.<caseId>.* | Temporary investigation | No — safe sandbox |
| media.* | Shaka playback QoE | Yes |
| media.filmstrip.* | Filmstrip lane / stall / divergence / waterfall | Yes (Filmstrip board) |
| media.session.analytics | End-of-session enterprise rollup | Yes (Filmstrip board) |
| media.ssai.* | Ad lifecycle | Yes |
| monitor.* | Auto-instrumentation | Monitor dashboards |
For obscure bugs, use debug.<caseId>.* + dedicated device_id. Details in RULE.md.
Investigation workflow (summary)
- Namespace:
debug.<case-id>.<step>, deviceps5-debug-<case-id>,app.env = 'lab-debug' - Instrument with
logger.*orcapture() - Query Grafana Explore (LogQL body unwrap:
| json | line_format "{{.body}}" | json) - Import a Scratch dashboard JSON (template in
RULE.md§5) - Promote stable events to
app.*when done
Gateway contract
| Endpoint | Method | Auth |
|----------|--------|------|
| {endpoint}/ingest | POST JSON | X-App-Key |
| {endpoint}/health | GET | — |
| {endpoint}/replay-viewer.html | GET | — (if rrweb enabled) |
Request body:
{
"sdk": { "name": "@osiris-smarttv/beacon", "version": "1.1.0" },
"sent_at": "2026-07-02T12:00:00.000Z",
"events": [{ "level": "info", "event": "app.startup", "message": "…", "timestamp": "…", "session": { … }, "app": { … }, "context": { } }]
}Typical Loki labels (gateway-dependent): app_id, device_id, level, event, session_id, content_id, playback_id, stream_type, channel_id, content_kind, app_version, build, deploy_env.
Package exports
| Import | Symbols |
|--------|---------|
| @osiris-smarttv/beacon | init, destroy, logger, withScope, getInternals, monitors, ContentContext, setActiveContent, updateActiveProgram, types |
| @osiris-smarttv/beacon/plugins/shaka | monitorShaka, getSsaiTracker, setActiveContent, updateActiveProgram, mapFromMediaTailorAvails, auditHlsDiscontinuityMap, MANIFEST_AUDIT_EVENT, SSAI + audit types |
| @osiris-smarttv/beacon/plugins/rrweb | rrwebPlugin |
| @osiris-smarttv/beacon/plugins/faro | faroPlugin |
AI assistants
If you use Cursor or other coding agents, copy RULE.md into your project docs or point the agent at:
node_modules/@osiris-smarttv/beacon/RULE.mdIt defines debug namespaces, anti-patterns, and Grafana Scratch dashboard JSON templates.
License
MIT
