diff --git a/js/hang/src/publish/video/index.ts b/js/hang/src/publish/video/index.ts index c3dfb07ef..7eab22b90 100644 --- a/js/hang/src/publish/video/index.ts +++ b/js/hang/src/publish/video/index.ts @@ -161,7 +161,8 @@ export class Video { effect.set(this.#encoderConfig, config); while (frame) { - const keyFrame = groupTimestamp + GOP_DURATION_US < frame.timestamp; + // Force a keyframe if this is the first frame (no group yet), or GOP elapsed. + const keyFrame = !group || groupTimestamp + GOP_DURATION_US <= frame.timestamp; if (keyFrame) { groupTimestamp = frame.timestamp; } diff --git a/js/hang/src/watch/video/index.ts b/js/hang/src/watch/video/index.ts index 990b3d703..5c3fed8fa 100644 --- a/js/hang/src/watch/video/index.ts +++ b/js/hang/src/watch/video/index.ts @@ -1,5 +1,5 @@ import type * as Moq from "@kixelated/moq"; -import { Effect, Getter, Signal } from "@kixelated/signals"; +import { Effect, type Getter, Signal } from "@kixelated/signals"; import type * as Catalog from "../../catalog"; import * as Container from "../../container"; import * as Hex from "../../util/hex"; diff --git a/js/hang/src/watch/video/renderer.ts b/js/hang/src/watch/video/renderer.ts index 72753cb6a..c487f695c 100644 --- a/js/hang/src/watch/video/renderer.ts +++ b/js/hang/src/watch/video/renderer.ts @@ -29,7 +29,7 @@ export class VideoRenderer { this.#signals.effect((effect) => { const canvas = effect.get(this.canvas); - this.#ctx.set(canvas?.getContext("2d", { desynchronized: true }) ?? undefined); + this.#ctx.set(canvas?.getContext("2d") ?? undefined); }); this.#signals.effect(this.#schedule.bind(this)); @@ -112,26 +112,33 @@ export class VideoRenderer { throw new Error("scheduled without a canvas"); } - ctx.save(); - ctx.fillStyle = "#000"; - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - const frame = this.source.frame.peek(); if (frame) { - ctx.canvas.width = frame.displayWidth; - ctx.canvas.height = frame.displayHeight; + const w = frame.displayWidth; + const h = frame.displayHeight; + if (ctx.canvas.width !== w || ctx.canvas.height !== h) { + ctx.canvas.width = w; + ctx.canvas.height = h; + } + + // Prepare background and transformations for this draw + ctx.save(); + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); // Apply horizontal flip if specified in the video config const flip = this.source.flip.peek(); if (flip) { - ctx.save(); ctx.scale(-1, 1); ctx.translate(-ctx.canvas.width, 0); - ctx.drawImage(frame, 0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.restore(); - } else { - ctx.drawImage(frame, 0, 0, ctx.canvas.width, ctx.canvas.height); } + + ctx.drawImage(frame, 0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.restore(); + } else { + // Clear canvas when no frame + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); } // Draw a loading icon when the lag 2+ seconds @@ -157,8 +164,6 @@ export class VideoRenderer { ctx.restore(); } */ - - ctx.restore(); } // Close the track and all associated resources.