Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/components/canvas/players/image-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,13 @@ export class ImagePlayer extends Player {
throw new Error("Image asset has no src to load.");
}

const corsUrl = `${src}${src.includes("?") ? "&" : "?"}x-cors=1`;
const loadOptions: pixi.UnresolvedAsset = { src: corsUrl, crossorigin: "anonymous", data: {} };
const texture = await this.edit.assetLoader.load<pixi.Texture<pixi.ImageSource>>(corsUrl, loadOptions);
const loadOptions: pixi.UnresolvedAsset = { src, crossorigin: "anonymous", data: {} };
const texture = await this.edit.assetLoader.load<pixi.Texture<pixi.ImageSource>>(src, loadOptions);

if (!(texture?.source instanceof pixi.ImageSource)) {
if (texture) {
texture.destroy(true);
await this.edit.assetLoader.rejectAsset(corsUrl);
await this.edit.assetLoader.rejectAsset(src);
}
throw new Error(`Invalid image source '${src}'.`);
}
Expand Down
7 changes: 3 additions & 4 deletions src/components/canvas/players/image-to-video-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,13 @@ export class ImageToVideoPlayer extends Player {

private async tryLoadTexture(src: string): Promise<boolean> {
try {
const corsUrl = `${src}${src.includes("?") ? "&" : "?"}x-cors=1`;
const loadOptions: pixi.UnresolvedAsset = { src: corsUrl, crossorigin: "anonymous", data: {} };
const texture = await this.edit.assetLoader.load<pixi.Texture<pixi.ImageSource>>(corsUrl, loadOptions);
const loadOptions: pixi.UnresolvedAsset = { src, crossorigin: "anonymous", data: {} };
const texture = await this.edit.assetLoader.load<pixi.Texture<pixi.ImageSource>>(src, loadOptions);

if (!(texture?.source instanceof pixi.ImageSource)) {
if (texture) {
texture.destroy(true);
await this.edit.assetLoader.rejectAsset(corsUrl);
await this.edit.assetLoader.rejectAsset(src);
}
return false;
}
Expand Down
5 changes: 2 additions & 3 deletions src/components/canvas/players/video-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,11 @@ export class VideoPlayer extends Player {
throw new Error(`Video source '${src}' is not supported. .mov files cannot be played in the browser. Please convert to .webm or .mp4 first.`);
}

const corsUrl = `${src}${src.includes("?") ? "&" : "?"}x-cors=1`;
const loadOptions: pixi.UnresolvedAsset = { src: corsUrl, data: { autoPlay: false, muted: false } };
const loadOptions: pixi.UnresolvedAsset = { src, data: { autoPlay: false, muted: false } };

// Use unique loader to create independent video element per player
// This prevents conflicts when multiple clips use the same video source
const texture = await this.edit.assetLoader.loadVideoUnique(corsUrl, loadOptions);
const texture = await this.edit.assetLoader.loadVideoUnique(src, loadOptions);

if (!texture || !(texture.source instanceof pixi.VideoSource)) {
throw new Error(`Invalid video source '${src}'.`);
Expand Down
56 changes: 56 additions & 0 deletions tests/media-player-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ jest.mock("@canvas/players/placeholder-graphic", () => ({
createPlaceholderGraphic: mockCreatePlaceholderGraphic
}));

jest.mock("@canvas/players/ai-pending-overlay", () => ({
AiPendingOverlay: jest.fn().mockImplementation(() => ({
getContainer: jest.fn(() => ({ destroy: jest.fn() })),
dispose: jest.fn()
}))
}));

jest.mock("pixi.js", () => {
class MockPoint {
public x: number;
Expand Down Expand Up @@ -149,6 +156,8 @@ jest.mock("pixi.js", () => {
// eslint-disable-next-line import/first
import { ImagePlayer } from "@canvas/players/image-player";
// eslint-disable-next-line import/first
import { ImageToVideoPlayer } from "@canvas/players/image-to-video-player";
// eslint-disable-next-line import/first
import { VideoPlayer } from "@canvas/players/video-player";
// eslint-disable-next-line import/first
import type { ResolvedClip } from "@schemas";
Expand All @@ -160,6 +169,7 @@ function createEdit() {
size: { width: 1080, height: 1920 },
playbackTime: 0,
isPlaying: false,
getResolvedEdit: jest.fn(),
assetLoader: {
load: jest.fn(),
loadVideoUnique: jest.fn(),
Expand Down Expand Up @@ -259,3 +269,49 @@ describe("media player fallbacks", () => {
expect(Number.isFinite(player.getScale())).toBe(true);
});
});

describe("media player source URLs", () => {
// Presigned S3-style URL: any appended query parameter invalidates the signature
const signedSrc = "https://bucket.s3.amazonaws.com/media.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=abc123";

let warnSpy: jest.SpyInstance;

beforeEach(() => {
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
});

afterEach(() => {
warnSpy.mockRestore();
});

function createClip(type: string): ResolvedClip {
return { asset: { type, src: signedSrc }, start: 0, length: 5 } as ResolvedClip;
}

it("passes the image src to the loader unchanged", async () => {
const edit = createEdit();
edit.assetLoader.load.mockResolvedValueOnce(null);

await new ImagePlayer(edit as never, createClip("image")).load();

expect(edit.assetLoader.load).toHaveBeenCalledWith(signedSrc, { src: signedSrc, crossorigin: "anonymous", data: {} });
});

it("passes the video src to the loader unchanged", async () => {
const edit = createEdit();
edit.assetLoader.loadVideoUnique.mockResolvedValueOnce(null);

await new VideoPlayer(edit as never, createClip("video")).load();

expect(edit.assetLoader.loadVideoUnique).toHaveBeenCalledWith(signedSrc, { src: signedSrc, data: { autoPlay: false, muted: false } });
});

it("passes the image-to-video src to the loader unchanged", async () => {
const edit = createEdit();
edit.assetLoader.load.mockResolvedValueOnce(null);

await new ImageToVideoPlayer(edit as never, createClip("image-to-video")).load();

expect(edit.assetLoader.load).toHaveBeenCalledWith(signedSrc, { src: signedSrc, crossorigin: "anonymous", data: {} });
});
});
Loading