Files
ToolsRtmpPush/tests/RtmpPush.test.ts
2025-08-17 11:41:43 -04:00

310 lines
9.4 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { RtmpPush } from "../src/RtmpPush.js";
// Mock the child_process module
const mockProcess = {
stdout: { on: mock(() => {}) },
stderr: { on: mock(() => {}) },
on: mock(() => {}),
kill: mock(() => {}),
killed: false
};
const mockSpawn = mock(() => mockProcess);
mock.module("node:child_process", () => ({
spawn: mockSpawn
}));
describe("RtmpPush", () => {
let rtmpPush: RtmpPush;
const mockMediaSourceUri = "https://example.com/test-video.ts";
const mockRtmpIngestUri = "rtmp://ingest.example.com:80/ingest";
beforeEach(() => {
rtmpPush = new RtmpPush(mockMediaSourceUri, mockRtmpIngestUri);
// Reset mock calls
mockSpawn.mockClear();
mockProcess.stdout.on.mockClear();
mockProcess.stderr.on.mockClear();
mockProcess.on.mockClear();
mockProcess.kill.mockClear();
});
afterEach(() => {
// Clean up any running processes
if (rtmpPush.isRunning()) {
rtmpPush.stop();
}
});
describe("Constructor", () => {
it("should create instance with correct properties", () => {
expect(rtmpPush).toBeInstanceOf(RtmpPush);
expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri);
expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri);
});
it("should handle empty strings", () => {
const emptyRtmpPush = new RtmpPush("", "");
expect(emptyRtmpPush.mediaSourceUri).toBe("");
expect(emptyRtmpPush.rtmpIngestUri).toBe("");
});
});
describe("Getters", () => {
it("should return correct mediaSourceUri", () => {
expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri);
});
it("should return correct rtmpIngestUri", () => {
expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri);
});
it("should return immutable values", () => {
const originalMediaUri = rtmpPush.mediaSourceUri;
const originalRtmpUri = rtmpPush.rtmpIngestUri;
// These should not change the internal state
expect(rtmpPush.mediaSourceUri).toBe(originalMediaUri);
expect(rtmpPush.rtmpIngestUri).toBe(originalRtmpUri);
});
});
describe("start() method", () => {
it("should start RTMP push process with correct parameters", () => {
const streamKey = "test-stream-123";
const capabilities = ["h264", "aac"];
const result = rtmpPush.start(streamKey, capabilities);
expect(result).toBe(rtmpPush);
expect(rtmpPush.isRunning()).toBe(true);
});
it("should construct correct ffmpeg command", () => {
const streamKey = "test-stream";
const capabilities = ["h264", "aac"];
rtmpPush.start(streamKey, capabilities);
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([
"-re",
"-hide_banner",
"-stream_loop", "-1",
"-y",
"-flags", "low_delay",
"-fflags", "+nobuffer+flush_packets",
"-i", mockMediaSourceUri,
"-c:a", "copy",
"-c:v", "copy",
"-flush_packets", "1",
"-copyts",
"-f", "flv"
])
);
});
it("should construct correct ingest URI", () => {
const streamKey = "test-stream";
const capabilities = ["h264", "aac"];
rtmpPush.start(streamKey, capabilities);
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([expectedIngestUri])
);
});
it("should handle empty capabilities array", () => {
const streamKey = "test-stream";
const capabilities: string[] = [];
rtmpPush.start(streamKey, capabilities);
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=;tags=`;
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([expectedIngestUri])
);
});
it("should handle single capability", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=h264;tags=`;
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([expectedIngestUri])
);
});
it("should set up process event handlers", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
expect(mockProcess.stdout.on).toHaveBeenCalledWith("data", expect.any(Function));
expect(mockProcess.stderr.on).toHaveBeenCalledWith("data", expect.any(Function));
expect(mockProcess.on).toHaveBeenCalledWith("close", expect.any(Function));
expect(mockProcess.on).toHaveBeenCalledWith("error", expect.any(Function));
});
it("should be chainable", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
const result = rtmpPush.start(streamKey, capabilities);
expect(result).toBe(rtmpPush);
});
});
describe("stop() method", () => {
it("should stop running process", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
expect(rtmpPush.isRunning()).toBe(true);
const result = rtmpPush.stop();
expect(result).toBe(rtmpPush);
expect(rtmpPush.isRunning()).toBe(false);
});
it("should handle stopping when no process is running", () => {
expect(rtmpPush.isRunning()).toBe(false);
const result = rtmpPush.stop();
expect(result).toBe(rtmpPush);
expect(rtmpPush.isRunning()).toBe(false);
});
it("should be chainable", () => {
const result = rtmpPush.stop();
expect(result).toBe(rtmpPush);
});
});
describe("isRunning() method", () => {
it("should return false when no process is started", () => {
expect(rtmpPush.isRunning()).toBe(false);
});
it("should return true when process is running", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
expect(rtmpPush.isRunning()).toBe(true);
});
it("should return false after process is stopped", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
expect(rtmpPush.isRunning()).toBe(true);
rtmpPush.stop();
expect(rtmpPush.isRunning()).toBe(false);
});
});
describe("Process lifecycle", () => {
it("should handle process close event", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
// Simulate process close
const closeHandler = mockProcess.on.mock.calls.find(call => call[0] === "close")?.[1];
if (closeHandler && typeof closeHandler === 'function') {
closeHandler(0);
}
expect(rtmpPush.isRunning()).toBe(false);
});
it("should handle process error event", () => {
const streamKey = "test-stream";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
// Simulate process error
const errorHandler = mockProcess.on.mock.calls.find(call => call[0] === "error")?.[1];
if (errorHandler && typeof errorHandler === 'function') {
// Call the error handler with a mock error
errorHandler(new Error("Process error"));
}
// The error handler should set the process to null
expect(rtmpPush.isRunning()).toBe(false);
});
});
describe("Edge cases", () => {
it("should handle special characters in stream key", () => {
const streamKey = "test-stream-with-special-chars!@#$%^&*()";
const capabilities = ["h264"];
rtmpPush.start(streamKey, capabilities);
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([expectedIngestUri])
);
});
it("should handle special characters in capabilities", () => {
const streamKey = "test-stream";
const capabilities = ["h.264", "aac-lc", "stereo_audio"];
rtmpPush.start(streamKey, capabilities);
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([expectedIngestUri])
);
});
it("should handle very long URIs", () => {
const longMediaUri = "https://example.com/" + "a".repeat(1000) + ".ts";
const longRtmpUri = "rtmp://ingest.example.com:80/ingest/" + "b".repeat(1000);
const longRtmpPush = new RtmpPush(longMediaUri, longRtmpUri);
const streamKey = "test-stream";
const capabilities = ["h264"];
longRtmpPush.start(streamKey, capabilities);
const expectedIngestUri = `${longRtmpUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
expect(mockSpawn).toHaveBeenCalledWith(
"ffmpeg",
expect.arrayContaining([expectedIngestUri])
);
});
});
});