diff --git a/src/client/video.ts b/src/client/video.ts index 96f47f021a..879282ceee 100644 --- a/src/client/video.ts +++ b/src/client/video.ts @@ -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 { private _browser: Browser | null; @@ -36,4 +40,31 @@ export class Video extends ChannelOwner { + 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 { + const result = await this._channel.stream(); + if (!result.stream) + return null; + const stream = Stream.from(result.stream); + return stream.stream(); + } } diff --git a/src/dispatchers/videoDispatcher.ts b/src/dispatchers/videoDispatcher.ts index eb79d94587..b1290a2aed 100644 --- a/src/dispatchers/videoDispatcher.ts +++ b/src/dispatchers/videoDispatcher.ts @@ -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 implements channels.VideoChannel { constructor(scope: DispatcherScope, screencast: Video) { @@ -26,4 +30,18 @@ export class VideoDispatcher extends Dispatcher { return { value: await this._object.path() }; } + + async saveAs(params: channels.VideoSaveAsParams): Promise { + const fileName = await this._object.path(); + await mkdirIfNeeded(params.path); + await util.promisify(fs.copyFile)(fileName, params.path); + } + + async stream(): Promise { + 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) }; + } + } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 5dfa3930ad..ae68b808f1 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -2144,12 +2144,26 @@ export type DialogDismissResult = void; export type VideoInitializer = {}; export interface VideoChannel extends Channel { path(params?: VideoPathParams, metadata?: Metadata): Promise; + saveAs(params: VideoSaveAsParams, metadata?: Metadata): Promise; + stream(params?: VideoStreamParams, metadata?: Metadata): Promise; } 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 = { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 985f26c13e..ebaaed67c8 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -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: diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 4129106eae..bb9d7e5b50 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -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, diff --git a/test/screencast.spec.ts b/test/screencast.spec.ts index f275316c1f..79b8e79426 100644 --- a/test/screencast.spec.ts +++ b/test/screencast.spec.ts @@ -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); + }); }); \ No newline at end of file