Skip to content
Closed
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
17 changes: 17 additions & 0 deletions packages/docs/docs/media/audio.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ export const MyComposition = () => {
};
```

### `crossOrigin?`

Controls the CORS mode when the audio is fetched. Set to `'anonymous'` or `'use-credentials'` to pass the corresponding [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#credentials) options.

```tsx twoslash title="Loading a remote audio file with CORS enabled"
import {AbsoluteFill} from 'remotion';
import {Audio} from '@remotion/media';
// ---cut---
export const MyComposition = () => {
return (
<AbsoluteFill>
<Audio src="https://remotion.media/audio.wav" crossOrigin="anonymous" />
</AbsoluteFill>
);
};
```

### `showInTimeline?`

If set to `false`, no layer will be shown in the timeline of the Remotion Studio. The default is `true`.
Expand Down
17 changes: 17 additions & 0 deletions packages/docs/docs/media/video.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ export const MyComposition = () => {
};
```

### `crossOrigin?`

Controls the CORS mode when the video is fetched. Set to `'anonymous'` or `'use-credentials'` to pass the corresponding [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#credentials) options.

```tsx twoslash title="Loading a remote video with CORS enabled"
import {AbsoluteFill} from 'remotion';
import {Video} from '@remotion/media';
// ---cut---
export const MyComposition = () => {
return (
<AbsoluteFill>
<Video src="https://remotion-website-static-files.s3.eu-central-1.amazonaws.com/BigBuckBunny.mp4" crossOrigin="anonymous" />
</AbsoluteFill>
);
};
```

### `style?`

You can pass any style you can pass to a native HTML `<canvas>` element.
Expand Down
4 changes: 3 additions & 1 deletion packages/media/src/audio-extraction/extract-audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ExtractAudioParams = {
trimAfter: number | undefined;
fps: number;
maxCacheSize: number;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
};

const extractAudioInternal = async ({
Expand All @@ -41,6 +42,7 @@ const extractAudioInternal = async ({
trimAfter,
fps,
maxCacheSize,
crossOrigin,
}: ExtractAudioParams): Promise<
| {
data: PcmS16AudioData | null;
Expand All @@ -51,7 +53,7 @@ const extractAudioInternal = async ({
| 'network-error'
> => {
const {getAudio, actualMatroskaTimestamps, isMatroska, getDuration} =
await getSink(src, logLevel);
await getSink({src, logLevel, crossOrigin});

let mediaDurationInSeconds: number | null = null;
if (loop) {
Expand Down
8 changes: 8 additions & 0 deletions packages/media/src/audio/audio-for-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type NewAudioForPreviewProps = {
readonly toneFrequency: number | undefined;
readonly audioStreamIndex: number | undefined;
readonly fallbackHtml5AudioProps: FallbackHtml5AudioProps | undefined;
readonly crossOrigin?: '' | 'anonymous' | 'use-credentials';
};

const AudioForPreviewAssertedShowing: React.FC<NewAudioForPreviewProps> = ({
Expand All @@ -69,6 +70,7 @@ const AudioForPreviewAssertedShowing: React.FC<NewAudioForPreviewProps> = ({
toneFrequency,
audioStreamIndex,
fallbackHtml5AudioProps,
crossOrigin,
}) => {
const videoConfig = useUnsafeVideoConfig();
const frame = useCurrentFrame();
Expand Down Expand Up @@ -176,6 +178,7 @@ const AudioForPreviewAssertedShowing: React.FC<NewAudioForPreviewProps> = ({
isPostmounting,
isPremounting,
globalPlaybackRate,
crossOrigin,
});

mediaPlayerRef.current = player;
Expand Down Expand Up @@ -303,6 +306,7 @@ const AudioForPreviewAssertedShowing: React.FC<NewAudioForPreviewProps> = ({
isPremounting,
isPostmounting,
globalPlaybackRate,
crossOrigin,
]);

useEffect(() => {
Expand Down Expand Up @@ -436,6 +440,7 @@ const AudioForPreviewAssertedShowing: React.FC<NewAudioForPreviewProps> = ({
toneFrequency={toneFrequency}
audioStreamIndex={audioStreamIndex}
pauseWhenBuffering={fallbackHtml5AudioProps?.pauseWhenBuffering}
crossOrigin={crossOrigin}
{...fallbackHtml5AudioProps}
/>
);
Expand Down Expand Up @@ -469,6 +474,7 @@ type InnerAudioProps = {
readonly toneFrequency?: number;
readonly audioStreamIndex?: number;
readonly fallbackHtml5AudioProps?: FallbackHtml5AudioProps;
readonly crossOrigin?: '' | 'anonymous' | 'use-credentials';
};

export const AudioForPreview: React.FC<InnerAudioProps> = ({
Expand All @@ -488,6 +494,7 @@ export const AudioForPreview: React.FC<InnerAudioProps> = ({
toneFrequency,
audioStreamIndex,
fallbackHtml5AudioProps,
crossOrigin,
}) => {
const preloadedSrc = usePreload(src);

Expand Down Expand Up @@ -546,6 +553,7 @@ export const AudioForPreview: React.FC<InnerAudioProps> = ({
disallowFallbackToHtml5Audio={disallowFallbackToHtml5Audio ?? false}
toneFrequency={toneFrequency}
fallbackHtml5AudioProps={fallbackHtml5AudioProps}
crossOrigin={crossOrigin}
/>
);
};
4 changes: 4 additions & 0 deletions packages/media/src/audio/audio-for-rendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const AudioForRendering: React.FC<AudioProps> = ({
playbackRate,
src,
muted,
crossOrigin,
loopVolumeCurveBehavior,
delayRenderRetries,
delayRenderTimeoutInMilliseconds,
Expand Down Expand Up @@ -121,6 +122,7 @@ export const AudioForRendering: React.FC<AudioProps> = ({
trimBefore,
fps,
maxCacheSize,
crossOrigin,
})
.then((result) => {
if (result.type === 'unknown-container-format') {
Expand Down Expand Up @@ -257,6 +259,7 @@ export const AudioForRendering: React.FC<AudioProps> = ({
trimBefore,
replaceWithHtml5Audio,
maxCacheSize,
crossOrigin,
]);

if (replaceWithHtml5Audio) {
Expand All @@ -272,6 +275,7 @@ export const AudioForRendering: React.FC<AudioProps> = ({
style={style}
loopVolumeCurveBehavior={loopVolumeCurveBehavior}
audioStreamIndex={audioStreamIndex}
crossOrigin={crossOrigin}
useWebAudioApi={fallbackHtml5AudioProps?.useWebAudioApi}
onError={fallbackHtml5AudioProps?.onError}
toneFrequency={toneFrequency}
Expand Down
1 change: 1 addition & 0 deletions packages/media/src/audio/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type AudioProps = {
showInTimeline?: boolean;
playbackRate?: number;
muted?: boolean;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
style?: React.CSSProperties;
/**
* @deprecated For internal use only
Expand Down
4 changes: 4 additions & 0 deletions packages/media/src/extract-frame-and-audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const extractFrameAndAudio = async ({
trimBefore,
fps,
maxCacheSize,
crossOrigin,
}: {
src: string;
timeInSeconds: number;
Expand All @@ -33,6 +34,7 @@ export const extractFrameAndAudio = async ({
trimBefore: number | undefined;
fps: number;
maxCacheSize: number;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
}): Promise<ExtractFrameViaBroadcastChannelResult> => {
try {
const [frame, audio] = await Promise.all([
Expand All @@ -47,6 +49,7 @@ export const extractFrameAndAudio = async ({
trimBefore,
fps,
maxCacheSize,
crossOrigin,
})
: null,
includeAudio
Expand All @@ -62,6 +65,7 @@ export const extractFrameAndAudio = async ({
fps,
trimBefore,
maxCacheSize,
crossOrigin,
})
: null,
]);
Expand Down
24 changes: 20 additions & 4 deletions packages/media/src/get-sink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,24 @@ import {getSinks} from './video-extraction/get-frames-since-keyframe';

export const sinkPromises: Record<string, Promise<GetSink>> = {};

export const getSink = (src: string, logLevel: LogLevel) => {
let promise = sinkPromises[src];
const getSinkKey = (
src: string,
crossOrigin?: '' | 'anonymous' | 'use-credentials',
) => {
return JSON.stringify({src, crossOrigin: crossOrigin ?? null});
};

export const getSink = ({
src,
logLevel,
crossOrigin,
}: {
src: string;
logLevel: LogLevel;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
}) => {
const key = getSinkKey(src, crossOrigin);
let promise = sinkPromises[key];
if (!promise) {
Internals.Log.verbose(
{
Expand All @@ -15,8 +31,8 @@ export const getSink = (src: string, logLevel: LogLevel) => {
},
`Sink for ${src} was not found, creating new sink`,
);
promise = getSinks(src);
sinkPromises[src] = promise;
promise = getSinks(src, crossOrigin);
sinkPromises[key] = promise;
}

return promise;
Expand Down
21 changes: 21 additions & 0 deletions packages/media/src/helpers/get-request-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const getRequestInit = ({
crossOrigin,
}: {
crossOrigin?: '' | 'anonymous' | 'use-credentials';
}): RequestInit | undefined => {
if (crossOrigin === '' || crossOrigin === undefined) {
return undefined;
}

if (crossOrigin === 'use-credentials') {
return {
mode: 'cors',
credentials: 'include',
};
}

return {
mode: 'cors',
credentials: 'omit',
};
};
7 changes: 6 additions & 1 deletion packages/media/src/media-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {calculatePlaybackTime} from './calculate-playbacktime';
import {drawPreviewOverlay} from './debug-overlay/preview-overlay';
import {getTimeInSeconds} from './get-time-in-seconds';
import {getRequestInit} from './helpers/get-request-init';
import {isNetworkError} from './is-network-error';
import type {Nonce, NonceManager} from './nonce-manager';
import {makeNonceManager} from './nonce-manager';
Expand Down Expand Up @@ -71,6 +72,7 @@ export class MediaPlayer {
constructor({
canvas,
src,
crossOrigin,
logLevel,
sharedAudioContext,
loop,
Expand All @@ -92,6 +94,7 @@ export class MediaPlayer {
loop: boolean;
trimBefore: number | undefined;
trimAfter: number | undefined;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
playbackRate: number;
globalPlaybackRate: number;
audioStreamIndex: number;
Expand All @@ -118,8 +121,10 @@ export class MediaPlayer {
this.isPostmounting = isPostmounting;
this.nonceManager = makeNonceManager();

const requestInit = getRequestInit({crossOrigin});

this.input = new Input({
source: new UrlSource(this.src),
source: new UrlSource(this.src, requestInit ? {requestInit} : undefined),
formats: ALL_FORMATS,
});

Expand Down
79 changes: 79 additions & 0 deletions packages/media/src/test/cross-origin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {afterEach, beforeEach, expect, test, vi} from 'vitest';
import {getSink, sinkPromises} from '../get-sink';
import {getRequestInit} from '../helpers/get-request-init';
import {getSinks} from '../video-extraction/get-frames-since-keyframe';

vi.mock('../video-extraction/get-frames-since-keyframe', () => ({
getSinks: vi.fn(),
}));

type MockedGetSinks = ReturnType<typeof vi.fn<typeof getSinks>>;
const mockedGetSinks = getSinks as unknown as MockedGetSinks;

const clearSinkPromises = () => {
for (const key of Object.keys(sinkPromises)) {
delete sinkPromises[key];
}
};

beforeEach(() => {
mockedGetSinks.mockReset();
clearSinkPromises();
});

afterEach(() => {
clearSinkPromises();
});

test('getRequestInit returns expected RequestInit values', () => {
expect(getRequestInit({crossOrigin: undefined})).toBeUndefined();
expect(getRequestInit({crossOrigin: ''})).toBeUndefined();
expect(getRequestInit({crossOrigin: 'anonymous'})).toEqual({
mode: 'cors',
credentials: 'omit',
});
expect(getRequestInit({crossOrigin: 'use-credentials'})).toEqual({
mode: 'cors',
credentials: 'include',
});
});

test('getSink caches sinks separately by crossOrigin value', async () => {
mockedGetSinks.mockImplementation((src, crossOrigin) =>
Promise.resolve({src, crossOrigin} as never),
);

const defaultSink = getSink({
src: 'video.mp4',
logLevel: 'info',
crossOrigin: undefined,
});
const sameDefaultSink = getSink({
src: 'video.mp4',
logLevel: 'info',
crossOrigin: undefined,
});

expect(mockedGetSinks).toHaveBeenCalledTimes(1);
expect(defaultSink).toBe(sameDefaultSink);

const anonymousSink = getSink({
src: 'video.mp4',
logLevel: 'info',
crossOrigin: 'anonymous',
});

expect(mockedGetSinks).toHaveBeenCalledTimes(2);
expect(anonymousSink).not.toBe(defaultSink);

const credentialedSink = getSink({
src: 'video.mp4',
logLevel: 'info',
crossOrigin: 'use-credentials',
});

expect(mockedGetSinks).toHaveBeenCalledTimes(3);
expect(credentialedSink).not.toBe(anonymousSink);

expect(Object.keys(sinkPromises).length).toBe(3);
});
Loading
Loading