fix/call-status-and-video-recording #22
@ -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');
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user