feat(screencast): add saveAs and createReadableStream (#3879)

This commit is contained in:
Yury Semikhatsky 2020-09-14 18:40:55 -07:00 committed by GitHub
parent e4e3f82337
commit 459d857bc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 0 deletions

View File

@ -14,10 +14,14 @@
* limitations under the License.
*/
import { Readable } from 'stream';
import * as channels from '../protocol/channels';
import * as fs from 'fs';
import { mkdirIfNeeded } from '../utils/utils';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { Stream } from './stream';
export class Video extends ChannelOwner<channels.VideoChannel, channels.VideoInitializer> {
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().`);
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();
}
}

View File

@ -14,9 +14,13 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as util from 'util';
import * as channels from '../protocol/channels';
import { Video } from '../server/browserContext';
import { mkdirIfNeeded } from '../utils/utils';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher';
export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer> implements channels.VideoChannel {
constructor(scope: DispatcherScope, screencast: Video) {
@ -26,4 +30,18 @@ export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer
async path(): Promise<channels.VideoPathResult> {
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) };
}
}

View File

@ -2144,12 +2144,26 @@ export type DialogDismissResult = void;
export type VideoInitializer = {};
export interface VideoChannel extends Channel {
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 VideoPathOptions = {};
export type VideoPathResult = {
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 -----------
export type DownloadInitializer = {

View File

@ -1808,6 +1808,15 @@ Video:
returns:
value: string
# Blocks path until saved to the local |path|.
saveAs:
parameters:
path: string
stream:
returns:
stream: Stream?
Download:

View File

@ -814,6 +814,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
});
scheme.DialogDismissParams = tOptional(tObject({}));
scheme.VideoPathParams = tOptional(tObject({}));
scheme.VideoSaveAsParams = tObject({
path: tString,
});
scheme.VideoStreamParams = tOptional(tObject({}));
scheme.DownloadPathParams = tOptional(tObject({}));
scheme.DownloadSaveAsParams = tObject({
path: tString,

View File

@ -423,4 +423,43 @@ describe('screencast', suite => {
expect(await videoPlayer.videoWidth()).toBe(1280);
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);
});
});