UNPKG

bbpro

Version:

BrowserBox - remote browser isolation tool

420 lines (394 loc) 14.2 kB
//import {CWebp} from 'cwebp'; import {DEBUG, sleep, CONFIG} from '../common.js'; const FORMAT = "jpeg"; // "png" const MIN_JPG_QUAL = process.env.TORBB ? 25 : 5; const MAX_JPG_QUAL = process.env.TORBB ? 75 : 80; const MAX_NTH_FRAME = process.env.TORBB ? 17 : 8; export const JPEG_QUAL = MAX_JPG_QUAL; export const MAX_ACK_BUFFER = 3; export const COMMON_FORMAT = Object.freeze({ width: 1920, height: 1080, everyNth: 1, deviceScaleFactor: 1, mobile: false, }); export const DEVICE_FEATURES = { deviceScaleFactor: COMMON_FORMAT.deviceScaleFactor, mobile: COMMON_FORMAT.mobile, }; export const SCREEN_OPTS = { format: FORMAT, quality: JPEG_QUAL, maxWidth: COMMON_FORMAT.width, maxHeight: COMMON_FORMAT.height, everyNthFrame: COMMON_FORMAT.everyNth, }; const getScreenshotViewport = () => ({ x: 0, y: 0, width: SCREEN_OPTS.maxWidth, height: SCREEN_OPTS.maxHeight, scale: DEVICE_FEATURES.deviceScaleFactor, }); export const MIN_WIDTH = 300; export const MIN_HEIGHT = 300; export const WEBP_QUAL = 32; export const JPEG_WEBP_QUAL = MAX_JPG_QUAL; // these can be tuned UP on better bandwidth and DOWN on lower bandwidth export const ACK_COUNT = process.platform == 'darwin' ? 1 : 2; // how many frames per ack? this should be adapted per link capacity export const MAX_FRAMES = process.env.TORBB ? 1 : 2; /* 1, 2, 4 */ export const MIN_TIME_BETWEEN_SHOTS = process.env.TORBB ? 100 : 40; /* 20, 40, 100, 250, 500 */ export const MIN_TIME_BETWEEN_TAIL_SHOTS = process.env.TORBB ? 1000 : 175; export const MAX_TIME_BETWEEN_TAIL_SHOTS = 4000; export const MAX_TIME_TO_WAIT_FOR_SCREENSHOT = 100; // local testing values so small haha export const MAX_ROUNDTRIP = DEBUG.localTestRTT ? 100 : process.env.TORBB ? 4000 : 725; export const MIN_ROUNDTRIP = DEBUG.localTestRTT ? 80 : process.env.TORBB ? 2000 : 600; export const MIN_SPOT_ROUNDTRIP = process.env.TORBB ? 600 : 125; export const BUF_SEND_TIMEOUT = 50; const NOIMAGE = {img: '', frame:0}; const KEYS = [ 1, 11, 13, 629, 1229, 2046, 17912, 37953, 92194, 151840 ]; export const RACE_SAMPLE = 0.74; // image formats for capture depend on what the client can accept const WEBP_FORMAT = { format: FORMAT, }; const SAFARI_FORMAT = { format: FORMAT, quality: JPEG_QUAL }; const SAFARI_SHOT = { command: { name: "Page.captureScreenshot", params: { ...SAFARI_FORMAT, // the below two options fuck up screenshots /* get clip() { return getScreenshotViewport() }, */ /*optimizeForSpeed: true,*/ } } }; export function makeCamera(connection) { let shooting = false; let lastScreenOpts = null; let frameId = 1; let lastHash; let lastShot = Date.now(); let nextShot; let tailShot, tailShotDelay = MIN_TIME_BETWEEN_TAIL_SHOTS; const nextTailShot = async () => { DEBUG.shotDebug && console.log("Tail shot"); return; tailShotDelay *= 1.618; if ( tailShotDelay < MAX_TIME_BETWEEN_TAIL_SHOTS ) { if ( tailShot ) { clearTimeout(tailShot); } tailShot = setTimeout(nextTailShot, tailShotDelay); } else { tailShotDelay = MIN_TIME_BETWEEN_TAIL_SHOTS; tailShot = false; } try { await doShot(); } catch(e) { console.warn('Error on tail shot', e, 'delay:', tailShotDelay); clearTimeout(tailShot); } }; return { queueTailShot, doShot, shrinkImagery, growImagery, restartCast, stopCast, startCast, }; async function stopCast() { DEBUG.logRestartCast && console.log(`Stopping cast`); await connection.sessionSend({ name: "Page.stopScreencast", params: {} }); } async function startCast() { DEBUG.logRestartCast && console.log(`Starting cast`); const { format, quality, everyNthFrame, maxWidth, maxHeight } = SCREEN_OPTS; await connection.sessionSend({ name: "Page.startScreencast", params: { format, quality, everyNthFrame, ...(DEBUG.noCastMaxDims ? {} : {maxWidth, maxHeight} ), } }); lastScreenOpts = { quality, everyNthFrame, maxWidth, maxHeight, }; } async function restartCast() { let restart = true; if ( lastScreenOpts ) { restart = false; if ( lastScreenOpts.quality !== SCREEN_OPTS.quality || lastScreenOpts.maxWidth !== SCREEN_OPTS.maxWidth || lastScreenOpts.maxHeight !== SCREEN_OPTS.maxHeight || lastScreenOpts.everyNthFrame !== SCREEN_OPTS.everyNthFrame ) { restart = true; } } if ( CONFIG.alwaysRestartCast ) { restart = true; DEBUG.logRestartCast && console.log(`ALWAYS restarting cast.`); } if ( restart ) { DEBUG.logRestartCast && console.log(`Restarting cast`); await connection.sessionSend({ name: "Page.stopScreencast", params: {} }); const { format, quality, everyNthFrame, maxWidth, maxHeight } = SCREEN_OPTS; await connection.sessionSend({ name: "Page.startScreencast", params: { format, quality, everyNthFrame, ...(DEBUG.noCastMaxDims ? {} : {maxWidth, maxHeight} ), } }); lastScreenOpts = { quality, everyNthFrame, maxWidth, maxHeight, }; } else { DEBUG.logRestartCast && console.log(`Restart requested by nothing changed so not restarting cast.`); } } async function shrinkImagery() { if ( SCREEN_OPTS.everyNthFrame >= MAX_NTH_FRAME && SCREEN_OPTS.quality <= MIN_JPG_QUAL ) { // we don't go any lower return; } SAFARI_SHOT.command.params.quality -= 20; SCREEN_OPTS.quality -= 20; if ( SAFARI_SHOT.command.params.quality < MIN_JPG_QUAL ) { SAFARI_SHOT.command.params.quality = MIN_JPG_QUAL; } if ( SCREEN_OPTS.quality < MIN_JPG_QUAL ) { SCREEN_OPTS.quality = MIN_JPG_QUAL; SCREEN_OPTS.everyNthFrame += 2; if ( SCREEN_OPTS.everyNthFrame > MAX_NTH_FRAME ) { SCREEN_OPTS.everyNthFrame = MAX_NTH_FRAME; } DEBUG.debugAdaptiveImagery && console.log(`Will only send every ${SCREEN_OPTS.everyNthFrame}th frame`); } if ( DEBUG.debugAdaptiveImagery ) { console.log(`Shrinking JPEG quality to ${SAFARI_SHOT.command.params.quality}`); } restartCast(); } async function growImagery() { if ( SCREEN_OPTS.quality >= MAX_JPG_QUAL ) { // we don't go any higher return; } SAFARI_SHOT.command.params.quality = 80; SCREEN_OPTS.quality = 80; if ( SAFARI_SHOT.command.params.quality > MAX_JPG_QUAL ) { SAFARI_SHOT.command.params.quality = MAX_JPG_QUAL; } if ( SCREEN_OPTS.everyNthFrame != 1 ) { SCREEN_OPTS.everyNthFrame = 1; DEBUG.debugAdaptiveImagery && console.log(`Will now send every ${SCREEN_OPTS.everyNthFrame}th frame`); } if ( SCREEN_OPTS.quality > MAX_JPG_QUAL ) { SCREEN_OPTS.quality = MAX_JPG_QUAL; } if ( DEBUG.debugAdaptiveImagery ) { console.log(`Growing JPEG quality to ${SAFARI_SHOT.command.params.quality}`); } restartCast(); } function queueTailShot() { if ( tailShot ) { clearTimeout(tailShot); tailShotDelay = MIN_TIME_BETWEEN_TAIL_SHOTS; tailShot = false; } tailShot = setTimeout(nextTailShot, tailShotDelay); } async function shot(opts = {}) { if ( DEBUG.frameDebug && opts.ignoreHash ) { setTimeout(() => console.log(opts), 20); } // DEBUG if ( DEBUG.noShot ) return NOIMAGE; const timeNow = Date.now(); const dur = timeNow - lastShot; if ( !opts.ignoreHash && dur < MIN_TIME_BETWEEN_SHOTS ) { if ( DEBUG.shotDebug && DEBUG.val > DEBUG.low ) { console.log(`Dropping as duration (${dur}) too short.`); } return NOIMAGE; } if ( DEBUG.shotDebug && DEBUG.val > DEBUG.low ) { console.log(`Do shot ${dur}ms`); } const targetId = connection.sessions.get(connection.sessionId); let response; //const ShotCommand = ((connection.isSafari || connection.isFirefox) ? SAFARI_SHOT : WEBP_SHOT).command; const ShotCommand = SAFARI_SHOT.command; if ( opts.blockExempt ) { ShotCommand.blockExempt = true; } DEBUG.shotDebug && console.log(`XCHK screenShot.js (${ShotCommand.name}) call response`, ShotCommand, response ? JSON.stringify(response).slice(0,140) : response ); response = await Promise.race([ connection.sessionSend(ShotCommand), sleep(MAX_TIME_TO_WAIT_FOR_SCREENSHOT) ]); lastShot = timeNow; response = response || {}; const {data,screenshotData} = response; frameId++; if ( !! data || !! screenshotData ) { const img = data || screenshotData; const F = {img, frameId, targetId}; F.hash = DEBUG.useHash && !opts.ignoreHash && `${F.img.length}${KEYS.map(k => F.img[k]).join('')}${F.img[F.img.length-1]}`; if ( DEBUG.useHash && !opts.ignoreHash && lastHash == F.hash ) { if ( DEBUG.shotDebug && DEBUG.val > DEBUG.low ) { console.log(`Dropping as image did not change.`); } return NOIMAGE; } else { lastHash = F.hash; return F; } } else { DEBUG.frameDebug && console.log('no frame!'); DEBUG.val > DEBUG.med && console.log("Sending no frame"); if ( DEBUG.shotDebug && DEBUG.val > DEBUG.low ) { console.log(`Dropping as shot produced no data.`); } return NOIMAGE; } } async function saveShot(opts) { const F = await Promise.race([ shot(opts), sleep(MAX_TIME_TO_WAIT_FOR_SCREENSHOT) ]); if ( F && F.img ) { if ( DEBUG.binaryFrames ) { if ( DEBUG.sendFramesWhenTheyArrive && ! CONFIG.screencastOnly ) { // ISSUE ?: should this really be disabled on screencast ? // bugs here // no need to encode 1 time for every client // isSafari should be connectionId linked not connection let imgBuf = Buffer.from(F.img, 'base64'); if ( DEBUG.cwebp && ! connection.isSafari ) { console.warn(`Not currently supported due to when dependency of cwebp not correctly building in esbuild build script ./scripts/only_build.sh`); /* const encoder = new CWebp(imgBuf); encoder.quality(WEBP_QUAL); imgBuf = await encoder.toBuffer(); */ } const header = Buffer.alloc(28); /* magic instead of cast session Id when not in cast mode */ header.writeUInt32LE(0xCC55151C, 0); header.writeUInt32LE(F.frameId, 4); header.writeUInt32LE(parseInt(F.targetId.slice(0,8), 16), 8); header.writeUInt32LE(parseInt(F.targetId.slice(8,16), 16), 12); header.writeUInt32LE(parseInt(F.targetId.slice(16,24), 16), 16); header.writeUInt32LE(parseInt(F.targetId.slice(24,32), 16), 20); imgBuf = Buffer.concat([header,imgBuf]); //const vals = [...connection.links.entries()]; for ( const {ack, fastest, peer, socket, connectionId} of connection.links.values() ) { if ( ! ack.count && !opts.ignoreHash ) { DEBUG.acks && console.log(`No ack for client ${'connectionId'}, so will skip sending frame to client.`); if ( DEBUG.bufSend ) { ack.buffer.unshift([imgBuf,F.frameId]); if ( ack.buffer.length > MAX_ACK_BUFFER ) { ack.buffer.length = MAX_ACK_BUFFER; } } continue; } DEBUG.acks && console.log({ack:{received:ack.received, count:ack.count}, opts}, new Date); DEBUG.acks && console.log('Ack or ignoreHash received so will send frame'); ack.received = 0; ack.count--; if ( ack.count < 0 ) ack.count = 0; //console.log(connection.links.size, ...connection.links.keys()); //console.log(peer + socket + ''); const channel = DEBUG.chooseFastest && fastest ? fastest : DEBUG.useWebRTC && peer ? peer : socket; connection.so(channel, imgBuf); DEBUG.adaptiveImagery && ack.sent.set(F.frameId, Date.now()); DEBUG.acks && console.log(`Sent frame to ${connectionId}`); if ( DEBUG.chooseFastest && DEBUG.useWebRTC && socket && peer ) { const choice = Math.random() >= RACE_SAMPLE; if ( choice ) { const otherChannel = channel === peer ? socket : peer; connection.so(otherChannel, imgBuf); DEBUG.logFastest && console.log('Race started'); } } if ( DEBUG.bufSend ) { ack.buffer.length = 0; //ack.bufSend = false; clearTimeout(ack.debounceBufSend); ack.debounceBufSend = setTimeout(() => ack.bufSend = true, BUF_SEND_TIMEOUT); } } } else { DEBUG.watchFrameStack && console.log('Push frame', (new Error('frame')).stack); connection.frameBuffer.push(Buffer.from(F.img, 'base64')); } } else { connection.frameBuffer.push(F); } while ( connection.frameBuffer.length > MAX_FRAMES ) { connection.frameBuffer.shift(); } } if ( CONFIG.tailShots ) queueTailShot(); DEBUG.shotDebug && console.log({framesWaiting:connection.frameBuffer.length, now: Date.now()}); } async function doShot(opts = {}) { if ( !(opts.ignoreHash || opts.forceFrame || opts.blockExempt) && (nextShot || shooting) ) { DEBUG.shotDebug && DEBUG.val > DEBUG.low && console.log(`Dropping shot`, opts.ignoreHash, opts.forceFrame, nextShot, shooting); return; } shooting = true; await Promise.race([ saveShot(opts), sleep(MAX_TIME_TO_WAIT_FOR_SCREENSHOT) ]); nextShot = setTimeout(() => nextShot = false, MIN_TIME_BETWEEN_SHOTS); shooting = false; } }