From 511408b85888d99623346cadcdd207b3aaa3c805 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sun, 17 Aug 2025 15:37:42 -0400 Subject: [PATCH] add .nvmrc --- .nvmrc | 1 + .prettierrc | 12 ++ README.md | 10 +- examples/example-1.js | 6 +- package.json | 3 +- src/RtmpPush.ts | 37 ++-- src/index.ts | 6 +- tests/RtmpPush.integration.test.ts | 97 +++++---- tests/RtmpPush.test.ts | 306 ++++++++++++++--------------- 9 files changed, 241 insertions(+), 237 deletions(-) create mode 100644 .nvmrc create mode 100644 .prettierrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..caa814d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "arrowParens": "avoid", + "bracketSameLine": true, + "bracketSpacing": false, + "printWidth": 160, + "semi": true, + "singleAttributePerLine": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false +} diff --git a/README.md b/README.md index 40a1b86..50243e7 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,9 @@ bun install ## Usage ```typescript -import { RtmpPush } from '@techniker-me/rtmp-push'; +import {RtmpPush} from '@techniker-me/rtmp-push'; -const rtmpPush = new RtmpPush( - 'https://example.com/video.ts', - 'rtmp://ingest.example.com:80/ingest' -); +const rtmpPush = new RtmpPush('https://example.com/video.ts', 'rtmp://ingest.example.com:80/ingest'); // Start streaming rtmpPush.start('stream-key', ['h264', 'aac']); @@ -43,16 +40,19 @@ rtmpPush.stop(); This package includes comprehensive tests for all features: ### Run all tests + ```bash bun test ``` ### Run tests in watch mode + ```bash bun test --watch ``` ### Run tests with coverage + ```bash bun test --coverage ``` diff --git a/examples/example-1.js b/examples/example-1.js index 6427fef..f541327 100644 --- a/examples/example-1.js +++ b/examples/example-1.js @@ -1,6 +1,7 @@ import {RtmpPush} from '../dist/node/index.js'; -const mediaSourceUri = 'https://storage.googleapis.com/phenix-testing-assets/NFL/nfl-h264-constrained-baseline-1920x1080p-cbr-10000kbps-29.97fps-aac-lc-255kbps-0h5m0s.mp4'; +const mediaSourceUri = + 'https://storage.googleapis.com/phenix-testing-assets/NFL/nfl-h264-constrained-baseline-1920x1080p-cbr-10000kbps-29.97fps-aac-lc-255kbps-0h5m0s.mp4'; const streamKey = 'K4KZAlSyqPwyunGIbI7JEajen44S1V6xK37FAVhsVqqiGWoGI7DuJOR1ylortFffInHEZ2juAWQqUA2EIFD59uacryVgRnfB'; const ingestUri = `rtmp://ingest-stg.phenixrts.com:80/ingest/${streamKey}`; @@ -8,5 +9,4 @@ const rtmpPush = new RtmpPush(mediaSourceUri, ingestUri).start(mediaSourceUri, [ setTimeout(() => { rtmpPush.stop(); - -}, 60_000) +}, 60_000); diff --git a/package.json b/package.json index d17aa8c..d4247b8 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,14 @@ "module": "src/index.ts", "type": "module", "scripts": { + "format": "prettier --write ./", "test": "bun test", "test:watch": "bun test --watch", "test:coverage": "bun test --coverage", "ci-build": "bun run build:node && bun run build:browser", "build:node": "bun build src/index.ts --outdir dist/node --target node --format esm --minify --production", "build:browser": "bun build src/index.ts --outdir dist/browser --target browser --format esm --minify --production" - }, + }, "devDependencies": { "@types/bun": "latest", "@types/node": "^20.0.0" diff --git a/src/RtmpPush.ts b/src/RtmpPush.ts index d368e80..2344e11 100644 --- a/src/RtmpPush.ts +++ b/src/RtmpPush.ts @@ -1,4 +1,4 @@ -import {spawn, ChildProcess } from 'node:child_process'; +import {spawn, ChildProcess} from 'node:child_process'; export class RtmpPush { private _mediaSourceUri: string; @@ -26,16 +26,24 @@ export class RtmpPush { 'ffmpeg', '-re', '-hide_banner', - '-stream_loop', '-1', + '-stream_loop', + '-1', '-y', - '-flags', 'low_delay', - '-fflags', '+nobuffer+flush_packets', - '-i', mediaSourceUri, - '-c:a', 'copy', - '-c:v', 'copy', - '-flush_packets', '1', + '-flags', + 'low_delay', + '-fflags', + '+nobuffer+flush_packets', + '-i', + mediaSourceUri, + '-c:a', + 'copy', + '-c:v', + 'copy', + '-flush_packets', + '1', '-copyts', - '-f', 'flv', + '-f', + 'flv', ingestUri ]; @@ -43,31 +51,30 @@ export class RtmpPush { if (!command[0]) { throw new Error('Invalid command: ffmpeg not found'); } - + this._activeProcess = spawn(command[0], command.slice(1)); if (!this._activeProcess) { throw new Error('Failed to spawn ffmpeg process'); } - this._activeProcess.stdout?.on('data', (data) => { + this._activeProcess.stdout?.on('data', data => { console.log(`stdout: ${data}`); }); - this._activeProcess.stderr?.on('data', (data) => { + this._activeProcess.stderr?.on('data', data => { console.log(`stderr: ${data}`); }); - this._activeProcess.on('close', (code) => { + this._activeProcess.on('close', code => { console.log(`child process exited with code ${code}`); this._activeProcess = null; }); - this._activeProcess.on('error', (error) => { + this._activeProcess.on('error', error => { console.error('ffmpeg process error:', error); this._activeProcess = null; }); - } catch (error) { console.error('Failed to start RTMP push:', error); throw error; diff --git a/src/index.ts b/src/index.ts index 7e0546b..a159806 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { RtmpPush } from "./RtmpPush.js"; +import {RtmpPush} from './RtmpPush.js'; -export { RtmpPush }; -export default {RtmpPush}; \ No newline at end of file +export {RtmpPush}; +export default {RtmpPush}; diff --git a/tests/RtmpPush.integration.test.ts b/tests/RtmpPush.integration.test.ts index 83935f6..6e1cef8 100644 --- a/tests/RtmpPush.integration.test.ts +++ b/tests/RtmpPush.integration.test.ts @@ -1,10 +1,11 @@ -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { RtmpPush } from "../src/RtmpPush.js"; +import {describe, it, expect, beforeEach, afterEach} from 'bun:test'; +import {RtmpPush} from '../src/RtmpPush.js'; -describe("RtmpPush Integration Tests", () => { +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"; + 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); @@ -17,45 +18,45 @@ describe("RtmpPush Integration Tests", () => { } }); - describe("Real Process Management", () => { - it("should create instance with real URIs", () => { + 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)", () => { + 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"]; - + 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); + console.log('Skipping integration test - ffmpeg may not be available:', error); expect(true).toBe(true); // Mark as passed } }); - it("should handle invalid ffmpeg gracefully", () => { + it('should handle invalid ffmpeg gracefully', () => { // Test with a non-existent command - const invalidRtmpPush = new RtmpPush("invalid://uri", "invalid://rtmp"); - + const invalidRtmpPush = new RtmpPush('invalid://uri', 'invalid://rtmp'); + try { - invalidRtmpPush.start("test", ["h264"]); + invalidRtmpPush.start('test', ['h264']); expect(invalidRtmpPush.isRunning()).toBe(false); } catch (error) { // Expected to fail @@ -64,48 +65,40 @@ describe("RtmpPush Integration Tests", () => { }); }); - describe("URI Construction", () => { - it("should construct valid RTMP URIs", () => { - const streamKey = "test-stream-key"; - const capabilities = ["h264", "aac", "stereo"]; - + 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="); + 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" - ]; - + 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 capabilities = ['h264']; const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`; - + expect(expectedUri).toContain(streamKey); - expect(expectedUri).toContain(";capabilities=h264;tags="); + 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"; + 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"]); + invalidRtmpPush.start('test', ['h264']); // If it doesn't throw, it should at least not be running expect(invalidRtmpPush.isRunning()).toBe(false); } catch (error) { @@ -114,12 +107,12 @@ describe("RtmpPush Integration Tests", () => { } }); - it("should handle invalid RTMP URIs", () => { - const invalidRtmpUri = "invalid://rtmp-uri"; + it('should handle invalid RTMP URIs', () => { + const invalidRtmpUri = 'invalid://rtmp-uri'; const invalidRtmpPush = new RtmpPush(testMediaSourceUri, invalidRtmpUri); - + try { - invalidRtmpPush.start("test", ["h264"]); + invalidRtmpPush.start('test', ['h264']); // If it doesn't throw, it should at least not be running expect(invalidRtmpPush.isRunning()).toBe(false); } catch (error) { diff --git a/tests/RtmpPush.test.ts b/tests/RtmpPush.test.ts index 938d552..1eb935e 100644 --- a/tests/RtmpPush.test.ts +++ b/tests/RtmpPush.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; -import { RtmpPush } from "../src/RtmpPush.js"; +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(() => {}) }, + stdout: {on: mock(() => {})}, + stderr: {on: mock(() => {})}, on: mock(() => {}), kill: mock(() => {}), killed: false @@ -12,14 +12,14 @@ const mockProcess = { const mockSpawn = mock(() => mockProcess); -mock.module("node:child_process", () => ({ +mock.module('node:child_process', () => ({ spawn: mockSpawn })); -describe("RtmpPush", () => { +describe('RtmpPush', () => { let rtmpPush: RtmpPush; - const mockMediaSourceUri = "https://example.com/test-video.ts"; - const mockRtmpIngestUri = "rtmp://ingest.example.com:80/ingest"; + const mockMediaSourceUri = 'https://example.com/test-video.ts'; + const mockRtmpIngestUri = 'rtmp://ingest.example.com:80/ingest'; beforeEach(() => { rtmpPush = new RtmpPush(mockMediaSourceUri, mockRtmpIngestUri); @@ -38,272 +38,262 @@ describe("RtmpPush", () => { } }); - describe("Constructor", () => { - it("should create instance with correct properties", () => { + 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(""); + it('should handle empty strings', () => { + const emptyRtmpPush = new RtmpPush('', ''); + expect(emptyRtmpPush.mediaSourceUri).toBe(''); + expect(emptyRtmpPush.rtmpIngestUri).toBe(''); }); }); - describe("Getters", () => { - it("should return correct mediaSourceUri", () => { + describe('Getters', () => { + it('should return correct mediaSourceUri', () => { expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri); }); - it("should return correct rtmpIngestUri", () => { + it('should return correct rtmpIngestUri', () => { expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri); }); - it("should return immutable values", () => { + 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"]; - + 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"]; - + it('should construct correct ffmpeg command', () => { + const streamKey = 'test-stream'; + const capabilities = ['h264', 'aac']; + rtmpPush.start(streamKey, capabilities); - + expect(mockSpawn).toHaveBeenCalledWith( - "ffmpeg", + '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" + '-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"]; - + 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]) - ); + + expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri])); }); - it("should handle empty capabilities array", () => { - const streamKey = "test-stream"; + 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]) - ); + + expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri])); }); - it("should handle single capability", () => { - const streamKey = "test-stream"; - const capabilities = ["h264"]; - + 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]) - ); + + expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri])); }); - it("should set up process event handlers", () => { - const streamKey = "test-stream"; - const capabilities = ["h264"]; - + 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)); + + 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"]; - + 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"]; - + 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", () => { + 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", () => { + it('should be chainable', () => { const result = rtmpPush.stop(); expect(result).toBe(rtmpPush); }); }); - describe("isRunning() method", () => { - it("should return false when no process is started", () => { + 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"]; - + 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"]; - + 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"]; - + 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]; + 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"]; - + 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]; + 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")); + 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"]; - + 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]) - ); + + 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"]; - + 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]) - ); + + 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); - + 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"]; - + 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]) - ); + + expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri])); }); }); });