diff --git a/src/components/canvas/players/image-player.ts b/src/components/canvas/players/image-player.ts index db47746d..f58eb52b 100644 --- a/src/components/canvas/players/image-player.ts +++ b/src/components/canvas/players/image-player.ts @@ -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>(corsUrl, loadOptions); + const loadOptions: pixi.UnresolvedAsset = { src, crossorigin: "anonymous", data: {} }; + const texture = await this.edit.assetLoader.load>(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}'.`); } diff --git a/src/components/canvas/players/image-to-video-player.ts b/src/components/canvas/players/image-to-video-player.ts index d377f13f..a6660de1 100644 --- a/src/components/canvas/players/image-to-video-player.ts +++ b/src/components/canvas/players/image-to-video-player.ts @@ -114,14 +114,13 @@ export class ImageToVideoPlayer extends Player { private async tryLoadTexture(src: string): Promise { 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>(corsUrl, loadOptions); + const loadOptions: pixi.UnresolvedAsset = { src, crossorigin: "anonymous", data: {} }; + const texture = await this.edit.assetLoader.load>(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; } diff --git a/src/components/canvas/players/video-player.ts b/src/components/canvas/players/video-player.ts index 5399aa63..33334a48 100644 --- a/src/components/canvas/players/video-player.ts +++ b/src/components/canvas/players/video-player.ts @@ -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}'.`); diff --git a/tests/media-player-fallback.test.ts b/tests/media-player-fallback.test.ts index a0324633..4cf166da 100644 --- a/tests/media-player-fallback.test.ts +++ b/tests/media-player-fallback.test.ts @@ -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; @@ -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"; @@ -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(), @@ -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: {} }); + }); +});