add .nvmrc
This commit is contained in:
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"printWidth": 160,
|
||||||
|
"semi": true,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
@@ -21,10 +21,7 @@ bun install
|
|||||||
```typescript
|
```typescript
|
||||||
import {RtmpPush} from '@techniker-me/rtmp-push';
|
import {RtmpPush} from '@techniker-me/rtmp-push';
|
||||||
|
|
||||||
const rtmpPush = new RtmpPush(
|
const rtmpPush = new RtmpPush('https://example.com/video.ts', 'rtmp://ingest.example.com:80/ingest');
|
||||||
'https://example.com/video.ts',
|
|
||||||
'rtmp://ingest.example.com:80/ingest'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start streaming
|
// Start streaming
|
||||||
rtmpPush.start('stream-key', ['h264', 'aac']);
|
rtmpPush.start('stream-key', ['h264', 'aac']);
|
||||||
@@ -43,16 +40,19 @@ rtmpPush.stop();
|
|||||||
This package includes comprehensive tests for all features:
|
This package includes comprehensive tests for all features:
|
||||||
|
|
||||||
### Run all tests
|
### Run all tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun test
|
bun test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run tests in watch mode
|
### Run tests in watch mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun test --watch
|
bun test --watch
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run tests with coverage
|
### Run tests with coverage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun test --coverage
|
bun test --coverage
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {RtmpPush} from '../dist/node/index.js';
|
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 streamKey = 'K4KZAlSyqPwyunGIbI7JEajen44S1V6xK37FAVhsVqqiGWoGI7DuJOR1ylortFffInHEZ2juAWQqUA2EIFD59uacryVgRnfB';
|
||||||
const ingestUri = `rtmp://ingest-stg.phenixrts.com:80/ingest/${streamKey}`;
|
const ingestUri = `rtmp://ingest-stg.phenixrts.com:80/ingest/${streamKey}`;
|
||||||
|
|
||||||
@@ -8,5 +9,4 @@ const rtmpPush = new RtmpPush(mediaSourceUri, ingestUri).start(mediaSourceUri, [
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
rtmpPush.stop();
|
rtmpPush.stop();
|
||||||
|
}, 60_000);
|
||||||
}, 60_000)
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"format": "prettier --write ./",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:watch": "bun test --watch",
|
"test:watch": "bun test --watch",
|
||||||
"test:coverage": "bun test --coverage",
|
"test:coverage": "bun test --coverage",
|
||||||
|
|||||||
@@ -26,16 +26,24 @@ export class RtmpPush {
|
|||||||
'ffmpeg',
|
'ffmpeg',
|
||||||
'-re',
|
'-re',
|
||||||
'-hide_banner',
|
'-hide_banner',
|
||||||
'-stream_loop', '-1',
|
'-stream_loop',
|
||||||
|
'-1',
|
||||||
'-y',
|
'-y',
|
||||||
'-flags', 'low_delay',
|
'-flags',
|
||||||
'-fflags', '+nobuffer+flush_packets',
|
'low_delay',
|
||||||
'-i', mediaSourceUri,
|
'-fflags',
|
||||||
'-c:a', 'copy',
|
'+nobuffer+flush_packets',
|
||||||
'-c:v', 'copy',
|
'-i',
|
||||||
'-flush_packets', '1',
|
mediaSourceUri,
|
||||||
|
'-c:a',
|
||||||
|
'copy',
|
||||||
|
'-c:v',
|
||||||
|
'copy',
|
||||||
|
'-flush_packets',
|
||||||
|
'1',
|
||||||
'-copyts',
|
'-copyts',
|
||||||
'-f', 'flv',
|
'-f',
|
||||||
|
'flv',
|
||||||
ingestUri
|
ingestUri
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -50,24 +58,23 @@ export class RtmpPush {
|
|||||||
throw new Error('Failed to spawn ffmpeg process');
|
throw new Error('Failed to spawn ffmpeg process');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._activeProcess.stdout?.on('data', (data) => {
|
this._activeProcess.stdout?.on('data', data => {
|
||||||
console.log(`stdout: ${data}`);
|
console.log(`stdout: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._activeProcess.stderr?.on('data', (data) => {
|
this._activeProcess.stderr?.on('data', data => {
|
||||||
console.log(`stderr: ${data}`);
|
console.log(`stderr: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._activeProcess.on('close', (code) => {
|
this._activeProcess.on('close', code => {
|
||||||
console.log(`child process exited with code ${code}`);
|
console.log(`child process exited with code ${code}`);
|
||||||
this._activeProcess = null;
|
this._activeProcess = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._activeProcess.on('error', (error) => {
|
this._activeProcess.on('error', error => {
|
||||||
console.error('ffmpeg process error:', error);
|
console.error('ffmpeg process error:', error);
|
||||||
this._activeProcess = null;
|
this._activeProcess = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start RTMP push:', error);
|
console.error('Failed to start RTMP push:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RtmpPush } from "./RtmpPush.js";
|
import {RtmpPush} from './RtmpPush.js';
|
||||||
|
|
||||||
export {RtmpPush};
|
export {RtmpPush};
|
||||||
export default {RtmpPush};
|
export default {RtmpPush};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
import {describe, it, expect, beforeEach, afterEach} from 'bun:test';
|
||||||
import { RtmpPush } from "../src/RtmpPush.js";
|
import {RtmpPush} from '../src/RtmpPush.js';
|
||||||
|
|
||||||
describe("RtmpPush Integration Tests", () => {
|
describe('RtmpPush Integration Tests', () => {
|
||||||
let rtmpPush: RtmpPush;
|
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 testMediaSourceUri =
|
||||||
const testRtmpIngestUri = "rtmp://ingest-stg.phenixrts.com:80/ingest";
|
'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(() => {
|
beforeEach(() => {
|
||||||
rtmpPush = new RtmpPush(testMediaSourceUri, testRtmpIngestUri);
|
rtmpPush = new RtmpPush(testMediaSourceUri, testRtmpIngestUri);
|
||||||
@@ -17,17 +18,17 @@ describe("RtmpPush Integration Tests", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Real Process Management", () => {
|
describe('Real Process Management', () => {
|
||||||
it("should create instance with real URIs", () => {
|
it('should create instance with real URIs', () => {
|
||||||
expect(rtmpPush).toBeInstanceOf(RtmpPush);
|
expect(rtmpPush).toBeInstanceOf(RtmpPush);
|
||||||
expect(rtmpPush.mediaSourceUri).toBe(testMediaSourceUri);
|
expect(rtmpPush.mediaSourceUri).toBe(testMediaSourceUri);
|
||||||
expect(rtmpPush.rtmpIngestUri).toBe(testRtmpIngestUri);
|
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
|
// This test will only pass if ffmpeg is installed on the system
|
||||||
const streamKey = "test-integration-stream";
|
const streamKey = 'test-integration-stream';
|
||||||
const capabilities = ["h264", "aac"];
|
const capabilities = ['h264', 'aac'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
@@ -45,17 +46,17 @@ describe("RtmpPush Integration Tests", () => {
|
|||||||
expect(rtmpPush.isRunning()).toBe(false);
|
expect(rtmpPush.isRunning()).toBe(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If ffmpeg is not available, this test should be skipped
|
// 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
|
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
|
// Test with a non-existent command
|
||||||
const invalidRtmpPush = new RtmpPush("invalid://uri", "invalid://rtmp");
|
const invalidRtmpPush = new RtmpPush('invalid://uri', 'invalid://rtmp');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
invalidRtmpPush.start("test", ["h264"]);
|
invalidRtmpPush.start('test', ['h264']);
|
||||||
expect(invalidRtmpPush.isRunning()).toBe(false);
|
expect(invalidRtmpPush.isRunning()).toBe(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Expected to fail
|
// Expected to fail
|
||||||
@@ -64,48 +65,40 @@ describe("RtmpPush Integration Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("URI Construction", () => {
|
describe('URI Construction', () => {
|
||||||
it("should construct valid RTMP URIs", () => {
|
it('should construct valid RTMP URIs', () => {
|
||||||
const streamKey = "test-stream-key";
|
const streamKey = 'test-stream-key';
|
||||||
const capabilities = ["h264", "aac", "stereo"];
|
const capabilities = ['h264', 'aac', 'stereo'];
|
||||||
|
|
||||||
const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||||
|
|
||||||
// The URI should be properly formatted
|
// The URI should be properly formatted
|
||||||
expect(expectedUri).toContain(streamKey);
|
expect(expectedUri).toContain(streamKey);
|
||||||
expect(expectedUri).toContain(capabilities.join(','));
|
expect(expectedUri).toContain(capabilities.join(','));
|
||||||
expect(expectedUri).toContain(";capabilities=");
|
expect(expectedUri).toContain(';capabilities=');
|
||||||
expect(expectedUri).toContain(";tags=");
|
expect(expectedUri).toContain(';tags=');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle various stream key formats", () => {
|
it('should handle various stream key formats', () => {
|
||||||
const testCases = [
|
const testCases = ['simple-key', 'key_with_underscores', 'key-with-dashes', 'key123', 'key!@#$%^&*()', 'key with spaces', 'key.with.dots'];
|
||||||
"simple-key",
|
|
||||||
"key_with_underscores",
|
|
||||||
"key-with-dashes",
|
|
||||||
"key123",
|
|
||||||
"key!@#$%^&*()",
|
|
||||||
"key with spaces",
|
|
||||||
"key.with.dots"
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(streamKey => {
|
testCases.forEach(streamKey => {
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
const expectedUri = `${testRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||||
|
|
||||||
expect(expectedUri).toContain(streamKey);
|
expect(expectedUri).toContain(streamKey);
|
||||||
expect(expectedUri).toContain(";capabilities=h264;tags=");
|
expect(expectedUri).toContain(';capabilities=h264;tags=');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Error Handling", () => {
|
describe('Error Handling', () => {
|
||||||
it("should handle network errors gracefully", () => {
|
it('should handle network errors gracefully', () => {
|
||||||
const invalidMediaUri = "https://invalid-domain-that-does-not-exist.com/video.ts";
|
const invalidMediaUri = 'https://invalid-domain-that-does-not-exist.com/video.ts';
|
||||||
const invalidRtmpPush = new RtmpPush(invalidMediaUri, testRtmpIngestUri);
|
const invalidRtmpPush = new RtmpPush(invalidMediaUri, testRtmpIngestUri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
invalidRtmpPush.start("test", ["h264"]);
|
invalidRtmpPush.start('test', ['h264']);
|
||||||
// If it doesn't throw, it should at least not be running
|
// If it doesn't throw, it should at least not be running
|
||||||
expect(invalidRtmpPush.isRunning()).toBe(false);
|
expect(invalidRtmpPush.isRunning()).toBe(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -114,12 +107,12 @@ describe("RtmpPush Integration Tests", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle invalid RTMP URIs", () => {
|
it('should handle invalid RTMP URIs', () => {
|
||||||
const invalidRtmpUri = "invalid://rtmp-uri";
|
const invalidRtmpUri = 'invalid://rtmp-uri';
|
||||||
const invalidRtmpPush = new RtmpPush(testMediaSourceUri, invalidRtmpUri);
|
const invalidRtmpPush = new RtmpPush(testMediaSourceUri, invalidRtmpUri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
invalidRtmpPush.start("test", ["h264"]);
|
invalidRtmpPush.start('test', ['h264']);
|
||||||
// If it doesn't throw, it should at least not be running
|
// If it doesn't throw, it should at least not be running
|
||||||
expect(invalidRtmpPush.isRunning()).toBe(false);
|
expect(invalidRtmpPush.isRunning()).toBe(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
import {describe, it, expect, beforeEach, afterEach, mock} from 'bun:test';
|
||||||
import { RtmpPush } from "../src/RtmpPush.js";
|
import {RtmpPush} from '../src/RtmpPush.js';
|
||||||
|
|
||||||
// Mock the child_process module
|
// Mock the child_process module
|
||||||
const mockProcess = {
|
const mockProcess = {
|
||||||
@@ -12,14 +12,14 @@ const mockProcess = {
|
|||||||
|
|
||||||
const mockSpawn = mock(() => mockProcess);
|
const mockSpawn = mock(() => mockProcess);
|
||||||
|
|
||||||
mock.module("node:child_process", () => ({
|
mock.module('node:child_process', () => ({
|
||||||
spawn: mockSpawn
|
spawn: mockSpawn
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("RtmpPush", () => {
|
describe('RtmpPush', () => {
|
||||||
let rtmpPush: RtmpPush;
|
let rtmpPush: RtmpPush;
|
||||||
const mockMediaSourceUri = "https://example.com/test-video.ts";
|
const mockMediaSourceUri = 'https://example.com/test-video.ts';
|
||||||
const mockRtmpIngestUri = "rtmp://ingest.example.com:80/ingest";
|
const mockRtmpIngestUri = 'rtmp://ingest.example.com:80/ingest';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rtmpPush = new RtmpPush(mockMediaSourceUri, mockRtmpIngestUri);
|
rtmpPush = new RtmpPush(mockMediaSourceUri, mockRtmpIngestUri);
|
||||||
@@ -38,30 +38,30 @@ describe("RtmpPush", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Constructor", () => {
|
describe('Constructor', () => {
|
||||||
it("should create instance with correct properties", () => {
|
it('should create instance with correct properties', () => {
|
||||||
expect(rtmpPush).toBeInstanceOf(RtmpPush);
|
expect(rtmpPush).toBeInstanceOf(RtmpPush);
|
||||||
expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri);
|
expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri);
|
||||||
expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri);
|
expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle empty strings", () => {
|
it('should handle empty strings', () => {
|
||||||
const emptyRtmpPush = new RtmpPush("", "");
|
const emptyRtmpPush = new RtmpPush('', '');
|
||||||
expect(emptyRtmpPush.mediaSourceUri).toBe("");
|
expect(emptyRtmpPush.mediaSourceUri).toBe('');
|
||||||
expect(emptyRtmpPush.rtmpIngestUri).toBe("");
|
expect(emptyRtmpPush.rtmpIngestUri).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Getters", () => {
|
describe('Getters', () => {
|
||||||
it("should return correct mediaSourceUri", () => {
|
it('should return correct mediaSourceUri', () => {
|
||||||
expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri);
|
expect(rtmpPush.mediaSourceUri).toBe(mockMediaSourceUri);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return correct rtmpIngestUri", () => {
|
it('should return correct rtmpIngestUri', () => {
|
||||||
expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri);
|
expect(rtmpPush.rtmpIngestUri).toBe(mockRtmpIngestUri);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return immutable values", () => {
|
it('should return immutable values', () => {
|
||||||
const originalMediaUri = rtmpPush.mediaSourceUri;
|
const originalMediaUri = rtmpPush.mediaSourceUri;
|
||||||
const originalRtmpUri = rtmpPush.rtmpIngestUri;
|
const originalRtmpUri = rtmpPush.rtmpIngestUri;
|
||||||
|
|
||||||
@@ -71,10 +71,10 @@ describe("RtmpPush", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("start() method", () => {
|
describe('start() method', () => {
|
||||||
it("should start RTMP push process with correct parameters", () => {
|
it('should start RTMP push process with correct parameters', () => {
|
||||||
const streamKey = "test-stream-123";
|
const streamKey = 'test-stream-123';
|
||||||
const capabilities = ["h264", "aac"];
|
const capabilities = ['h264', 'aac'];
|
||||||
|
|
||||||
const result = rtmpPush.start(streamKey, capabilities);
|
const result = rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
@@ -82,88 +82,87 @@ describe("RtmpPush", () => {
|
|||||||
expect(rtmpPush.isRunning()).toBe(true);
|
expect(rtmpPush.isRunning()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should construct correct ffmpeg command", () => {
|
it('should construct correct ffmpeg command', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264", "aac"];
|
const capabilities = ['h264', 'aac'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith(
|
||||||
"ffmpeg",
|
'ffmpeg',
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
"-re",
|
'-re',
|
||||||
"-hide_banner",
|
'-hide_banner',
|
||||||
"-stream_loop", "-1",
|
'-stream_loop',
|
||||||
"-y",
|
'-1',
|
||||||
"-flags", "low_delay",
|
'-y',
|
||||||
"-fflags", "+nobuffer+flush_packets",
|
'-flags',
|
||||||
"-i", mockMediaSourceUri,
|
'low_delay',
|
||||||
"-c:a", "copy",
|
'-fflags',
|
||||||
"-c:v", "copy",
|
'+nobuffer+flush_packets',
|
||||||
"-flush_packets", "1",
|
'-i',
|
||||||
"-copyts",
|
mockMediaSourceUri,
|
||||||
"-f", "flv"
|
'-c:a',
|
||||||
|
'copy',
|
||||||
|
'-c:v',
|
||||||
|
'copy',
|
||||||
|
'-flush_packets',
|
||||||
|
'1',
|
||||||
|
'-copyts',
|
||||||
|
'-f',
|
||||||
|
'flv'
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should construct correct ingest URI", () => {
|
it('should construct correct ingest URI', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264", "aac"];
|
const capabilities = ['h264', 'aac'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri]));
|
||||||
"ffmpeg",
|
|
||||||
expect.arrayContaining([expectedIngestUri])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle empty capabilities array", () => {
|
it('should handle empty capabilities array', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities: string[] = [];
|
const capabilities: string[] = [];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=;tags=`;
|
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=;tags=`;
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri]));
|
||||||
"ffmpeg",
|
|
||||||
expect.arrayContaining([expectedIngestUri])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle single capability", () => {
|
it('should handle single capability', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=h264;tags=`;
|
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=h264;tags=`;
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri]));
|
||||||
"ffmpeg",
|
|
||||||
expect.arrayContaining([expectedIngestUri])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set up process event handlers", () => {
|
it('should set up process event handlers', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
expect(mockProcess.stdout.on).toHaveBeenCalledWith("data", expect.any(Function));
|
expect(mockProcess.stdout.on).toHaveBeenCalledWith('data', expect.any(Function));
|
||||||
expect(mockProcess.stderr.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('close', expect.any(Function));
|
||||||
expect(mockProcess.on).toHaveBeenCalledWith("error", expect.any(Function));
|
expect(mockProcess.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be chainable", () => {
|
it('should be chainable', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
const result = rtmpPush.start(streamKey, capabilities);
|
const result = rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
@@ -171,10 +170,10 @@ describe("RtmpPush", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("stop() method", () => {
|
describe('stop() method', () => {
|
||||||
it("should stop running process", () => {
|
it('should stop running process', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
expect(rtmpPush.isRunning()).toBe(true);
|
expect(rtmpPush.isRunning()).toBe(true);
|
||||||
@@ -185,7 +184,7 @@ describe("RtmpPush", () => {
|
|||||||
expect(rtmpPush.isRunning()).toBe(false);
|
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);
|
expect(rtmpPush.isRunning()).toBe(false);
|
||||||
|
|
||||||
const result = rtmpPush.stop();
|
const result = rtmpPush.stop();
|
||||||
@@ -194,28 +193,28 @@ describe("RtmpPush", () => {
|
|||||||
expect(rtmpPush.isRunning()).toBe(false);
|
expect(rtmpPush.isRunning()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be chainable", () => {
|
it('should be chainable', () => {
|
||||||
const result = rtmpPush.stop();
|
const result = rtmpPush.stop();
|
||||||
expect(result).toBe(rtmpPush);
|
expect(result).toBe(rtmpPush);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isRunning() method", () => {
|
describe('isRunning() method', () => {
|
||||||
it("should return false when no process is started", () => {
|
it('should return false when no process is started', () => {
|
||||||
expect(rtmpPush.isRunning()).toBe(false);
|
expect(rtmpPush.isRunning()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when process is running", () => {
|
it('should return true when process is running', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
expect(rtmpPush.isRunning()).toBe(true);
|
expect(rtmpPush.isRunning()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false after process is stopped", () => {
|
it('should return false after process is stopped', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
expect(rtmpPush.isRunning()).toBe(true);
|
expect(rtmpPush.isRunning()).toBe(true);
|
||||||
@@ -225,15 +224,15 @@ describe("RtmpPush", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Process lifecycle", () => {
|
describe('Process lifecycle', () => {
|
||||||
it("should handle process close event", () => {
|
it('should handle process close event', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
// Simulate process close
|
// 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') {
|
if (closeHandler && typeof closeHandler === 'function') {
|
||||||
closeHandler(0);
|
closeHandler(0);
|
||||||
}
|
}
|
||||||
@@ -241,17 +240,17 @@ describe("RtmpPush", () => {
|
|||||||
expect(rtmpPush.isRunning()).toBe(false);
|
expect(rtmpPush.isRunning()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle process error event", () => {
|
it('should handle process error event', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
// Simulate process error
|
// 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') {
|
if (errorHandler && typeof errorHandler === 'function') {
|
||||||
// Call the error handler with a mock error
|
// 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
|
// The error handler should set the process to null
|
||||||
@@ -259,51 +258,42 @@ describe("RtmpPush", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Edge cases", () => {
|
describe('Edge cases', () => {
|
||||||
it("should handle special characters in stream key", () => {
|
it('should handle special characters in stream key', () => {
|
||||||
const streamKey = "test-stream-with-special-chars!@#$%^&*()";
|
const streamKey = 'test-stream-with-special-chars!@#$%^&*()';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri]));
|
||||||
"ffmpeg",
|
|
||||||
expect.arrayContaining([expectedIngestUri])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle special characters in capabilities", () => {
|
it('should handle special characters in capabilities', () => {
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h.264", "aac-lc", "stereo_audio"];
|
const capabilities = ['h.264', 'aac-lc', 'stereo_audio'];
|
||||||
|
|
||||||
rtmpPush.start(streamKey, capabilities);
|
rtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
const expectedIngestUri = `${mockRtmpIngestUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri]));
|
||||||
"ffmpeg",
|
|
||||||
expect.arrayContaining([expectedIngestUri])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle very long URIs", () => {
|
it('should handle very long URIs', () => {
|
||||||
const longMediaUri = "https://example.com/" + "a".repeat(1000) + ".ts";
|
const longMediaUri = 'https://example.com/' + 'a'.repeat(1000) + '.ts';
|
||||||
const longRtmpUri = "rtmp://ingest.example.com:80/ingest/" + "b".repeat(1000);
|
const longRtmpUri = 'rtmp://ingest.example.com:80/ingest/' + 'b'.repeat(1000);
|
||||||
|
|
||||||
const longRtmpPush = new RtmpPush(longMediaUri, longRtmpUri);
|
const longRtmpPush = new RtmpPush(longMediaUri, longRtmpUri);
|
||||||
const streamKey = "test-stream";
|
const streamKey = 'test-stream';
|
||||||
const capabilities = ["h264"];
|
const capabilities = ['h264'];
|
||||||
|
|
||||||
longRtmpPush.start(streamKey, capabilities);
|
longRtmpPush.start(streamKey, capabilities);
|
||||||
|
|
||||||
const expectedIngestUri = `${longRtmpUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
const expectedIngestUri = `${longRtmpUri}/${streamKey};capabilities=${capabilities.join(',')};tags=`;
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenCalledWith(
|
expect(mockSpawn).toHaveBeenCalledWith('ffmpeg', expect.arrayContaining([expectedIngestUri]));
|
||||||
"ffmpeg",
|
|
||||||
expect.arrayContaining([expectedIngestUri])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user