feat(screencast): use ffmpeg to produce webm in chromium (#3668)
This commit is contained in:
parent
3cc91093a1
commit
8ec55e1fb2
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -1081,6 +1081,62 @@
|
||||||
"sumchecker": "^3.0.1"
|
"sumchecker": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@ffmpeg-installer/darwin-x64": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/ffmpeg": {
|
||||||
|
"version": "1.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.0.20.tgz",
|
||||||
|
"integrity": "sha512-wbgd//6OdwbFXYgV68ZyKrIcozEQpUKlvV66XHaqO2h3sFbX0jYLzx62Q0v8UcFWN21LoxT98NU2P+K0OWsKNA==",
|
||||||
|
"requires": {
|
||||||
|
"@ffmpeg-installer/darwin-x64": "4.1.0",
|
||||||
|
"@ffmpeg-installer/linux-arm": "4.1.3",
|
||||||
|
"@ffmpeg-installer/linux-arm64": "4.1.4",
|
||||||
|
"@ffmpeg-installer/linux-ia32": "4.1.0",
|
||||||
|
"@ffmpeg-installer/linux-x64": "4.1.0",
|
||||||
|
"@ffmpeg-installer/win32-ia32": "4.1.0",
|
||||||
|
"@ffmpeg-installer/win32-x64": "4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/linux-arm": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/linux-arm64": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/linux-ia32": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/linux-x64": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/win32-ia32": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ffmpeg-installer/win32-x64": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"@jest/types": {
|
"@jest/types": {
|
||||||
"version": "26.3.0",
|
"version": "26.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ffmpeg-installer/ffmpeg": "^1.0.20",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"https-proxy-agent": "^5.0.0",
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import { ConsoleMessage } from '../console';
|
||||||
import * as sourceMap from '../../utils/sourceMap';
|
import * as sourceMap from '../../utils/sourceMap';
|
||||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||||
import { assert, headersArrayToObject } from '../../utils/utils';
|
import { assert, headersArrayToObject } from '../../utils/utils';
|
||||||
|
import { VideoRecorder } from './videoRecorder';
|
||||||
|
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
@ -209,11 +210,11 @@ export class CRPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async startScreencast(options: types.PageScreencastOptions): Promise<void> {
|
async startScreencast(options: types.PageScreencastOptions): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
await this._mainFrameSession._startScreencast(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopScreencast(): Promise<void> {
|
async stopScreencast(): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
await this._mainFrameSession._stopScreencast();
|
||||||
}
|
}
|
||||||
|
|
||||||
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {
|
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {
|
||||||
|
|
@ -324,6 +325,7 @@ class FrameSession {
|
||||||
// Marks the oopif session that remote -> local transition has happened in the parent.
|
// Marks the oopif session that remote -> local transition has happened in the parent.
|
||||||
// See Target.detachedFromTarget handler for details.
|
// See Target.detachedFromTarget handler for details.
|
||||||
private _swappedIn = false;
|
private _swappedIn = false;
|
||||||
|
private _videoRecorder: VideoRecorder | null = null;
|
||||||
|
|
||||||
constructor(crPage: CRPage, client: CRSession, targetId: string, parentSession: FrameSession | null) {
|
constructor(crPage: CRPage, client: CRSession, targetId: string, parentSession: FrameSession | null) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
|
|
@ -358,6 +360,7 @@ class FrameSession {
|
||||||
helper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
|
helper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
|
||||||
helper.addEventListener(this._client, 'Page.downloadWillBegin', event => this._onDownloadWillBegin(event)),
|
helper.addEventListener(this._client, 'Page.downloadWillBegin', event => this._onDownloadWillBegin(event)),
|
||||||
helper.addEventListener(this._client, 'Page.downloadProgress', event => this._onDownloadProgress(event)),
|
helper.addEventListener(this._client, 'Page.downloadProgress', event => this._onDownloadProgress(event)),
|
||||||
|
helper.addEventListener(this._client, 'Page.screencastFrame', event => this._onScreencastFrame(event)),
|
||||||
helper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)),
|
helper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)),
|
||||||
helper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)),
|
helper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)),
|
||||||
helper.addEventListener(this._client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)),
|
helper.addEventListener(this._client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)),
|
||||||
|
|
@ -724,6 +727,34 @@ class FrameSession {
|
||||||
this._crPage._browserContext._browser._downloadFinished(payload.guid, 'canceled');
|
this._crPage._browserContext._browser._downloadFinished(payload.guid, 'canceled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onScreencastFrame(payload: Protocol.Page.screencastFramePayload) {
|
||||||
|
if (!this._videoRecorder)
|
||||||
|
return;
|
||||||
|
const buffer = Buffer.from(payload.data, 'base64');
|
||||||
|
this._videoRecorder.writeFrame(buffer, payload.metadata.timestamp!);
|
||||||
|
this._client.send('Page.screencastFrameAck', {sessionId: payload.sessionId});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _startScreencast(options: types.PageScreencastOptions): Promise<void> {
|
||||||
|
assert(!this._videoRecorder, 'Already started');
|
||||||
|
this._videoRecorder = await VideoRecorder.launch(options);
|
||||||
|
await this._client.send('Page.startScreencast', {
|
||||||
|
format: 'jpeg',
|
||||||
|
quality: 90,
|
||||||
|
maxWidth: options.width,
|
||||||
|
maxHeight: options.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _stopScreencast(): Promise<void> {
|
||||||
|
if (!this._videoRecorder)
|
||||||
|
return;
|
||||||
|
const recorder = this._videoRecorder;
|
||||||
|
this._videoRecorder = null;
|
||||||
|
await this._client.send('Page.stopScreencast');
|
||||||
|
await recorder.stop();
|
||||||
|
}
|
||||||
|
|
||||||
async _updateExtraHTTPHeaders(): Promise<void> {
|
async _updateExtraHTTPHeaders(): Promise<void> {
|
||||||
const headers = network.mergeHeaders([
|
const headers = network.mergeHeaders([
|
||||||
this._crPage._browserContext._options.extraHTTPHeaders,
|
this._crPage._browserContext._options.extraHTTPHeaders,
|
||||||
|
|
|
||||||
128
src/server/chromium/videoRecorder.ts
Normal file
128
src/server/chromium/videoRecorder.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
|
||||||
|
import { launchProcess } from '../processLauncher';
|
||||||
|
import { ChildProcess } from 'child_process';
|
||||||
|
import { Progress, runAbortableTask } from '../progress';
|
||||||
|
import * as types from '../types';
|
||||||
|
import { assert } from '../../utils/utils';
|
||||||
|
|
||||||
|
const fps = 25;
|
||||||
|
|
||||||
|
export class VideoRecorder {
|
||||||
|
private _process: ChildProcess | null = null;
|
||||||
|
private _gracefullyClose: (() => Promise<void>) | null = null;
|
||||||
|
private _lastWritePromise: Promise<void>;
|
||||||
|
private _lastFrameTimestamp: number = 0;
|
||||||
|
private _lastFrameBuffer: Buffer | null = null;
|
||||||
|
private _lastWriteTimestamp: number = 0;
|
||||||
|
private readonly _progress: Progress;
|
||||||
|
|
||||||
|
static async launch(options: types.PageScreencastOptions): Promise<VideoRecorder> {
|
||||||
|
if (!options.outputFile.endsWith('.webm'))
|
||||||
|
throw new Error('File must have .webm extension');
|
||||||
|
|
||||||
|
return await runAbortableTask(async progress => {
|
||||||
|
const recorder = new VideoRecorder(progress);
|
||||||
|
await recorder._launch(options);
|
||||||
|
return recorder;
|
||||||
|
}, 0, 'browser');
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(progress: Progress) {
|
||||||
|
this._progress = progress;
|
||||||
|
this._lastWritePromise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _launch(options: types.PageScreencastOptions) {
|
||||||
|
assert(!this._isRunning());
|
||||||
|
const w = options.width;
|
||||||
|
const h = options.height;
|
||||||
|
const args = `-f image2pipe -c:v mjpeg -i - -y -an -r ${fps} -c:v vp8 -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(' ');
|
||||||
|
args.push(options.outputFile);
|
||||||
|
const progress = this._progress;
|
||||||
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||||
|
executablePath: ffmpegPath,
|
||||||
|
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;
|
||||||
|
const duration = this._lastFrameTimestamp ? Math.max(1, Math.round(25 * (timestamp - this._lastFrameTimestamp))) : 1;
|
||||||
|
this._progress.log(`writing ${duration} frame(s)`);
|
||||||
|
this._lastFrameBuffer = frame;
|
||||||
|
this._lastFrameTimestamp = timestamp;
|
||||||
|
this._lastWriteTimestamp = Date.now();
|
||||||
|
|
||||||
|
const previousWrites = this._lastWritePromise;
|
||||||
|
let finishedWriting: () => void;
|
||||||
|
this._lastWritePromise = new Promise(fulfill => finishedWriting = fulfill);
|
||||||
|
const writePromise = this._lastWritePromise;
|
||||||
|
await previousWrites;
|
||||||
|
for (let i = 0; i < duration; i++) {
|
||||||
|
const callFinish = i === (duration - 1);
|
||||||
|
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;
|
||||||
|
if (durationSec > 1 / fps)
|
||||||
|
this.writeFrame(this._lastFrameBuffer!, this._lastFrameTimestamp + durationSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = this._gracefullyClose;
|
||||||
|
this._gracefullyClose = null;
|
||||||
|
await this._lastWritePromise;
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isRunning(): boolean {
|
||||||
|
return !!this._gracefullyClose;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ export type LaunchProcessOptions = {
|
||||||
handleSIGTERM?: boolean,
|
handleSIGTERM?: boolean,
|
||||||
handleSIGHUP?: boolean,
|
handleSIGHUP?: boolean,
|
||||||
pipe?: boolean,
|
pipe?: boolean,
|
||||||
|
pipeStdin?: boolean,
|
||||||
tempDirectories: string[],
|
tempDirectories: string[],
|
||||||
|
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
|
|
@ -62,6 +63,8 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
|
|
||||||
const progress = options.progress;
|
const progress = options.progress;
|
||||||
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
||||||
|
if (options.pipeStdin)
|
||||||
|
stdio[0] = 'pipe';
|
||||||
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
||||||
const spawnedProcess = childProcess.spawn(
|
const spawnedProcess = childProcess.spawn(
|
||||||
options.executablePath,
|
options.executablePath,
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@ it('should reject referer option when setExtraHTTPHeaders provides referer', asy
|
||||||
it('should override referrer-policy', async ({page, server}) => {
|
it('should override referrer-policy', async ({page, server}) => {
|
||||||
server.setRoute('/grid.html', (req, res) => {
|
server.setRoute('/grid.html', (req, res) => {
|
||||||
res.setHeader('Referrer-Policy', 'no-referrer');
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
||||||
server.serveFile(req, res, '/grid.html');
|
server.serveFile(req, res);
|
||||||
});
|
});
|
||||||
const [request1, request2] = await Promise.all([
|
const [request1, request2] = await Promise.all([
|
||||||
server.waitForRequest('/grid.html'),
|
server.waitForRequest('/grid.html'),
|
||||||
|
|
@ -482,7 +482,7 @@ it('extraHttpHeaders should be pushed to provisional page', test => {
|
||||||
const pagePath = '/one-style.html';
|
const pagePath = '/one-style.html';
|
||||||
server.setRoute(pagePath, async (req, res) => {
|
server.setRoute(pagePath, async (req, res) => {
|
||||||
page.setExtraHTTPHeaders({ foo: 'bar' });
|
page.setExtraHTTPHeaders({ foo: 'bar' });
|
||||||
server.serveFile(req, res, pagePath);
|
server.serveFile(req, res);
|
||||||
});
|
});
|
||||||
const [htmlReq, cssReq] = await Promise.all([
|
const [htmlReq, cssReq] = await Promise.all([
|
||||||
server.waitForRequest(pagePath),
|
server.waitForRequest(pagePath),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import type { Page } from '..';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import url from 'url';
|
import { TestServer } from '../utils/testserver';
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -29,7 +29,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFixture('videoPlayer', async ({playwright, context}, test) => {
|
registerFixture('videoPlayer', async ({playwright, context, server}, test) => {
|
||||||
let firefox;
|
let firefox;
|
||||||
if (options.WEBKIT && !LINUX) {
|
if (options.WEBKIT && !LINUX) {
|
||||||
// WebKit on Mac & Windows cannot replay webm/vp8 video, so we launch Firefox.
|
// WebKit on Mac & Windows cannot replay webm/vp8 video, so we launch Firefox.
|
||||||
|
|
@ -38,7 +38,7 @@ registerFixture('videoPlayer', async ({playwright, context}, test) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const player = new VideoPlayer(page);
|
const player = new VideoPlayer(page, server);
|
||||||
await test(player);
|
await test(player);
|
||||||
if (firefox)
|
if (firefox)
|
||||||
await firefox.close();
|
await firefox.close();
|
||||||
|
|
@ -90,12 +90,19 @@ function expectAll(pixels, rgbaPredicate) {
|
||||||
|
|
||||||
class VideoPlayer {
|
class VideoPlayer {
|
||||||
private readonly _page: Page;
|
private readonly _page: Page;
|
||||||
constructor(page: Page) {
|
private readonly _server: TestServer;
|
||||||
|
constructor(page: Page, server: TestServer) {
|
||||||
this._page = page;
|
this._page = page;
|
||||||
|
this._server = server;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(videoFile) {
|
async load(videoFile: string) {
|
||||||
await this._page.goto(url.pathToFileURL(videoFile).href);
|
const servertPath = '/v.webm';
|
||||||
|
this._server.setRoute(servertPath, (req, response) => {
|
||||||
|
this._server.serveFile(req, response, videoFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._page.goto(this._server.PREFIX + servertPath);
|
||||||
await this._page.$eval('video', (v: HTMLVideoElement) => {
|
await this._page.$eval('video', (v: HTMLVideoElement) => {
|
||||||
return new Promise(fulfil => {
|
return new Promise(fulfil => {
|
||||||
// In case video playback autostarts.
|
// In case video playback autostarts.
|
||||||
|
|
@ -172,36 +179,35 @@ class VideoPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('should capture static page', test => {
|
||||||
|
test.skip(options.WIRE);
|
||||||
|
}, async ({page, tmpDir, videoPlayer, toImpl}) => {
|
||||||
|
const videoFile = path.join(tmpDir, 'v.webm');
|
||||||
|
await page.evaluate(() => document.body.style.backgroundColor = 'red');
|
||||||
|
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
||||||
|
// TODO: in WebKit figure out why video size is not reported correctly for
|
||||||
|
// static pictures.
|
||||||
|
if (options.HEADLESS && options.WEBKIT)
|
||||||
|
await page.setViewportSize({width: 1270, height: 950});
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
await toImpl(page)._delegate.stopScreencast();
|
||||||
|
expect(fs.existsSync(videoFile)).toBe(true);
|
||||||
|
|
||||||
|
await videoPlayer.load(videoFile);
|
||||||
|
const duration = await videoPlayer.duration();
|
||||||
|
expect(duration).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(await videoPlayer.videoWidth()).toBe(640);
|
||||||
|
expect(await videoPlayer.videoHeight()).toBe(480);
|
||||||
|
|
||||||
|
await videoPlayer.seekLastNonEmptyFrame();
|
||||||
|
const pixels = await videoPlayer.pixels();
|
||||||
|
expectAll(pixels, almostRed);
|
||||||
|
});
|
||||||
|
|
||||||
describe('screencast', suite => {
|
describe('screencast', suite => {
|
||||||
suite.skip(options.WIRE);
|
suite.skip(options.WIRE || options.CHROMIUM);
|
||||||
suite.fixme(options.CHROMIUM);
|
|
||||||
}, () => {
|
}, () => {
|
||||||
it('should capture static page', test => {
|
|
||||||
test.fixme();
|
|
||||||
}, async ({page, tmpDir, videoPlayer, toImpl}) => {
|
|
||||||
const videoFile = path.join(tmpDir, 'v.webm');
|
|
||||||
await page.evaluate(() => document.body.style.backgroundColor = 'red');
|
|
||||||
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
|
||||||
// TODO: in WebKit figure out why video size is not reported correctly for
|
|
||||||
// static pictures.
|
|
||||||
if (options.HEADLESS && options.WEBKIT)
|
|
||||||
await page.setViewportSize({width: 1270, height: 950});
|
|
||||||
await new Promise(r => setTimeout(r, 300));
|
|
||||||
await toImpl(page)._delegate.stopScreencast();
|
|
||||||
expect(fs.existsSync(videoFile)).toBe(true);
|
|
||||||
|
|
||||||
await videoPlayer.load(videoFile);
|
|
||||||
const duration = await videoPlayer.duration();
|
|
||||||
expect(duration).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
expect(await videoPlayer.videoWidth()).toBe(640);
|
|
||||||
expect(await videoPlayer.videoHeight()).toBe(480);
|
|
||||||
|
|
||||||
await videoPlayer.seekLastNonEmptyFrame();
|
|
||||||
const pixels = await videoPlayer.pixels();
|
|
||||||
expectAll(pixels, almostRed);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should capture navigation', test => {
|
it('should capture navigation', test => {
|
||||||
test.flaky(options.WEBKIT);
|
test.flaky(options.WEBKIT);
|
||||||
test.flaky(options.FIREFOX);
|
test.flaky(options.FIREFOX);
|
||||||
|
|
|
||||||
3
utils/testserver/index.d.ts
vendored
3
utils/testserver/index.d.ts
vendored
|
|
@ -29,7 +29,8 @@ export class TestServer {
|
||||||
setRedirect(from: string, to: string);
|
setRedirect(from: string, to: string);
|
||||||
waitForRequest(path: string): Promise<IncomingMessage & {postBody: Buffer}>;
|
waitForRequest(path: string): Promise<IncomingMessage & {postBody: Buffer}>;
|
||||||
reset();
|
reset();
|
||||||
serveFile(request: IncomingMessage, response: ServerResponse, pathName: string);
|
serveFile(request: IncomingMessage, response: ServerResponse);
|
||||||
|
serveFile(request: IncomingMessage, response: ServerResponse, filePath: string);
|
||||||
|
|
||||||
PORT: number;
|
PORT: number;
|
||||||
PREFIX: string;
|
PREFIX: string;
|
||||||
|
|
|
||||||
|
|
@ -228,20 +228,22 @@ class TestServer {
|
||||||
if (handler) {
|
if (handler) {
|
||||||
handler.call(null, request, response);
|
handler.call(null, request, response);
|
||||||
} else {
|
} else {
|
||||||
const pathName = url.parse(request.url).path;
|
this.serveFile(request, response);
|
||||||
this.serveFile(request, response, pathName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {!http.IncomingMessage} request
|
* @param {!http.IncomingMessage} request
|
||||||
* @param {!http.ServerResponse} response
|
* @param {!http.ServerResponse} response
|
||||||
* @param {string} pathName
|
* @param {string|undefined} filePath
|
||||||
*/
|
*/
|
||||||
serveFile(request, response, pathName) {
|
serveFile(request, response, filePath) {
|
||||||
if (pathName === '/')
|
let pathName = url.parse(request.url).path;
|
||||||
pathName = '/index.html';
|
if (!filePath) {
|
||||||
const filePath = path.join(this._dirPath, pathName.substring(1));
|
if (pathName === '/')
|
||||||
|
pathName = '/index.html';
|
||||||
|
filePath = path.join(this._dirPath, pathName.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) {
|
if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) {
|
||||||
if (request.headers['if-modified-since']) {
|
if (request.headers['if-modified-since']) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue