MWSE/sdk/webrtc/CanvasCompositor.js

221 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// CanvasCompositor — gelen video track'lerini tek bir canvas'ta birleştirir.
//
// Kullanım:
// const comp = new CanvasCompositor({ width: 1280, height: 720, fps: 30 });
// comp.addTrack('alice', videoTrack);
// comp.addTrack('bob', videoTrack2);
// comp.setLayout('grid'); // 'grid' | 'pip' | 'side-by-side' | 'focus'
// const stream = comp.stream(); // MediaStream → StreamManager.addStream() ile gönder
// comp.destroy(); // cleanup
//
// Ses için AudioContext mix bus:
// const { ctx, dest, stream: audioStream } = MediaSources.createAudioMix();
// ctx.createMediaStreamSource(peerAudioStream).connect(dest);
// // audioStream'ı da StreamManager.addStream('audio-mix', audioStream) ile gönder
export default class CanvasCompositor {
constructor({ width = 1280, height = 720, fps = 30, background = '#000' } = {}) {
this._width = width;
this._height = height;
this._fps = fps;
this._bg = background;
// Use OffscreenCanvas when available (no DOM required, runs in workers too).
if (typeof OffscreenCanvas !== 'undefined') {
this._canvas = new OffscreenCanvas(width, height);
} else {
this._canvas = document.createElement('canvas');
this._canvas.width = width;
this._canvas.height = height;
}
this._ctx = this._canvas.getContext('2d');
this._tracks = new Map(); // label → { track, video, active }
this._layout = 'grid';
this._focus = null; // label of the focused track in 'focus' mode
this._timer = null;
this._stream = null;
this._start();
}
// ---- Track management -----------------------------------------------
// Add a VideoTrack to the compositor. A hidden <video> element is used
// internally to decode the track into a drawable surface.
addTrack(label, track) {
if (track.kind !== 'video') return;
let video;
if (typeof document !== 'undefined') {
video = document.createElement('video');
video.autoplay = true;
video.muted = true;
video.playsInline = true;
video.srcObject = new MediaStream([track]);
video.play().catch(() => {});
} else {
// Non-browser context (e.g. Node/worker): track drawing is a no-op.
video = null;
}
this._tracks.set(label, { track, video, active: true });
}
removeTrack(label) {
const entry = this._tracks.get(label);
if (!entry) return;
if (entry.video) {
entry.video.srcObject = null;
}
this._tracks.delete(label);
if (this._focus === label) this._focus = null;
}
// Mute/show a track in the compositor without removing it.
setVisible(label, visible) {
const entry = this._tracks.get(label);
if (entry) entry.active = visible;
}
// ---- Layout ---------------------------------------------------------
// 'grid' — equal tiles
// 'pip' — first track large, rest small overlay in corner
// 'side-by-side'— two equal columns (more than 2 tracks still grid)
// 'focus' — one track large (setFocus), rest tiny strip at bottom
setLayout(layout) {
this._layout = layout;
}
setFocus(label) {
this._focus = label;
this._layout = 'focus';
}
// ---- Output ---------------------------------------------------------
// Returns the composite as a MediaStream (video only).
// Pass the result to StreamManager.addStream('composite', stream).
stream() {
if (this._stream) return this._stream;
if (typeof this._canvas.captureStream === 'function') {
this._stream = this._canvas.captureStream(this._fps);
} else {
// OffscreenCanvas doesn't have captureStream; fall back to no-op.
this._stream = new MediaStream();
}
return this._stream;
}
// Change the output framerate.
setFPS(fps) {
this._fps = fps;
this._stop();
this._start();
}
// ---- Lifecycle ------------------------------------------------------
destroy() {
this._stop();
for (const [label] of this._tracks) this.removeTrack(label);
this._stream = null;
}
// ---- Internal -------------------------------------------------------
_start() {
const interval = Math.round(1000 / this._fps);
this._timer = setInterval(() => this._draw(), interval);
}
_stop() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
}
_draw() {
const ctx = this._ctx;
const W = this._width;
const H = this._height;
ctx.fillStyle = this._bg;
ctx.fillRect(0, 0, W, H);
const entries = [...this._tracks.values()].filter(e => e.active && e.video);
if (entries.length === 0) return;
const rects = this._layout === 'pip' ? this._pipRects(entries, W, H)
: this._layout === 'side-by-side' ? this._sideBySideRects(entries, W, H)
: this._layout === 'focus' ? this._focusRects(entries, W, H)
: this._gridRects(entries, W, H);
for (let i = 0; i < entries.length; i++) {
const { video } = entries[i];
const r = rects[i];
if (!r || video.readyState < 2) continue;
try {
ctx.drawImage(video, r.x, r.y, r.w, r.h);
} catch (_) {}
}
}
_gridRects(entries, W, H) {
const n = entries.length;
const cols = Math.ceil(Math.sqrt(n));
const rows = Math.ceil(n / cols);
const w = Math.floor(W / cols);
const h = Math.floor(H / rows);
return entries.map((_, i) => ({
x: (i % cols) * w,
y: Math.floor(i / cols) * h,
w, h
}));
}
_pipRects(entries, W, H) {
const rects = [{ x: 0, y: 0, w: W, h: H }];
const pipW = Math.floor(W / 4);
const pipH = Math.floor(H / 4);
const pad = 12;
for (let i = 1; i < entries.length; i++) {
rects.push({
x: W - pipW - pad - (i - 1) * (pipW + pad),
y: H - pipH - pad,
w: pipW,
h: pipH
});
}
return rects;
}
_sideBySideRects(entries, W, H) {
if (entries.length <= 2) {
const w = Math.floor(W / Math.max(2, entries.length));
return entries.map((_, i) => ({ x: i * w, y: 0, w, h: H }));
}
return this._gridRects(entries, W, H);
}
_focusRects(entries, W, H) {
const focusIdx = entries.findIndex(
(_, i) => [...this._tracks.keys()][i] === this._focus
);
const idx = focusIdx >= 0 ? focusIdx : 0;
const stripH = Math.floor(H / 5);
const mainH = H - stripH - 8;
const rects = new Array(entries.length);
rects[idx] = { x: 0, y: 0, w: W, h: mainH };
const others = entries.map((_, i) => i).filter(i => i !== idx);
const tileW = others.length ? Math.floor(W / others.length) : W;
others.forEach((oi, j) => {
rects[oi] = { x: j * tileW, y: mainH + 8, w: tileW, h: stripH };
});
return rects;
}
}