Discussions
Performance Stuttering with Heygen Streaming Video
Hi,
I’m implementing canvas-based chroma key processing on top of a Heygen streaming avatar in an Angular 14 kiosk application.
While the experience is mostly smooth on macOS, we are seeing frequent stuttering and frame drops on Windows, especially in a kiosk environment.
Key details about the setup:
-Video element uses native autoplay and loop
-Chroma key is applied via 2D canvas
-Canvas size is set only once (not inside the render loop)
-FPS is limited (around 8–10 FPS)
-Different rendering strategies have been tested
-Despite this, on Windows kiosk we observe:
--Visible stuttering
--Occasional moments where the video appears to “restart”,
On the Heygen Tools page, the chroma key demo does not show this behavior, so I suspect the difference might be related to:
Stream frame timing
Video decoding behavior on Windows
Canvas + chroma key processing under kiosk constraints
I’m sharing a sample of the chroma key and rendering code below for reference.
I’d really appreciate your guidance on whether there are Heygen specific optimizations or a recommended approach for handling chroma key on streaming avatars in Windows kiosk environments.
Thank you for your time and support.
public applyChromaKey(
sourceVideo: HTMLVideoElement,
targetCanvas: HTMLCanvasElement,
options: {
minHue: number; // 60 - minimum hue value (0-360)
maxHue: number; // 180 - maximum hue value (0-360)
minSaturation: number; // 0.10 - minimum saturation (0-1)
threshold: number; // 1.00 - threshold for green detection
}): void {
// Get canvas context
const ctx = targetCanvas.getContext("2d", {
willReadFrequently: true,
alpha: true,
});
if (!ctx || sourceVideo.readyState < 2) return;
// Clear canvas
ctx.clearRect(0, 0, targetCanvas.width, targetCanvas.height);
// Draw video frame to canvas
ctx.drawImage(sourceVideo, 0, 0, targetCanvas.width, targetCanvas.height);
// Get image data for processing
const imageData = ctx.getImageData(
0,
0,
targetCanvas.width,
targetCanvas.height
);
const data = imageData.data;
// Process each pixel
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Convert RGB to HSV
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
// Calculate hue
let h = 0;
if (delta === 0) {
h = 0;
} else if (max === r) {
h = ((g - b) / delta) % 6;
} else if (max === g) {
h = (b - r) / delta + 2;
} else {
h = (r - g) / delta + 4;
}
h = Math.round(h * 60);
if (h < 0) h += 360;
// Calculate saturation and value
const s = max === 0 ? 0 : delta / max;
const v = max / 255;
// Check if pixel is in the green screen range
const isGreen =
h >= options.minHue &&
h <= options.maxHue &&
s > options.minSaturation &&
v > 0.15 &&
g > r * options.threshold &&
g > b * options.threshold;
// Apply transparency for green pixels
if (isGreen) {
const greenness = (g - Math.max(r, b)) / (g || 1);
const alphaValue = Math.max(0, 1 - greenness * 4);
data[i + 3] = alphaValue < 0.2 ? 0 : Math.round(alphaValue * 255);
}
}
// Put processed image data back to canvas
ctx.putImageData(imageData, 0, 0);
}
public setupChromaKey(
sourceVideo: HTMLVideoElement,
targetCanvas: HTMLCanvasElement,
options: {
minHue: number;
maxHue: number;
minSaturation: number;
threshold: number;
}
): () => void {
let animationFrameId: number | null = null;
let lastFrameTime = 0;
let fps: number = 10;
let frameInterval = 1000 / fps;
let scale: number = 0.5
let initialized = false;
const render = (time: number) => {
if (!initialized && sourceVideo.readyState >= 2) {
targetCanvas.width = sourceVideo.videoWidth;
targetCanvas.height = sourceVideo.videoHeight;
initialized = true;
}
if (initialized && time - lastFrameTime >= frameInterval) {
this.applyChromaKey(sourceVideo, targetCanvas, options);
lastFrameTime = time;
}
animationFrameId = requestAnimationFrame(render);
};
render(0);
return () => {
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId);
};
}