stream-doctor 0.0.3 → 0.0.4

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/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # stream-doctor
2
+
3
+ Automated end-to-end testing for video infrastructure: publish a stream into
4
+ your pipeline, watch what comes out the other end and assert on the metrics.
5
+ An early proof of concept, currently a single RTMP to HLS scenario with a
6
+ single metric, the audio/video drift.
7
+
8
+ This is the TypeScript client for the StreamDoctor daemon. It brings the
9
+ daemon binary along, as an optional dependency on `@stream-doctor/<platform>`.
10
+ Setup, usage and the roadmap are described in the
11
+ [StreamDoctor repository](https://github.com/membraneframework-labs/stream_doctor).
@@ -6,7 +6,7 @@ export interface StreamerStatus {
6
6
  rtmp_url: string;
7
7
  status: Status;
8
8
  error: string | null;
9
- frames_sent: number;
9
+ live: boolean;
10
10
  }
11
11
  export interface AvDrift {
12
12
  drift_ms: number | null;
@@ -35,7 +35,8 @@ export declare function session({ server, binary, }?: {
35
35
  declare class Session {
36
36
  server: string;
37
37
  child: ChildProcess | null;
38
- constructor(server: string, child: ChildProcess | null);
38
+ logFile: string | null;
39
+ constructor(server: string, child: ChildProcess | null, logFile?: string | null);
39
40
  publish(rtmpUrl: string, { file }?: {
40
41
  file?: string;
41
42
  }): Streamer;
@@ -1,9 +1,11 @@
1
1
  // Client for the stream_doctor server. `session()` spawns the binary (the one
2
2
  // bundled for this platform, or `binary`) when no server is listening;
3
- // `session.close()` stops it.
3
+ // `session.close()` stops it. The daemon's output goes to `session.logFile`.
4
4
  import { spawn } from "node:child_process";
5
5
  import fs from "node:fs";
6
6
  import { createRequire } from "node:module";
7
+ import os from "node:os";
8
+ import path from "node:path";
7
9
  const DEFAULT_SERVER = "http://localhost:4040";
8
10
  const PLATFORM_PACKAGES = {
9
11
  "darwin-arm64": "@stream-doctor/darwin-arm64",
@@ -48,23 +50,41 @@ export async function session({ server = DEFAULT_SERVER, binary, } = {}) {
48
50
  return new Session(server, null);
49
51
  }
50
52
  catch (e) {
51
- binary ??= bundledBinary() ?? undefined;
53
+ let installDir;
54
+ const logFile = path.join(os.tmpdir(), "stream_doctor.log");
52
55
  if (!binary) {
53
- throw new Error(`${e.message}; no stream_doctor binary bundled for ${process.platform}-${process.arch}, pass one with \`binary\``);
56
+ binary = bundledBinary() ?? undefined;
57
+ if (!binary) {
58
+ throw new Error(`${e.message}; no stream_doctor binary bundled for ${process.platform}-${process.arch}, pass one with \`binary\``);
59
+ }
60
+ // Burrito unpacks the payload once per release name + ERTS + app version,
61
+ // none of which change between npm releases, so its default cache under
62
+ // ~/.local/share would keep serving the previous package's daemon.
63
+ installDir = path.join(path.dirname(binary), "..", ".burrito");
54
64
  }
65
+ const child = await spawnServer(binary, server, logFile, installDir);
66
+ return new Session(server, child, logFile);
55
67
  }
56
- return new Session(server, await spawnServer(binary, server));
57
68
  }
58
- async function spawnServer(binary, server) {
69
+ async function spawnServer(binary, server, logFile, installDir) {
59
70
  if (!fs.existsSync(binary)) {
60
71
  throw new Error(`${binary} not found, build it with: MIX_ENV=prod mix release`);
61
72
  }
62
- console.log(`starting ${binary}`);
73
+ const env = {
74
+ ...process.env,
75
+ // the daemon quits once its stdin (the pipe below) breaks, i.e. when this
76
+ // process dies, however it dies
77
+ STREAM_DOCTOR_EXIT_ON_STDIN_EOF: "1",
78
+ ...(installDir ? { STREAM_DOCTOR_INSTALL_DIR: installDir } : {}),
79
+ };
80
+ const log = fs.openSync(logFile, "a");
63
81
  // own process group, so that killing the burrito launcher takes the BEAM with it
64
- const child = spawn(binary, [], { stdio: ["ignore", "inherit", "inherit"], detached: true });
82
+ const child = spawn(binary, [], { stdio: ["pipe", log, log], detached: true, env });
83
+ child.on("exit", () => fs.closeSync(log));
65
84
  for (const deadline = Date.now() + 120_000; Date.now() < deadline;) {
66
- if (child.exitCode !== null)
67
- throw new Error(`server exited with ${child.exitCode}`);
85
+ if (child.exitCode !== null) {
86
+ throw new Error(`server exited with ${child.exitCode}, see ${logFile}`);
87
+ }
68
88
  await sleep(1000);
69
89
  try {
70
90
  await api("GET", "/status", null, server);
@@ -72,14 +92,16 @@ async function spawnServer(binary, server) {
72
92
  }
73
93
  catch { }
74
94
  }
75
- throw new Error("server didn't come up in 2 minutes");
95
+ throw new Error(`server didn't come up in 2 minutes, see ${logFile}`);
76
96
  }
77
97
  class Session {
78
98
  server;
79
99
  child;
80
- constructor(server, child) {
100
+ logFile;
101
+ constructor(server, child, logFile = null) {
81
102
  this.server = server;
82
103
  this.child = child;
104
+ this.logFile = logFile;
83
105
  }
84
106
  publish(rtmpUrl, { file = "test.mp4" } = {}) {
85
107
  const ready = api("POST", "/streamer", { input: file, rtmp_url: rtmpUrl }, this.server);
@@ -120,7 +142,7 @@ class Streamer {
120
142
  const deadline = Date.now() + timeoutMs;
121
143
  for (;;) {
122
144
  const streamer = await api("GET", "/streamer", null, this.server);
123
- if (streamer.frames_sent > 0)
145
+ if (streamer.live)
124
146
  return streamer;
125
147
  if (TERMINAL_STATUSES.includes(streamer.status)) {
126
148
  throw new Error(`streamer ${streamer.status} before going live${streamer.error ? `: ${streamer.error}` : ""}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-doctor",
3
- "version": "0.0.3",
4
- "description": "Measures the audio/video drift of a live stream, end to end",
3
+ "version": "0.0.4",
4
+ "description": "Automated end-to-end testing for video infrastructure (early proof of concept)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -20,8 +20,8 @@
20
20
  "build": "tsc -p tsconfig.build.json",
21
21
  "prepack": "npm run build",
22
22
  "test": "node --test src/*.test.ts",
23
- "lint": "tsc --noEmit && eslint src examples && prettier --check src examples",
24
- "format": "prettier --write src examples"
23
+ "lint": "tsc --noEmit && eslint src && prettier --check src",
24
+ "format": "prettier --write src"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@eslint/js": "^9.0.0",
@@ -33,8 +33,8 @@
33
33
  "typescript-eslint": "^8.70.0"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@stream-doctor/darwin-arm64": "0.0.3",
37
- "@stream-doctor/linux-arm64": "0.0.3",
38
- "@stream-doctor/linux-x64": "0.0.3"
36
+ "@stream-doctor/darwin-arm64": "0.0.4",
37
+ "@stream-doctor/linux-arm64": "0.0.4",
38
+ "@stream-doctor/linux-x64": "0.0.4"
39
39
  }
40
40
  }