@norskvideo/ctl-test-harness 0.1.7 → 0.1.9
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.
- package/container-net.d.ts +110 -0
- package/container-net.js +170 -0
- package/package.json +5 -1
- package/studio-state.d.ts +12 -10
- package/studio-state.js +10 -6
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export declare const DOCKER_NETWORK_NAME = "norsk-net";
|
|
2
|
+
export declare const STUDIO_INTERNAL_PORT = 8000;
|
|
3
|
+
export declare const MEDIA_HTTP_INTERNAL_PORT = 8080;
|
|
4
|
+
export type NetReachMode = "publish" | "direct";
|
|
5
|
+
/** How the suite reaches instance containers. `direct` = norsk-net service DNS
|
|
6
|
+
* (no host publish, no collision) — only viable on a Linux DooD runner whose
|
|
7
|
+
* test container is on norsk-net. `publish` = today's host-published port,
|
|
8
|
+
* the Docker-Desktop-safe default. Read from NORSK_TEST_NET. */
|
|
9
|
+
export declare function netReachMode(env?: Record<string, string | undefined>): NetReachMode;
|
|
10
|
+
export interface Endpoint {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const authority: (e: Endpoint) => string;
|
|
15
|
+
/** Compose's container DNS for a template-launched instance: `<id>-<service>-1`.
|
|
16
|
+
* Byte-identical to norsk-ctl's productInstanceHost — the compose project is the
|
|
17
|
+
* instance id and each service runs a single replica. */
|
|
18
|
+
export declare function productServiceHost(instanceId: string, service: string): string;
|
|
19
|
+
export interface ResolveOpts {
|
|
20
|
+
instanceId: string;
|
|
21
|
+
service: "studio" | "media";
|
|
22
|
+
/** Port the listener binds INSIDE the container (studio 8000, media SRT its
|
|
23
|
+
* own configured port). Used in `direct` mode. */
|
|
24
|
+
internalPort: number;
|
|
25
|
+
/** Host + port the container is published on today. Used in `publish` mode. */
|
|
26
|
+
publishHost: string;
|
|
27
|
+
publishPort: number;
|
|
28
|
+
/** Defaults to netReachMode() so a suite flips the whole harness with one env var. */
|
|
29
|
+
mode?: NetReachMode;
|
|
30
|
+
}
|
|
31
|
+
/** The seam: one function every reach decision goes through. `publish` returns
|
|
32
|
+
* the host-published endpoint unchanged (zero behaviour change); `direct`
|
|
33
|
+
* returns the unique service DNS at the internal port. */
|
|
34
|
+
export declare function resolveEndpoint(o: ResolveOpts): Endpoint;
|
|
35
|
+
/** Base URL for Studio's `/live/api/*` control surface. Publish mode reaches the
|
|
36
|
+
* host-published studioHostPort (as studio-state.ts does today); direct mode
|
|
37
|
+
* reaches `<id>-studio-1:8000`. Drop-in for the `http://${STUDIO_HOST}:${port}`
|
|
38
|
+
* prefix the studio fetchers build. */
|
|
39
|
+
export declare function studioApiBase(o: {
|
|
40
|
+
instanceId: string;
|
|
41
|
+
publishHost: string;
|
|
42
|
+
publishPort: number;
|
|
43
|
+
mode?: NetReachMode;
|
|
44
|
+
}): string;
|
|
45
|
+
/** Base URL for the Norsk media container's HTTP surface (HLS/CMAF pull —
|
|
46
|
+
* `/ts/*.m3u8`, `/cmaf/*`). Publish mode dials the host-published media port on
|
|
47
|
+
* TEST_HOST (as funke's hls-capture does today); direct mode dials
|
|
48
|
+
* `<id>-media-1:8080`. Mirrors studio-state.mediaDirectUrl, generalised for the
|
|
49
|
+
* direct-pull capture path. */
|
|
50
|
+
export declare function mediaHttpBase(o: {
|
|
51
|
+
instanceId: string;
|
|
52
|
+
publishHost: string;
|
|
53
|
+
publishPort: number;
|
|
54
|
+
mode?: NetReachMode;
|
|
55
|
+
}): string;
|
|
56
|
+
/** Caller-mode SRT URL for an output.srt egress listener. Publish mode dials the
|
|
57
|
+
* host-published egress port on TEST_HOST (as output-capture.ts does today);
|
|
58
|
+
* direct mode dials `<id>-media-1:<internalPort>`. Because remapConfigPorts
|
|
59
|
+
* currently sets the internal listener port == the published port, `internalPort`
|
|
60
|
+
* and `publishPort` are the same band value today — going direct is what later
|
|
61
|
+
* lets that band be dropped entirely. */
|
|
62
|
+
export declare function srtEgressUrl(o: {
|
|
63
|
+
instanceId: string;
|
|
64
|
+
internalPort: number;
|
|
65
|
+
publishHost: string;
|
|
66
|
+
publishPort: number;
|
|
67
|
+
mode?: NetReachMode;
|
|
68
|
+
}): string;
|
|
69
|
+
/** What a caller needs to reach an instance's Studio control surface under
|
|
70
|
+
* either mode: the published port (publish) plus the instance id (direct, to
|
|
71
|
+
* form `<id>-studio-1`). Threaded in place of a bare `studioHostPort: number`. */
|
|
72
|
+
export interface StudioTarget {
|
|
73
|
+
instanceId: string;
|
|
74
|
+
studioHostPort: number;
|
|
75
|
+
}
|
|
76
|
+
/** What a fetcher/control helper accepts to reach Studio: EITHER a bare published
|
|
77
|
+
* port (legacy — every existing caller, other products included) OR a
|
|
78
|
+
* StudioTarget (instanceId-aware, opts the call into direct-net). */
|
|
79
|
+
export type StudioReach = number | StudioTarget;
|
|
80
|
+
/** Resolve Studio's base URL from EITHER a bare published port (legacy — dials
|
|
81
|
+
* `publishHost:port`, exactly today's behaviour) OR a StudioTarget (instanceId-
|
|
82
|
+
* aware, so direct mode dials `<id>-studio-1:8000`). The union is the whole
|
|
83
|
+
* migration story: every existing `studioHostPort: number` caller — including
|
|
84
|
+
* other products' suites — keeps working untouched, and a suite opts a call into
|
|
85
|
+
* direct-net simply by passing the target instead of the number. */
|
|
86
|
+
export declare function studioBaseFrom(studio: StudioReach, publishHost: string, mode?: NetReachMode): string;
|
|
87
|
+
/** The 64-hex container id from a /proc/self/cgroup dump, or null on a bare host.
|
|
88
|
+
* In a container the id also appears as the hostname, but cgroup is present in
|
|
89
|
+
* both cgroup v1 (`/docker/<id>`) and v2 (`docker-<id>.scope`) layouts. */
|
|
90
|
+
export declare function parseOwnContainerId(cgroupText: string): string | null;
|
|
91
|
+
export declare function ownContainerId(read?: () => string): string | null;
|
|
92
|
+
/** `docker network connect` is not idempotent — a second attach errors. Treat
|
|
93
|
+
* the "already attached" variants as success so setup can call this blindly. */
|
|
94
|
+
export declare function isAlreadyOnNetworkError(stderr: string): boolean;
|
|
95
|
+
type RunResult = {
|
|
96
|
+
status: number | null;
|
|
97
|
+
stderr: string;
|
|
98
|
+
};
|
|
99
|
+
type Runner = (cmd: string, args: string[]) => RunResult;
|
|
100
|
+
/** Idempotently join the test container to norsk-net so `direct` service DNS
|
|
101
|
+
* resolves. Returns a verdict rather than throwing: `not-in-container` (nothing
|
|
102
|
+
* to attach — publish mode is the only sane choice), `attached`, `already`, or
|
|
103
|
+
* `failed` (a real error the caller should surface). */
|
|
104
|
+
export declare function ensureRunnerOnNetwork(opts?: {
|
|
105
|
+
network?: string;
|
|
106
|
+
/** Explicit id (tests). Omitted -> read from /proc/self/cgroup. `null` forces the not-in-container branch. */
|
|
107
|
+
containerId?: string | null;
|
|
108
|
+
run?: Runner;
|
|
109
|
+
}): "attached" | "already" | "not-in-container" | "failed";
|
|
110
|
+
export {};
|
package/container-net.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Reach launched product instances by their norsk-net service DNS instead of a
|
|
2
|
+
// host-published port. PROTOTYPE for dissolving the cross-product host-port
|
|
3
|
+
// collision the release gate trips on.
|
|
4
|
+
//
|
|
5
|
+
// WHY host ports collide: in the docker-outside-of-docker CI runner the test
|
|
6
|
+
// process is itself a container and studio/media run as HOST-sibling containers,
|
|
7
|
+
// so their published ports live on the shared host. Two products' harnesses band
|
|
8
|
+
// their ports off `hashSlot(slug, 50)` over an IDENTICAL base, so two slugs that
|
|
9
|
+
// land in the same slot publish the same host port and the second launch dies
|
|
10
|
+
// with "Bind for 0.0.0.0:<port> failed: port is already allocated". The slug->slot
|
|
11
|
+
// map is deterministic, so re-running never dodges it.
|
|
12
|
+
//
|
|
13
|
+
// WHY direct-net removes the whole class: every product instance is its own
|
|
14
|
+
// compose project on the shared external `norsk-net`, so compose names its
|
|
15
|
+
// containers `<id>-studio-1` / `<id>-media-1` (see norsk-ctl's productInstanceHost).
|
|
16
|
+
// Reaching those by name at the container-INTERNAL port (studio 8000, media
|
|
17
|
+
// 8080, the SRT egress listener's own port) needs NO host publish: internal
|
|
18
|
+
// ports live in per-container network namespaces, so a thousand instances can
|
|
19
|
+
// all bind 8000 internally without clashing, and the unique service DNS keeps
|
|
20
|
+
// them addressable. This is the same move `studio-state.mediaDirectUrl` already
|
|
21
|
+
// makes for media HTTP — generalised to the studio-control + SRT-egress ports
|
|
22
|
+
// that are the actual colliders.
|
|
23
|
+
//
|
|
24
|
+
// THE ONE CONSTRAINT: container-DNS reachability needs the test container joined
|
|
25
|
+
// to norsk-net (bridge DNS is per-network). On Docker Desktop / OrbStack the
|
|
26
|
+
// bridge is hidden inside a VM and is not routable from the host at all, so the
|
|
27
|
+
// direct path can't work there — those runs MUST keep publishing. Hence the mode
|
|
28
|
+
// switch: `direct` on the Linux DooD CI box, `publish` (the default) everywhere
|
|
29
|
+
// else. Nothing here changes behaviour until a caller opts a suite into `direct`.
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
// Mirrors of norsk-ctl's @norsk-ctl/shared constants — re-declared (not imported)
|
|
33
|
+
// because this package doesn't depend on the ctl backend, exactly as
|
|
34
|
+
// studio-state.ts re-declares MEDIA_HTTP_PORT. Source of truth:
|
|
35
|
+
// packages/norsk-ctl/shared/src/base-constants.ts.
|
|
36
|
+
export const DOCKER_NETWORK_NAME = "norsk-net";
|
|
37
|
+
export const STUDIO_INTERNAL_PORT = 8000;
|
|
38
|
+
export const MEDIA_HTTP_INTERNAL_PORT = 8080;
|
|
39
|
+
/** How the suite reaches instance containers. `direct` = norsk-net service DNS
|
|
40
|
+
* (no host publish, no collision) — only viable on a Linux DooD runner whose
|
|
41
|
+
* test container is on norsk-net. `publish` = today's host-published port,
|
|
42
|
+
* the Docker-Desktop-safe default. Read from NORSK_TEST_NET. */
|
|
43
|
+
export function netReachMode(env = process.env) {
|
|
44
|
+
return env.NORSK_TEST_NET === "direct" ? "direct" : "publish";
|
|
45
|
+
}
|
|
46
|
+
export const authority = (e) => `${e.host}:${e.port}`;
|
|
47
|
+
/** Compose's container DNS for a template-launched instance: `<id>-<service>-1`.
|
|
48
|
+
* Byte-identical to norsk-ctl's productInstanceHost — the compose project is the
|
|
49
|
+
* instance id and each service runs a single replica. */
|
|
50
|
+
export function productServiceHost(instanceId, service) {
|
|
51
|
+
return `${instanceId}-${service}-1`;
|
|
52
|
+
}
|
|
53
|
+
/** The seam: one function every reach decision goes through. `publish` returns
|
|
54
|
+
* the host-published endpoint unchanged (zero behaviour change); `direct`
|
|
55
|
+
* returns the unique service DNS at the internal port. */
|
|
56
|
+
export function resolveEndpoint(o) {
|
|
57
|
+
const mode = o.mode ?? netReachMode();
|
|
58
|
+
return mode === "direct"
|
|
59
|
+
? { host: productServiceHost(o.instanceId, o.service), port: o.internalPort }
|
|
60
|
+
: { host: o.publishHost, port: o.publishPort };
|
|
61
|
+
}
|
|
62
|
+
/** Base URL for Studio's `/live/api/*` control surface. Publish mode reaches the
|
|
63
|
+
* host-published studioHostPort (as studio-state.ts does today); direct mode
|
|
64
|
+
* reaches `<id>-studio-1:8000`. Drop-in for the `http://${STUDIO_HOST}:${port}`
|
|
65
|
+
* prefix the studio fetchers build. */
|
|
66
|
+
export function studioApiBase(o) {
|
|
67
|
+
const e = resolveEndpoint({
|
|
68
|
+
instanceId: o.instanceId,
|
|
69
|
+
service: "studio",
|
|
70
|
+
internalPort: STUDIO_INTERNAL_PORT,
|
|
71
|
+
publishHost: o.publishHost,
|
|
72
|
+
publishPort: o.publishPort,
|
|
73
|
+
...(o.mode !== undefined ? { mode: o.mode } : {}),
|
|
74
|
+
});
|
|
75
|
+
return `http://${authority(e)}`;
|
|
76
|
+
}
|
|
77
|
+
/** Base URL for the Norsk media container's HTTP surface (HLS/CMAF pull —
|
|
78
|
+
* `/ts/*.m3u8`, `/cmaf/*`). Publish mode dials the host-published media port on
|
|
79
|
+
* TEST_HOST (as funke's hls-capture does today); direct mode dials
|
|
80
|
+
* `<id>-media-1:8080`. Mirrors studio-state.mediaDirectUrl, generalised for the
|
|
81
|
+
* direct-pull capture path. */
|
|
82
|
+
export function mediaHttpBase(o) {
|
|
83
|
+
const e = resolveEndpoint({
|
|
84
|
+
instanceId: o.instanceId,
|
|
85
|
+
service: "media",
|
|
86
|
+
internalPort: MEDIA_HTTP_INTERNAL_PORT,
|
|
87
|
+
publishHost: o.publishHost,
|
|
88
|
+
publishPort: o.publishPort,
|
|
89
|
+
...(o.mode !== undefined ? { mode: o.mode } : {}),
|
|
90
|
+
});
|
|
91
|
+
return `http://${authority(e)}`;
|
|
92
|
+
}
|
|
93
|
+
/** Caller-mode SRT URL for an output.srt egress listener. Publish mode dials the
|
|
94
|
+
* host-published egress port on TEST_HOST (as output-capture.ts does today);
|
|
95
|
+
* direct mode dials `<id>-media-1:<internalPort>`. Because remapConfigPorts
|
|
96
|
+
* currently sets the internal listener port == the published port, `internalPort`
|
|
97
|
+
* and `publishPort` are the same band value today — going direct is what later
|
|
98
|
+
* lets that band be dropped entirely. */
|
|
99
|
+
export function srtEgressUrl(o) {
|
|
100
|
+
const e = resolveEndpoint({
|
|
101
|
+
instanceId: o.instanceId,
|
|
102
|
+
service: "media",
|
|
103
|
+
internalPort: o.internalPort,
|
|
104
|
+
publishHost: o.publishHost,
|
|
105
|
+
publishPort: o.publishPort,
|
|
106
|
+
...(o.mode !== undefined ? { mode: o.mode } : {}),
|
|
107
|
+
});
|
|
108
|
+
return `srt://${authority(e)}?mode=caller`;
|
|
109
|
+
}
|
|
110
|
+
/** Resolve Studio's base URL from EITHER a bare published port (legacy — dials
|
|
111
|
+
* `publishHost:port`, exactly today's behaviour) OR a StudioTarget (instanceId-
|
|
112
|
+
* aware, so direct mode dials `<id>-studio-1:8000`). The union is the whole
|
|
113
|
+
* migration story: every existing `studioHostPort: number` caller — including
|
|
114
|
+
* other products' suites — keeps working untouched, and a suite opts a call into
|
|
115
|
+
* direct-net simply by passing the target instead of the number. */
|
|
116
|
+
export function studioBaseFrom(studio, publishHost, mode) {
|
|
117
|
+
if (typeof studio === "number")
|
|
118
|
+
return `http://${publishHost}:${studio}`;
|
|
119
|
+
return studioApiBase({
|
|
120
|
+
instanceId: studio.instanceId,
|
|
121
|
+
publishHost,
|
|
122
|
+
publishPort: studio.studioHostPort,
|
|
123
|
+
...(mode !== undefined ? { mode } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// ── Runner network attach ───────────────────────────────────────────────────
|
|
127
|
+
// `direct` DNS only resolves if the test container is on norsk-net. These
|
|
128
|
+
// helpers attach it idempotently; all pure/injectable so the logic is tested
|
|
129
|
+
// without touching docker.
|
|
130
|
+
/** The 64-hex container id from a /proc/self/cgroup dump, or null on a bare host.
|
|
131
|
+
* In a container the id also appears as the hostname, but cgroup is present in
|
|
132
|
+
* both cgroup v1 (`/docker/<id>`) and v2 (`docker-<id>.scope`) layouts. */
|
|
133
|
+
export function parseOwnContainerId(cgroupText) {
|
|
134
|
+
const m = cgroupText.match(/[0-9a-f]{64}/);
|
|
135
|
+
return m ? m[0] : null;
|
|
136
|
+
}
|
|
137
|
+
export function ownContainerId(read = () => readFileSync("/proc/self/cgroup", "utf-8")) {
|
|
138
|
+
try {
|
|
139
|
+
return parseOwnContainerId(read());
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** `docker network connect` is not idempotent — a second attach errors. Treat
|
|
146
|
+
* the "already attached" variants as success so setup can call this blindly. */
|
|
147
|
+
export function isAlreadyOnNetworkError(stderr) {
|
|
148
|
+
return /already exists in network|is already connected|endpoint with name .* already exists/i.test(stderr);
|
|
149
|
+
}
|
|
150
|
+
const defaultRunner = (cmd, args) => {
|
|
151
|
+
const r = spawnSync(cmd, args, { encoding: "utf-8" });
|
|
152
|
+
return { status: r.status, stderr: r.stderr ?? "" };
|
|
153
|
+
};
|
|
154
|
+
/** Idempotently join the test container to norsk-net so `direct` service DNS
|
|
155
|
+
* resolves. Returns a verdict rather than throwing: `not-in-container` (nothing
|
|
156
|
+
* to attach — publish mode is the only sane choice), `attached`, `already`, or
|
|
157
|
+
* `failed` (a real error the caller should surface). */
|
|
158
|
+
export function ensureRunnerOnNetwork(opts = {}) {
|
|
159
|
+
const network = opts.network ?? DOCKER_NETWORK_NAME;
|
|
160
|
+
const id = opts.containerId === undefined ? ownContainerId() : opts.containerId;
|
|
161
|
+
if (!id)
|
|
162
|
+
return "not-in-container";
|
|
163
|
+
const run = opts.run ?? defaultRunner;
|
|
164
|
+
const r = run("docker", ["network", "connect", network, id]);
|
|
165
|
+
if (r.status === 0)
|
|
166
|
+
return "attached";
|
|
167
|
+
if (isAlreadyOnNetworkError(r.stderr))
|
|
168
|
+
return "already";
|
|
169
|
+
return "failed";
|
|
170
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-test-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./daemon": {
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"./studio-load": {
|
|
27
27
|
"types": "./studio-load.d.ts",
|
|
28
28
|
"default": "./studio-load.js"
|
|
29
|
+
},
|
|
30
|
+
"./container-net": {
|
|
31
|
+
"types": "./container-net.d.ts",
|
|
32
|
+
"default": "./container-net.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"dependencies": {
|
package/studio-state.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type StudioReach, type StudioTarget } from "./container-net.js";
|
|
2
|
+
export type { StudioReach, StudioTarget };
|
|
1
3
|
export interface ComponentSummary {
|
|
2
4
|
componentId: string;
|
|
3
5
|
componentType: string;
|
|
@@ -9,8 +11,8 @@ export interface ComponentsResponse {
|
|
|
9
11
|
}
|
|
10
12
|
export declare function applyTestHost(rawUrl: string): string;
|
|
11
13
|
export declare function mediaDirectUrl(rawUrl: string): string;
|
|
12
|
-
export declare function fetchComponents(
|
|
13
|
-
export declare function fetchComponentState(
|
|
14
|
+
export declare function fetchComponents(studio: StudioReach): Promise<ComponentsResponse | null>;
|
|
15
|
+
export declare function fetchComponentState(studio: StudioReach, componentId: string): Promise<unknown | null>;
|
|
14
16
|
/** SRT-listener state shape — see
|
|
15
17
|
* ~/src/norsk-studio/workspaces/built-ins/src/input.srt-listener/types.source.yaml:192-235.
|
|
16
18
|
* Required fields per the YAML schema; harness asserts on the first two. */
|
|
@@ -50,12 +52,12 @@ export interface StreamMappingEntry {
|
|
|
50
52
|
preview_jpeg?: string;
|
|
51
53
|
[k: string]: unknown;
|
|
52
54
|
}
|
|
53
|
-
export declare function fetchStreamMappings(studioHostPort:
|
|
54
|
-
export declare function fetchStreamSources(studioHostPort:
|
|
55
|
-
export declare function fetchStreamUnmapped(studioHostPort:
|
|
56
|
-
export declare function fetchStreamFill(studioHostPort:
|
|
55
|
+
export declare function fetchStreamMappings(studioHostPort: StudioReach, componentId: string): Promise<StreamMappingEntry[] | null>;
|
|
56
|
+
export declare function fetchStreamSources(studioHostPort: StudioReach, componentId: string): Promise<unknown | null>;
|
|
57
|
+
export declare function fetchStreamUnmapped(studioHostPort: StudioReach, componentId: string): Promise<unknown | null>;
|
|
58
|
+
export declare function fetchStreamFill(studioHostPort: StudioReach, componentId: string): Promise<unknown | null>;
|
|
57
59
|
export interface AssertStreamOutputOpts {
|
|
58
|
-
studioHostPort:
|
|
60
|
+
studioHostPort: StudioReach;
|
|
59
61
|
componentId: string;
|
|
60
62
|
media: "video" | "audio" | "playlist";
|
|
61
63
|
/** Optional — pin the stream's name. For input.srt-listener it is the
|
|
@@ -72,7 +74,7 @@ export declare function assertStreamOutput(opts: AssertStreamOutputOpts): Promis
|
|
|
72
74
|
/** Polls Studio's per-component state for an SRT-listener until at least one
|
|
73
75
|
* stream is reported as connected with a non-zero connectedAt timestamp. */
|
|
74
76
|
export declare function assertSrtConnected(opts: {
|
|
75
|
-
studioHostPort:
|
|
77
|
+
studioHostPort: StudioReach;
|
|
76
78
|
componentId: string;
|
|
77
79
|
timeoutMs?: number;
|
|
78
80
|
intervalMs?: number;
|
|
@@ -89,7 +91,7 @@ export interface CmafMultiVariantState {
|
|
|
89
91
|
* `#EXT-X-MEDIA:TYPE=AUDIO ... URI="<rendition>/norsk.m3u8"` line. The caller
|
|
90
92
|
* passes the set of expected labels; the body must reference every one. */
|
|
91
93
|
export declare function assertMultivariantHasRenditions(opts: {
|
|
92
|
-
studioHostPort:
|
|
94
|
+
studioHostPort: StudioReach;
|
|
93
95
|
componentId: string;
|
|
94
96
|
expectedRenditionLabels: readonly string[];
|
|
95
97
|
timeoutMs?: number;
|
|
@@ -97,7 +99,7 @@ export declare function assertMultivariantHasRenditions(opts: {
|
|
|
97
99
|
resolveFetchUrl?: (url: string) => string;
|
|
98
100
|
}): Promise<void>;
|
|
99
101
|
export declare function assertNoUnmapped(opts: {
|
|
100
|
-
studioHostPort:
|
|
102
|
+
studioHostPort: StudioReach;
|
|
101
103
|
componentId: string;
|
|
102
104
|
timeoutMs?: number;
|
|
103
105
|
intervalMs?: number;
|
package/studio-state.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
// fetchers). Extracted verbatim from the playout copy — the canonical text.
|
|
16
16
|
// Commentary's WHIP-driver tail (RemoteCommentary*, discoverCommentaryWhipUrls,
|
|
17
17
|
// assertCommentaryChannelOccupied) stays local to commentary.
|
|
18
|
+
// Studio's /live/api/components returns each entry as `{ componentId, componentType, capabilities }`
|
|
19
|
+
// — NOT `{ id, type }` (which is what the source-yaml schema uses internally).
|
|
20
|
+
// Verified empirically against a live Studio instance.
|
|
21
|
+
import { studioBaseFrom } from "./container-net.js";
|
|
18
22
|
// The host the studio port is reachable on. Normally the product template
|
|
19
23
|
// publishes it on the loopback of the machine running the tests, so localhost is
|
|
20
24
|
// right. When the test process itself runs inside a container that launches the
|
|
@@ -74,8 +78,8 @@ export function mediaDirectUrl(rawUrl) {
|
|
|
74
78
|
}
|
|
75
79
|
return applyTestHost(rawUrl);
|
|
76
80
|
}
|
|
77
|
-
export async function fetchComponents(
|
|
78
|
-
const r = await fetch(
|
|
81
|
+
export async function fetchComponents(studio) {
|
|
82
|
+
const r = await fetch(`${studioBaseFrom(studio, STUDIO_HOST)}/live/api/components`, {
|
|
79
83
|
signal: AbortSignal.timeout(5000),
|
|
80
84
|
});
|
|
81
85
|
if (r.status === 503)
|
|
@@ -84,8 +88,8 @@ export async function fetchComponents(studioHostPort) {
|
|
|
84
88
|
throw new Error(`GET /live/api/components -> ${r.status}: ${await r.text()}`);
|
|
85
89
|
return (await r.json());
|
|
86
90
|
}
|
|
87
|
-
export async function fetchComponentState(
|
|
88
|
-
const r = await fetch(
|
|
91
|
+
export async function fetchComponentState(studio, componentId) {
|
|
92
|
+
const r = await fetch(`${studioBaseFrom(studio, STUDIO_HOST)}/live/api/${encodeURIComponent(componentId)}/state`, {
|
|
89
93
|
signal: AbortSignal.timeout(5000),
|
|
90
94
|
});
|
|
91
95
|
if (r.status === 503 || r.status === 404)
|
|
@@ -100,8 +104,8 @@ export function isSrtListenerState(value) {
|
|
|
100
104
|
const v = value;
|
|
101
105
|
return Array.isArray(v.connectedStreams) && typeof v.connectedAt === "object" && v.connectedAt !== null;
|
|
102
106
|
}
|
|
103
|
-
async function fetchComponentStreamRoute(
|
|
104
|
-
const r = await fetch(
|
|
107
|
+
async function fetchComponentStreamRoute(studio, componentId, route) {
|
|
108
|
+
const r = await fetch(`${studioBaseFrom(studio, STUDIO_HOST)}/live/api/${encodeURIComponent(componentId)}/streams/${route}`, { signal: AbortSignal.timeout(5000) });
|
|
105
109
|
if (r.status === 503 || r.status === 404)
|
|
106
110
|
return null;
|
|
107
111
|
if (!r.ok)
|