Merge pull request #491 from getmaxun/browser-clarity
feat: enhance screencast quality
This commit is contained in:
@@ -35,11 +35,11 @@ const SCREENCAST_CONFIG: {
|
|||||||
compressionQuality: number;
|
compressionQuality: number;
|
||||||
maxQueueSize: number;
|
maxQueueSize: number;
|
||||||
} = {
|
} = {
|
||||||
format: 'jpeg',
|
format: 'png',
|
||||||
maxWidth: 1280,
|
maxWidth: 1280,
|
||||||
maxHeight: 720,
|
maxHeight: 720,
|
||||||
targetFPS: 30,
|
targetFPS: 30,
|
||||||
compressionQuality: 0.8,
|
compressionQuality: 0.95,
|
||||||
maxQueueSize: 2
|
maxQueueSize: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,6 +255,8 @@ export class RemoteBrowser {
|
|||||||
"--disable-extensions",
|
"--disable-extensions",
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
|
"--force-color-profile=srgb",
|
||||||
|
"--force-device-scale-factor=2",
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
const proxyConfig = await getDecryptedProxyConfig(userId);
|
const proxyConfig = await getDecryptedProxyConfig(userId);
|
||||||
@@ -282,6 +284,7 @@ export class RemoteBrowser {
|
|||||||
isMobile: false,
|
isMobile: false,
|
||||||
hasTouch: false,
|
hasTouch: false,
|
||||||
userAgent: this.getUserAgent(),
|
userAgent: this.getUserAgent(),
|
||||||
|
deviceScaleFactor: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (proxyOptions.server) {
|
if (proxyOptions.server) {
|
||||||
@@ -530,15 +533,18 @@ export class RemoteBrowser {
|
|||||||
private async optimizeScreenshot(screenshot: Buffer): Promise<Buffer> {
|
private async optimizeScreenshot(screenshot: Buffer): Promise<Buffer> {
|
||||||
try {
|
try {
|
||||||
return await sharp(screenshot)
|
return await sharp(screenshot)
|
||||||
.jpeg({
|
.png({
|
||||||
quality: Math.round(SCREENCAST_CONFIG.compressionQuality * 100),
|
quality: Math.round(SCREENCAST_CONFIG.compressionQuality * 100),
|
||||||
progressive: true
|
compressionLevel: 3,
|
||||||
|
adaptiveFiltering: true,
|
||||||
|
force: true
|
||||||
})
|
})
|
||||||
.resize({
|
.resize({
|
||||||
width: SCREENCAST_CONFIG.maxWidth,
|
width: SCREENCAST_CONFIG.maxWidth,
|
||||||
height: SCREENCAST_CONFIG.maxHeight,
|
height: SCREENCAST_CONFIG.maxHeight,
|
||||||
fit: 'inside',
|
fit: 'inside',
|
||||||
withoutEnlargement: true
|
withoutEnlargement: true,
|
||||||
|
kernel: sharp.kernel.mitchell
|
||||||
})
|
})
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -700,6 +706,9 @@ export class RemoteBrowser {
|
|||||||
try {
|
try {
|
||||||
await this.client.send('Page.startScreencast', {
|
await this.client.send('Page.startScreencast', {
|
||||||
format: SCREENCAST_CONFIG.format,
|
format: SCREENCAST_CONFIG.format,
|
||||||
|
quality: Math.round(SCREENCAST_CONFIG.compressionQuality * 100),
|
||||||
|
maxWidth: SCREENCAST_CONFIG.maxWidth,
|
||||||
|
maxHeight: SCREENCAST_CONFIG.maxHeight,
|
||||||
});
|
});
|
||||||
// Set flag to indicate screencast is active
|
// Set flag to indicate screencast is active
|
||||||
this.isScreencastActive = true;
|
this.isScreencastActive = true;
|
||||||
@@ -752,27 +761,41 @@ export class RemoteBrowser {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessingScreenshot = true;
|
this.isProcessingScreenshot = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const optimizedScreenshot = await this.optimizeScreenshot(payload);
|
const optimizationPromise = this.optimizeScreenshot(payload);
|
||||||
|
|
||||||
|
const timeoutPromise = new Promise<Buffer>((resolve) => {
|
||||||
|
setTimeout(() => resolve(payload), 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizedScreenshot = await Promise.race([optimizationPromise, timeoutPromise]);
|
||||||
const base64Data = optimizedScreenshot.toString('base64');
|
const base64Data = optimizedScreenshot.toString('base64');
|
||||||
const dataWithMimeType = `data:image/jpeg;base64,${base64Data}`;
|
const dataWithMimeType = `data:image/png;base64,${base64Data}`;
|
||||||
|
|
||||||
// Emit with user context to ensure the frontend can identify which browser's screenshot this is
|
|
||||||
this.socket.emit('screencast', {
|
this.socket.emit('screencast', {
|
||||||
image: dataWithMimeType,
|
image: dataWithMimeType,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
viewport: viewportSize || await this.currentPage?.viewportSize() || null
|
viewport: viewportSize || await this.currentPage?.viewportSize() || null
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('Screenshot emitted');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Screenshot emission failed:', error);
|
logger.error('Screenshot emission failed:', error);
|
||||||
|
try {
|
||||||
|
const base64Data = payload.toString('base64');
|
||||||
|
const dataWithMimeType = `data:image/png;base64,${base64Data}`;
|
||||||
|
this.socket.emit('screencast', {
|
||||||
|
image: dataWithMimeType,
|
||||||
|
userId: this.userId,
|
||||||
|
viewport: viewportSize || await this.currentPage?.viewportSize() || null
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Fallback screenshot emission also failed:', e);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isProcessingScreenshot = false;
|
this.isProcessingScreenshot = false;
|
||||||
|
|
||||||
if (this.screenshotQueue.length > 0) {
|
if (this.screenshotQueue.length > 0) {
|
||||||
const nextScreenshot = this.screenshotQueue.shift();
|
const nextScreenshot = this.screenshotQueue.shift();
|
||||||
if (nextScreenshot) {
|
if (nextScreenshot) {
|
||||||
|
|||||||
@@ -42,27 +42,27 @@ export const getResponsiveDimensions = (): AppDimensions => {
|
|||||||
const windowWidth = window.innerWidth;
|
const windowWidth = window.innerWidth;
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
const browserWidth = windowWidth * 0.7;
|
const browserWidth = windowWidth * 0.735;
|
||||||
const outputPreviewWidth = windowWidth * 0.716;
|
const outputPreviewWidth = windowWidth * 0.743;
|
||||||
|
|
||||||
const heightBreakpoints = [
|
const heightBreakpoints = [
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxxxxl, fraction: 0.82 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxxxxl, fraction: 0.84 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxxxl, fraction: 0.81 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxxxl, fraction: 0.83 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxxl, fraction: 0.80 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxxl, fraction: 0.82 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxl, fraction: 0.79 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxxl, fraction: 0.81 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxl, fraction: 0.78 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxxl, fraction: 0.80 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxl, fraction: 0.77 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxxl, fraction: 0.79 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxxl, fraction: 0.76 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxxl, fraction: 0.78 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxxl, fraction: 0.75 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxxl, fraction: 0.77 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxxl, fraction: 0.74 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxxl, fraction: 0.76 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxxl, fraction: 0.73 },
|
{ height: HEIGHT_BREAKPOINTS.xxxxl, fraction: 0.75 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxxl, fraction: 0.72 },
|
{ height: HEIGHT_BREAKPOINTS.xxxl, fraction: 0.741 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xxl, fraction: 0.71 },
|
{ height: HEIGHT_BREAKPOINTS.xxl, fraction: 0.74 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.xl, fraction: 0.70 },
|
{ height: HEIGHT_BREAKPOINTS.xl, fraction: 0.72 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.lg, fraction: 0.68 },
|
{ height: HEIGHT_BREAKPOINTS.lg, fraction: 0.70 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.md, fraction: 0.66 },
|
{ height: HEIGHT_BREAKPOINTS.md, fraction: 0.68 },
|
||||||
{ height: HEIGHT_BREAKPOINTS.sm, fraction: 0.63 },
|
{ height: HEIGHT_BREAKPOINTS.sm, fraction: 0.67 },
|
||||||
{ height: 0, fraction: 0.62 }
|
{ height: 0, fraction: 0.67 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const heightFraction = heightBreakpoints.find(bp => windowHeight >= bp.height)?.fraction ?? 0.62;
|
const heightFraction = heightBreakpoints.find(bp => windowHeight >= bp.height)?.fraction ?? 0.62;
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ code {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: calc(100% - 4rem);
|
width: 100%;
|
||||||
height: calc(100vh - 4rem);
|
height: calc(100vh - 2rem);
|
||||||
margin: 2rem 2rem 2rem 2rem;
|
margin: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,6 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right-side-panel {
|
.right-side-panel {
|
||||||
margin-left: 1.5rem;
|
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -91,26 +90,4 @@ code {
|
|||||||
.MuiButton-root[sx*="position: 'absolute'"] {
|
.MuiButton-root[sx*="position: 'absolute'"] {
|
||||||
bottom: 2rem !important;
|
bottom: 2rem !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
|
||||||
|
|
||||||
/* Consistent layout across all screen sizes */
|
|
||||||
@media screen and (min-width: 1024px) {
|
|
||||||
#browser-recorder {
|
|
||||||
width: calc(100% - 4rem);
|
|
||||||
height: calc(100vh - 4rem);
|
|
||||||
margin: 2rem 2rem 2rem 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Adjust for very small screens */
|
|
||||||
@media screen and (max-width: 1023px) {
|
|
||||||
#browser-recorder {
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
height: calc(100vh - 3rem);
|
|
||||||
margin: 1.5rem 1rem 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side-panel {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user