playwright/src/cli/traceViewer/videoTileGenerator.ts

88 lines
3.7 KiB
TypeScript
Raw Normal View History

/**
* 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 { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { TraceModel, videoById, VideoMetaInfo } from './traceModel';
import type { PageVideoTraceEvent } from '../../trace/traceTypes';
import { ffmpegExecutable } from '../../utils/binaryPaths';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
export class VideoTileGenerator {
private _traceModel: TraceModel;
constructor(traceModel: TraceModel) {
this._traceModel = traceModel;
}
tilePath(urlPath: string) {
const index = urlPath.lastIndexOf('/');
const tile = urlPath.substring(index + 1);
const videoId = urlPath.substring(0, index);
const { context, page } = videoById(this._traceModel, videoId);
const videoFilePath = path.join(path.dirname(context.filePath), page.video!.video.fileName);
return videoFilePath + '-' + tile;
}
async render(videoId: string): Promise<VideoMetaInfo | undefined> {
const { context, page } = videoById(this._traceModel, videoId);
const video = page.video!.video;
const videoFilePath = path.join(path.dirname(context.filePath), video.fileName);
const metaInfoFilePath = videoFilePath + '-metainfo.txt';
try {
const metaInfo = await fsReadFileAsync(metaInfoFilePath, 'utf8');
return metaInfo ? JSON.parse(metaInfo) : undefined;
} catch (e) {
}
const ffmpeg = ffmpegExecutable()!;
console.log('Generating frames for ' + videoFilePath); // eslint-disable-line no-console
// Force output frame rate to 25 fps as otherwise it would produce one image per timebase unit
// which is currently 1 / (25 * 1000).
const result = spawnSync(ffmpeg, ['-i', videoFilePath, '-r', '25', `${videoFilePath}-%03d.png`]);
const metaInfo = parseMetaInfo(result.stderr.toString(), video);
await fsWriteFileAsync(metaInfoFilePath, metaInfo ? JSON.stringify(metaInfo) : '');
return metaInfo;
}
}
function parseMetaInfo(text: string, video: PageVideoTraceEvent): VideoMetaInfo | undefined {
const lines = text.split('\n');
let framesLine = lines.find(l => l.startsWith('frame='));
if (!framesLine)
return;
framesLine = framesLine.substring(framesLine.lastIndexOf('frame='));
const framesMatch = framesLine.match(/frame=\s+(\d+)/);
const outputLineIndex = lines.findIndex(l => l.trim().startsWith('Output #0'));
const streamLine = lines.slice(outputLineIndex).find(l => l.trim().startsWith('Stream #0:0'))!;
const fpsMatch = streamLine.match(/, (\d+) fps,/);
const resolutionMatch = streamLine.match(/, (\d+)x(\d+)\D/);
const durationMatch = lines.find(l => l.trim().startsWith('Duration'))!.match(/Duration: (\d+):(\d\d):(\d\d.\d\d)/);
const duration = (((parseInt(durationMatch![1], 10) * 60) + parseInt(durationMatch![2], 10)) * 60 + parseFloat(durationMatch![3])) * 1000;
return {
frames: parseInt(framesMatch![1], 10),
width: parseInt(resolutionMatch![1], 10),
height: parseInt(resolutionMatch![2], 10),
fps: parseInt(fpsMatch![1], 10),
startTime: video.timestamp,
endTime: video.timestamp + duration
};
}