Skip to content

Commit d3cf0da

Browse files
committed
feat(video): add quality option to recordVideo
1 parent e88823e commit d3cf0da

15 files changed

Lines changed: 358 additions & 13 deletions

File tree

docs/src/api/params.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,9 @@ When set to `minimal`, only record information necessary for routing from HAR. T
827827
- `duration` ?<[float]> How long each annotation is displayed in milliseconds. Defaults to `500`.
828828
- `position` ?<[AnnotatePosition]<"top-left"|"top"|"top-right"|"bottom-left"|"bottom"|"bottom-right">> Position of the action title overlay. Defaults to `"top-right"`.
829829
- `fontSize` ?<[int]> Font size of the action title in pixels. Defaults to `24`.
830+
- `quality` ?<[Object]> Encoding quality. When omitted, the tuned VP8 defaults are used.
831+
- `mode` <[VideoQualityMode]<"crf"|"bitrate">> `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate (variable visual quality, predictable file size).
832+
- `value` <[int]> For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per second (e.g. `1_000_000` for 1 Mbit/s).
830833

831834
Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. Make
832835
sure to await [`method: BrowserContext.close`] for videos to be saved.
@@ -851,6 +854,16 @@ Dimensions of the recorded videos. If not specified the size will be equal to `v
851854
scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450.
852855
Actual picture of each page will be scaled down if necessary to fit the specified size.
853856

857+
## context-option-recordvideo-quality
858+
* langs: csharp, java, python
859+
- alias-python: record_video_quality
860+
- `recordVideoQuality` <[Object]>
861+
* alias-java: RecordVideoQuality
862+
- `mode` <[VideoQualityMode]<"crf"|"bitrate">> `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate (variable visual quality, predictable file size).
863+
- `value` <[int]> For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per second (e.g. `1_000_000` for 1 Mbit/s).
864+
865+
Encoding quality. When omitted, the tuned VP8 defaults are used.
866+
854867
## context-option-proxy
855868
- `proxy` <[Object]>
856869
* alias: Proxy
@@ -1075,6 +1088,7 @@ between the same pixel in compared images, between zero (strict) and one (lax),
10751088
- %%-context-option-recordvideo-%%
10761089
- %%-context-option-recordvideo-dir-%%
10771090
- %%-context-option-recordvideo-size-%%
1091+
- %%-context-option-recordvideo-quality-%%
10781092
- %%-context-option-strict-%%
10791093
- %%-context-option-service-worker-policy-%%
10801094

docs/src/test-api/class-testoptions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,9 @@ export default defineConfig({
648648
- `level` ?<[TestAnnotationLevel]<"file"|"test"|"step">> Level of the detail to include about the current test.
649649
- `position` ?<[AnnotatePosition]<"top-left"|"top"|"top-right"|"bottom-left"|"bottom"|"bottom-right">> Position of the test information overlay. Defaults to `"top-left"`.
650650
- `fontSize` ?<[int]> Font size of the test information in pixels. Defaults to `14`.
651+
- `quality` ?<[Object]> Encoding quality. When omitted, the tuned VP8 defaults are used.
652+
- `mode` <[VideoQualityMode]<"crf"|"bitrate">> `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate (variable visual quality, predictable file size).
653+
- `value` <[int]> For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per second (e.g. `1_000_000` for 1 Mbit/s).
651654

652655
Whether to record video for each test. Defaults to `'off'`.
653656
* `'off'`: Do not record video.

packages/playwright-client/types/types.d.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10499,6 +10499,23 @@ export interface Browser {
1049910499
*/
1050010500
fontSize?: number;
1050110501
};
10502+
10503+
/**
10504+
* Encoding quality. When omitted, the tuned VP8 defaults are used.
10505+
*/
10506+
quality?: {
10507+
/**
10508+
* `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate
10509+
* (variable visual quality, predictable file size).
10510+
*/
10511+
mode: "crf"|"bitrate";
10512+
10513+
/**
10514+
* For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per
10515+
* second (e.g. `1_000_000` for 1 Mbit/s).
10516+
*/
10517+
value: number;
10518+
};
1050210519
};
1050310520

1050410521
/**
@@ -15946,6 +15963,23 @@ export interface BrowserType<Unused = {}> {
1594615963
*/
1594715964
fontSize?: number;
1594815965
};
15966+
15967+
/**
15968+
* Encoding quality. When omitted, the tuned VP8 defaults are used.
15969+
*/
15970+
quality?: {
15971+
/**
15972+
* `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate
15973+
* (variable visual quality, predictable file size).
15974+
*/
15975+
mode: "crf"|"bitrate";
15976+
15977+
/**
15978+
* For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per
15979+
* second (e.g. `1_000_000` for 1 Mbit/s).
15980+
*/
15981+
value: number;
15982+
};
1594915983
};
1595015984

