Visitar URL original
chore(video): emit frames with real timestamps, drop frame number qua… · microsoft/playwright@bd6e552 · GitHub
Skip to content

Commit bd6e552

Browse files
authored
chore(video): emit frames with real timestamps, drop frame number quantization (#42480)
1 parent a76bdda commit bd6e552

3 files changed

Lines changed: 24 additions & 25 deletions

File tree

‎packages/playwright-core/src/server/firefox/ffPage.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export class FFPage implements PageDelegate {
5757
private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
5858
private _webSocketRequests = new Map<string, { url: string, headers: types.HeadersArray }>();
5959
private _webSocketResponses = new Map<string, { status: number, statusText: string, headers: types.HeadersArray }>();
60+
// FIXME: remove this once Firefox is on wall clock.
61+
private _screencastClockOffset = 0;
6062

6163
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
6264
this._session = session;
@@ -544,9 +546,12 @@ export class FFPage implements PageDelegate {
544546

545547
private _onScreencastFrame(event: Protocol.Page.screencastFramePayload) {
546548
const buffer = Buffer.from(event.data, 'base64');
549+
// event.timestamp is monotonic seconds, anchor it to the wall clock at the first frame.
550+
if (!this._screencastClockOffset)
551+
this._screencastClockOffset = Date.now() - event.timestamp * 1000;
547552
void this._page.screencast.onScreencastFrame({
548553
buffer,
549-
frameSwapWallTime: event.timestamp * 1000, // timestamp is in seconds, we need to convert to milliseconds.
554+
frameSwapWallTime: event.timestamp * 1000 + this._screencastClockOffset,
550555
viewportWidth: event.deviceWidth,
551556
viewportHeight: event.deviceHeight,
552557
}).then(() => {

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

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class VideoRecorder {
5151
const outputFile = options.fileName ?? path.join(this._screencast.page.browserContext._browser.options.artifactsDir, createGuid() + '.webm');
5252

5353
this._client = {
54-
onFrame: frame => this._videoRecorder!.writeFrame(frame.buffer, frame.frameSwapWallTime / 1000),
54+
onFrame: frame => this._videoRecorder!.writeFrame(frame.buffer, frame.frameSwapWallTime),
5555
gracefulClose: () => this.stop(),
5656
dispose: () => this.stop().catch(e => debugLogger.log('error', `Failed to stop video recorder: ${String(e)}`)),
5757
size: options.size,
@@ -100,8 +100,8 @@ class FfmpegVideoRecorder {
100100
private _size: types.Size;
101101
private _process: ChildProcess | null = null;
102102
private _gracefullyClose: (() => Promise<void>) | null = null;
103-
private _firstFrameTimestamp: number = 0;
104-
private _lastFrame: { timestamp: number, frameNumber: number, buffer: Buffer } | null = null;
103+
private _creationTimeMs: number;
104+
private _lastFrame: { timestamp: number, buffer: Buffer } | null = null;
105105
private _lastWriteNodeTime: number = 0;
106106
private _isStopped = false;
107107
private _ffmpegPath: string;
@@ -114,6 +114,7 @@ class FfmpegVideoRecorder {
114114
this._outputFile = outputFile;
115115
this._ffmpegPath = ffmpegPath;
116116
this._size = size;
117+
this._creationTimeMs = Date.now();
117118
this._launchPromise = this._launch(page).catch(e => e);
118119
}
119120

@@ -165,6 +166,7 @@ class FfmpegVideoRecorder {
165166
const h = this._size.height;
166167
const videoFilterArgs = page.getFFmpegVideoFilterArgs?.({ width: w, height: h }) ?? `pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`;
167168
const args = `-loglevel error -f matroska -fpsprobesize 0 -probesize 32 -analyzeduration 0 -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 ${videoFilterArgs}`.split(' ');
169+
args.push('-metadata', `creation_time=${new Date(this._creationTimeMs).toISOString()}`);
168170
args.push(this._outputFile);
169171

170172
const { launchedProcess, gracefullyClose } = await launchProcess({
@@ -205,20 +207,13 @@ class FfmpegVideoRecorder {
205207
if (this._isStopped)
206208
return;
207209

208-
if (!this._firstFrameTimestamp)
209-
this._firstFrameTimestamp = timestamp;
210-
211-
const frameNumber = Math.floor((timestamp - this._firstFrameTimestamp) * fps);
212-
if (this._lastFrame && frameNumber !== this._lastFrame.frameNumber)
213-
this._emitFrame(this._lastFrame.buffer, this._lastFrame.frameNumber);
214-
215-
this._lastFrame = { buffer: frame, timestamp, frameNumber };
210+
this._emitFrame(frame, timestamp - this._creationTimeMs);
211+
this._lastFrame = { buffer: frame, timestamp };
216212
this._lastWriteNodeTime = monotonicTime();
217213
}
218214

219-
private _emitFrame(frame: Buffer, frameNumber: number) {
220-
const timestampMs = Math.max(0, Math.round(frameNumber * 1000 / fps));
221-
this._process!.stdin!.write(writeClusterHeader(timestampMs, frame.length));
215+
private _emitFrame(frame: Buffer, timestampMs: number) {
216+
this._process!.stdin!.write(writeClusterHeader(Math.max(0, Math.round(timestampMs)), frame.length));
222217
this._process!.stdin!.write(frame);
223218
}
224219

@@ -231,15 +226,10 @@ class FfmpegVideoRecorder {
231226
return;
232227
if (!this._lastFrame) {
233228
// ffmpeg only creates a file upon some non-empty input.
234-
this._writeFrame(createWhiteImage(this._size.width, this._size.height), monotonicTime() / 1000);
229+
this._writeFrame(createWhiteImage(this._size.width, this._size.height), Date.now());
235230
}
236-
// Emit the last received frame at its own slot, then repeat it at the end so it stays visible
237-
// for at least 1s. This also ensures non-empty videos with 1 frame and gives the output stream
238-
// a final timestamp.
239-
this._emitFrame(this._lastFrame!.buffer, this._lastFrame!.frameNumber);
240-
const addTime = Math.max((monotonicTime() - this._lastWriteNodeTime) / 1000, 1);
241-
const endFrameNumber = Math.floor((this._lastFrame!.timestamp + addTime - this._firstFrameTimestamp) * fps);
242-
this._emitFrame(this._lastFrame!.buffer, endFrameNumber);
231+
const addTimeMs = Math.max(monotonicTime() - this._lastWriteNodeTime, 1000);
232+
this._emitFrame(this._lastFrame!.buffer, this._lastFrame!.timestamp + addTimeMs - this._creationTimeMs);
243233
this._isStopped = true;
244234
try {
245235
await this._gracefullyClose!();

‎packages/playwright-core/src/server/webkit/wkPage.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export class WKPage implements PageDelegate {
8080
// until the popup page proxy arrives.
8181
private _nextWindowOpenPopupFeatures?: string[];
8282
private _screencastGeneration: number = 0;
83+
// FIXME: remove this once WebKit is on wall clock.
84+
private _screencastClockOffset = 0;
8385

8486
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) {
8587
this._pageProxySession = pageProxySession;
@@ -956,10 +958,12 @@ export class WKPage implements PageDelegate {
956958
private _onScreencastFrame(event: Protocol.Screencast.screencastFramePayload) {
957959
const generation = this._screencastGeneration;
958960
const buffer = Buffer.from(event.data, 'base64');
961+
// event.timestamp is monotonic seconds, anchor it to the wall clock at the first frame.
962+
if (!this._screencastClockOffset)
963+
this._screencastClockOffset = Date.now() - event.timestamp * 1000;
959964
void this._page.screencast.onScreencastFrame({
960965
buffer,
961-
// timestamp is in seconds, we need to convert to milliseconds.
962-
frameSwapWallTime: event.timestamp * 1000,
966+
frameSwapWallTime: event.timestamp * 1000 + this._screencastClockOffset,
963967
viewportWidth: event.deviceWidth,
964968
viewportHeight: event.deviceHeight,
965969
}).then(() => {

0 commit comments

Comments
 (0)