fix: crop recorded video

- Calculate video dimensions dynamically in the compositor.
- Ensure recorded videos are rendered with the correct size and aspect ratio.
This commit is contained in:
kushal.saha 2026-08-24 05:35:11 +00:00
parent a22a8915e1
commit 9fdd101bfc
2 changed files with 115 additions and 60 deletions

View File

@ -2505,7 +2505,7 @@ export function startCallRecording() {
recordingCompositor = new RecordingCompositor({ recordingCompositor = new RecordingCompositor({
width: 1280, width: 1280,
height: 720, height: 720,
fps: 24, fps: 30,
getParticipantsData: () => { getParticipantsData: () => {
const candStream = (candVid && candVid.srcObject) ? candVid.srcObject : null; const candStream = (candVid && candVid.srcObject) ? candVid.srcObject : null;
const candCamIcon = document.getElementById('cand-cam-status'); const candCamIcon = document.getElementById('cand-cam-status');

View File

@ -16,8 +16,8 @@ export class RecordingCompositor {
* @param {{width:number, height:number, fps: number, getParticipantsData: () => {}}} options * @param {{width:number, height:number, fps: number, getParticipantsData: () => {}}} options
*/ */
constructor(options = {}) { constructor(options = {}) {
this.width = options.width || 1920; this.width = options.width || 1280;
this.height = options.height || 1080; this.height = options.height || (options.width ? Math.round((options.width * 9) / 16) : 720);
this.fps = options.fps || 30; this.fps = options.fps || 30;
this.canvas = document.createElement("canvas"); this.canvas = document.createElement("canvas");
@ -257,17 +257,29 @@ export class RecordingCompositor {
secondaryFeeds = [selfFeed, ...panelistFeeds]; secondaryFeeds = [selfFeed, ...panelistFeeds];
} }
// Compute geometry with strict 16:9 aspect ratio preservation for every tile // Compute dynamic geometry with strict 16:9 aspect ratio preservation for every tile
const gap = 16; const padX = Math.max(8, Math.round(W * 0.015));
const padX = 24; const padY = Math.max(8, Math.round(H * 0.02));
const gap = Math.max(6, Math.round(W * 0.01));
const totalSecondary = secondaryFeeds.length; const totalSecondary = secondaryFeeds.length;
if (totalSecondary === 0) { if (totalSecondary === 0) {
// Case 0: Only Main Feed (Full Canvas 16:9) // Case 0: Only Main Feed (Full Canvas 16:9 with padding)
const mainH = 1032; const maxAvailW = W - 2 * padX;
const mainW = Math.round(mainH * (16 / 9)); // 1835px 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 mainX = Math.round((W - mainW) / 2);
const mainY = Math.round((H - mainH) / 2); const mainY = Math.round((H - mainH) / 2);
this.renderTile( this.renderTile(
ctx, ctx,
mainFeed, mainFeed,
@ -276,10 +288,20 @@ export class RecordingCompositor {
); );
} else if (totalSecondary === 1) { } else if (totalSecondary === 1) {
// Case 1: Main + 1 Sidebar Tile (Both 16:9) // Case 1: Main + 1 Sidebar Tile (Both 16:9)
const sideW = 480; const availW = W - 2 * padX - gap;
const sideH = Math.round(sideW * (9 / 16)); // 270px const availH = H - 2 * padY;
const mainW = 1376;
const mainH = Math.round(mainW * (9 / 16)); // 774px 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 mainX = padX;
const mainY = Math.round((H - mainH) / 2); const mainY = Math.round((H - mainH) / 2);
@ -299,17 +321,30 @@ export class RecordingCompositor {
false, false,
); );
} else if (totalSecondary === 2) { } else if (totalSecondary === 2) {
// Case 2: Main + 2 Sidebar Tiles (All 16:9) // Case 2: Main + 2 Sidebar Tiles stacked vertically (All 16:9)
const sideW = 480; const availW = W - 2 * padX - gap;
const sideH = Math.round(sideW * (9 / 16)); // 270px const availH = H - 2 * padY;
const mainW = 1376;
const mainH = Math.round(mainW * (9 / 16)); // 774px 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 mainX = padX;
const mainY = Math.round((H - mainH) / 2); const mainY = Math.round((H - mainH) / 2);
const sideX = padX + mainW + gap; const sideX = W - padX - sideW;
const totalSideH = 2 * sideH + gap; // 556px
const sideYStart = Math.round((H - totalSideH) / 2); const sideYStart = Math.round((H - totalSideH) / 2);
this.renderTile( this.renderTile(
@ -331,18 +366,24 @@ export class RecordingCompositor {
false, false,
); );
} else if (totalSecondary === 3) { } else if (totalSecondary === 3) {
// Case 3: Main + 3 Sidebar Tiles (All 16:9) // Case 3: Main + 3 Sidebar Tiles stacked vertically (All 16:9)
const sideH = 330; const availH = H - 2 * padY;
const sideW = Math.round(sideH * (16 / 9)); // 587px const sideH = Math.floor((availH - 2 * gap) / 3);
const totalSideH = 3 * sideH + 2 * gap; // 1022px const sideW = Math.round(sideH * (16 / 9));
const topY = Math.round((H - totalSideH) / 2); // 29px const totalSideH = 3 * sideH + 2 * gap;
const topY = Math.round((H - totalSideH) / 2);
const sideX = W - padX - sideW; const sideX = W - padX - sideW;
const mainW = sideX - gap - padX; // 1270px let mainW = sideX - gap - padX;
const mainH = Math.round(mainW * (9 / 16)); // 714px let mainH = Math.round(mainW * (9 / 16));
if (mainH > totalSideH) {
mainH = totalSideH;
mainW = Math.round(mainH * (16 / 9));
}
const mainX = padX; const mainX = padX;
const mainY = topY; const mainY = Math.round((H - mainH) / 2);
this.renderTile( this.renderTile(
ctx, ctx,
@ -363,16 +404,25 @@ export class RecordingCompositor {
} }
} else { } else {
// Case 4: Main + 3 Sidebar Tiles + Bottom Overflow Row (All 16:9) // Case 4: Main + 3 Sidebar Tiles + Bottom Overflow Row (All 16:9)
const sideH = 330; const availH = H - 2 * padY;
const sideW = Math.round(sideH * (16 / 9)); // 587px const sideH = Math.floor((availH - 2 * gap) / 3);
const totalSideH = 3 * sideH + 2 * gap; // 1022px const sideW = Math.round(sideH * (16 / 9));
const topY = Math.round((H - totalSideH) / 2); // 29px const totalSideH = 3 * sideH + 2 * gap;
const topY = Math.round((H - totalSideH) / 2);
const sideX = W - padX - sideW; const sideX = W - padX - sideW;
const mainW = sideX - gap - padX; // 1270px const mainAreaW = sideX - gap - padX;
const mainH = Math.round(mainW * (9 / 16)); // 714px
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; const mainY = topY;
// 1. Render Main // 1. Render Main
@ -398,12 +448,11 @@ export class RecordingCompositor {
// 3. Render Bottom Overflow Tiles (each 16:9) // 3. Render Bottom Overflow Tiles (each 16:9)
const overflowFeeds = secondaryFeeds.slice(3); const overflowFeeds = secondaryFeeds.slice(3);
const numOverflow = overflowFeeds.length; const numOverflow = overflowFeeds.length;
const botMaxH = totalSideH - mainH - gap; // 292px const botMaxH = totalSideH - mainH - gap;
const botYBase = topY + mainH + gap; // 759px const botYBase = topY + mainH + gap;
// Calculate width per tile to fit in mainW
const candBotW = Math.floor( const candBotW = Math.floor(
(mainW - (numOverflow - 1) * gap) / numOverflow, (mainAreaW - (numOverflow - 1) * gap) / numOverflow,
); );
const candBotH = Math.round(candBotW * (9 / 16)); const candBotH = Math.round(candBotW * (9 / 16));
@ -417,7 +466,7 @@ export class RecordingCompositor {
} }
const totalBotW = numOverflow * botW + (numOverflow - 1) * gap; 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); const botY = botYBase + Math.round((botMaxH - botH) / 2);
overflowFeeds.forEach((feed, j) => { overflowFeeds.forEach((feed, j) => {
@ -434,7 +483,7 @@ export class RecordingCompositor {
renderTile(ctx, feed, rect, isMain = false) { renderTile(ctx, feed, rect, isMain = false) {
const { x, y, w, h } = rect; const { x, y, w, h } = rect;
const radius = 12; const radius = Math.max(6, Math.round(Math.min(w, h) * 0.035));
ctx.save(); ctx.save();
this.drawRoundedClip(ctx, x, y, w, h, radius); this.drawRoundedClip(ctx, x, y, w, h, radius);
@ -465,7 +514,7 @@ export class RecordingCompositor {
ctx.restore(); ctx.restore();
// Overlay Pill Tag (Bottom-Left) // 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) { drawVideoCover(ctx, video, x, y, w, h) {
@ -524,10 +573,10 @@ export class RecordingCompositor {
drawPlaceholder(ctx, feed, x, y, w, h, isMain = false) { drawPlaceholder(ctx, feed, x, y, w, h, isMain = false) {
const cx = x + w / 2; 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 const circleRadius = isMain
? Math.min(54, h * 0.18) ? Math.min(Math.round(h * 0.18), Math.round(w * 0.12), 54)
: Math.min(32, h * 0.22); : Math.min(Math.round(h * 0.2), Math.round(w * 0.14), 36);
// Circular gradient avatar // Circular gradient avatar
const gradient = ctx.createLinearGradient( const gradient = ctx.createLinearGradient(
@ -546,54 +595,60 @@ export class RecordingCompositor {
ctx.fillStyle = gradient; ctx.fillStyle = gradient;
ctx.beginPath(); 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(); ctx.fill();
// Avatar Initials Text // Avatar Initials Text
const initialsFont = Math.max(10, Math.round(circleRadius * 0.85));
ctx.fillStyle = "#ffffff"; 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.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
ctx.fillText(feed.initials || "IV", cx, cy); ctx.fillText(feed.initials || "IV", cx, cy);
// Participant Name // 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.fillStyle = "#ffffff";
ctx.font = `600 ${isMain ? 18 : 13}px sans-serif`; ctx.font = `600 ${nameFont}px sans-serif`;
ctx.textBaseline = "top"; 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 // 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.fillStyle = "#94a3b8";
ctx.font = `400 ${isMain ? 13 : 10.5}px sans-serif`; ctx.font = `400 ${subFont}px sans-serif`;
ctx.fillText( ctx.fillText(
feed.isCamOn ? "Connecting video..." : "Camera turned off", feed.isCamOn ? "Connecting video..." : "Camera turned off",
cx, 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 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`; ctx.font = `600 ${fontSize}px sans-serif`;
const textMetrics = ctx.measureText(text); 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 tagW = textMetrics.width + tagPaddingX * 2;
const tagH = 20; const tagH = Math.max(16, Math.round(fontSize * 1.8));
const tagX = x; const marginX = Math.max(6, Math.round(tileW * 0.02));
const tagY = y - tagH; 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(); ctx.save();
// Background // Background
ctx.fillStyle = "rgba(9, 13, 22, 0.8)"; 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 // Border
ctx.strokeStyle = "rgba(255, 255, 255, 0.12)"; ctx.strokeStyle = "rgba(255, 255, 255, 0.12)";
ctx.lineWidth = 1; ctx.lineWidth = 1;
this.drawRoundedStroke(ctx, tagX, tagY, tagW, tagH, 5); this.drawRoundedStroke(ctx, tagX, tagY, tagW, tagH, tagRadius);
// Name text // Name text
ctx.fillStyle = "#e2e8f0"; ctx.fillStyle = "#e2e8f0";