1595115985
/**
@@ -22517,6 +22551,23 @@ export interface Electron {
2251722551
*/
2251822552
fontSize?: number;
2251922553
};
22554+
22555+
/**
22556+
* Encoding quality. When omitted, the tuned VP8 defaults are used.
22557+
*/
22558+
quality?: {
22559+
/**
22560+
* `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate
22561+
* (variable visual quality, predictable file size).
22562+
*/
22563+
mode: "crf"|"bitrate";
22564+
22565+
/**
22566+
* For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per
22567+
* second (e.g. `1_000_000` for 1 Mbit/s).
22568+
*/
22569+
value: number;
22570+
};
2252022571
};
2252122572

2252222573
/**
@@ -23205,6 +23256,23 @@ export interface AndroidDevice {
2320523256
*/
2320623257
fontSize?: number;
2320723258
};
23259+
23260+
/**
23261+
* Encoding quality. When omitted, the tuned VP8 defaults are used.
23262+
*/
23263+
quality?: {
23264+
/**
23265+
* `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate
23266+
* (variable visual quality, predictable file size).
23267+
*/
23268+
mode: "crf"|"bitrate";
23269+
23270+
/**
23271+
* For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per
23272+
* second (e.g. `1_000_000` for 1 Mbit/s).
23273+
*/
23274+
value: number;
23275+
};
2320823276
};
2320923277

2321023278
/**
@@ -24385,6 +24453,23 @@ export interface BrowserContextOptions {
2438524453
*/
2438624454
fontSize?: number;
2438724455
};
24456+
24457+
/**
24458+
* Encoding quality. When omitted, the tuned VP8 defaults are used.
24459+
*/
24460+
quality?: {
24461+
/**
24462+
* `"crf"` for constant rate factor (constant visual quality, variable file size). `"bitrate"` for a target bitrate
24463+
* (variable visual quality, predictable file size).
24464+
*/
24465+
mode: "crf"|"bitrate";
24466+
24467+
/**
24468+
* For `"crf"`, an integer between `0` (lossless) and `63` (worst). For `"bitrate"`, the target bitrate in bits per
24469+
* second (e.g. `1_000_000` for 1 Mbit/s).
24470+
*/
24471+
value: number;
24472+
};
2438824473
};
2438924474

