mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
Render clip gaps as solid black
This commit is contained in:
@@ -46,6 +46,7 @@ import {
|
||||
type AnnotationRegion,
|
||||
type AutoCaptionSettings,
|
||||
type CaptionCue,
|
||||
type ClipRegion,
|
||||
type CursorClickEffectStyle,
|
||||
type CursorStyle,
|
||||
DEFAULT_CONNECTED_ZOOM_DURATION_MS,
|
||||
@@ -75,7 +76,9 @@ import {
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
findClipAtTimelineTime,
|
||||
getDefaultCaptionFontFamily,
|
||||
mapTimelineTimeToSourceTime,
|
||||
type Padding,
|
||||
type WebcamOverlaySettings,
|
||||
type ZoomDepth,
|
||||
@@ -88,6 +91,7 @@ import {
|
||||
isAnnotationActiveAtTime,
|
||||
shouldClearSelectedAnnotation,
|
||||
} from "./videoPlayback/annotationVisibility";
|
||||
import { createClipPlayback } from "./videoPlayback/clipPlayback";
|
||||
import { DEFAULT_FOCUS } from "./videoPlayback/constants";
|
||||
import {
|
||||
type CursorFollowCameraState,
|
||||
@@ -116,8 +120,6 @@ import {
|
||||
resolveSceneZoomTarget,
|
||||
shouldComposePreviewFrame,
|
||||
} from "./videoPlayback/sceneMotion";
|
||||
import { createClipPlayback } from "./videoPlayback/clipPlayback";
|
||||
import { type ClipRegion, findClipAtTimelineTime, mapTimelineTimeToSourceTime } from "./types";
|
||||
import {
|
||||
getWebcamMediaTargetTimeSeconds,
|
||||
isWebcamMediaSynchronized,
|
||||
@@ -2474,6 +2476,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
style={{
|
||||
width: "100%",
|
||||
aspectRatio: formatAspectRatioForCSS(aspectRatio, nativeAspectRatio),
|
||||
backgroundColor: "#000000",
|
||||
borderRadius: 0,
|
||||
clipPath: "none",
|
||||
}}
|
||||
@@ -2489,6 +2492,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
loop
|
||||
playsInline
|
||||
style={{
|
||||
visibility: isGap ? "hidden" : "visible",
|
||||
filter:
|
||||
sceneEffects.backgroundBlurPx > 0
|
||||
? `blur(${sceneEffects.backgroundBlurPx}px)`
|
||||
@@ -2503,6 +2507,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
...backgroundStyle,
|
||||
visibility: isGap ? "hidden" : "visible",
|
||||
filter:
|
||||
sceneEffects.backgroundBlurPx > 0
|
||||
? `blur(${sceneEffects.backgroundBlurPx}px)`
|
||||
@@ -2519,7 +2524,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
visibility: isGap ? "hidden" : "visible",
|
||||
}}
|
||||
/>
|
||||
{hasRendererFallback && (
|
||||
{hasRendererFallback && !isGap && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/60 p-2 text-center">
|
||||
<div className="rounded-md bg-black/70 px-3 py-1.5 text-xs text-white">
|
||||
{`Pixi renderer unavailable on this environment (${pixiRendererBackend ?? "unknown"}).`}
|
||||
@@ -2534,7 +2539,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="absolute inset-0 select-none"
|
||||
style={{ pointerEvents: "none" }}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
visibility: isGap ? "hidden" : "visible",
|
||||
}}
|
||||
onPointerDown={handleOverlayPointerDown}
|
||||
onPointerMove={handleOverlayPointerMove}
|
||||
onPointerUp={handleOverlayPointerUp}
|
||||
@@ -2916,6 +2924,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
ref={attachVideo}
|
||||
src={videoPath}
|
||||
className={fallbackVideoClassName}
|
||||
style={{ visibility: isGap ? "hidden" : "visible" }}
|
||||
preload="metadata"
|
||||
playsInline
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -112,6 +112,7 @@ type MockContext = {
|
||||
scale: MockFunction;
|
||||
clearRect: MockFunction;
|
||||
filter: string;
|
||||
fillStyle: string;
|
||||
};
|
||||
type MockCanvas = ReturnType<typeof createMockCanvas>;
|
||||
type FrameRendererTestAccess = {
|
||||
@@ -251,6 +252,7 @@ function createMockContext() {
|
||||
scale: vi.fn(),
|
||||
clearRect: vi.fn(),
|
||||
filter: "",
|
||||
fillStyle: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,19 +289,22 @@ function createRenderer() {
|
||||
}
|
||||
|
||||
describe("FrameRenderer webcam export path", () => {
|
||||
it("draws the background without the screen or webcam during a timeline gap", async () => {
|
||||
it("renders a timeline gap as solid black", async () => {
|
||||
const renderer = createRenderer();
|
||||
const camera = { visible: true };
|
||||
const composite = vi.fn();
|
||||
const context = createMockContext();
|
||||
const app = { stage: {}, renderer: { render: vi.fn() } };
|
||||
Object.assign(renderer, {
|
||||
app: { stage: {}, renderer: { render: vi.fn() } },
|
||||
app,
|
||||
cameraContainer: camera,
|
||||
videoContainer: {},
|
||||
compositeWithShadows: composite,
|
||||
compositeCtx: context,
|
||||
});
|
||||
await renderer.renderFrame(null, 0, 0, 33333, 1500000);
|
||||
expect(camera.visible).toBe(false);
|
||||
expect(composite).toHaveBeenCalledWith(false);
|
||||
expect(context.fillStyle).toBe("#000000");
|
||||
expect(context.fillRect).toHaveBeenCalledWith(0, 0, 1920, 1080);
|
||||
expect(app.renderer.render).not.toHaveBeenCalled();
|
||||
});
|
||||
const createdCanvases: ReturnType<typeof createMockCanvas>[] = [];
|
||||
|
||||
|
||||
@@ -1410,30 +1410,15 @@ export class FrameRenderer {
|
||||
frameDurationUs?: number,
|
||||
backgroundTimelineTimestamp = timestamp,
|
||||
): Promise<void> {
|
||||
if (!this.app || !this.videoContainer || !this.cameraContainer) {
|
||||
if (!this.app || !this.videoContainer || !this.cameraContainer || !this.compositeCtx) {
|
||||
throw new Error("Renderer not initialized");
|
||||
}
|
||||
|
||||
this.currentVideoTime = timestamp / 1000000;
|
||||
this.cameraContainer.visible = videoFrame !== null;
|
||||
if (!videoFrame) {
|
||||
if (this.backgroundForwardFrameSource || this.backgroundVideoElement) {
|
||||
await this.syncBackgroundFrame(backgroundTimelineTimestamp / 1_000_000);
|
||||
}
|
||||
this.app.renderer.render(this.app.stage);
|
||||
this.compositeWithShadows(false);
|
||||
if (this.compositeCtx && this.config.annotationRegions) {
|
||||
await renderAnnotations(
|
||||
this.compositeCtx,
|
||||
this.config.annotationRegions,
|
||||
this.config.width,
|
||||
this.config.height,
|
||||
backgroundTimelineTimestamp / 1000,
|
||||
(this.config.width / BASE_PREVIEW_WIDTH +
|
||||
this.config.height / BASE_PREVIEW_HEIGHT) /
|
||||
2,
|
||||
);
|
||||
}
|
||||
this.compositeCtx.fillStyle = "#000000";
|
||||
this.compositeCtx.fillRect(0, 0, this.config.width, this.config.height);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -202,11 +202,12 @@ function createRenderer() {
|
||||
});
|
||||
}
|
||||
|
||||
it("hides source layers in gaps while rendering timeline annotations at the output time", async () => {
|
||||
it("renders a timeline gap as solid black", async () => {
|
||||
const renderer = createRenderer();
|
||||
const camera = { visible: true };
|
||||
const webcam = { visible: true };
|
||||
const captions = { visible: true };
|
||||
const output = createMockCanvas();
|
||||
output.width = 1920;
|
||||
output.height = 1080;
|
||||
const annotations = vi.fn();
|
||||
const renderOutput = vi.fn(async () => {});
|
||||
Object.assign(renderer, {
|
||||
@@ -214,15 +215,17 @@ it("hides source layers in gaps while rendering timeline annotations at the outp
|
||||
videoContainer: {},
|
||||
videoMaskGraphics: {},
|
||||
cameraContainer: camera,
|
||||
webcamRootContainer: webcam,
|
||||
captionContainer: captions,
|
||||
ensureExportCompositeCanvas: () => ({ canvas: output, context: output.context }),
|
||||
updateAnnotationLayer: annotations,
|
||||
renderOutput,
|
||||
});
|
||||
await renderer.renderFrame(null, 0, 0, 33333, 1500000);
|
||||
for (const layer of [camera, webcam, captions]) expect(layer.visible).toBe(false);
|
||||
expect(annotations).toHaveBeenCalledWith(1500);
|
||||
expect(renderOutput).toHaveBeenCalledWith(1500);
|
||||
expect(camera.visible).toBe(false);
|
||||
expect(output.context.fillStyle).toBe("#000000");
|
||||
expect(output.context.fillRect).toHaveBeenCalledWith(0, 0, 1920, 1080);
|
||||
expect(annotations).not.toHaveBeenCalled();
|
||||
expect(renderOutput).not.toHaveBeenCalled();
|
||||
expect(renderer.getCanvas()).toBe(output);
|
||||
});
|
||||
|
||||
describe("ModernFrameRenderer Pixi lifecycle", () => {
|
||||
|
||||
@@ -3118,13 +3118,11 @@ export class FrameRenderer {
|
||||
this.currentVideoTime = timestamp / 1_000_000;
|
||||
this.cameraContainer.visible = videoFrame !== null;
|
||||
if (!videoFrame) {
|
||||
if (this.backgroundForwardFrameSource || this.backgroundVideoElement) {
|
||||
await this.syncBackgroundFrame(backgroundTimelineTimestamp / 1_000_000);
|
||||
}
|
||||
if (this.webcamRootContainer) this.webcamRootContainer.visible = false;
|
||||
if (this.captionContainer) this.captionContainer.visible = false;
|
||||
this.updateAnnotationLayer(backgroundTimelineTimestamp / 1000);
|
||||
await this.renderOutput(backgroundTimelineTimestamp / 1000);
|
||||
const output = this.ensureExportCompositeCanvas();
|
||||
if (!output) throw new Error("Failed to create gap frame canvas");
|
||||
output.context.fillStyle = "#000000";
|
||||
output.context.fillRect(0, 0, output.canvas.width, output.canvas.height);
|
||||
this.outputCanvasOverride = output.canvas;
|
||||
return;
|
||||
}
|
||||
if (this.captionContainer) this.captionContainer.visible = true;
|
||||
|
||||
Reference in New Issue
Block a user