UNPKG

@aurora-agency/puppeteer-screen-recorder

Version:

A puppeteer Plugin that uses the native chrome devtool protocol for capturing video frame by frame. Also supports an option to follow pages that are opened by the current page object

296 lines 24.6 kB
import { EventEmitter } from 'events'; import os from 'os'; import { extname } from 'path'; import { PassThrough } from 'stream'; import ffmpeg, { setFfmpegPath } from 'fluent-ffmpeg'; import { SupportedFileFormats, VIDEO_WRITE_STATUS, } from './pageVideoStreamTypes'; const SUPPORTED_FILE_FORMATS = [ SupportedFileFormats.MP4, SupportedFileFormats.AVI, SupportedFileFormats.MOV, SupportedFileFormats.WEBM, ]; export default class PageVideoStreamWriter extends EventEmitter { screenLimit = 10; screenCastFrames = []; duration = '00:00:00:00'; frameGain = 0; frameLoss = 0; status = VIDEO_WRITE_STATUS.NOT_STARTED; options; videoMediatorStream = new PassThrough(); writerPromise; destinationExt = null; constructor(destinationSource, options) { super(); if (options) { this.options = options; } const isWritable = this.isWritableStream(destinationSource); this.configureFFmPegPath(); if (!isWritable && typeof destinationSource === 'string') { this.destinationExt = this.getDestinationPathExtension(destinationSource); } if (this.options.rtmpUrl) { this.configureRtmpStream(this.options.rtmpUrl); } else if (isWritable) { this.configureVideoWritableStream(destinationSource); } else { this.configureVideoFile(destinationSource); } } get videoFrameSize() { const { width, height } = this.options.videoFrame; return width !== null && height !== null ? `${width}x${height}` : '100%'; } get autopad() { const autopad = this.options.autopad; return !autopad ? { activation: false } : { activation: true, color: autopad.color }; } getFfmpegPath() { if (this.options.ffmpeg_Path) return this.options.ffmpeg_Path; try { const ffmpeg = require('@ffmpeg-installer/ffmpeg'); return ffmpeg.path || null; } catch { return null; } } getDestinationPathExtension(destinationFile) { const fileExtension = extname(destinationFile); return fileExtension.includes('.') ? fileExtension.replace('.', '') : fileExtension; } configureFFmPegPath() { const ffmpegPath = this.getFfmpegPath(); if (!ffmpegPath) { throw new Error('FFmpeg path is missing, \n Set the FFMPEG_PATH env variable'); } setFfmpegPath(ffmpegPath); } isWritableStream(destinationSource) { return destinationSource && typeof destinationSource !== 'string'; } configureRtmpStream(rtmpUrl) { this.writerPromise = new Promise((resolve) => { const outputStream = this.getDestinationStream(); outputStream .format('flv') .videoCodec(this.options.videoCodec || 'libx264') .size(this.videoFrameSize) .aspect(this.options.aspectRatio || '4:3') .autopad(this.autopad.activation, this.autopad?.color) .on('progress', (progress) => { this.duration = progress.timemark; }) .on('error', (e) => { this.handleWriteStreamError(e.message); resolve(false); }) .on('stderr', (e) => { this.handleWriteStreamError(e); resolve(false); }) .on('end', () => resolve(true)) .output(rtmpUrl) .run(); }); } configureVideoFile(destinationPath) { const fileExt = this.destinationExt; if (!SUPPORTED_FILE_FORMATS.includes(fileExt)) { throw new Error('File format is not supported'); } this.writerPromise = new Promise((resolve) => { const outputStream = this.getDestinationStream(); outputStream .on('error', (e) => { this.handleWriteStreamError(e.message); resolve(false); }) .on('stderr', (e) => { this.handleWriteStreamError(e); resolve(false); }) .on('end', () => resolve(true)); if (fileExt === SupportedFileFormats.WEBM) { outputStream .videoCodec('libvpx-vp9') .videoBitrate(this.options.videoBitrate || 1000, true) .format('webm') .outputOptions([ '-deadline', 'realtime', '-cpu-used', '4', `-crf`, `${this.options.videoCrf ?? 32}`, ]); } else { outputStream.format(fileExt); } outputStream.save(destinationPath); }); } configureVideoWritableStream(writableStream) { const isWebm = this.options.videoCodec?.toLowerCase().includes('vp9') || false; this.writerPromise = new Promise((resolve) => { const outputStream = this.getDestinationStream(); outputStream .on('error', (e) => { writableStream.emit('error', e); resolve(false); }) .on('stderr', (e) => { writableStream.emit('error', { message: e }); resolve(false); }) .on('end', () => { writableStream.end(); resolve(true); }); if (isWebm) { outputStream .format('webm') .videoCodec('libvpx-vp9') .videoBitrate(this.options.videoBitrate || 1000, true) .outputOptions([ '-deadline', 'realtime', '-cpu-used', '4', `-crf`, `${this.options.videoCrf ?? 32}`, ]); } else { outputStream .format('mp4') .addOutputOptions('-movflags +frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov'); } outputStream.pipe(writableStream); }); } getOutputOption() { const cpu = Math.max(1, os.cpus().length - 1); const videoOutputOptions = this.options.videOutputOptions ?? []; const outputOptions = [ `-crf ${this.options.videoCrf ?? 23}`, `-preset ${this.options.videoPreset || 'ultrafast'}`, `-pix_fmt ${this.options.videoPixelFormat || 'yuv420p'}`, `-minrate ${this.options.videoBitrate || 1000}`, `-maxrate ${this.options.videoBitrate || 1000}`, '-framerate 1', `-threads ${cpu}`, '-loglevel error', ...videoOutputOptions, ]; return outputOptions; } addVideoMetadata(outputStream) { const metadataOptions = this.options.metadata ?? []; for (const metadata of metadataOptions) { outputStream.outputOptions('-metadata', metadata); } } getDestinationStream() { const outputStream = ffmpeg({ source: this.videoMediatorStream, priority: 20, }) .videoCodec(this.options.videoCodec || 'libx264') .size(this.videoFrameSize) .aspect(this.options.aspectRatio || '4:3') .autopad(this.autopad.activation, this.autopad?.color) .inputFormat('image2pipe') .inputFPS(this.options.fps) .outputOptions(this.getOutputOption()) .on('progress', (progressDetails) => { this.duration = progressDetails.timemark; }); this.addVideoMetadata(outputStream); if (this.options.recordDurationLimit) { outputStream.duration(this.options.recordDurationLimit); } return outputStream; } handleWriteStreamError(errorMessage) { this.emit('videoStreamWriterError', errorMessage); if (this.status !== VIDEO_WRITE_STATUS.IN_PROGRESS && errorMessage.includes('pipe:0: End of file')) return; console.error(`Error unable to capture video stream: ${errorMessage}`); } findSlot(timestamp) { if (this.screenCastFrames.length === 0) return 0; let i; let frame; for (i = this.screenCastFrames.length - 1; i >= 0; i--) { frame = this.screenCastFrames[i]; if (timestamp > frame.timestamp) break; } return i + 1; } insert(frame) { if (this.screenCastFrames.length === this.screenLimit) { const numberOfFramesToSplice = Math.floor(this.screenLimit / 2); const framesToProcess = this.screenCastFrames.splice(0, numberOfFramesToSplice); this.processFrameBeforeWrite(framesToProcess, this.screenCastFrames[0].timestamp); } const insertionIndex = this.findSlot(frame.timestamp); if (insertionIndex === this.screenCastFrames.length) { this.screenCastFrames.push(frame); } else { this.screenCastFrames.splice(insertionIndex, 0, frame); } } trimFrame(fameList, chunkEndTime) { return fameList.map((currentFrame, index) => { const endTime = index !== fameList.length - 1 ? fameList[index + 1].timestamp : chunkEndTime; return { ...currentFrame, duration: endTime - currentFrame.timestamp }; }); } processFrameBeforeWrite(frames, chunkEndTime) { const processedFrames = this.trimFrame(frames, chunkEndTime); processedFrames.forEach(({ blob, duration }) => { this.write(blob, duration); }); } write(data, durationSeconds = 1) { this.status = VIDEO_WRITE_STATUS.IN_PROGRESS; const totalFrames = durationSeconds * this.options.fps; const floored = Math.floor(totalFrames); let numberOfFPS = Math.max(floored, 1); if (floored === 0) this.frameGain += 1 - totalFrames; else this.frameLoss += totalFrames - floored; while (this.frameLoss > 1) { this.frameLoss--; numberOfFPS++; } while (this.frameGain > 1) { this.frameGain--; numberOfFPS--; } for (let i = 0; i < numberOfFPS; i++) { this.videoMediatorStream.write(data); } } drainFrames(stoppedTime) { this.processFrameBeforeWrite(this.screenCastFrames, stoppedTime); this.screenCastFrames = []; } stop(stoppedTime = Date.now() / 1000) { if (this.status === VIDEO_WRITE_STATUS.COMPLETED) return this.writerPromise; this.drainFrames(stoppedTime); this.videoMediatorStream.end(); this.status = VIDEO_WRITE_STATUS.COMPLETED; return this.writerPromise; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGFnZVZpZGVvU3RyZWFtV3JpdGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2xpYi9wYWdlVmlkZW9TdHJlYW1Xcml0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLFFBQVEsQ0FBQztBQUN0QyxPQUFPLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFDcEIsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUMvQixPQUFPLEVBQUUsV0FBVyxFQUFZLE1BQU0sUUFBUSxDQUFDO0FBRS9DLE9BQU8sTUFBTSxFQUFFLEVBQUUsYUFBYSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRXRELE9BQU8sRUFFTCxvQkFBb0IsRUFDcEIsa0JBQWtCLEdBRW5CLE1BQU0sd0JBQXdCLENBQUM7QUFFaEMsTUFBTSxzQkFBc0IsR0FBRztJQUM3QixvQkFBb0IsQ0FBQyxHQUFHO0lBQ3hCLG9CQUFvQixDQUFDLEdBQUc7SUFDeEIsb0JBQW9CLENBQUMsR0FBRztJQUN4QixvQkFBb0IsQ0FBQyxJQUFJO0NBQzFCLENBQUM7QUFFRixNQUFNLENBQUMsT0FBTyxPQUFPLHFCQUFzQixTQUFRLFlBQVk7SUFDNUMsV0FBVyxHQUFHLEVBQUUsQ0FBQztJQUMxQixnQkFBZ0IsR0FBRyxFQUFFLENBQUM7SUFDdkIsUUFBUSxHQUFHLGFBQWEsQ0FBQztJQUN6QixTQUFTLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsU0FBUyxHQUFHLENBQUMsQ0FBQztJQUViLE1BQU0sR0FBRyxrQkFBa0IsQ0FBQyxXQUFXLENBQUM7SUFDeEMsT0FBTyxDQUFlO0lBRXRCLG1CQUFtQixHQUFnQixJQUFJLFdBQVcsRUFBRSxDQUFDO0lBQ3JELGFBQWEsQ0FBbUI7SUFDaEMsY0FBYyxHQUFnQyxJQUFJLENBQUM7SUFFM0QsWUFBWSxpQkFBb0MsRUFBRSxPQUFzQjtRQUN0RSxLQUFLLEVBQUUsQ0FBQztRQUVSLElBQUksT0FBTyxFQUFFO1lBQ1gsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7U0FDeEI7UUFFRCxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsaUJBQWlCLENBQUMsQ0FBQztRQUM1RCxJQUFJLENBQUMsbUJBQW1CLEVBQUUsQ0FBQztRQUUzQixJQUFJLENBQUMsVUFBVSxJQUFJLE9BQU8saUJBQWlCLEtBQUssUUFBUSxFQUFFO1lBQ3hELElBQUksQ0FBQyxjQUFjLEdBQUcsSUFBSSxDQUFDLDJCQUEyQixDQUFDLGlCQUFpQixDQUFDLENBQUM7U0FDM0U7UUFFRCxJQUFJLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFO1lBQ3hCLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1NBQ2hEO2FBQU0sSUFBSSxVQUFVLEVBQUU7WUFDckIsSUFBSSxDQUFDLDRCQUE0QixDQUFDLGlCQUE2QixDQUFDLENBQUM7U0FDbEU7YUFBTTtZQUNMLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxpQkFBMkIsQ0FBQyxDQUFDO1NBQ3REO0lBQ0gsQ0FBQztJQUVELElBQVksY0FBYztRQUN4QixNQUFNLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDO1FBQ2xELE9BQU8sS0FBSyxLQUFLLElBQUksSUFBSSxNQUFNLEtBQUssSUFBSSxDQUFDLENBQUMsQ0FBQyxHQUFHLEtBQUssSUFBSSxNQUFNLEVBQUUsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0lBQzNFLENBQUM7SUFFRCxJQUFZLE9BQU87UUFDakIsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUM7UUFDckMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsVUFBVSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsT0FBTyxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQ3ZGLENBQUM7SUFFTyxhQUFhO1FBQ25CLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXO1lBQUUsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQztRQUM5RCxJQUFJO1lBQ0YsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLDBCQUEwQixDQUFDLENBQUM7WUFDbkQsT0FBTyxNQUFNLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBQztTQUM1QjtRQUFDLE1BQU07WUFDTixPQUFPLElBQUksQ0FBQztTQUNiO0lBQ0gsQ0FBQztJQUVPLDJCQUEyQixDQUFDLGVBQWU7UUFDakQsTUFBTSxhQUFhLEdBQUcsT0FBTyxDQUFDLGVBQWUsQ0FBQyxDQUFDO1FBQy9DLE9BQU8sYUFBYSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUM7WUFDaEMsQ0FBQyxDQUFFLGFBQWEsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBMEI7WUFDMUQsQ0FBQyxDQUFFLGFBQXNDLENBQUM7SUFDOUMsQ0FBQztJQUVPLG1CQUFtQjtRQUN6QixNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDeEMsSUFBSSxDQUFDLFVBQVUsRUFBRTtZQUNmLE1BQU0sSUFBSSxLQUFLLENBQUMsNkRBQTZELENBQUMsQ0FBQztTQUNoRjtRQUNELGFBQWEsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUM1QixDQUFDO0lBRU8sZ0JBQWdCLENBQUMsaUJBQW9DO1FBQzNELE9BQU8saUJBQWlCLElBQUksT0FBTyxpQkFBaUIsS0FBSyxRQUFRLENBQUM7SUFDcEUsQ0FBQztJQUVPLG1CQUFtQixDQUFDLE9BQWU7UUFDekMsSUFBSSxDQUFDLGFBQWEsR0FBRyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1lBQzNDLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxvQkFBb0IsRUFBRSxDQUFDO1lBRWpELFlBQVk7aUJBQ1QsTUFBTSxDQUFDLEtBQUssQ0FBQztpQkFDYixVQUFVLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLElBQUksU0FBUyxDQUFDO2lCQUNoRCxJQUFJLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQztpQkFDekIsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsV0FBVyxJQUFJLEtBQUssQ0FBQztpQkFDekMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLElBQUksQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDO2lCQUNyRCxFQUFFLENBQUMsVUFBVSxFQUFFLENBQUMsUUFBUSxFQUFFLEVBQUU7Z0JBQzNCLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQztZQUNwQyxDQUFDLENBQUM7aUJBQ0QsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFO2dCQUNqQixJQUFJLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDO2dCQUN2QyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDakIsQ0FBQyxDQUFDO2lCQUNELEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLEVBQUUsRUFBRTtnQkFDbEIsSUFBSSxDQUFDLHNCQUFzQixDQUFDLENBQUMsQ0FBQyxDQUFDO2dCQUMvQixPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDakIsQ0FBQyxDQUFDO2lCQUNELEVBQUUsQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO2lCQUM5QixNQUFNLENBQUMsT0FBTyxDQUFDO2lCQUNmLEdBQUcsRUFBRSxDQUFDO1FBQ1gsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBR08sa0JBQWtCLENBQUMsZUFBdUI7UUFDaEQsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLGNBQWMsQ0FBQztRQUVwQyxJQUFJLENBQUMsc0JBQXNCLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxFQUFFO1lBQzdDLE1BQU0sSUFBSSxLQUFLLENBQUMsOEJBQThCLENBQUMsQ0FBQztTQUNqRDtRQUVELElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtZQUMzQyxNQUFNLFlBQVksR0FBRyxJQUFJLENBQUMsb0JBQW9CLEVBQUUsQ0FBQztZQUVqRCxZQUFZO2lCQUNULEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEVBQUUsRUFBRTtnQkFDakIsSUFBSSxDQUFDLHNCQUFzQixDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQztnQkFDdkMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1lBQ2pCLENBQUMsQ0FBQztpQkFDRCxFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUU7Z0JBQ2xCLElBQUksQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDLENBQUMsQ0FBQztnQkFDL0IsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1lBQ2pCLENBQUMsQ0FBQztpQkFDRCxFQUFFLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1lBRWxDLElBQUksT0FBTyxLQUFLLG9CQUFvQixDQUFDLElBQUksRUFBRTtnQkFDekMsWUFBWTtxQkFDVCxVQUFVLENBQUMsWUFBWSxDQUFDO3FCQUN4QixZQUFZLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxZQUFZLElBQUksSUFBSSxFQUFFLElBQUksQ0FBQztxQkFDckQsTUFBTSxDQUFDLE1BQU0sQ0FBQztxQkFDZCxhQUFhLENBQUM7b0JBQ2IsV0FBVyxFQUFFLFVBQVU7b0JBQ3ZCLFdBQVcsRUFBRSxHQUFHO29CQUNoQixNQUFNLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsSUFBSSxFQUFFLEVBQUU7aUJBQ3pDLENBQUMsQ0FBQzthQUNOO2lCQUFNO2dCQUNMLFlBQVksQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUM7YUFDOUI7WUFFRCxZQUFZLENBQUMsSUFBSSxDQUFDLGVBQWUsQ0FBQyxDQUFDO1FBQ3JDLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVPLDRCQUE0QixDQUFDLGNBQXdCO1FBQzNELE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLENBQUM7UUFFL0UsSUFBSSxDQUFDLGFBQWEsR0FBRyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1lBQzNDLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxvQkFBb0IsRUFBRSxDQUFDO1lBRWpELFlBQVk7aUJBQ1QsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFO2dCQUNqQixjQUFjLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQztnQkFDaEMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1lBQ2pCLENBQUMsQ0FBQztpQkFDRCxFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUU7Z0JBQ2xCLGNBQWMsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBQzdDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNqQixDQUFDLENBQUM7aUJBQ0QsRUFBRSxDQUFDLEtBQUssRUFBRSxHQUFHLEVBQUU7Z0JBQ2QsY0FBYyxDQUFDLEdBQUcsRUFBRSxDQUFDO2dCQUNyQixPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDaEIsQ0FBQyxDQUFDLENBQUM7WUFFTCxJQUFJLE1BQU0sRUFBRTtnQkFDVixZQUFZO3FCQUNULE1BQU0sQ0FBQyxNQUFNLENBQUM7cUJBQ2QsVUFBVSxDQUFDLFlBQVksQ0FBQztxQkFDeEIsWUFBWSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsWUFBWSxJQUFJLElBQUksRUFBRSxJQUFJLENBQUM7cUJBQ3JELGFBQWEsQ0FBQztvQkFDYixXQUFXLEVBQUUsVUFBVTtvQkFDdkIsV0FBVyxFQUFFLEdBQUc7b0JBQ2hCLE1BQU0sRUFBRSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxJQUFJLEVBQUUsRUFBRTtpQkFDekMsQ0FBQyxDQUFDO2FBQ047aUJBQU07Z0JBQ0wsWUFBWTtxQkFDVCxNQUFNLENBQUMsS0FBSyxDQUFDO3FCQUNiLGdCQUFnQixDQUFDLG9FQUFvRSxDQUFDLENBQUM7YUFDM0Y7WUFFRCxZQUFZLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ3BDLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVPLGVBQWU7UUFDckIsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLElBQUksRUFBRSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQztRQUM5QyxNQUFNLGtCQUFrQixHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsaUJBQWlCLElBQUksRUFBRSxDQUFDO1FBRWhFLE1BQU0sYUFBYSxHQUFHO1lBQ3BCLFFBQVEsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLElBQUksRUFBRSxFQUFFO1lBQ3JDLFdBQVcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLElBQUksV0FBVyxFQUFFO1lBQ3BELFlBQVksSUFBSSxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsSUFBSSxTQUFTLEVBQUU7WUFDeEQsWUFBWSxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksSUFBSSxJQUFJLEVBQUU7WUFDL0MsWUFBWSxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksSUFBSSxJQUFJLEVBQUU7WUFDL0MsY0FBYztZQUNkLFlBQVksR0FBRyxFQUFFO1lBQ2pCLGlCQUFpQjtZQUNqQixHQUFHLGtCQUFrQjtTQUN0QixDQUFDO1FBRUYsT0FBTyxhQUFhLENBQUM7SUFDdkIsQ0FBQztJQUVPLGdCQUFnQixDQUFDLFlBQXVDO1FBQzlELE1BQU0sZUFBZSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxJQUFJLEVBQUUsQ0FBQztRQUNwRCxLQUFLLE1BQU0sUUFBUSxJQUFJLGVBQWUsRUFBRTtZQUN0QyxZQUFZLENBQUMsYUFBYSxDQUFDLFdBQVcsRUFBRSxRQUFRLENBQUMsQ0FBQztTQUNuRDtJQUNILENBQUM7SUFFTyxvQkFBb0I7UUFDMUIsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDO1lBQzFCLE1BQU0sRUFBRSxJQUFJLENBQUMsbUJBQW1CO1lBQ2hDLFFBQVEsRUFBRSxFQUFFO1NBQ2IsQ0FBQzthQUNDLFVBQVUsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsSUFBSSxTQUFTLENBQUM7YUFDaEQsSUFBSSxDQUFDLElBQUksQ0FBQyxjQUFjLENBQUM7YUFDekIsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsV0FBVyxJQUFJLEtBQUssQ0FBQzthQUN6QyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUM7YUFDckQsV0FBVyxDQUFDLFlBQVksQ0FBQzthQUN6QixRQUFRLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUM7YUFDMUIsYUFBYSxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsQ0FBQzthQUNyQyxFQUFFLENBQUMsVUFBVSxFQUFFLENBQUMsZUFBZSxFQUFFLEVBQUU7WUFDbEMsSUFBSSxDQUFDLFFBQVEsR0FBRyxlQUFlLENBQUMsUUFBUSxDQUFDO1FBQzNDLENBQUMsQ0FBQyxDQUFDO1FBRUwsSUFBSSxDQUFDLGdCQUFnQixDQUFDLFlBQVksQ0FBQyxDQUFDO1FBRXBDLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsRUFBRTtZQUNwQyxZQUFZLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsQ0FBQztTQUN6RDtRQUVELE9BQU8sWUFBWSxDQUFDO0lBQ3RCLENBQUM7SUFFTyxzQkFBc0IsQ0FBQyxZQUFZO1FBQ3pDLElBQUksQ0FBQyxJQUFJLENBQUMsd0JBQXdCLEVBQUUsWUFBWSxDQUFDLENBQUM7UUFDbEQsSUFDRSxJQUFJLENBQUMsTUFBTSxLQUFLLGtCQUFrQixDQUFDLFdBQVc7WUFDOUMsWUFBWSxDQUFDLFFBQVEsQ0FBQyxxQkFBcUIsQ0FBQztZQUM1QyxPQUFPO1FBQ1QsT0FBTyxDQUFDLEtBQUssQ0FBQyx5Q0FBeUMsWUFBWSxFQUFFLENBQUMsQ0FBQztJQUN6RSxDQUFDO0lBRU8sUUFBUSxDQUFDLFNBQWlCO1FBQ2hDLElBQUksSUFBSSxDQUFDLGdCQUFnQixDQUFDLE1BQU0sS0FBSyxDQUFDO1lBQUUsT0FBTyxDQUFDLENBQUM7UUFFakQsSUFBSSxDQUFTLENBQUM7UUFDZCxJQUFJLEtBQXNCLENBQUM7UUFFM0IsS0FBSyxDQUFDLEdBQUcsSUFBSSxDQUFDLGdCQUFnQixDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUN0RCxLQUFLLEdBQUcsSUFBSSxDQUFDLGdCQUFnQixDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ2pDLElBQUksU0FBUyxHQUFHLEtBQUssQ0FBQyxTQUFTO2dCQUFFLE1BQU07U0FDeEM7UUFFRCxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDZixDQUFDO0lBRU0sTUFBTSxDQUFDLEtBQXNCO1FBQ2xDLElBQUksSUFBSSxDQUFDLGdCQUFnQixDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUMsV0FBVyxFQUFFO1lBQ3JELE1BQU0sc0JBQXNCLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsV0FBVyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQ2hFLE1BQU0sZUFBZSxHQUFHLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQyxFQUFFLHNCQUFzQixDQUFDLENBQUM7WUFDaEYsSUFBSSxDQUFDLHVCQUF1QixDQUFDLGVBQWUsRUFBRSxJQUFJLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7U0FDbkY7UUFFRCxNQUFNLGNBQWMsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUN0RCxJQUFJLGNBQWMsS0FBSyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsTUFBTSxFQUFFO1lBQ25ELElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7U0FDbkM7YUFBTTtZQUNMLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsY0FBYyxFQUFFLENBQUMsRUFBRSxLQUFLLENBQUMsQ0FBQztTQUN4RDtJQUNILENBQUM7SUFFTyxTQUFTLENBQUMsUUFBMkIsRUFBRSxZQUFvQjtRQUNqRSxPQUFPLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxZQUFZLEVBQUUsS0FBSyxFQUFFLEVBQUU7WUFDMUMsTUFBTSxPQUFPLEdBQUcsS0FBSyxLQUFLLFFBQVEsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDO1lBQzdGLE9BQU8sRUFBRSxHQUFHLFlBQVksRUFBRSxRQUFRLEVBQUUsT0FBTyxHQUFHLFlBQVksQ0FBQyxTQUFTLEVBQUUsQ0FBQztRQUN6RSxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFTyx1QkFBdUIsQ0FBQyxNQUF5QixFQUFFLFlBQW9CO1FBQzdFLE1BQU0sZUFBZSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxFQUFFLFlBQVksQ0FBQyxDQUFDO1FBQzdELGVBQWUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsRUFBRSxFQUFFO1lBQzdDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1FBQzdCLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVNLEtBQUssQ0FBQyxJQUFZLEVBQUUsZUFBZSxHQUFHLENBQUM7UUFDNUMsSUFBSSxDQUFDLE1BQU0sR0FBRyxrQkFBa0IsQ0FBQyxXQUFXLENBQUM7UUFDN0MsTUFBTSxXQUFXLEdBQUcsZUFBZSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDO1FBQ3ZELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLENBQUM7UUFFeEMsSUFBSSxXQUFXLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUM7UUFDdkMsSUFBSSxPQUFPLEtBQUssQ0FBQztZQUFFLElBQUksQ0FBQyxTQUFTLElBQUksQ0FBQyxHQUFHLFdBQVcsQ0FBQzs7WUFDaEQsSUFBSSxDQUFDLFNBQVMsSUFBSSxXQUFXLEdBQUcsT0FBTyxDQUFDO1FBRTdDLE9BQU8sSUFBSSxDQUFDLFNBQVMsR0FBRyxDQUFDLEVBQUU7WUFDekIsSUFBSSxDQUFDLFNBQVMsRUFBRSxDQUFDO1lBQ2pCLFdBQVcsRUFBRSxDQUFDO1NBQ2Y7UUFDRCxPQUFPLElBQUksQ0FBQyxTQUFTLEdBQUcsQ0FBQyxFQUFFO1lBQ3pCLElBQUksQ0FBQyxTQUFTLEVBQUUsQ0FBQztZQUNqQixXQUFXLEVBQUUsQ0FBQztTQUNmO1FBRUQsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLFdBQVcsRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUNwQyxJQUFJLENBQUMsbUJBQW1CLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDO1NBQ3RDO0lBQ0gsQ0FBQztJQUVPLFdBQVcsQ0FBQyxXQUFtQjtRQUNyQyxJQUFJLENBQUMsdUJBQXVCLENBQUMsSUFBSSxDQUFDLGdCQUFnQixFQUFFLFdBQVcsQ0FBQyxDQUFDO1FBQ2pFLElBQUksQ0FBQyxnQkFBZ0IsR0FBRyxFQUFFLENBQUM7SUFDN0IsQ0FBQztJQUVNLElBQUksQ0FBQyxXQUFXLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUk7UUFDekMsSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLGtCQUFrQixDQUFDLFNBQVM7WUFBRSxPQUFPLElBQUksQ0FBQyxhQUFhLENBQUM7UUFFNUUsSUFBSSxDQUFDLFdBQVcsQ0FBQyxXQUFXLENBQUMsQ0FBQztRQUM5QixJQUFJLENBQUMsbUJBQW1CLENBQUMsR0FBRyxFQUFFLENBQUM7UUFDL0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxrQkFBa0IsQ0FBQyxTQUFTLENBQUM7UUFDM0MsT0FBTyxJQUFJLENBQUMsYUFBYSxDQUFDO0lBQzVCLENBQUM7Q0FDRiJ9