2439024475
/**

packages/playwright-core/src/protocol/validator.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
209209
fontSize: tOptional(tInt),
210210
cursor: tOptional(tEnum(['none', 'pointer'])),
211211
})),
212+
quality: tOptional(tObject({
213+
mode: tEnum(['crf', 'bitrate']),
214+
value: tInt,
215+
})),
212216
})),
213217
strictSelectors: tOptional(tBoolean),
214218
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
@@ -494,6 +498,10 @@ scheme.BrowserNewContextParams = tObject({
494498
fontSize: tOptional(tInt),
495499
cursor: tOptional(tEnum(['none', 'pointer'])),
496500
})),
501+
quality: tOptional(tObject({
502+
mode: tEnum(['crf', 'bitrate']),
503+
value: tInt,
504+
})),
497505
})),
498506
strictSelectors: tOptional(tBoolean),
499507
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
@@ -571,6 +579,10 @@ scheme.BrowserNewContextForReuseParams = tObject({
571579
fontSize: tOptional(tInt),
572580
cursor: tOptional(tEnum(['none', 'pointer'])),
573581
})),
582+
quality: tOptional(tObject({
583+
mode: tEnum(['crf', 'bitrate']),
584+
value: tInt,
585+
})),
574586
})),
575587
strictSelectors: tOptional(tBoolean),
576588
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
@@ -670,6 +682,10 @@ scheme.BrowserContextInitializer = tObject({
670682
fontSize: tOptional(tInt),
671683
cursor: tOptional(tEnum(['none', 'pointer'])),
672684
})),
685+
quality: tOptional(tObject({
686+
mode: tEnum(['crf', 'bitrate']),
687+
value: tInt,
688+
})),
673689
})),
674690
strictSelectors: tOptional(tBoolean),
675691
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
@@ -1074,6 +1090,10 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
10741090
fontSize: tOptional(tInt),
10751091
cursor: tOptional(tEnum(['none', 'pointer'])),
10761092
})),
1093+
quality: tOptional(tObject({
1094+
mode: tEnum(['crf', 'bitrate']),
1095+
value: tInt,
1096+
})),
10771097
})),
10781098
strictSelectors: tOptional(tBoolean),
10791099
serviceWorkers: tOptional(tEnum(['allow', 'block'])),

packages/playwright-core/src/server/dispatchers/pageDispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
410410
let artifact: Artifact | undefined;
411411
if (params.record) {
412412
this._videoRecorder = new VideoRecorder(this._page.screencast);
413-
artifact = this._videoRecorder.start(params);
413+
artifact = this._videoRecorder.start({ size: params.size });
414414
}
415415
return { artifact: artifact ? createVideoDispatcher(this.parentScope(), artifact) : undefined };
416416
}

packages/playwright-core/src/server/videoRecorder.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ export class VideoRecorder {
4343
this._screencast = screencast;
4444
}
4545

46-
start(options: { fileName?: string, size?: { width: number, height: number } }) {
46+
start(options: {
47+
fileName?: string,
48+
size?: { width: number, height: number },
49+
quality?: { mode: 'crf' | 'bitrate', value: number },
50+
}) {
4751
assert(!this._artifact);
4852
// Do this first, it likes to throw.
4953
const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._screencast.page.browserContext._browser.sdkLanguage());
@@ -59,7 +63,7 @@ export class VideoRecorder {
5963
const { size } = this._screencast.addClient(this._client);
6064
// For video files only, prioritize encoding into the given size, regardless of the actual pixel data.
6165
const videoSize = options.size ?? size;
62-
this._videoRecorder = new FfmpegVideoRecorder(ffmpegPath, videoSize, outputFile);
66+
this._videoRecorder = new FfmpegVideoRecorder(ffmpegPath, videoSize, outputFile, { quality: options.quality });
6367
this._artifact = new Artifact(this._screencast.page.browserContext, outputFile);
6468
return this._artifact;
6569
}
@@ -91,10 +95,16 @@ export function startAutomaticVideoRecording(page: Page) {
9195
if (page.browserContext._options.recordVideo?.showActions)
9296
page.screencast.showActions(page.browserContext._options.recordVideo?.showActions);
9397
const dir = recordVideo.dir ?? page.browserContext._browser.options.artifactsDir;
94-
const artifact = recorder.start({ size: recordVideo.size, fileName: path.join(dir, page.guid + '.webm') });
98+
const artifact = recorder.start({
99+
size: recordVideo.size,
100+
fileName: path.join(dir, page.guid + '.webm'),
101+
quality: recordVideo.quality,
102+
});
95103
page.video = artifact;
96104
}
97105

106+
type QualityOption = { mode: 'crf' | 'bitrate', value: number };
107+
98108
class FfmpegVideoRecorder {
99109
private _size: types.Size;
100110
private _process: ChildProcess | null = null;
@@ -108,13 +118,15 @@ class FfmpegVideoRecorder {
108118
private _ffmpegPath: string;
109119
private _launchPromise: Promise<Error | null>;
110120
private _outputFile: string;
121+
private _quality: QualityOption | undefined;
111122

112-
constructor(ffmpegPath: string, size: types.Size, outputFile: string) {
123+
constructor(ffmpegPath: string, size: types.Size, outputFile: string, options: { quality?: QualityOption } = {}) {
113124
if (!outputFile.endsWith('.webm'))
114125
throw new Error('File must have .webm extension');
115126
this._outputFile = outputFile;
116127
this._ffmpegPath = ffmpegPath;
117128
this._size = size;
129+
this._quality = options.quality;
118130
this._launchPromise = this._launch().catch(e => e);
119131
}
120132

@@ -159,9 +171,7 @@ class FfmpegVideoRecorder {
159171
// "-threads 1" means using one thread. This drastically reduces stalling when
160172
// cpu is overbooked. By default vp8 tries to use all available threads?
161173

162-
const w = this._size.width;
163-
const h = this._size.height;
164-
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(' ');
174+
const args = buildFfmpegArgs(this._size, this._quality);
165175
args.push(this._outputFile);
166176

167177
const { launchedProcess, gracefullyClose } = await launchProcess({
@@ -258,3 +268,14 @@ function createWhiteImage(width: number, height: number): Buffer {
258268
const data = Buffer.alloc(width * height * 4, 255);
259269
return jpegjs.encode({ data, width, height }, 80).data;
260270
}
271+
272+
function buildFfmpegArgs(size: types.Size, quality: QualityOption | undefined): string[] {
273+
const w = size.width;
274+
const h = size.height;
275+
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 -deadline realtime -speed 8 -threads 1 -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(' ');
276+
if (quality?.mode === 'bitrate')
277+
args.push('-b:v', String(quality.value));
278+
else
279+
args.push('-crf', String(quality?.value ?? 8), '-b:v', '1M');
280+
return args;
281+
}

0 commit comments

Comments
 (0)