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:
parent
a22a8915e1
commit
9fdd101bfc
@ -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');
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user