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
package/engine/server.ts CHANGED
@@ -29,10 +29,39 @@ const eventStreams = {};
29
29
  const DefaultDummySubtitleEndpointPath = "/dummyUrl"
30
30
  const DefaultSubtitleSpliceEndpointPath = "/sliceUrl"
31
31
 
32
+ // Configuration for SGAI HLS-interstitial ad breaks. Config surface only for
33
+ // now (issue #367): this describes when/where a channel may open an ad break
34
+ // and which slate to fall back to. No interstitial tags are emitted from this
35
+ // config yet — that lands in a follow-up (#368). Defaults to disabled.
36
+ export interface AdBreakOpts {
37
+ // Master enable/disable flag for interstitial ad breaks on the channel.
38
+ // When false (the default) no ad-break plumbing is active.
39
+ enabled: boolean;
40
+ // URL of the ad-serving endpoint queried to fill an interstitial break.
41
+ // Required and validated (must be an absolute http/https URL) when enabled.
42
+ adServerUri?: string;
43
+ // Optional slate reference shown while the break is being resolved. Reuses
44
+ // the existing slate shape (uri/repetitions/duration); when omitted the
45
+ // channel/engine slate configuration is used.
46
+ slate?: SlateOpts;
47
+ }
48
+
49
+ // Shared slate reference shape, mirroring the loosely-typed `channel.slate`
50
+ // object already consumed by the session plumbing (slateUri/slateRepetitions/
51
+ // slateDuration).
52
+ export interface SlateOpts {
53
+ uri: string;
54
+ repetitions?: number;
55
+ duration?: number;
56
+ }
57
+
32
58
  export interface ChannelEngineOpts {
33
59
  defaultSlateUri?: string;
34
60
  slateRepetitions?: number;
35
61
  slateDuration?: number;
62
+ // Default ad-break configuration applied to channels that do not carry their
63
+ // own per-channel `adBreak`. Defaults to disabled when unset.
64
+ adBreak?: AdBreakOpts;
36
65
  redisUrl?: string;
37
66
  memcachedUrl?: string;
38
67
  sharedStoreCacheTTL?: number;
@@ -66,6 +95,35 @@ export interface ChannelEngineOpts {
66
95
  sessionEventStream?: boolean;
67
96
  sessionHealthKey?: string;
68
97
  rollingPDT?: boolean;
98
+ event?: boolean;
99
+ // Allowlist of query-parameter names permitted to be forwarded to the asset
100
+ // manager's getNextVod() request (issue #377). Only parameters whose name is
101
+ // present in this list may be passed through; all others are ignored. This
102
+ // issue only establishes the config surface — the actual pass-through is wired
103
+ // up in follow-ups (#378/#379/#380). Defaults to no allowlist (an empty list),
104
+ // i.e. no pass-through, when omitted.
105
+ customVodRequestParams?: string[];
106
+ // Base stream-switch polling interval in milliseconds (default 3000).
107
+ // This is the cadence at which the global StreamSwitchLoop calls
108
+ // updateStreamSwitchAsync() to evaluate live-schedule switches.
109
+ streamSwitchTimeIntervalMs?: number;
110
+ // Polling interval in milliseconds used while an event is ongoing (default 3000).
111
+ // "Ongoing event": the switcher is on a live stream (stream_switcher.js
112
+ // `this.streamTypeLive === true`) and the current scheduleObj is within its
113
+ // window, i.e. now >= scheduleObj.start_time && now < scheduleObj.end_time.
114
+ // A slower/less-frequent poll is acceptable in this steady state.
115
+ // NOTE: config surface only for now; the polling cadence still uses the base
116
+ // interval. Wire-up lands in the follow-up (#366).
117
+ streamSwitchOngoingEventIntervalMs?: number;
118
+ // Polling interval in milliseconds used when an event is near its end (default 1000).
119
+ // "Near end of event": an ongoing event whose remaining time is within the
120
+ // end-of-event guard window used by the switcher, i.e.
121
+ // scheduleObj.end_time - now <= 10000 (the 10000ms guard in
122
+ // stream_switcher.js ~L200). A faster poll here reduces switch latency at the
123
+ // boundary. Derived from that guard rather than a new unrelated constant.
124
+ // NOTE: config surface only for now; the polling cadence still uses the base
125
+ // interval. Wire-up lands in the follow-up (#366).
126
+ streamSwitchNearEndEventIntervalMs?: number;
69
127
  }
70
128
 
71
129
  interface StreamerOpts {
@@ -82,6 +140,12 @@ export interface VodRequest {
82
140
  sessionId: string;
83
141
  category?: string;
84
142
  playlistId: string;
143
+ // Optional custom pass-through parameters forwarded to the asset manager's
144
+ // getNextVod() (issue #378). Sourced from per-session state populated by the
145
+ // master-manifest request handler (wired up in #379) and filtered against the
146
+ // customVodRequestParams allowlist (#377). Optional and defaults to absent so
147
+ // existing IAssetManager implementations are unaffected.
148
+ customParams?: { [key: string]: string };
85
149
  }
86
150
 
87
151
  export interface VodResponseMetadata {
@@ -137,6 +201,10 @@ export interface Channel {
137
201
  audioTracks?: AudioTracks[];
138
202
  subtitleTracks?: SubtitleTracks[];
139
203
  closedCaptions?: ClosedCaptions[];
204
+ slate?: SlateOpts;
205
+ // Per-channel ad-break configuration. Overrides the engine-level `adBreak`
206
+ // default when present. Defaults to disabled when neither is set.
207
+ adBreak?: AdBreakOpts;
140
208
  }
141
209
 
142
210
  export interface ClosedCaptions {
@@ -196,6 +264,7 @@ export class ChannelEngine {
196
264
  private alwaysMapBandwidthByNearest: boolean;
197
265
  private defaultSlateUri?: string;
198
266
  private slateDuration?: number;
267
+ private adBreak?: AdBreakOpts;
199
268
  private assetMgr: IAssetManager;
200
269
  private streamSwitchManager?: any;
201
270
  private slateRepetitions?: number;
@@ -204,6 +273,8 @@ export class ChannelEngine {
204
273
  private serverStartTime: number;
205
274
  private instanceId: string;
206
275
  private streamSwitchTimeIntervalMs: number;
276
+ private streamSwitchOngoingEventIntervalMs: number;
277
+ private streamSwitchNearEndEventIntervalMs: number;
207
278
  private sessionStore: any;
208
279
  private sessionLiveStore: any;
209
280
  private streamerOpts: StreamerOpts;
@@ -214,7 +285,50 @@ export class ChannelEngine {
214
285
  private sessionResetKey: string = "";
215
286
  private sessionEventStream: boolean = false;
216
287
  private sessionHealthKey: string = "";
217
-
288
+ // Allowlist of query-parameter names permitted to be forwarded to
289
+ // getNextVod() (issue #377). Config surface only — defaults to an empty list
290
+ // (no pass-through) when the option is omitted. Consumed in follow-ups
291
+ // (#378/#379/#380).
292
+ private customVodRequestParams: string[] = [];
293
+
294
+ // Validate and normalize an ad-break configuration (issue #367).
295
+ // Returns a config that is always defined and disabled-by-default:
296
+ // - undefined/omitted -> { enabled: false }
297
+ // - { enabled: false, ... } -> { enabled: false } (endpoint ignored)
298
+ // - { enabled: true, adServerUri } -> validated, with the slate reference
299
+ // (if any) carried through
300
+ // Throws when an enabled break is missing a syntactically valid absolute
301
+ // http(s) ad-serving endpoint URL. `scope` is only used for error context.
302
+ private static normalizeAdBreak(adBreak: AdBreakOpts | undefined, scope: string): AdBreakOpts {
303
+ if (!adBreak || !adBreak.enabled) {
304
+ return { enabled: false };
305
+ }
306
+ if (!adBreak.adServerUri) {
307
+ throw new Error(`Ad-break configuration (${scope}) is enabled but no adServerUri was provided`);
308
+ }
309
+ let parsed: URL;
310
+ try {
311
+ parsed = new URL(adBreak.adServerUri);
312
+ } catch (err) {
313
+ throw new Error(`Ad-break configuration (${scope}) has an invalid adServerUri: ${adBreak.adServerUri}`);
314
+ }
315
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
316
+ throw new Error(`Ad-break configuration (${scope}) adServerUri must be an http(s) URL: ${adBreak.adServerUri}`);
317
+ }
318
+ const normalized: AdBreakOpts = {
319
+ enabled: true,
320
+ adServerUri: adBreak.adServerUri
321
+ };
322
+ if (adBreak.slate && adBreak.slate.uri) {
323
+ normalized.slate = {
324
+ uri: adBreak.slate.uri,
325
+ repetitions: adBreak.slate.repetitions || 10,
326
+ duration: adBreak.slate.duration || 4000
327
+ };
328
+ }
329
+ return normalized;
330
+ }
331
+
218
332
  constructor(assetMgr: IAssetManager, options?: ChannelEngineOpts) {
219
333
  this.options = options;
220
334
  if (options && options.adCopyMgrUri) {
@@ -257,12 +371,23 @@ export class ChannelEngine {
257
371
  this.slateRepetitions = options.slateRepetitions || 10;
258
372
  this.slateDuration = options.slateDuration || 4000;
259
373
  }
374
+ // Ad-break config surface (issue #367). Validate and normalize the
375
+ // engine-level default; disabled when unset. Throws on an enabled break
376
+ // that lacks a valid ad-serving endpoint URL.
377
+ this.adBreak = ChannelEngine.normalizeAdBreak(options && options.adBreak, "engine");
260
378
  if (options && options.streamSwitchManager) {
261
379
  this.streamSwitchManager = options.streamSwitchManager;
262
380
  }
263
381
  if (options && options.autoCreateSession !== undefined) {
264
382
  this.autoCreateSession = options.autoCreateSession;
265
383
  }
384
+ // Custom VOD request-parameter allowlist (issue #377). Config surface only —
385
+ // stored on the instance so follow-ups (#378/#379/#380) can consume it.
386
+ // Defaults to an empty list (no pass-through) when omitted.
387
+ this.customVodRequestParams =
388
+ options && Array.isArray(options.customVodRequestParams)
389
+ ? options.customVodRequestParams
390
+ : [];
266
391
  this.assetMgr = assetMgr;
267
392
  this.monitorTimer = {};
268
393
  this.server = fastify;
@@ -276,7 +401,27 @@ export class ChannelEngine {
276
401
  this.serverStartTime = Date.now();
277
402
  this.instanceId = uuidv4();
278
403
 
279
- this.streamSwitchTimeIntervalMs = 3000;
404
+ // Base stream-switch polling interval (ms). Configurable via options,
405
+ // defaults to the previously-hardcoded 3000ms. See ChannelEngineOpts.
406
+ this.streamSwitchTimeIntervalMs =
407
+ options && options.streamSwitchTimeIntervalMs !== undefined
408
+ ? options.streamSwitchTimeIntervalMs
409
+ : 3000;
410
+ // Polling interval (ms) while an event is ongoing. See "ongoing event"
411
+ // definition in ChannelEngineOpts. Defaults to the base interval (3000ms).
412
+ // Config surface only for now — not yet applied to the loop cadence (#366).
413
+ this.streamSwitchOngoingEventIntervalMs =
414
+ options && options.streamSwitchOngoingEventIntervalMs !== undefined
415
+ ? options.streamSwitchOngoingEventIntervalMs
416
+ : 3000;
417
+ // Polling interval (ms) when an event is near its end. See "near end of
418
+ // event" definition in ChannelEngineOpts, derived from the 10000ms
419
+ // end-of-event guard in stream_switcher.js. Defaults to a faster 1000ms.
420
+ // Config surface only for now — not yet applied to the loop cadence (#366).
421
+ this.streamSwitchNearEndEventIntervalMs =
422
+ options && options.streamSwitchNearEndEventIntervalMs !== undefined
423
+ ? options.streamSwitchNearEndEventIntervalMs
424
+ : 1000;
280
425
 
281
426
  this.sessionStore = {
282
427
  sessionStateStore: new SessionStateStore({
@@ -422,10 +567,15 @@ export class ChannelEngine {
422
567
  const ts_1 = Date.now();
423
568
  await this.updateStreamSwitchAsync()
424
569
  const ts_2 = Date.now();
425
- const interval = (timeIntervalMs - (ts_2 - ts_1)) < 0 ? minIntervalMs : (timeIntervalMs - (ts_2 - ts_1));
570
+ // Adaptive cadence: back off while all active channels are mid-event and
571
+ // speed up as any channel nears its end. The poll loop is global but event
572
+ // state is per-channel, so we take the MINIMUM interval required across all
573
+ // channels — never under-polling a channel that needs responsiveness.
574
+ const desiredIntervalMs = this.computeStreamSwitchIntervalMs(timeIntervalMs);
575
+ const interval = (desiredIntervalMs - (ts_2 - ts_1)) < 0 ? minIntervalMs : (desiredIntervalMs - (ts_2 - ts_1));
426
576
  const tickInterval = await WTG.getWaitTime(interval);
427
577
  await timer(tickInterval)
428
- debug(`StreamSwitchLoop waited for all channels. Next tick in: ${tickInterval}ms`)
578
+ debug(`StreamSwitchLoop waited for all channels. Next tick in: ${tickInterval}ms (desired base=${desiredIntervalMs}ms)`)
429
579
  } catch (err) {
430
580
  console.error(err)
431
581
  debug(`StreamSwitchLoop iteration failed. Trying Again in 1000ms!`);
@@ -438,6 +588,41 @@ export class ChannelEngine {
438
588
  }
439
589
  }
440
590
 
591
+ // Compute the desired next poll interval for the global StreamSwitchLoop from the
592
+ // aggregated per-channel event state. Because the loop is global but event state is
593
+ // per-channel, the result is the MINIMUM interval any channel requires so we never
594
+ // under-poll a channel that needs responsiveness:
595
+ // - a channel that is near-end contributes the fast near-end interval;
596
+ // - a channel that is mid-event but not near-end contributes the slower ongoing
597
+ // interval;
598
+ // - a channel with no ongoing event contributes the base interval.
599
+ // The loop uses the minimum across all channels. When no channel reports an ongoing
600
+ // event (no schedule / no ongoing event) this falls back to the base interval,
601
+ // keeping behavior identical to today.
602
+ computeStreamSwitchIntervalMs(baseIntervalMs) {
603
+ const channels = Object.keys(sessionSwitchers);
604
+ let desiredMs = baseIntervalMs;
605
+ for (const channel of channels) {
606
+ const switcher = sessionSwitchers[channel];
607
+ if (!switcher || typeof switcher.getEventPollingState !== "function") {
608
+ continue;
609
+ }
610
+ const state = switcher.getEventPollingState();
611
+ let channelMs;
612
+ if (state.nearEnd) {
613
+ channelMs = this.streamSwitchNearEndEventIntervalMs;
614
+ } else if (state.midEvent) {
615
+ channelMs = this.streamSwitchOngoingEventIntervalMs;
616
+ } else {
617
+ channelMs = baseIntervalMs;
618
+ }
619
+ if (channelMs < desiredMs) {
620
+ desiredMs = channelMs;
621
+ }
622
+ }
623
+ return desiredMs;
624
+ }
625
+
441
626
  async updateStreamSwitchAsync() {
442
627
  const channels = Object.keys(sessionSwitchers);
443
628
  const getSwitchStatusAndPerformSwitch = async (channel) => {
@@ -455,7 +640,7 @@ export class ChannelEngine {
455
640
  }
456
641
  switcherStatus[channel] = status;
457
642
  } catch (err) {
458
- throw new Error (err);
643
+ throw err instanceof Error ? err : new Error(String(err));
459
644
  }
460
645
  } else {
461
646
  debug(`Tried to switch stream on a non-existing channel=[${channel}]. Switching Ignored!)`);
@@ -465,7 +650,7 @@ export class ChannelEngine {
465
650
  await Promise.all(channels.map(channel => getSwitchStatusAndPerformSwitch(channel)));
466
651
  } catch (err) {
467
652
  debug('Problem occured when updating streamSwitchers');
468
- throw new Error (err);
653
+ throw err instanceof Error ? err : new Error(String(err));
469
654
  }
470
655
 
471
656
  }
@@ -499,9 +684,11 @@ export class ChannelEngine {
499
684
  slateUri: channel.slate && channel.slate.uri ? channel.slate.uri : this.defaultSlateUri,
500
685
  slateRepetitions: channel.slate && channel.slate.repetitions ? channel.slate.repetitions : this.slateRepetitions,
501
686
  slateDuration: channel.slate && channel.slate.duration ? channel.slate.duration : this.slateDuration,
687
+ adBreak: channel.adBreak ? ChannelEngine.normalizeAdBreak(channel.adBreak, `channel ${channel.id}`) : this.adBreak,
502
688
  cloudWatchMetrics: this.logCloudWatchMetrics,
503
689
  sessionEventStream: options.sessionEventStream,
504
- rollingPDT: options.rollingPDT
690
+ rollingPDT: options.rollingPDT,
691
+ event: options.event
505
692
  }, this.sessionStore);
506
693
 
507
694
  sessionsLive[channel.id] = new SessionLive({
@@ -762,7 +949,22 @@ export class ChannelEngine {
762
949
  if (session) {
763
950
  const eventStream = new EventStream(session);
764
951
  eventStreams[session.sessionId] = eventStream;
765
-
952
+
953
+ // Plumb allowlisted master.m3u8 query params into the session so the next
954
+ // _getNextVod() call includes them (#379). Sessions are persistent (created
955
+ // once in updateChannelsAsync and reused), so scope custom params per request:
956
+ // set them on every master-manifest request — using an empty object when the
957
+ // allowlist is unset or no allowlisted params are present — so a later request
958
+ // without them doesn't inherit a previous request's values. Non-allowlisted
959
+ // params are ignored.
960
+ const customParams: { [key: string]: string } = {};
961
+ for (const key of this.customVodRequestParams) {
962
+ if (request.query[key] !== undefined) {
963
+ customParams[key] = request.query[key];
964
+ }
965
+ }
966
+ session.setCustomParams(customParams);
967
+
766
968
  let filter;
767
969
  if (request.query['filter']) {
768
970
  debug(`Applying filter on master manifest ${request.query['filter']}`);