195 lines
8.0 KiB
Plaintext
195 lines
8.0 KiB
Plaintext
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var videoRecorder_exports = {};
|
|
__export(videoRecorder_exports, {
|
|
VideoRecorder: () => VideoRecorder,
|
|
startAutomaticVideoRecording: () => startAutomaticVideoRecording
|
|
});
|
|
module.exports = __toCommonJS(videoRecorder_exports);
|
|
var import_path = __toESM(require("path"));
|
|
var import_utils = require("../utils");
|
|
var import_processLauncher = require("./utils/processLauncher");
|
|
var import_utilsBundle = require("../utilsBundle");
|
|
var import_artifact = require("./artifact");
|
|
var import__ = require(".");
|
|
const fps = 25;
|
|
class VideoRecorder {
|
|
constructor(screencast) {
|
|
this._screencast = screencast;
|
|
}
|
|
start(options) {
|
|
(0, import_utils.assert)(!this._artifact);
|
|
const ffmpegPath = import__.registry.findExecutable("ffmpeg").executablePathOrDie(this._screencast.page.browserContext._browser.sdkLanguage());
|
|
const outputFile = options.fileName ?? import_path.default.join(this._screencast.page.browserContext._browser.options.artifactsDir, (0, import_utils.createGuid)() + ".webm");
|
|
this._client = {
|
|
onFrame: (frame) => this._videoRecorder.writeFrame(frame.buffer, frame.frameSwapWallTime / 1e3),
|
|
gracefulClose: () => this.stop(),
|
|
dispose: () => this.stop().catch((e) => import_utils.debugLogger.log("error", `Failed to stop video recorder: ${String(e)}`)),
|
|
size: options.size
|
|
};
|
|
const { size } = this._screencast.addClient(this._client);
|
|
const videoSize = options.size ?? size;
|
|
this._videoRecorder = new FfmpegVideoRecorder(ffmpegPath, videoSize, outputFile);
|
|
this._artifact = new import_artifact.Artifact(this._screencast.page.browserContext, outputFile);
|
|
return this._artifact;
|
|
}
|
|
async stop() {
|
|
if (!this._artifact)
|
|
return;
|
|
const artifact = this._artifact;
|
|
this._artifact = void 0;
|
|
const client = this._client;
|
|
this._client = void 0;
|
|
const videoRecorder = this._videoRecorder;
|
|
this._videoRecorder = void 0;
|
|
this._screencast.removeClient(client);
|
|
await videoRecorder.stop();
|
|
await artifact.reportFinished();
|
|
}
|
|
}
|
|
function startAutomaticVideoRecording(page) {
|
|
const recordVideo = page.browserContext._options.recordVideo;
|
|
if (!recordVideo)
|
|
return;
|
|
const recorder = new VideoRecorder(page.screencast);
|
|
if (page.browserContext._options.recordVideo?.showActions)
|
|
page.screencast.showActions(page.browserContext._options.recordVideo?.showActions);
|
|
const dir = recordVideo.dir ?? page.browserContext._browser.options.artifactsDir;
|
|
const artifact = recorder.start({ size: recordVideo.size, fileName: import_path.default.join(dir, page.guid + ".webm") });
|
|
page.video = artifact;
|
|
}
|
|
class FfmpegVideoRecorder {
|
|
constructor(ffmpegPath, size, outputFile) {
|
|
this._process = null;
|
|
this._gracefullyClose = null;
|
|
this._lastWritePromise = Promise.resolve();
|
|
this._firstFrameTimestamp = 0;
|
|
this._lastFrame = null;
|
|
this._lastWriteNodeTime = 0;
|
|
this._frameQueue = [];
|
|
this._isStopped = false;
|
|
if (!outputFile.endsWith(".webm"))
|
|
throw new Error("File must have .webm extension");
|
|
this._outputFile = outputFile;
|
|
this._ffmpegPath = ffmpegPath;
|
|
this._size = size;
|
|
this._launchPromise = this._launch().catch((e) => e);
|
|
}
|
|
async _launch() {
|
|
await (0, import_utils.mkdirIfNeeded)(this._outputFile);
|
|
const w = this._size.width;
|
|
const h = this._size.height;
|
|
const args = `-loglevel error -f image2pipe -avioflags direct -fpsprobesize 0 -probesize 32 -analyzeduration 0 -c:v mjpeg -i pipe:0 -y -an -r ${fps} -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1 -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(" ");
|
|
args.push(this._outputFile);
|
|
const { launchedProcess, gracefullyClose } = await (0, import_processLauncher.launchProcess)({
|
|
command: this._ffmpegPath,
|
|
args,
|
|
stdio: "stdin",
|
|
log: (message) => import_utils.debugLogger.log("browser", message),
|
|
tempDirectories: [],
|
|
attemptToGracefullyClose: async () => {
|
|
import_utils.debugLogger.log("browser", "Closing stdin...");
|
|
launchedProcess.stdin.end();
|
|
},
|
|
onExit: (exitCode, signal) => {
|
|
import_utils.debugLogger.log("browser", `ffmpeg onkill exitCode=${exitCode} signal=${signal}`);
|
|
}
|
|
});
|
|
launchedProcess.stdin.on("finish", () => {
|
|
import_utils.debugLogger.log("browser", "ffmpeg finished input.");
|
|
});
|
|
launchedProcess.stdin.on("error", () => {
|
|
import_utils.debugLogger.log("browser", "ffmpeg error.");
|
|
});
|
|
this._process = launchedProcess;
|
|
this._gracefullyClose = gracefullyClose;
|
|
}
|
|
writeFrame(frame, timestamp) {
|
|
this._launchPromise.then((error) => {
|
|
if (error)
|
|
return;
|
|
this._writeFrame(frame, timestamp);
|
|
});
|
|
}
|
|
_writeFrame(frame, timestamp) {
|
|
(0, import_utils.assert)(this._process);
|
|
if (this._isStopped)
|
|
return;
|
|
if (!this._firstFrameTimestamp)
|
|
this._firstFrameTimestamp = timestamp;
|
|
const frameNumber = Math.floor((timestamp - this._firstFrameTimestamp) * fps);
|
|
if (this._lastFrame) {
|
|
const repeatCount = frameNumber - this._lastFrame.frameNumber;
|
|
for (let i = 0; i < repeatCount; ++i)
|
|
this._frameQueue.push(this._lastFrame.buffer);
|
|
this._lastWritePromise = this._lastWritePromise.then(() => this._sendFrames());
|
|
}
|
|
this._lastFrame = { buffer: frame, timestamp, frameNumber };
|
|
this._lastWriteNodeTime = (0, import_utils.monotonicTime)();
|
|
}
|
|
async _sendFrames() {
|
|
while (this._frameQueue.length)
|
|
await this._sendFrame(this._frameQueue.shift());
|
|
}
|
|
async _sendFrame(frame) {
|
|
return new Promise((f) => this._process.stdin.write(frame, f)).then((error) => {
|
|
if (error)
|
|
import_utils.debugLogger.log("browser", `ffmpeg failed to write: ${String(error)}`);
|
|
});
|
|
}
|
|
async stop() {
|
|
const error = await this._launchPromise;
|
|
if (error)
|
|
throw error;
|
|
if (this._isStopped)
|
|
return;
|
|
if (!this._lastFrame) {
|
|
this._writeFrame(createWhiteImage(this._size.width, this._size.height), (0, import_utils.monotonicTime)());
|
|
}
|
|
const addTime = Math.max(((0, import_utils.monotonicTime)() - this._lastWriteNodeTime) / 1e3, 1);
|
|
this._writeFrame(Buffer.from([]), this._lastFrame.timestamp + addTime);
|
|
this._isStopped = true;
|
|
try {
|
|
await this._lastWritePromise;
|
|
await this._gracefullyClose();
|
|
} catch (e) {
|
|
import_utils.debugLogger.log("error", `ffmpeg failed to stop: ${String(e)}`);
|
|
}
|
|
}
|
|
}
|
|
function createWhiteImage(width, height) {
|
|
const data = Buffer.alloc(width * height * 4, 255);
|
|
return import_utilsBundle.jpegjs.encode({ data, width, height }, 80).data;
|
|
}
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
VideoRecorder,
|
|
startAutomaticVideoRecording
|
|
});
|