Skip to content

Commit

Permalink
Can't stop recording in a docker container (#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
boonya authored Jul 13, 2022
1 parent f71fc2f commit d014cbb
Show file tree
Hide file tree
Showing 19 changed files with 106 additions and 31 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project should be documented in this file.

## [2.1.0] - [Fix] Can't stop recording in a docker container

**[Breaking change]**

If you were rely on `playlistName` option that it was able to accept value like `$(date +%Y.%m.%d-%H.%M.%S)`, now it doesn't work. You have to prepare dynamic value somewhere in your code before you pass it into Recorder instance. But by default `playlistName` still dynamic and completely the same. So, you code should work with no changes and issues.

## [2.0.3] - Bugfix & update

- [Issue #195](https://github.com/boonya/rtsp-video-recorder/issues/195) acknowledged, investigated and fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ By default the name is going to be `$(date +%Y.%m.%d-%H.%M.%S)` (e.g. `2020.01.0

File path pattern. By default it is `%Y.%m.%d/%H.%M.%S` which will be translated to e.g. `2020.01.03/03.19.15`

_Accepts C++ strftime specifiers:_ http://www.cplusplus.com/reference/ctime/strftime/
[_Accepts C++ strftime specifiers:_](http://www.cplusplus.com/reference/ctime/strftime/)

### segmentTime

Expand Down
8 changes: 6 additions & 2 deletions example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,21 @@ try {
const source = SOURCE || `rtsp://${IP}:554/user=admin_password=tlJwpbo6_channel=1_stream=1.sdp?real_stream`;

const title = TITLE || 'Example cam';
const safeTitle = title
.replace(/[:]+/ug, '_')
.replace(/_+/ug, '_');
const segmentTime = SEGMENT_TIME || '10m';
const dirSizeThreshold = THRESHOLD || '500M';
const noAudio = NO_AUDIO === 'true' ? true : false;
const filePattern = FILE_PATTERN || `${title}-%Y.%m.%d/%H.%M.%S`;
const filePattern = FILE_PATTERN || `${safeTitle}-%Y.%m.%d/%H.%M.%S`;
const playlistName = PLAYLIST_NAME || safeTitle;

const recorder = new Recorder(source, DESTINATION,
{
title,
segmentTime,
filePattern,
playlistName: PLAYLIST_NAME,
playlistName,
dirSizeThreshold,
noAudio,
},
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rtsp-video-recorder",
"version": "2.0.3",
"version": "2.1.0",
"description": "Provide an API to record rtsp video stream to filesystem.",
"main": "dist/recorder.js",
"types": "dist/recorder.d.ts",
Expand Down
16 changes: 16 additions & 0 deletions src/helpers/playlistName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default function playlistName(customValue?: string) {
if (customValue) {
return customValue
.replace(/[:]+/ug, '_')
.replace(/_+/ug, '_');
}

const now = new Date();
const [date] = now.toISOString().split('T');
const [time] = now.toTimeString().split(' ');

return [
date.replace(/-/ug, '.'),
time.replace(/:/ug, '.'),
].join('-');
}
16 changes: 9 additions & 7 deletions src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { IRecorder, Options, Events, EventCallback } from './types';
import { RecorderError, RecorderValidationError } from './error';
import { verifyAllOptions } from './validators';
import dirSize from './helpers/space';
import playlistName from './helpers/playlistName';
import transformDirSizeThreshold from './helpers/sizeThreshold';
import transformSegmentTime from './helpers/segmentTime';

export {Recorder, Events as RecorderEvents, RecorderError, RecorderValidationError};
export { Recorder, Events as RecorderEvents, RecorderError, RecorderValidationError };
export type { IRecorder };

const APPROXIMATION_PERCENTAGE = 1;
Expand Down Expand Up @@ -35,16 +36,18 @@ export default class Recorder implements IRecorder {
private process: ChildProcessWithoutNullStreams | null = null;
private eventEmitter: EventEmitter;

constructor (private uri: string, private destination: string, options: Options = {}) {
constructor(private uri: string, private destination: string, options: Options = {}) {
const errors = verifyAllOptions(destination, options);
if (errors.length) {
throw new RecorderValidationError('Options invalid', errors);
}

this.title = options.title;
this.ffmpegBinary = options.ffmpegBinary || this.ffmpegBinary;
this.playlistName = options.playlistName || '$(date +%Y.%m.%d-%H.%M.%S)';
this.filePattern = (options.filePattern || this.filePattern).replace(/(?:[\s:]+)/gu, '_');
this.playlistName = playlistName(options.playlistName);
this.filePattern = (options.filePattern || this.filePattern)
.replace(/[\s:]+/gu, '_')
.replace(/_+/ug, '_');

this.segmentTime = options.segmentTime
? transformSegmentTime(options.segmentTime)
Expand Down Expand Up @@ -114,12 +117,11 @@ export default class Recorder implements IRecorder {
'-strftime_mkdir', '1',
'-hls_time', String(this.segmentTime),
'-hls_list_size', '0',
'-hls_segment_filename', `"${this.filePattern}.mp4"`,
`"./${this.playlistName}.m3u8"`,
'-hls_segment_filename', `${this.filePattern}.mp4`,
`./${this.playlistName}.m3u8`,
],
{
detached: false,
shell: true,
cwd: this.destination,
},
);
Expand Down
5 changes: 4 additions & 1 deletion test/events/error.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ChildProcessWithoutNullStreams } from 'child_process';
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import { mockSpawnProcess, URI, DESTINATION } from '../test.helpers';
import Recorder, { RecorderEvents, RecorderError } from '../../src/recorder';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/playlistName');

let fakeProcess: ChildProcessWithoutNullStreams;
let eventHandler: () => void;
Expand All @@ -12,6 +14,7 @@ beforeEach(() => {
jest.mocked(verifyAllOptions).mockReturnValue([]);
fakeProcess = mockSpawnProcess();
eventHandler = jest.fn().mockName('onError');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should return RecorderError with message given by ffmpeg', async () => {
Expand Down
3 changes: 3 additions & 0 deletions test/events/file_created.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { ChildProcessWithoutNullStreams } from 'child_process';
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import Recorder, { RecorderEvents } from '../../src/recorder';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/playlistName');

let fakeProcess: ChildProcessWithoutNullStreams;
let eventHandler: () => void;
Expand All @@ -12,6 +14,7 @@ beforeEach(() => {
jest.mocked(verifyAllOptions).mockReturnValue([]);
fakeProcess = mockSpawnProcess();
eventHandler = jest.fn().mockName('onFileCreated');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should return filename if ffmpeg says: "Opening \'*.mp4\' for writing"', async () => {
Expand Down
3 changes: 3 additions & 0 deletions test/events/progress.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { ChildProcessWithoutNullStreams } from 'child_process';
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import Recorder, { RecorderEvents } from '../../src/recorder';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/playlistName');

let fakeProcess: ChildProcessWithoutNullStreams;
let eventHandler: () => void;
Expand All @@ -13,6 +15,7 @@ beforeEach(() => {
jest.mocked(verifyAllOptions).mockReturnValue([]);
fakeProcess = mockSpawnProcess();
eventHandler = jest.fn().mockName('onProgress');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should return any ffmpeg progress message', async () => {
Expand Down
5 changes: 4 additions & 1 deletion test/events/space_full.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ChildProcessWithoutNullStreams } from 'child_process';
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import { mockSpawnProcess, URI, DESTINATION } from '../test.helpers';
import dirSize from '../../src/helpers/space';
import Recorder, { RecorderEvents, RecorderError } from '../../src/recorder';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/space');
jest.mock('../../src/helpers/playlistName');

let fakeProcess: ChildProcessWithoutNullStreams;
let onSpaceFull: () => void;
Expand All @@ -14,6 +16,7 @@ beforeEach(() => {
jest.mocked(verifyAllOptions).mockReturnValue([]);
fakeProcess = mockSpawnProcess();
onSpaceFull = jest.fn().mockName('onSpaceFull');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should not evaluate space if "threshold" is undefined', async () => {
Expand Down
5 changes: 4 additions & 1 deletion test/events/start.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import { mockSpawnProcess, URI, DESTINATION } from '../test.helpers';
import Recorder, { RecorderEvents } from '../../src/recorder';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/playlistName');

let onStart: () => void;

beforeEach(() => {
jest.mocked(verifyAllOptions).mockReturnValue([]);
mockSpawnProcess();
onStart = jest.fn().mockName('onStart');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should return "programmatically" if .stop() executed', () => {
Expand Down
5 changes: 4 additions & 1 deletion test/events/started.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ChildProcessWithoutNullStreams } from 'child_process';
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import { mockSpawnProcess, URI, DESTINATION } from '../test.helpers';
import Recorder, { RecorderEvents } from '../../src/recorder';
import dirSize from '../../src/helpers/space';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/space');
jest.mock('../../src/helpers/playlistName');

let fakeProcess: ChildProcessWithoutNullStreams;
let eventHandler: () => void;
Expand All @@ -15,6 +17,7 @@ beforeEach(() => {
fakeProcess = mockSpawnProcess();
eventHandler = jest.fn().mockName('onStarted');
jest.mocked(dirSize).mockReturnValue(0);
jest.mocked(playlistName).mockReturnValue('playlist');
});

const FFMPEG_MESSAGE = `[libx264 @ 0x148816200] 264 - core 163 r3060 5db6aa6 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=12 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=15 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Expand Down
3 changes: 3 additions & 0 deletions test/events/stop.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { verifyAllOptions } from '../../src/validators';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import Recorder, { RecorderEvents } from '../../src/recorder';
import playlistName from '../../src/helpers/playlistName';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/playlistName');

let onStop: () => void;
let onStopped: () => void;
Expand All @@ -12,6 +14,7 @@ beforeEach(() => {
mockSpawnProcess();
onStop = jest.fn().mockName('onStop');
onStopped = jest.fn().mockName('onStopped');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should return "programmatically" if .stop() executed', async () => {
Expand Down
3 changes: 3 additions & 0 deletions test/events/stopped.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ChildProcessWithoutNullStreams } from 'child_process';
import { verifyAllOptions } from '../../src/validators';
import playlistName from '../../src/helpers/playlistName';
import {mockSpawnProcess, URI, DESTINATION} from '../test.helpers';
import Recorder, { RecorderEvents } from '../../src/recorder';

jest.mock('../../src/validators');
jest.mock('../../src/helpers/playlistName');

let fakeProcess: ChildProcessWithoutNullStreams;
let eventHandler: () => void;
Expand All @@ -12,6 +14,7 @@ beforeEach(() => {
jest.mocked(verifyAllOptions).mockReturnValue([]);
fakeProcess = mockSpawnProcess();
eventHandler = jest.fn().mockName('onStopped');
jest.mocked(playlistName).mockReturnValue('playlist');
});

test('should return FFMPEG exit code', async () => {
Expand Down
23 changes: 20 additions & 3 deletions test/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import dirSize from '../src/helpers/space';
import fs from 'fs';
import transformDirSizeThreshold from '../src/helpers/sizeThreshold';
import transformSegmentTime from '../src/helpers/segmentTime';
import playlistName from '../src/helpers/playlistName';

jest.mock('fs');
jest.mock('path');

describe('directoryExists', () => {
test('exists', () => {
jest.mocked(fs).lstatSync.mockReturnValue({isDirectory: () => true});
jest.mocked(fs).lstatSync.mockReturnValue({ isDirectory: () => true });

expect(directoryExists('path')).toBeTruthy();
});
Expand All @@ -23,7 +24,7 @@ describe('directoryExists', () => {
});

test('not a directory', () => {
jest.mocked(fs).lstatSync.mockReturnValue({isDirectory: () => false});
jest.mocked(fs).lstatSync.mockReturnValue({ isDirectory: () => false });

expect(() => directoryExists('path')).toThrowError('path exists but it is not a directory.');
});
Expand All @@ -41,9 +42,25 @@ test('transformSegmentTime', () => {

test('should return directory size in bytes', () => {
jest.mocked(fs).readdirSync.mockReturnValue(new Array(3).fill(0));
jest.mocked(fs).statSync.mockReturnValue({isDirectory: () => false, size: 3});
jest.mocked(fs).statSync.mockReturnValue({ isDirectory: () => false, size: 3 });

const size = dirSize('');

expect(size).toEqual(9);
});

describe('playlistName', () => {
test('should return current date based name.', () => {
jest.useFakeTimers().setSystemTime(new Date('Feb 24 2022 04:45:00').getTime());

const result = playlistName();

expect(result).toBe('2022.02.24-04.45.00');
});

test('should return custom name.', () => {
const result = playlistName('custom - name : і Colon');

expect(result).toBe('custom - name _ і Colon');
});
});
Loading

0 comments on commit d014cbb

Please sign in to comment.