eyevinn-channel-engine 5.1.2 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.github/workflows/nodejs.yml +1 -1
  2. package/.github/workflows/publish.yml +1 -1
  3. package/.github/workflows/rc.yml +1 -1
  4. package/CONTRIBUTING.md +40 -0
  5. package/Dockerfile +1 -1
  6. package/README.md +70 -8
  7. package/dist/engine/chaos_monkey.d.ts +1 -1
  8. package/dist/engine/chaos_monkey.js +1 -0
  9. package/dist/engine/event_stream.d.ts +1 -6
  10. package/dist/engine/event_stream.js +2 -0
  11. package/dist/engine/memcached_state_store.d.ts +1 -13
  12. package/dist/engine/memcached_state_store.js +4 -0
  13. package/dist/engine/memory_state_store.d.ts +1 -13
  14. package/dist/engine/memory_state_store.js +5 -0
  15. package/dist/engine/playhead_state.d.ts +0 -26
  16. package/dist/engine/playhead_state.js +9 -0
  17. package/dist/engine/preflight.d.ts +1 -1
  18. package/dist/engine/preflight.js +1 -0
  19. package/dist/engine/redis_state_store.d.ts +1 -19
  20. package/dist/engine/redis_state_store.js +5 -0
  21. package/dist/engine/server.d.ts +27 -0
  22. package/dist/engine/server.js +140 -6
  23. package/dist/engine/session.d.ts +1 -148
  24. package/dist/engine/session.js +398 -11
  25. package/dist/engine/session_live.d.ts +1 -181
  26. package/dist/engine/session_live.js +19 -4
  27. package/dist/engine/session_live_state.d.ts +0 -23
  28. package/dist/engine/session_live_state.js +5 -0
  29. package/dist/engine/session_state.d.ts +0 -45
  30. package/dist/engine/session_state.js +10 -0
  31. package/dist/engine/shared_state_store.d.ts +1 -25
  32. package/dist/engine/shared_state_store.js +10 -0
  33. package/dist/engine/state_store_types.d.ts +32 -0
  34. package/dist/engine/state_store_types.js +5 -0
  35. package/dist/engine/stream_switcher.d.ts +1 -40
  36. package/dist/engine/stream_switcher.js +108 -20
  37. package/dist/engine/subtitle_slicer.d.ts +1 -8
  38. package/dist/engine/subtitle_slicer.js +1 -0
  39. package/dist/engine/util.d.ts +1 -29
  40. package/dist/engine/util.js +2 -1
  41. package/dist/examples/adbreak.d.ts +1 -0
  42. package/dist/examples/adbreak.js +104 -0
  43. package/dist/index.d.ts +1 -1
  44. package/dist/package.json +20 -4
  45. package/docs/design/mixing-muxed-demuxed-audio.md +153 -0
  46. package/docs/design/subtitles-live-mix-demux.md +135 -0
  47. package/docs/reference.md +67 -0
  48. package/engine/chaos_monkey.ts +17 -0
  49. package/engine/{event_stream.js → event_stream.ts} +5 -3
  50. package/engine/{memcached_state_store.js → memcached_state_store.ts} +19 -12
  51. package/engine/memory_state_store.ts +71 -0
  52. package/engine/{playhead_state.js → playhead_state.ts} +92 -82
  53. package/engine/{preflight.js → preflight.ts} +1 -1
  54. package/engine/{redis_state_store.js → redis_state_store.ts} +216 -208
  55. package/engine/server.ts +210 -8
  56. package/engine/{session.js → session.ts} +421 -22
  57. package/engine/{session_live.js → session_live.ts} +54 -31
  58. package/engine/{session_live_state.js → session_live_state.ts} +129 -120
  59. package/engine/{session_state.js → session_state.ts} +269 -248
  60. package/engine/{shared_state_store.js → shared_state_store.ts} +31 -19
  61. package/engine/state_store_types.ts +35 -0
  62. package/engine/{stream_switcher.js → stream_switcher.ts} +133 -34
  63. package/engine/{subtitle_slicer.js → subtitle_slicer.ts} +10 -8
  64. package/engine/{util.js → util.ts} +28 -24
  65. package/examples/adbreak.ts +115 -0
  66. package/index.ts +2 -0
  67. package/package.json +20 -4
  68. package/spec/engine/custom_vod_request_params_spec.ts +99 -0
  69. package/spec/engine/drm_spec.ts +128 -0
  70. package/spec/engine/{engine_spec.js → engine_spec.ts} +44 -1
  71. package/spec/engine/{ha_spec.js → ha_spec.ts} +7 -4
  72. package/spec/engine/{init_switching_spec.js → init_switching_spec.ts} +10 -6
  73. package/spec/engine/{interface_spec.js → interface_spec.ts} +16 -4
  74. package/spec/engine/{playhead_live_spec.js → playhead_live_spec.ts} +2 -1
  75. package/spec/engine/{playhead_spec.js → playhead_spec.ts} +15 -8
  76. package/spec/engine/session_spec.ts +800 -0
  77. package/spec/engine/stream_switcher_audio_reconcile_spec.ts +158 -0
  78. package/spec/engine/{stream_switcher_spec.js → stream_switcher_spec.ts} +111 -13
  79. package/spec/engine/subtitle_missing_language_spec.ts +196 -0
  80. package/spec/engine/{subtitle_spec.js → subtitle_spec.ts} +28 -27
  81. package/spec/helpers/ts-resolve.js +65 -0
  82. package/spec/support/jasmine.json +1 -1
  83. package/spec/testvectors/hls_subs_no_language/config.subtitleTracks.json +12 -0
  84. package/spec/testvectors/hls_subs_no_language/evidence_dummy_segments_name_mismatch.m3u8 +24 -0
  85. package/spec/testvectors/hls_subs_no_language/evidence_real_cues_name_match.m3u8 +24 -0
  86. package/spec/testvectors/hls_subs_no_language/french.m3u8 +9 -0
  87. package/spec/testvectors/hls_subs_no_language/master.m3u8 +11 -0
  88. package/spec/testvectors/hls_subs_no_language/video.m3u8 +25 -0
  89. package/spec/testvectors/slate/master.m3u8 +3 -0
  90. package/spec/testvectors/slate/media.m3u8 +9 -0
  91. package/tsconfig.json +21 -4
  92. package/tsconfig.spec.json +16 -0
  93. package/engine/chaos_monkey.js +0 -17
  94. package/engine/memory_state_store.js +0 -63
  95. package/spec/engine/session_spec.js +0 -116
  96. /package/spec/engine/{session_live_spec.js → session_live_spec.ts} +0 -0
  97. /package/spec/engine/{util_spec.js → util_spec.ts} +0 -0
