mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(screencast): add saveAs and createReadableStream (#3879)
This commit is contained in:
parent
e4e3f82337
commit
459d857bc3
@ -14,10 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Readable } from 'stream';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { mkdirIfNeeded } from '../utils/utils';
|
||||||
import { Browser } from './browser';
|
import { Browser } from './browser';
|
||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
|
import { Stream } from './stream';
|
||||||
|
|
||||||
export class Video extends ChannelOwner<channels.VideoChannel, channels.VideoInitializer> {
|
export class Video extends ChannelOwner<channels.VideoChannel, channels.VideoInitializer> {
|
||||||
private _browser: Browser | null;
|
private _browser: Browser | null;
|
||||||
@ -36,4 +40,31 @@ export class Video extends ChannelOwner<channels.VideoChannel, channels.VideoIni
|
|||||||
throw new Error(`Path is not available when using browserType.connect().`);
|
throw new Error(`Path is not available when using browserType.connect().`);
|
||||||
return (await this._channel.path()).value;
|
return (await this._channel.path()).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveAs(path: string): Promise<void> {
|
||||||
|
return this._wrapApiCall('video.saveAs', async () => {
|
||||||
|
if (!this._browser || !this._browser._isRemote) {
|
||||||
|
await this._channel.saveAs({ path });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await this.createReadStream();
|
||||||
|
if (!stream)
|
||||||
|
throw new Error('Failed to copy video from server');
|
||||||
|
await mkdirIfNeeded(path);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
stream.pipe(fs.createWriteStream(path))
|
||||||
|
.on('finish' as any, resolve)
|
||||||
|
.on('error' as any, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createReadStream(): Promise<Readable | null> {
|
||||||
|
const result = await this._channel.stream();
|
||||||
|
if (!result.stream)
|
||||||
|
return null;
|
||||||
|
const stream = Stream.from(result.stream);
|
||||||
|
return stream.stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as util from 'util';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { Video } from '../server/browserContext';
|
import { Video } from '../server/browserContext';
|
||||||
|
import { mkdirIfNeeded } from '../utils/utils';
|
||||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||||
|
import { StreamDispatcher } from './streamDispatcher';
|
||||||
|
|
||||||
export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer> implements channels.VideoChannel {
|
export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer> implements channels.VideoChannel {
|
||||||
constructor(scope: DispatcherScope, screencast: Video) {
|
constructor(scope: DispatcherScope, screencast: Video) {
|
||||||
@ -26,4 +30,18 @@ export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer
|
|||||||
async path(): Promise<channels.VideoPathResult> {
|
async path(): Promise<channels.VideoPathResult> {
|
||||||
return { value: await this._object.path() };
|
return { value: await this._object.path() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveAs(params: channels.VideoSaveAsParams): Promise<channels.VideoSaveAsResult> {
|
||||||
|
const fileName = await this._object.path();
|
||||||
|
await mkdirIfNeeded(params.path);
|
||||||
|
await util.promisify(fs.copyFile)(fileName, params.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stream(): Promise<channels.VideoStreamResult> {
|
||||||
|
const fileName = await this._object.path();
|
||||||
|
const readable = fs.createReadStream(fileName);
|
||||||
|
await new Promise(f => readable.on('readable', f));
|
||||||
|
return { stream: new StreamDispatcher(this._scope, readable) };
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2144,12 +2144,26 @@ export type DialogDismissResult = void;
|
|||||||
export type VideoInitializer = {};
|
export type VideoInitializer = {};
|
||||||
export interface VideoChannel extends Channel {
|
export interface VideoChannel extends Channel {
|
||||||
path(params?: VideoPathParams, metadata?: Metadata): Promise<VideoPathResult>;
|
path(params?: VideoPathParams, metadata?: Metadata): Promise<VideoPathResult>;
|
||||||
|
saveAs(params: VideoSaveAsParams, metadata?: Metadata): Promise<VideoSaveAsResult>;
|
||||||
|
stream(params?: VideoStreamParams, metadata?: Metadata): Promise<VideoStreamResult>;
|
||||||
}
|
}
|
||||||
export type VideoPathParams = {};
|
export type VideoPathParams = {};
|
||||||
export type VideoPathOptions = {};
|
export type VideoPathOptions = {};
|
||||||
export type VideoPathResult = {
|
export type VideoPathResult = {
|
||||||
value: string,
|
value: string,
|
||||||
};
|
};
|
||||||
|
export type VideoSaveAsParams = {
|
||||||
|
path: string,
|
||||||
|
};
|
||||||
|
export type VideoSaveAsOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type VideoSaveAsResult = void;
|
||||||
|
export type VideoStreamParams = {};
|
||||||
|
export type VideoStreamOptions = {};
|
||||||
|
export type VideoStreamResult = {
|
||||||
|
stream?: StreamChannel,
|
||||||
|
};
|
||||||
|
|
||||||
// ----------- Download -----------
|
// ----------- Download -----------
|
||||||
export type DownloadInitializer = {
|
export type DownloadInitializer = {
|
||||||
|
@ -1808,6 +1808,15 @@ Video:
|
|||||||
returns:
|
returns:
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
|
# Blocks path until saved to the local |path|.
|
||||||
|
saveAs:
|
||||||
|
parameters:
|
||||||
|
path: string
|
||||||
|
|
||||||
|
stream:
|
||||||
|
returns:
|
||||||
|
stream: Stream?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Download:
|
Download:
|
||||||
|
@ -814,6 +814,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
});
|
});
|
||||||
scheme.DialogDismissParams = tOptional(tObject({}));
|
scheme.DialogDismissParams = tOptional(tObject({}));
|
||||||
scheme.VideoPathParams = tOptional(tObject({}));
|
scheme.VideoPathParams = tOptional(tObject({}));
|
||||||
|
scheme.VideoSaveAsParams = tObject({
|
||||||
|
path: tString,
|
||||||
|
});
|
||||||
|
scheme.VideoStreamParams = tOptional(tObject({}));
|
||||||
scheme.DownloadPathParams = tOptional(tObject({}));
|
scheme.DownloadPathParams = tOptional(tObject({}));
|
||||||
scheme.DownloadSaveAsParams = tObject({
|
scheme.DownloadSaveAsParams = tObject({
|
||||||
path: tString,
|
path: tString,
|
||||||
|
@ -423,4 +423,43 @@ describe('screencast', suite => {
|
|||||||
expect(await videoPlayer.videoWidth()).toBe(1280);
|
expect(await videoPlayer.videoWidth()).toBe(1280);
|
||||||
expect(await videoPlayer.videoHeight()).toBe(720);
|
expect(await videoPlayer.videoHeight()).toBe(720);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create read stream', async ({browser, server}) => {
|
||||||
|
const context = await browser.newContext({_recordVideos: true});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
const video = await page.waitForEvent('_videostarted') as any;
|
||||||
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
const [stream, path] = await Promise.all([
|
||||||
|
video.createReadStream(),
|
||||||
|
video.path(),
|
||||||
|
// TODO: make it work with dead context!
|
||||||
|
page.close(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const bufs = [];
|
||||||
|
stream.on('data', data => bufs.push(data));
|
||||||
|
await new Promise(f => stream.on('end', f));
|
||||||
|
const streamedData = Buffer.concat(bufs);
|
||||||
|
expect(fs.readFileSync(path).compare(streamedData)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should saveAs', async ({browser, server, tmpDir}) => {
|
||||||
|
const context = await browser.newContext({_recordVideos: true});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
const video = await page.waitForEvent('_videostarted') as any;
|
||||||
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
const saveAsPath = path.join(tmpDir, 'v.webm');
|
||||||
|
const [videoPath] = await Promise.all([
|
||||||
|
video.path(),
|
||||||
|
video.saveAs(saveAsPath),
|
||||||
|
// TODO: make it work with dead context!
|
||||||
|
page.close(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(fs.readFileSync(videoPath).compare(fs.readFileSync(saveAsPath))).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
Loading…
x
Reference in New Issue
Block a user