Skip to content

Commit 4975cea

Browse files
pavelfeldmanaslushnikov
authored andcommitted
chore(screencast): respect i/o backpressure when writing into ffmpeg (#4164)
1 parent 0b7976a commit 4975cea

File tree

1 file changed

+30
-44
lines changed

1 file changed

+30
-44
lines changed

src/server/chromium/videoRecorder.ts

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { ChildProcess } from 'child_process';
1818
import { ffmpegExecutable } from '../../utils/binaryPaths';
19-
import { assert } from '../../utils/utils';
19+
import { assert, monotonicTime } from '../../utils/utils';
2020
import { launchProcess } from '../processLauncher';
2121
import { Progress, ProgressController } from '../progress';
2222
import * as types from '../types';
@@ -26,11 +26,13 @@ const fps = 25;
2626
export class VideoRecorder {
2727
private _process: ChildProcess | null = null;
2828
private _gracefullyClose: (() => Promise<void>) | null = null;
29-
private _lastWritePromise: Promise<void> | undefined;
29+
private _lastWritePromise: Promise<void> = Promise.resolve();
3030
private _lastFrameTimestamp: number = 0;
3131
private _lastFrameBuffer: Buffer | null = null;
3232
private _lastWriteTimestamp: number = 0;
3333
private readonly _progress: Progress;
34+
private _frameQueue: Buffer[] = [];
35+
private _isStopped = false;
3436

3537
static async launch(options: types.PageScreencastOptions): Promise<VideoRecorder> {
3638
if (!options.outputFile.endsWith('.webm'))
@@ -50,7 +52,6 @@ export class VideoRecorder {
5052
}
5153

5254
private async _launch(options: types.PageScreencastOptions) {
53-
assert(!this._isRunning());
5455
const w = options.width;
5556
const h = options.height;
5657
const args = `-loglevel error -f image2pipe -c:v mjpeg -i - -y -an -r ${fps} -c:v vp8 -qmin 0 -qmax 50 -crf 8 -b:v 1M -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(' ');
@@ -84,58 +85,43 @@ export class VideoRecorder {
8485
this._gracefullyClose = gracefullyClose;
8586
}
8687

87-
async writeFrame(frame: Buffer, timestamp: number) {
88+
writeFrame(frame: Buffer, timestamp: number) {
8889
assert(this._process);
89-
if (!this._isRunning())
90+
if (this._isStopped)
9091
return;
9192
this._progress.log(`writing frame ` + timestamp);
92-
if (this._lastFrameBuffer)
93-
this._lastWritePromise = this._flushLastFrame(timestamp - this._lastFrameTimestamp).catch(e => this._progress.log('Error while writing frame: ' + e));
93+
94+
if (this._lastFrameBuffer) {
95+
const durationSec = timestamp - this._lastFrameTimestamp;
96+
const repeatCount = Math.max(1, Math.round(fps * durationSec));
97+
for (let i = 0; i < repeatCount; ++i)
98+
this._frameQueue.push(this._lastFrameBuffer);
99+
this._lastWritePromise = this._lastWritePromise.then(() => this._sendFrames());
100+
}
101+
94102
this._lastFrameBuffer = frame;
95103
this._lastFrameTimestamp = timestamp;
96-
this._lastWriteTimestamp = Date.now();
104+
this._lastWriteTimestamp = monotonicTime();
97105
}
98106

99-
private async _flushLastFrame(durationSec: number): Promise<void> {
100-
assert(this._process);
101-
const frame = this._lastFrameBuffer;
102-
if (!frame)
103-
return;
104-
const previousWrites = this._lastWritePromise;
105-
let finishedWriting: () => void;
106-
const writePromise = new Promise<void>(fulfill => finishedWriting = fulfill);
107-
const repeatCount = Math.max(1, Math.round(fps * durationSec));
108-
this._progress.log(`flushing ${repeatCount} frame(s)`);
109-
await previousWrites;
110-
for (let i = 0; i < repeatCount; i++) {
111-
const callFinish = i === (repeatCount - 1);
112-
this._process.stdin.write(frame, (error: Error | null | undefined) => {
113-
if (error)
114-
this._progress.log(`ffmpeg failed to write: ${error}`);
115-
if (callFinish)
116-
finishedWriting();
117-
});
118-
}
119-
return writePromise;
107+
private async _sendFrames() {
108+
while (this._frameQueue.length)
109+
await this._sendFrame(this._frameQueue.shift()!);
110+
}
111+
112+
private async _sendFrame(frame: Buffer) {
113+
return new Promise(f => this._process!.stdin.write(frame, f)).then(error => {
114+
if (error)
115+
this._progress.log(`ffmpeg failed to write: ${error}`);
116+
});
120117
}
121118

122119
async stop() {
123-
if (!this._gracefullyClose)
120+
if (this._isStopped)
124121
return;
125-
126-
if (this._lastWriteTimestamp) {
127-
const durationSec = (Date.now() - this._lastWriteTimestamp) / 1000;
128-
if (!this._lastWritePromise || durationSec > 1 / fps)
129-
this._flushLastFrame(durationSec).catch(e => this._progress.log('Error while writing frame: ' + e));
130-
}
131-
132-
const close = this._gracefullyClose;
133-
this._gracefullyClose = null;
122+
this.writeFrame(Buffer.from([]), this._lastFrameTimestamp + (monotonicTime() - this._lastWriteTimestamp) / 1000);
123+
this._isStopped = true;
134124
await this._lastWritePromise;
135-
await close();
136-
}
137-
138-
private _isRunning(): boolean {
139-
return !!this._gracefullyClose;
125+
await this._gracefullyClose!();
140126
}
141127
}

0 commit comments

Comments
 (0)