2020-08-31 08:43:14 -07:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { ChildProcess } from 'child_process';
|
2020-10-09 15:56:03 -07:00
|
|
|
import { ffmpegExecutable } from '../../utils/binaryPaths';
|
2020-08-31 08:43:14 -07:00
|
|
|
import { assert } from '../../utils/utils';
|
2020-09-14 10:26:44 -07:00
|
|
|
import { launchProcess } from '../processLauncher';
|
|
|
|
import { Progress, ProgressController } from '../progress';
|
|
|
|
import * as types from '../types';
|
2020-08-31 08:43:14 -07:00
|
|
|
|
|
|
|
const fps = 25;
|
|
|
|
|
|
|
|
export class VideoRecorder {
|
|
|
|
private _process: ChildProcess | null = null;
|
|
|
|
private _gracefullyClose: (() => Promise<void>) | null = null;
|
2020-10-14 14:10:35 -07:00
|
|
|
private _lastWritePromise: Promise<void> | undefined;
|
2020-08-31 08:43:14 -07:00
|
|
|
private _lastFrameTimestamp: number = 0;
|
|
|
|
private _lastFrameBuffer: Buffer | null = null;
|
|
|
|
private _lastWriteTimestamp: number = 0;
|
|
|
|
private readonly _progress: Progress;
|
|
|
|
|
2020-09-08 15:10:36 -07:00
|
|
|
static async launch(options: types.PageScreencastOptions): Promise<VideoRecorder> {
|
2020-08-31 08:43:14 -07:00
|
|
|
if (!options.outputFile.endsWith('.webm'))
|
|
|
|
throw new Error('File must have .webm extension');
|
|
|
|
|
2020-09-17 09:32:54 -07:00
|
|
|
const controller = new ProgressController();
|
2020-09-10 15:34:13 -07:00
|
|
|
controller.setLogName('browser');
|
|
|
|
return await controller.run(async progress => {
|
2020-09-08 15:10:36 -07:00
|
|
|
const recorder = new VideoRecorder(progress);
|
2020-08-31 08:43:14 -07:00
|
|
|
await recorder._launch(options);
|
|
|
|
return recorder;
|
2020-09-10 15:34:13 -07:00
|
|
|
});
|
2020-08-31 08:43:14 -07:00
|
|
|
}
|
|
|
|
|
2020-09-08 15:10:36 -07:00
|
|
|
private constructor(progress: Progress) {
|
2020-08-31 08:43:14 -07:00
|
|
|
this._progress = progress;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async _launch(options: types.PageScreencastOptions) {
|
|
|
|
assert(!this._isRunning());
|
|
|
|
const w = options.width;
|
|
|
|
const h = options.height;
|
2020-09-08 15:39:18 -07:00
|
|
|
const args = `-loglevel error -f image2pipe -c:v mjpeg -i - -y -an -r ${fps} -c:v vp8 -qmin 0 -qmax 50 -crf 8 -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(' ');
|
2020-08-31 08:43:14 -07:00
|
|
|
args.push(options.outputFile);
|
|
|
|
const progress = this._progress;
|
2020-09-08 15:10:36 -07:00
|
|
|
|
2020-10-09 15:56:03 -07:00
|
|
|
const executablePath = ffmpegExecutable();
|
|
|
|
if (!executablePath)
|
|
|
|
throw new Error('ffmpeg executable was not found');
|
2020-08-31 08:43:14 -07:00
|
|
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
2020-10-09 15:56:03 -07:00
|
|
|
executablePath,
|
2020-08-31 08:43:14 -07:00
|
|
|
args,
|
|
|
|
pipeStdin: true,
|
|
|
|
progress,
|
|
|
|
tempDirectories: [],
|
|
|
|
attemptToGracefullyClose: async () => {
|
|
|
|
progress.log('Closing stdin...');
|
|
|
|
launchedProcess.stdin.end();
|
|
|
|
},
|
|
|
|
onExit: (exitCode, signal) => {
|
|
|
|
progress.log(`ffmpeg onkill exitCode=${exitCode} signal=${signal}`);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
launchedProcess.stdin.on('finish', () => {
|
|
|
|
progress.log('ffmpeg finished input.');
|
|
|
|
});
|
|
|
|
launchedProcess.stdin.on('error', () => {
|
|
|
|
progress.log('ffmpeg error.');
|
|
|
|
});
|
|
|
|
this._process = launchedProcess;
|
|
|
|
this._gracefullyClose = gracefullyClose;
|
|
|
|
}
|
|
|
|
|
|
|
|
async writeFrame(frame: Buffer, timestamp: number) {
|
|
|
|
assert(this._process);
|
|
|
|
if (!this._isRunning())
|
|
|
|
return;
|
2020-10-14 14:10:35 -07:00
|
|
|
this._progress.log(`writing frame ` + timestamp);
|
|
|
|
if (this._lastFrameBuffer)
|
|
|
|
this._lastWritePromise = this._flushLastFrame(timestamp - this._lastFrameTimestamp).catch(e => this._progress.log('Error while writing frame: ' + e));
|
2020-08-31 08:43:14 -07:00
|
|
|
this._lastFrameBuffer = frame;
|
|
|
|
this._lastFrameTimestamp = timestamp;
|
|
|
|
this._lastWriteTimestamp = Date.now();
|
2020-09-15 15:21:50 -07:00
|
|
|
}
|
2020-08-31 08:43:14 -07:00
|
|
|
|
2020-10-14 14:10:35 -07:00
|
|
|
private async _flushLastFrame(durationSec: number): Promise<void> {
|
2020-09-15 15:21:50 -07:00
|
|
|
assert(this._process);
|
|
|
|
const frame = this._lastFrameBuffer;
|
|
|
|
if (!frame)
|
|
|
|
return;
|
2020-08-31 08:43:14 -07:00
|
|
|
const previousWrites = this._lastWritePromise;
|
|
|
|
let finishedWriting: () => void;
|
2020-09-15 15:21:50 -07:00
|
|
|
const writePromise = new Promise<void>(fulfill => finishedWriting = fulfill);
|
2020-10-14 14:10:35 -07:00
|
|
|
const repeatCount = Math.max(1, Math.round(fps * durationSec));
|
|
|
|
this._progress.log(`flushing ${repeatCount} frame(s)`);
|
2020-08-31 08:43:14 -07:00
|
|
|
await previousWrites;
|
2020-09-15 15:21:50 -07:00
|
|
|
for (let i = 0; i < repeatCount; i++) {
|
|
|
|
const callFinish = i === (repeatCount - 1);
|
2020-08-31 08:43:14 -07:00
|
|
|
this._process.stdin.write(frame, (error: Error | null | undefined) => {
|
|
|
|
if (error)
|
|
|
|
this._progress.log(`ffmpeg failed to write: ${error}`);
|
|
|
|
if (callFinish)
|
|
|
|
finishedWriting();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return writePromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
if (!this._gracefullyClose)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (this._lastWriteTimestamp) {
|
|
|
|
const durationSec = (Date.now() - this._lastWriteTimestamp) / 1000;
|
2020-10-14 14:10:35 -07:00
|
|
|
if (!this._lastWritePromise || durationSec > 1 / fps)
|
|
|
|
this._flushLastFrame(durationSec).catch(e => this._progress.log('Error while writing frame: ' + e));
|
2020-08-31 08:43:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const close = this._gracefullyClose;
|
|
|
|
this._gracefullyClose = null;
|
|
|
|
await this._lastWritePromise;
|
|
|
|
await close();
|
|
|
|
}
|
|
|
|
|
|
|
|
private _isRunning(): boolean {
|
|
|
|
return !!this._gracefullyClose;
|
|
|
|
}
|
2020-09-04 04:17:51 -07:00
|
|
|
}
|