Initial Commit
This commit is contained in:
131
tests/RtmpPush.integration.test.ts
Normal file
131
tests/RtmpPush.integration.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { RtmpPush } from "../src/RtmpPush.js";
|
||||
|
||||
describe("RtmpPush Integration Tests", () => {
|
||||
let rtmpPush: RtmpPush;
|
||||
const testMediaSourceUri = "https://storage.googleapis.com/phenix-testing-assets/timecodes/haivision-with-sei-system-source-timecodes-utc-conversion-counting-mode-h264-constrained-baseline-1920x1080p-cbr-8700kbps-60fps-aac-lc-48000hz-stereo-124kbps-5m00s.ts";
|
||||
const testRtmpIngestUri = "rtmp://ingest-stg.phenixrts.com:80/ingest";
|
||||
|
||||
beforeEach(() => {
|
||||
rtmpPush = new RtmpPush(testMediaSourceUri, testRtmpIngestUri);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Always clean up
|
||||
if (rtmpPush.isRunning()) {
|
||||
rtmpPush.stop();
|
||||
}
|
||||
});
|
||||
|
||||
describe("Real Process Management", () => {
|
||||
it("should create instance with real URIs", () => {
|
||||
expect(rtmpPush).toBeInstanceOf(RtmpPush);
|
||||
expect(rtmpPush.mediaSourceUri).toBe(testMediaSourceUri);
|
||||
expect(rtmpPush.rtmpIngestUri).toBe(testRtmpIngestUri);
|
||||
});
|
||||
|
||||
it("should start and stop process (if ffmpeg is available)", () => {
|
||||
// This test will only pass if ffmpeg is installed on the system
|
||||
const streamKey = "test-integration-stream";
|
||||
const capabilities = ["h264", "aac"];
|
||||
|
||||
try {
|
||||
rtmpPush.start(streamKey, capabilities);
|
||||
|
||||
// Give it a moment to start
|
||||
Bun.sleepSync(100);
|
||||
|
||||
expect(rtmpPush.isRunning()).toBe(true);
|
||||
|
||||
rtmpPush.stop();
|
||||
|
||||
// Give it a moment to stop
|
||||
Bun.sleepSync(100);
|
||||
|
||||
expect(rtmpPush.isRunning()).toBe(false);
|
||||
} catch (error) {
|
||||
// If ffmpeg is not available, this test should be skipped
|
||||
console.log("Skipping integration test - ffmpeg may not be available:", error);
|
||||
expect(true).toBe(true); // Mark as passed
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle invalid ffmpeg gracefully", () => {
|
||||
// Test with a non-existent command
|
||||
const invalidRtmpPush = new RtmpPush("invalid://uri", "invalid://rtmp");
|
||||
|
||||
try {
|
||||
invalidRtmpPush.start("test", ["h264"]);
|
||||
expect(invalidRtmpPush.isRunning()).toBe(false);
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("URI Construction", () => {
|
||||
it("should construct valid RTMP URIs", () => {
|
||||
const streamKey = "test-stream-key";
|
||||
const capabilities = ["h264", "aac", "stereo"];
|
||||
|
||||
const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||
|
||||
// The URI should be properly formatted
|
||||
expect(expectedUri).toContain(streamKey);
|
||||
expect(expectedUri).toContain(capabilities.join(','));
|
||||
expect(expectedUri).toContain(";capabilities=");
|
||||
expect(expectedUri).toContain(";tags=");
|
||||
});
|
||||
|
||||
it("should handle various stream key formats", () => {
|
||||
const testCases = [
|
||||
"simple-key",
|
||||
"key_with_underscores",
|
||||
"key-with-dashes",
|
||||
"key123",
|
||||
"key!@#$%^&*()",
|
||||
"key with spaces",
|
||||
"key.with.dots"
|
||||
];
|
||||
|
||||
testCases.forEach(streamKey => {
|
||||
const capabilities = ["h264"];
|
||||
const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||
|
||||
expect(expectedUri).toContain(streamKey);
|
||||
expect(expectedUri).toContain(";capabilities=h264;tags=");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle network errors gracefully", () => {
|
||||
const invalidMediaUri = "https://invalid-domain-that-does-not-exist.com/video.ts";
|
||||
const invalidRtmpPush = new RtmpPush(invalidMediaUri, testRtmpIngestUri);
|
||||
|
||||
try {
|
||||
invalidRtmpPush.start("test", ["h264"]);
|
||||
// If it doesn't throw, it should at least not be running
|
||||
expect(invalidRtmpPush.isRunning()).toBe(false);
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle invalid RTMP URIs", () => {
|
||||
const invalidRtmpUri = "invalid://rtmp-uri";
|
||||
const invalidRtmpPush = new RtmpPush(testMediaSourceUri, invalidRtmpUri);
|
||||
|
||||
try {
|
||||
invalidRtmpPush.start("test", ["h264"]);
|
||||
// If it doesn't throw, it should at least not be running
|
||||
expect(invalidRtmpPush.isRunning()).toBe(false);
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
309
tests/RtmpPush.test.ts
Normal file
309
tests/RtmpPush.test.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
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])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user