From 9fdd101bfcdc522c238af862599d76732e7dbe65 Mon Sep 17 00:00:00 2001 From: "kushal.saha" Date: Mon, 24 Aug 2026 05:35:11 +0000 Subject: [PATCH] fix: crop recorded video - Calculate video dimensions dynamically in the compositor. - Ensure recorded videos are rendered with the correct size and aspect ratio. --- resources/js/interview-call.js | 2 +- resources/js/recording-compositor.js | 173 ++++++++++++++++++--------- 2 files changed, 115 insertions(+), 60 deletions(-) diff --git a/resources/js/interview-call.js b/resources/js/interview-call.js index cdbea41..93cff5b 100644 --- a/resources/js/interview-call.js +++ b/resources/js/interview-call.js @@ -2505,7 +2505,7 @@ export function startCallRecording() { recordingCompositor = new RecordingCompositor({ width: 1280, height: 720, - fps: 24, + fps: 30, getParticipantsData: () => { const candStream = (candVid && candVid.srcObject) ? candVid.srcObject : null; const candCamIcon = document.getElementById('cand-cam-status'); diff --git a/resources/js/recording-compositor.js b/resources/js/recording-compositor.js index 5756daa..368c28e 100644 --- a/resources/js/recording-compositor.js +++ b/resources/js/recording-compositor.js @@ -16,8 +16,8 @@ export class RecordingCompositor { * @param {{width:number, height:number, fps: number, getParticipantsData: () => {}}} options */ constructor(options = {}) { - this.width = options.width || 1920; - this.height = options.height || 1080; + this.width = options.width || 1280; + this.height = options.height || (options.width ? Math.round((options.width * 9) / 16) : 720); this.fps = options.fps || 30; this.canvas = document.createElement("canvas"); @@ -257,17 +257,29 @@ export class RecordingCompositor { secondaryFeeds = [selfFeed, ...panelistFeeds]; } - // Compute geometry with strict 16:9 aspect ratio preservation for every tile - const gap = 16; - const padX = 24; + // Compute dynamic geometry with strict 16:9 aspect ratio preservation for every tile + const padX = Math.max(8, Math.round(W * 0.015)); + const padY = Math.max(8, Math.round(H * 0.02)); + const gap = Math.max(6, Math.round(W * 0.01)); const totalSecondary = secondaryFeeds.length; if (totalSecondary === 0) { - // Case 0: Only Main Feed (Full Canvas 16:9) - const mainH = 1032; - const mainW = Math.round(mainH * (16 / 9)); // 1835px + // Case 0: Only Main Feed (Full Canvas 16:9 with padding) + const maxAvailW = W - 2 * padX; + const maxAvailH = H - 2 * padY; + + let mainW, mainH; + if (maxAvailW / maxAvailH > 16 / 9) { + mainH = maxAvailH; + mainW = Math.round(mainH * (16 / 9)); + } else { + mainW = maxAvailW; + mainH = Math.round(mainW * (9 / 16)); + } + const mainX = Math.round((W - mainW) / 2); const mainY = Math.round((H - mainH) / 2); + this.renderTile( ctx, mainFeed, @@ -276,10 +288,20 @@ export class RecordingCompositor { ); } else if (totalSecondary === 1) { // Case 1: Main + 1 Sidebar Tile (Both 16:9) - const sideW = 480; - const sideH = Math.round(sideW * (9 / 16)); // 270px - const mainW = 1376; - const mainH = Math.round(mainW * (9 / 16)); // 774px + const availW = W - 2 * padX - gap; + const availH = H - 2 * padY; + + let sideW = Math.round(availW * 0.26); + let sideH = Math.round(sideW * (9 / 16)); + let mainW = availW - sideW; + let mainH = Math.round(mainW * (9 / 16)); + + if (mainH > availH) { + mainH = availH; + mainW = Math.round(mainH * (16 / 9)); + sideW = Math.min(availW - mainW, Math.round(availH * (16 / 9))); + sideH = Math.round(sideW * (9 / 16)); + } const mainX = padX; const mainY = Math.round((H - mainH) / 2); @@ -299,17 +321,30 @@ export class RecordingCompositor { false, ); } else if (totalSecondary === 2) { - // Case 2: Main + 2 Sidebar Tiles (All 16:9) - const sideW = 480; - const sideH = Math.round(sideW * (9 / 16)); // 270px - const mainW = 1376; - const mainH = Math.round(mainW * (9 / 16)); // 774px + // Case 2: Main + 2 Sidebar Tiles stacked vertically (All 16:9) + const availW = W - 2 * padX - gap; + const availH = H - 2 * padY; + + let sideW = Math.round(availW * 0.28); + let sideH = Math.round(sideW * (9 / 16)); + + if (2 * sideH + gap > availH) { + sideH = Math.floor((availH - gap) / 2); + sideW = Math.round(sideH * (16 / 9)); + } + + const totalSideH = 2 * sideH + gap; + let mainW = availW - sideW; + let mainH = Math.round(mainW * (9 / 16)); + + if (mainH > availH) { + mainH = availH; + mainW = Math.round(mainH * (16 / 9)); + } const mainX = padX; const mainY = Math.round((H - mainH) / 2); - const sideX = padX + mainW + gap; - - const totalSideH = 2 * sideH + gap; // 556px + const sideX = W - padX - sideW; const sideYStart = Math.round((H - totalSideH) / 2); this.renderTile( @@ -331,18 +366,24 @@ export class RecordingCompositor { false, ); } else if (totalSecondary === 3) { - // Case 3: Main + 3 Sidebar Tiles (All 16:9) - const sideH = 330; - const sideW = Math.round(sideH * (16 / 9)); // 587px - const totalSideH = 3 * sideH + 2 * gap; // 1022px - const topY = Math.round((H - totalSideH) / 2); // 29px + // Case 3: Main + 3 Sidebar Tiles stacked vertically (All 16:9) + const availH = H - 2 * padY; + const sideH = Math.floor((availH - 2 * gap) / 3); + const sideW = Math.round(sideH * (16 / 9)); + const totalSideH = 3 * sideH + 2 * gap; + const topY = Math.round((H - totalSideH) / 2); const sideX = W - padX - sideW; - const mainW = sideX - gap - padX; // 1270px - const mainH = Math.round(mainW * (9 / 16)); // 714px + let mainW = sideX - gap - padX; + let mainH = Math.round(mainW * (9 / 16)); + + if (mainH > totalSideH) { + mainH = totalSideH; + mainW = Math.round(mainH * (16 / 9)); + } const mainX = padX; - const mainY = topY; + const mainY = Math.round((H - mainH) / 2); this.renderTile( ctx, @@ -363,16 +404,25 @@ export class RecordingCompositor { } } else { // Case 4: Main + 3 Sidebar Tiles + Bottom Overflow Row (All 16:9) - const sideH = 330; - const sideW = Math.round(sideH * (16 / 9)); // 587px - const totalSideH = 3 * sideH + 2 * gap; // 1022px - const topY = Math.round((H - totalSideH) / 2); // 29px + const availH = H - 2 * padY; + const sideH = Math.floor((availH - 2 * gap) / 3); + const sideW = Math.round(sideH * (16 / 9)); + const totalSideH = 3 * sideH + 2 * gap; + const topY = Math.round((H - totalSideH) / 2); const sideX = W - padX - sideW; - const mainW = sideX - gap - padX; // 1270px - const mainH = Math.round(mainW * (9 / 16)); // 714px + const mainAreaW = sideX - gap - padX; - const mainX = padX; + const targetMainH = Math.round((totalSideH - gap) * 0.68); + let mainW = Math.round(targetMainH * (16 / 9)); + let mainH = targetMainH; + + if (mainW > mainAreaW) { + mainW = mainAreaW; + mainH = Math.round(mainW * (9 / 16)); + } + + const mainX = padX + Math.round((mainAreaW - mainW) / 2); const mainY = topY; // 1. Render Main @@ -398,12 +448,11 @@ export class RecordingCompositor { // 3. Render Bottom Overflow Tiles (each 16:9) const overflowFeeds = secondaryFeeds.slice(3); const numOverflow = overflowFeeds.length; - const botMaxH = totalSideH - mainH - gap; // 292px - const botYBase = topY + mainH + gap; // 759px + const botMaxH = totalSideH - mainH - gap; + const botYBase = topY + mainH + gap; - // Calculate width per tile to fit in mainW const candBotW = Math.floor( - (mainW - (numOverflow - 1) * gap) / numOverflow, + (mainAreaW - (numOverflow - 1) * gap) / numOverflow, ); const candBotH = Math.round(candBotW * (9 / 16)); @@ -417,7 +466,7 @@ export class RecordingCompositor { } const totalBotW = numOverflow * botW + (numOverflow - 1) * gap; - const botXStart = mainX + Math.round((mainW - totalBotW) / 2); + const botXStart = padX + Math.round((mainAreaW - totalBotW) / 2); const botY = botYBase + Math.round((botMaxH - botH) / 2); overflowFeeds.forEach((feed, j) => { @@ -434,7 +483,7 @@ export class RecordingCompositor { renderTile(ctx, feed, rect, isMain = false) { const { x, y, w, h } = rect; - const radius = 12; + const radius = Math.max(6, Math.round(Math.min(w, h) * 0.035)); ctx.save(); this.drawRoundedClip(ctx, x, y, w, h, radius); @@ -465,7 +514,7 @@ export class RecordingCompositor { ctx.restore(); // Overlay Pill Tag (Bottom-Left) - this.drawPillTag(ctx, feed, x + 10, y + h - 12); + this.drawPillTag(ctx, feed, x, y, w, h); } drawVideoCover(ctx, video, x, y, w, h) { @@ -524,10 +573,10 @@ export class RecordingCompositor { drawPlaceholder(ctx, feed, x, y, w, h, isMain = false) { const cx = x + w / 2; - const cy = y + h / 2 - (isMain ? 15 : 8); + const cy = y + h / 2 - (isMain ? Math.round(h * 0.03) : Math.round(h * 0.02)); const circleRadius = isMain - ? Math.min(54, h * 0.18) - : Math.min(32, h * 0.22); + ? Math.min(Math.round(h * 0.18), Math.round(w * 0.12), 54) + : Math.min(Math.round(h * 0.2), Math.round(w * 0.14), 36); // Circular gradient avatar const gradient = ctx.createLinearGradient( @@ -546,54 +595,60 @@ export class RecordingCompositor { ctx.fillStyle = gradient; ctx.beginPath(); - ctx.arc(cx, cy, circleRadius, 0, Math.PI * 2); + ctx.arc(cx, cy, Math.max(1, circleRadius), 0, Math.PI * 2); ctx.fill(); // Avatar Initials Text + const initialsFont = Math.max(10, Math.round(circleRadius * 0.85)); ctx.fillStyle = "#ffffff"; - ctx.font = `bold ${Math.round(circleRadius * 0.85)}px sans-serif`; + ctx.font = `bold ${initialsFont}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(feed.initials || "IV", cx, cy); // Participant Name + const nameFont = Math.max(10, Math.round(isMain ? Math.min(20, h * 0.045) : Math.min(14, h * 0.06))); ctx.fillStyle = "#ffffff"; - ctx.font = `600 ${isMain ? 18 : 13}px sans-serif`; + ctx.font = `600 ${nameFont}px sans-serif`; ctx.textBaseline = "top"; - ctx.fillText(feed.name, cx, cy + circleRadius + (isMain ? 14 : 8)); + ctx.fillText(feed.name, cx, cy + circleRadius + Math.max(6, Math.round(h * 0.02))); // Subtitle status + const subFont = Math.max(9, Math.round(isMain ? Math.min(14, h * 0.032) : Math.min(11, h * 0.045))); ctx.fillStyle = "#94a3b8"; - ctx.font = `400 ${isMain ? 13 : 10.5}px sans-serif`; + ctx.font = `400 ${subFont}px sans-serif`; ctx.fillText( feed.isCamOn ? "Connecting video..." : "Camera turned off", cx, - cy + circleRadius + (isMain ? 36 : 24), + cy + circleRadius + Math.max(6, Math.round(h * 0.02)) + nameFont + Math.max(4, Math.round(h * 0.01)), ); } - drawPillTag(ctx, feed, x, y) { + drawPillTag(ctx, feed, tileX, tileY, tileW, tileH) { const text = feed.name; - const fontSize = 11; + const fontSize = Math.max(9, Math.min(12, Math.round(tileH * 0.045))); ctx.font = `600 ${fontSize}px sans-serif`; const textMetrics = ctx.measureText(text); - const tagPaddingX = 8; + const tagPaddingX = Math.max(6, Math.round(fontSize * 0.7)); const tagW = textMetrics.width + tagPaddingX * 2; - const tagH = 20; + const tagH = Math.max(16, Math.round(fontSize * 1.8)); - const tagX = x; - const tagY = y - tagH; + const marginX = Math.max(6, Math.round(tileW * 0.02)); + const marginY = Math.max(6, Math.round(tileH * 0.03)); + const tagX = tileX + marginX; + const tagY = tileY + tileH - marginY - tagH; + const tagRadius = Math.max(3, Math.round(tagH * 0.25)); ctx.save(); // Background ctx.fillStyle = "rgba(9, 13, 22, 0.8)"; - this.drawRoundedFill(ctx, tagX, tagY, tagW, tagH, 5); + this.drawRoundedFill(ctx, tagX, tagY, tagW, tagH, tagRadius); // Border ctx.strokeStyle = "rgba(255, 255, 255, 0.12)"; ctx.lineWidth = 1; - this.drawRoundedStroke(ctx, tagX, tagY, tagW, tagH, 5); + this.drawRoundedStroke(ctx, tagX, tagY, tagW, tagH, tagRadius); // Name text ctx.fillStyle = "#e2e8f0";