@@ -0,0 +1,153 @@
1
+ # Design note: mixing muxed- and demuxed-audio sources in one channel
2
+
3
+ Scoping note for issue #381 (parent #271; implementation sub-issues #382/#383).
4
+ This document defines the target behaviour and acceptance criteria before any
5
+ implementation is started. It contains no code.
6
+
7
+ ## Problem
8
+
9
+ A channel is fed a sequence of VOD (and optionally live) sources by the asset
10
+ manager. Today those sources are assumed to be homogeneous with respect to
11
+ audio packaging: either every source is *muxed* (audio interleaved in the same
12
+ rendition as video) or every source is *demuxed* (audio delivered as a separate
13
+ `EXT-X-MEDIA` audio group). The channel-level `useDemuxedAudio` flag
14
+ (`engine/server.ts`) picks one mode for the whole channel and every source is
15
+ expected to comply with it.
16
+
17
+ When a channel's schedule mixes the two — e.g. a muxed VOD followed by a
18
+ demuxed VOD on a `useDemuxedAudio: true` channel — the mismatched source fails
19
+ to load rather than being reconciled. This is grounded in the current code (see
20
+ "Affected code paths" below): `engine/session.ts` passes
21
+ `forcedDemuxMode: this.use_demuxed_audio` into the underlying `HLSVod`
22
+ (`@eyevinn/hls-vodtolive`), and that library **rejects with
23
+ `Error("The vod is not a demux vod")`** when a source presented under forced
24
+ demux mode carries no audio group. So a muxed source on a demuxed channel is a
25
+ hard load failure today, not a soft mismatch.
26
+
27
+ ## (1) Target behaviour
28
+
29
+ For a single channel whose sources mix muxed-audio and demuxed-audio inputs:
30
+
31
+ - The sources must play **back-to-back across the VOD boundary without breaking
32
+ the manifest** — the output master playlist and the output media/audio
33
+ playlists must remain valid and internally consistent across the transition,
34
+ and a compliant player must continue playback across the boundary without a
35
+ fatal error or a manifest reload failure.
36
+ - A brief, bounded audio artefact at the discontinuity (one segment of silence
37
+ or a single hiccup, on the order of the average segment duration) is
38
+ acceptable for a first pass; a persistent audio/video desync or a manifest
39
+ that a player rejects is **not** acceptable.
40
+ - "Mix" here covers the VOD→VOD case as the primary target. Live-mix
41
+ (VOD↔live) transitions with mixed packaging are in scope for the acceptance
42
+ criteria only insofar as they must not regress the existing live-mix +
43
+ demux behaviour; full live-mix mixed-packaging support may be deferred to a
44
+ follow-up if it proves materially harder.
45
+
46
+ ## (2) Output manifest mode — recommendation
47
+
48
+ **Normalise to a single, consistent output presentation for the whole channel.**
49
+ Do not preserve per-source packaging in the output. A channel that flips the
50
+ output between muxed and demuxed at each VOD boundary would force the player to
51
+ re-init its audio pipeline mid-stream and is the most likely thing to break
52
+ playback.
53
+
54
+ Concretely: the channel's output mode is decided once, by the existing
55
+ `useDemuxedAudio` flag.
56
+
57
+ - `useDemuxedAudio: true` → output is **always demuxed**: an `EXT-X-MEDIA`
58
+ audio group is present and every served segment (from every source,
59
+ regardless of that source's own packaging) participates in that single
60
+ consistent audio-group presentation.
61
+ - `useDemuxedAudio: false` → output is **always muxed**: no separate audio
62
+ group; audio is carried in the video rendition for every source.
63
+
64
+ The flag stays the single source of truth for output presentation; the new work
65
+ is making non-conforming sources fit the chosen output mode rather than failing.
66
+
67
+ ## (3) Cross-packaging handling (muxed source, demuxed output — and vice-versa)
68
+
69
+ **On-the-fly remultiplexing / transcoding is explicitly out of scope.** The
70
+ engine is a manifest-manipulation / VOD2Live layer; it does not open, decode,
71
+ demux, or re-encode media segments, and it should not start doing so here.
72
+
73
+ Given that constraint, the two cross-packaging directions resolve as:
74
+
75
+ - **Muxed source, demuxed output (`useDemuxedAudio: true`)**: require the muxed
76
+ source to **already carry a separate audio rendition** (an `EXT-X-MEDIA`
77
+ audio group) that the engine can route into the output audio group. A source
78
+ that is *only* muxed (video-with-embedded-audio and no audio rendition)
79
+ cannot be losslessly presented as demuxed without extracting the audio track,
80
+ which is remux — out of scope. Such a source must be handled deterministically
81
+ (see acceptance criteria: reject-and-skip via the existing error/unassign
82
+ path, not a crash), not silently mis-served.
83
+ - **Demuxed source, muxed output (`useDemuxedAudio: false`)**: the engine
84
+ selects a single audio rendition from the source's audio group and presents
85
+ the source as muxed in the output. Where the source's video rendition does
86
+ not already contain that audio, this again requires the player to be served
87
+ audio it can associate with the video without a remux; if that cannot be done
88
+ losslessly it falls under the same deterministic reject-and-skip rule.
89
+
90
+ In short: **the engine reconciles packaging by routing existing renditions, not
91
+ by producing new media.** A source that would require new media to fit the
92
+ output mode is rejected cleanly rather than breaking the channel.
93
+
94
+ ## (4) Affected code paths
95
+
96
+ Verified against the current tree (the engine files are TypeScript, `*.ts`, not
97
+ `*.js` as the parent issue's shorthand suggested):
98
+
99
+ - **`engine/server.ts`** — declares and stores the `useDemuxedAudio` option
100
+ (`ChannelEngineOpts.useDemuxedAudio`, the private `useDemuxedAudio` field,
101
+ and its default-to-`false` handling), and forwards it into the session /
102
+ session-live / stream-switcher configs it constructs. This is where the
103
+ "output mode is a single channel-level decision" contract lives.
104
+ - **`engine/session.ts`** — reads the flag into `use_demuxed_audio` and, at VOD
105
+ creation, passes `forcedDemuxMode: this.use_demuxed_audio` into `new
106
+ HLSVod(...)`. This is the exact point where a muxed source on a demuxed
107
+ channel currently causes the underlying library to reject with "The vod is
108
+ not a demux vod". The reconciliation/validation decided above must be applied
109
+ here (and it must feed the existing per-item error handling rather than
110
+ throwing uncaught).
111
+ - **`engine/session_live.ts`** — mirrors the demuxed handling for the live
112
+ path (`this.useDemuxedAudio`, the audio-disc-sequence and audio-segment
113
+ branches). Any normalisation must keep the live path's audio group consistent
114
+ with the VOD path across a live-mix transition.
115
+ - **`engine/stream_switcher.ts`** — gates transitions on the presence of audio
116
+ segments when `useDemuxedAudio` is set (the `isValid` /
117
+ `ItemIsEmpty(segments.audioSegments)` checks and the numerous
118
+ `this.useDemuxedAudio` branches around the switch logic). This is where a
119
+ packaging mismatch at a boundary is currently detected as "no audio
120
+ segments"; the reject-and-skip vs. reconcile decision must be enforced
121
+ consistently here too, so the switcher never advances into a state that
122
+ produces an invalid manifest.
123
+
124
+ ## Acceptance criteria
125
+
126
+ The implementation sub-issues (#382/#383) can be validated against this list:
127
+
128
+ - [ ] The channel's output presentation is decided solely by `useDemuxedAudio`
129
+ and is identical for every source in the channel (never flips
130
+ muxed/demuxed mid-stream).
131
+ - [ ] A schedule that alternates a demuxed source and a *muxed source that
132
+ already carries an audio rendition* on a `useDemuxedAudio: true` channel
133
+ plays across the VOD boundary and produces a valid master + media + audio
134
+ playlist at and after the transition.
135
+ - [ ] The same alternation on a `useDemuxedAudio: false` channel produces valid
136
+ muxed output across the boundary.
137
+ - [ ] A muxed-only source (no audio rendition) presented to a
138
+ `useDemuxedAudio: true` channel is handled deterministically: it is
139
+ rejected/skipped through the existing error → unassign → next-item path
140
+ (per the repo's "on any unexpected failure" rule) and does **not** crash
141
+ the session, wedge the stream switcher, or emit an invalid manifest.
142
+ - [ ] No segment-level remux/transcode is introduced anywhere in
143
+ `engine/session.ts`, `engine/session_live.ts`, or
144
+ `engine/stream_switcher.ts` (reconciliation is rendition routing only).
145
+ - [ ] Existing homogeneous-packaging behaviour (all-muxed channel, all-demuxed
146
+ channel) is unchanged — the current session / session_live /
147
+ stream_switcher specs still pass.
148
+ - [ ] A regression spec drives a mixed-packaging schedule through the same
149
+ seam the engine uses (VOD load → live media sequence render, as in the
150
+ existing `spec/engine/session_spec.ts`) and asserts a valid transition
151
+ rather than the current `"The vod is not a demux vod"` rejection.
152
+ - [ ] The README "Supported Source Formats" section is updated to describe
153
+ mixed-packaging support once implemented.
@@ -0,0 +1,135 @@
1
+ # Design note: subtitles in the live-mix + demux setting
2
+
3
+ Scoping note for issue #402. This document defines what "subtitle support in the
4
+ live-mix + demux setting" should mean and the acceptance criteria a follow-up
5
+ implementation issue can be filed against. It contains no code.
6
+
7
+ ## Problem
8
+
9
+ `README.md`, directly under the "Supported Source Formats" compliance table,
10
+ states:
11
+
12
+ > NOTE: The engine does not support subtitles in the live-mix + demux setting.
13
+
14
+ The table backs this up: the `Mix w. Live & Subtitles` column is `No` for both
15
+ the `HLS + TS` and `HLS + CMAF` rows.
16
+
17
+ This is grounded in the current code:
18
+
19
+ - The VOD-only path (`engine/session.ts`) **does** implement subtitles — it has
20
+ a working `getCurrentSubtitleManifestAsync(...)` and the
21
+ `useVTTSubtitles` / `dummySubtitleEndpoint` / `subtitleSliceEndpoint`
22
+ configuration wiring.
23
+ - The live path (`engine/session_live.ts`) does **not**: its
24
+ `getCurrentSubtitleManifestAsync(...)` is a stub that logs
25
+ `"getCurrentSubtitleManifestAsync is NOT Implemented"` and returns nothing.
26
+ A code comment in that file explicitly notes subtitle support for live-mix is
27
+ future work.
28
+ - `engine/stream_switcher.ts` has no subtitle handling at all; it reconciles
29
+ video and (when demuxed) audio across a transition, but nothing for a
30
+ subtitle rendition.
31
+
32
+ So today, once a channel enters the live-mix path, any subtitle rendition the
33
+ VOD side was serving has no live-side counterpart and no switcher continuity —
34
+ the gap the README documents.
35
+
36
+ ## (a) What "supported" means — the explicit target
37
+
38
+ Two candidate definitions:
39
+
40
+ 1. **Continuity**: the subtitle track keeps playing across the mix transition —
41
+ the output subtitle rendition stays present and valid through the VOD→live
42
+ (and live→VOD) boundary so a player selecting subtitles never loses them.
43
+ 2. **No-crash-only**: subtitles may drop during the live portion, but audio and
44
+ video must not break and the manifest must stay valid.
45
+
46
+ **Chosen target: (1) continuity.**
47
+
48
+ Justification: the compliance table's `Subtitles` column is already `Yes` for
49
+ the non-live cases, so users who reach live-mix reasonably expect subtitles to
50
+ keep working, not to silently disappear. A player that is told (via
51
+ `EXT-X-MEDIA TYPE=SUBTITLES`) that a subtitle group exists and then finds it
52
+ empty/absent during the live window is a worse, more confusing experience than
53
+ a clean drop — and "no-crash-only" is largely already true in practice (the
54
+ current failure mode is a missing/blank subtitle manifest, not a video crash),
55
+ so it would be a near-no-op target. Committing to continuity is the change that
56
+ actually closes the documented gap.
57
+
58
+ Continuity is defined concretely as: across the mix boundary the output master
59
+ playlist continues to advertise the same `EXT-X-MEDIA TYPE=SUBTITLES` group,
60
+ and the subtitle media playlist for that group continues to return valid,
61
+ monotonically advancing segments (real cues where the source has them, and
62
+ timed empty/dummy WebVTT segments where the live source has no cues) with no
63
+ gap that a compliant player rejects.
64
+
65
+ ## (b) Which compliance-table cells this closes, and format scope
66
+
67
+ The two currently-`No` cells are `Mix w. Live & Subtitles` for `HLS + TS` and
68
+ for `HLS + CMAF`.
69
+
70
+ **First-pass scope: `HLS + CMAF` only.** Recommendation is to close the
71
+ `HLS + CMAF` cell first and leave `HLS + TS` for a follow-up.
72
+
73
+ Rationale: the demux + subtitle plumbing on the VOD side (and the existing
74
+ livemix-demux support noted in the README) is exercised most with CMAF/fMP4
75
+ content, and segment-aligned CMAF makes the timed-empty-segment continuity
76
+ approach simpler to reason about at the boundary. `HLS + TS` can follow once the
77
+ CMAF path is proven, reusing the same switcher continuity logic. The
78
+ acceptance criteria below are written so the `HLS + TS` cell can be closed later
79
+ by re-running the same fixture shape against TS content without a redesign.
80
+
81
+ So: this note's implementation issue closes the **`HLS + CMAF`** `Mix w. Live &
82
+ Subtitles` cell; the `HLS + TS` cell is explicitly deferred.
83
+
84
+ ## (c) Concrete test fixture
85
+
86
+ A live-mix session with an active subtitle track transitioning across a VOD
87
+ boundary:
88
+
89
+ - A `useDemuxedAudio: true`, `useVTTSubtitles: true` channel.
90
+ - Schedule: a **VOD source that carries a WebVTT subtitle rendition** →
91
+ transition into a **live source** (the live-mix path) → transition back to a
92
+ VOD source with subtitles.
93
+ - The live source is the `Mix w. Live` case, so it drives the
94
+ `engine/session_live.ts` + `engine/stream_switcher.ts` paths rather than the
95
+ VOD-only `engine/session.ts` path.
96
+ - Assertion seam: render the subtitle media playlist (the output of the
97
+ subtitle-manifest path) at media sequences spanning the VOD→live→VOD
98
+ boundaries and assert the subtitle group remains advertised in the master and
99
+ the subtitle playlist stays valid and continuous across each boundary — using
100
+ the same offline render-and-assert approach as the existing
101
+ `spec/engine/subtitle_spec.ts` / `spec/engine/session_spec.ts`, so it runs in
102
+ CI without a live encoder.
103
+
104
+ The fixture should reuse existing subtitle test vectors where possible (e.g. the
105
+ `hls_subs*` / `hls_subs7_demux` vectors in `@eyevinn/hls-vodtolive/testvectors`)
106
+ plus the existing local `spec/testvectors/subtitle_file*.webvtt` files, paired
107
+ with a live playlist fixture for the mix segment.
108
+
109
+ ## Acceptance criteria
110
+
111
+ A follow-up `feat:` implementation issue can be validated against:
112
+
113
+ - [ ] "Supported" is resolved to a single explicit target — **subtitle
114
+ continuity across the mix transition** (not merely no-crash) — as defined
115
+ in section (a).
116
+ - [ ] Format scope is stated: **`HLS + CMAF` in the first pass; `HLS + TS`
117
+ deferred to a follow-up.**
118
+ - [ ] A named test fixture exists: a live-mix session (`useDemuxedAudio: true`,
119
+ `useVTTSubtitles: true`) with an active WebVTT subtitle track
120
+ transitioning across a VOD→live→VOD boundary, asserted offline in CI.
121
+ - [ ] `engine/session_live.ts` `getCurrentSubtitleManifestAsync(...)` returns a
122
+ valid subtitle manifest for the live window instead of the current
123
+ "NOT Implemented" stub.
124
+ - [ ] `engine/stream_switcher.ts` reconciles the subtitle rendition across a
125
+ transition the same way it already reconciles the demuxed audio
126
+ rendition, so the subtitle group is never dropped from the output master
127
+ mid-channel.
128
+ - [ ] Across the boundary the output subtitle playlist advances monotonically
129
+ with no gap a compliant player rejects (real cues where present, timed
130
+ empty WebVTT segments where the live source has none).
131
+ - [ ] The README compliance table's `Mix w. Live & Subtitles` cell for
132
+ `HLS + CMAF` is changed from `No` to `Yes`, and the note under the table
133
+ is updated to reflect the remaining `HLS + TS` gap.
134
+ - [ ] Existing subtitle and live-mix specs still pass (no regression to the
135
+ VOD-only subtitle path or to livemix-demux without subtitles).
package/docs/reference.md CHANGED
@@ -28,3 +28,70 @@ uri | HLSURI | string | URI to HLS master manifest for the VOD
28
28
  title (optional) | TITLE | string | The title of the asset
29
29
  playlistPosition (optional) | POSITION | id | Current position of the VOD in the playlist
30
30
 
31
+ ## Ad-break configuration (HLS interstitials)
32
+
33
+ The engine can annotate a channel's manifest with SGAI HLS-interstitial
34
+ [EXT-X-DATERANGE](https://datatracker.ietf.org/doc/html/rfc8216) tags
35
+ (`CLASS="com.apple.hls.interstitial"`) so that an HLS player performs
36
+ client-side ad replacement at the VOD boundary. Ad breaks are configured
37
+ through the `adBreak` object, which can be set either as an engine-level
38
+ default (on `ChannelEngineOpts`) or per channel (on a `Channel`). A
39
+ per-channel `adBreak` overrides the engine-level default. Ad breaks default
40
+ to **disabled** when neither is set.
41
+
42
+ ### `adBreak` fields
43
+
44
+ Field | Type | Default | Description
45
+ ----- | ---- | ------- | -----------
46
+ enabled | boolean | `false` | Master enable flag. When `false`, no interstitial metadata is emitted and the served manifest is byte-identical to the non-ad-break output.
47
+ adServerUri | string | (none) | URL of the ad-serving endpoint queried to fill a break. **Required** when `enabled` is `true`, and validated to be an absolute `http`/`https` URL — an enabled break with a missing or invalid `adServerUri` throws at engine construction.
48
+ slate (optional) | object | (none) | Slate reference shown while the break is being resolved and used as the fallback asset if the ad-serving endpoint cannot be resolved. Omit to fall back to the channel/engine slate configuration.
49
+
50
+ ### `adBreak.slate` fields
51
+
52
+ Field | Type | Default | Description
53
+ ----- | ---- | ------- | -----------
54
+ uri | string | (none) | URI to the slate HLS VOD.
55
+ repetitions (optional) | number | `10` | Number of times the slate VOD is repeated to fill the break window.
56
+ duration (optional) | number | `4000` | Duration of a single slate repetition, in milliseconds. Together with `repetitions` this drives the interstitial `PLANNED-DURATION`.
57
+
58
+ ### Ad replacement flow
59
+
60
+ When a break is opened on an enabled channel:
61
+
62
+ 1. The engine queries the configured `adServerUri` for an ad asset (with an
63
+ `Accept: application/json` request and a 2-second timeout).
64
+ 2. The endpoint answers with a JSON body in one of these shapes:
65
+
66
+ Response body | Interstitial attribute
67
+ ------------- | ----------------------
68
+ `{ "assetUri": "<uri>" }` | `X-ASSET-URI` (single asset)
69
+ `{ "assetList": "<uri>" }` | `X-ASSET-LIST` (an asset-list endpoint)
70
+ `{ "assets": ["<uri>", ...] }` | `X-ASSET-URI` from the first entry
71
+
72
+ 3. The resolved asset is written into an EXT-X-DATERANGE HLS interstitial on
73
+ the served manifest, and the HLS player fetches and plays the ad in place
74
+ of (client-side ad replacement), leaving the underlying VOD2Live stream
75
+ untouched.
76
+ 4. If the endpoint times out, returns a non-2xx status, or returns a
77
+ malformed/empty body, the engine logs and falls back to the configured
78
+ slate (or the channel/engine slate) so the break still renders — the
79
+ session tick is never interrupted.
80
+
81
+ See `examples/adbreak.ts` for a runnable channel wired with an ad break.
82
+
83
+ ## Custom VOD request parameters
84
+
85
+ The engine can be configured with an allowlist of query-parameter names that are
86
+ permitted to be forwarded to the asset manager's `getNextVod()` request. This is
87
+ set through the `customVodRequestParams` option on `ChannelEngineOpts`. Only
88
+ parameters whose name appears in the allowlist may be passed through; every other
89
+ query parameter is ignored. When the option is omitted the allowlist is empty, so
90
+ no parameters are forwarded (the default, current behaviour).
91
+
92
+ ### `ChannelEngineOpts` fields
93
+
94
+ Field | Type | Default | Description
95
+ ----- | ---- | ------- | -----------
96
+ customVodRequestParams (optional) | string[] | `[]` (empty — no pass-through) | Allowlist of query-parameter names permitted to be forwarded to `getNextVod()`. Config surface only for now; the forwarding itself is wired up in follow-up work.
97
+
@@ -0,0 +1,17 @@
1
+ const debug = require('debug')('chaos-monkey');
2
+
3
+ const CHAOS_MONKEY_PROBABILITY: string | number = process.env.CHAOS_MONKEY_PROBABILITY || 0.5;
4
+ const doChaos = (): boolean => process.env.CHAOS_MONKEY === "true" && Math.random() < (CHAOS_MONKEY_PROBABILITY as number);
5
+
6
+ const loadVod = async <T>(p: Promise<T>): Promise<T> => {
7
+ if (doChaos()) {
8
+ debug("Chaos Monkey is doing stuff");
9
+ throw new Error("Chaos monkey prevented you from loading a VOD");
10
+ } else {
11
+ return await p;
12
+ }
13
+ };
14
+
15
+ module.exports = {
16
+ loadVod
17
+ };
@@ -1,11 +1,13 @@
1
1
  class EventStream {
2
- constructor(session) {
2
+ private _session: any;
3
+
4
+ constructor(session: any) {
3
5
  this._session = session;
4
6
  }
5
7
 
6
- poll() {
8
+ poll(): Promise<string> {
7
9
  return new Promise((resolve, reject) => {
8
- let event = {};
10
+ let event: any = {};
9
11
  event = this._session.consumeEvent();
10
12
  if (event) {
11
13
  resolve(JSON.stringify(event));
@@ -1,8 +1,15 @@
1
1
  const MemcacheClient = require("memcache-client");
2
2
  const debug = require("debug")("memcached-state-store");
3
3
 
4
- class MemcachedStateStore {
5
- constructor(keyPrefix, opts) {
4
+ import type { IStateStore } from "./state_store_types";
5
+
6
+ // Type-only migration (#373): a faithful port of the original JS.
7
+
8
+ class MemcachedStateStore implements IStateStore {
9
+ keyPrefix: string;
10
+ client: any;
11
+
12
+ constructor(keyPrefix: string, opts: any) {
6
13
  this.keyPrefix = keyPrefix;
7
14
  if (opts.version) {
8
15
  const prependPrefix = opts.version.replace(/\./g, "X");
@@ -12,9 +19,9 @@ class MemcachedStateStore {
12
19
  this.client = new MemcacheClient({ server: opts.memcachedUrl, cmdTimeout: 10000 });
13
20
  }
14
21
 
15
- async initAsync(id, initData) {
22
+ async initAsync(id: string, initData: Record<string, any>): Promise<Record<string, any>> {
16
23
  const isInitiated = await this.getAsync(id, "_initiated");
17
- let data = {};
24
+ let data: Record<string, any> = {};
18
25
  if (!isInitiated) {
19
26
  for(const key of Object.keys(initData)) {
20
27
  debug(`${this.keyPrefix}:${id}: Initiating key ${key} with init data`);
@@ -31,16 +38,16 @@ class MemcachedStateStore {
31
38
  return data;
32
39
  }
33
40
 
34
- async resetAsync(id, initData) {
41
+ async resetAsync(id: string, initData: Record<string, any>): Promise<void> {
35
42
  await this.setAsync(id, "_initiated", false);
36
43
  await this.initAsync(id, initData);
37
44
  }
38
45
 
39
- async resetAllAsync() {
46
+ async resetAllAsync(): Promise<void> {
40
47
  console.error("Shared Storage Reset Failed.\nMemcache-client: Flush All Command Not Implemented Yet");
41
48
  }
42
49
 
43
- async getAsync(id, key) {
50
+ async getAsync(id: string, key: string): Promise<any> {
44
51
  const storeKey = "" + this.keyPrefix + id + key;
45
52
  const data = await this.client.get(storeKey);
46
53
  if (data) {
@@ -49,19 +56,19 @@ class MemcachedStateStore {
49
56
  return null;
50
57
  }
51
58
 
52
- async setAsync(id, key, value) {
59
+ async setAsync(id: string, key: string, value: any): Promise<any> {
53
60
  const storeKey = "" + this.keyPrefix + id + key;
54
61
  await this.client.set(storeKey, JSON.stringify(value));
55
62
  return value;
56
63
  }
57
64
 
58
- async setVolatileAsync(id, key, value) {
65
+ async setVolatileAsync(id: string, key: string, value: any): Promise<any> {
59
66
  throw new Error("memcached: setVolatileAsync not implemented yet");
60
67
  }
61
68
 
62
- async removeAsync(id, key) {
69
+ async removeAsync(id: string, key: string): Promise<any> {
63
70
  throw new Error("memcached: removeAsync not implemented yet");
64
- }
71
+ }
65
72
  }
66
73
 
67
- module.exports = MemcachedStateStore;
74
+ module.exports = MemcachedStateStore;
@@ -0,0 +1,71 @@
1
+ const debug = require("debug")("memory-state-store");
2
+
3
+ import type { IStateStore, SharedStates } from "./state_store_types";
4
+
5
+ // Type-only migration (#373): a faithful port of the original JS. Runtime
6
+ // behavior and the exported constructor signature are unchanged.
7
+
8
+ class MemoryStateStore implements IStateStore {
9
+ sharedStates: SharedStates;
10
+ globalSharedStates: { [key: string]: any };
11
+
12
+ constructor(type?: string, opts?: any) {
13
+ this.sharedStates = {};
14
+ this.globalSharedStates = {};
15
+ }
16
+
17
+ async initAsync(id: string, initData: Record<string, any>): Promise<any> {
18
+ if (!this.sharedStates[id]) {
19
+ this.sharedStates[id] = {};
20
+ Object.keys(initData).forEach((key) => {
21
+ (this.sharedStates[id] as { [key: string]: any })[key] = initData[key];
22
+ });
23
+ }
24
+ return this.sharedStates[id];
25
+ }
26
+
27
+ async resetAsync(id: string, initData: Record<string, any>): Promise<void> {
28
+ this.sharedStates[id] = null;
29
+ await this.initAsync(id, initData);
30
+ }
31
+
32
+ async resetAllAsync(): Promise<void> {
33
+ this.sharedStates = {};
34
+ this.globalSharedStates = {};
35
+ }
36
+
37
+ async getAsync(id: string, key: string): Promise<any> {
38
+ let value;
39
+ if (id === "" || id === null) {
40
+ value = this.globalSharedStates[key];
41
+ } else {
42
+ if (!this.sharedStates[id]) {
43
+ return null;
44
+ }
45
+ value = (this.sharedStates[id] as { [key: string]: any })[key];
46
+ }
47
+ return value;
48
+ }
49
+
50
+ async setAsync(id: string, key: string, value: any): Promise<any> {
51
+ if (id === "" || id === null) {
52
+ this.globalSharedStates[key] = value;
53
+ } else {
54
+ if (!this.sharedStates[id]) {
55
+ this.sharedStates[id] = {};
56
+ }
57
+ (this.sharedStates[id] as { [key: string]: any })[key] = value;
58
+ return (this.sharedStates[id] as { [key: string]: any })[key];
59
+ }
60
+ }
61
+
62
+ async setVolatileAsync(id: string, key: string, value: any): Promise<any> {
63
+ return await this.setAsync(id, key, value);
64
+ }
65
+
66
+ async removeAsync(id: string, key: string): Promise<void> {
67
+ delete (this.sharedStates[id] as { [key: string]: any })[key];
68
+ }
69
+ }
70
+
71
+ module.exports = MemoryStateStore;