diff --git a/packages/core/src/use-media-in-timeline.ts b/packages/core/src/use-media-in-timeline.ts index e8be7804de2..8bffae69152 100644 --- a/packages/core/src/use-media-in-timeline.ts +++ b/packages/core/src/use-media-in-timeline.ts @@ -53,7 +53,7 @@ export const useBasicMediaInTimeline = ({ const [initialVolume] = useState(() => volume); const mediaDuration = calculateMediaDuration({ - mediaDurationInFrames: videoConfig.durationInFrames, + mediaDurationInFrames: videoConfig.durationInFrames + (trimBefore ?? 0), playbackRate, trimBefore, trimAfter, diff --git a/packages/docs/docs/media/audio.mdx b/packages/docs/docs/media/audio.mdx index bf8aa3c0c6a..c8c92d2870b 100644 --- a/packages/docs/docs/media/audio.mdx +++ b/packages/docs/docs/media/audio.mdx @@ -227,6 +227,27 @@ export const MyComposition = () => { }; ``` +### `credentials?` + +Controls the [`credentials`](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) option of the `fetch()` requests made to retrieve the audio data. + +Accepts `"omit"`, `"same-origin"` (default behavior of `fetch()`) or `"include"`. +Set to `"include"` if you need to send cookies or authentication headers to a cross-origin audio URL. + +```tsx twoslash +import {AbsoluteFill} from 'remotion'; +import {Audio} from '@remotion/media'; + +// ---cut--- +export const MyComposition = () => { + return ( + + + ); +}; +``` + ### `toneFrequency?` Accepts a number between `0.01` and `2`, where `1` represents the original pitch. Values less than `1` will decrease the pitch, while values greater than `1` will increase it. diff --git a/packages/docs/docs/media/video.mdx b/packages/docs/docs/media/video.mdx index 3b3dc850a28..893768e7a90 100644 --- a/packages/docs/docs/media/video.mdx +++ b/packages/docs/docs/media/video.mdx @@ -269,6 +269,27 @@ export const MyComposition = () => { }; ``` +### `credentials?` + +Controls the [`credentials`](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) option of the `fetch()` requests made to retrieve the video data. + +Accepts `"omit"`, `"same-origin"` (default behavior of `fetch()`) or `"include"`. +Set to `"include"` if you need to send cookies or authentication headers to a cross-origin video URL. + +```tsx twoslash +import {AbsoluteFill} from 'remotion'; +import {Video} from '@remotion/media'; + +// ---cut--- +export const MyComposition = () => { + return ( + + + ); +}; +``` + ### `toneFrequency?` Accepts a number between `0.01` and `2`, where `1` represents the original pitch. Values less than `1` will decrease the pitch, while values greater than `1` will increase it. diff --git a/packages/media/src/audio-extraction/extract-audio.ts b/packages/media/src/audio-extraction/extract-audio.ts index 52d32cf8492..e4a5b0cb852 100644 --- a/packages/media/src/audio-extraction/extract-audio.ts +++ b/packages/media/src/audio-extraction/extract-audio.ts @@ -31,6 +31,7 @@ type ExtractAudioParams = { trimAfter: number | undefined; fps: number; maxCacheSize: number; + credentials: RequestCredentials | undefined; }; const extractAudioInternal = async ({ @@ -45,6 +46,7 @@ const extractAudioInternal = async ({ trimAfter, fps, maxCacheSize, + credentials, }: ExtractAudioParams): Promise< | { data: PcmS16AudioData | null; @@ -55,7 +57,7 @@ const extractAudioInternal = async ({ | 'network-error' > => { const {getAudio, actualMatroskaTimestamps, isMatroska, getDuration} = - await getSink(src, logLevel); + await getSink(src, logLevel, credentials); let mediaDurationInSeconds: number | null = null; if (loop) { diff --git a/packages/media/src/audio/audio-for-preview.tsx b/packages/media/src/audio/audio-for-preview.tsx index bb9ccc64163..50a15c51fec 100644 --- a/packages/media/src/audio/audio-for-preview.tsx +++ b/packages/media/src/audio/audio-for-preview.tsx @@ -52,6 +52,7 @@ type NewAudioForPreviewProps = { readonly fallbackHtml5AudioProps: FallbackHtml5AudioProps | undefined; readonly debugAudioScheduling: boolean; readonly onError: MediaOnError | undefined; + readonly credentials: RequestCredentials | undefined; }; type AudioForPreviewAssertedShowingProps = NewAudioForPreviewProps & { @@ -79,6 +80,7 @@ const AudioForPreviewAssertedShowing: React.FC< fallbackHtml5AudioProps, debugAudioScheduling, onError, + credentials, controls, }) => { const videoConfig = useUnsafeVideoConfig(); @@ -237,6 +239,7 @@ const AudioForPreviewAssertedShowing: React.FC< onVideoFrameCallback: null, playing: initialPlaying.current, sequenceOffset: initialSequenceOffset.current, + credentials, }); mediaPlayerRef.current = player; @@ -373,6 +376,7 @@ const AudioForPreviewAssertedShowing: React.FC< buffer, onError, videoConfig.durationInFrames, + credentials, ]); if (shouldFallbackToNativeAudio && !disallowFallbackToHtml5Audio) { @@ -427,6 +431,7 @@ type InnerAudioProps = { readonly fallbackHtml5AudioProps?: FallbackHtml5AudioProps; readonly debugAudioScheduling?: boolean; readonly onError?: MediaOnError; + readonly credentials?: RequestCredentials; }; export const AudioForPreview: React.FC< @@ -452,6 +457,7 @@ export const AudioForPreview: React.FC< fallbackHtml5AudioProps, debugAudioScheduling, onError, + credentials, controls, }) => { const preloadedSrc = usePreload(src); @@ -508,6 +514,7 @@ export const AudioForPreview: React.FC< toneFrequency={toneFrequency} debugAudioScheduling={debugAudioScheduling ?? false} onError={onError} + credentials={credentials} fallbackHtml5AudioProps={fallbackHtml5AudioProps} controls={controls} /> diff --git a/packages/media/src/audio/audio-for-rendering.tsx b/packages/media/src/audio/audio-for-rendering.tsx index 41119b03ff6..f4b6559af13 100644 --- a/packages/media/src/audio/audio-for-rendering.tsx +++ b/packages/media/src/audio/audio-for-rendering.tsx @@ -37,6 +37,7 @@ export const AudioForRendering: React.FC = ({ trimAfter, trimBefore, onError, + credentials, }) => { const defaultLogLevel = Internals.useLogLevel(); const logLevel = overriddenLogLevel ?? defaultLogLevel; @@ -129,6 +130,7 @@ export const AudioForRendering: React.FC = ({ trimBefore, fps, maxCacheSize, + credentials, }) .then((result) => { const handleError = ( @@ -266,6 +268,7 @@ export const AudioForRendering: React.FC = ({ maxCacheSize, audioEnabled, onError, + credentials, ]); if (replaceWithHtml5Audio) { diff --git a/packages/media/src/audio/props.ts b/packages/media/src/audio/props.ts index 668f522ec67..ea3f3981a0d 100644 --- a/packages/media/src/audio/props.ts +++ b/packages/media/src/audio/props.ts @@ -35,4 +35,5 @@ export type AudioProps = { delayRenderTimeoutInMilliseconds?: number; debugAudioScheduling?: boolean; onError?: MediaOnError; + credentials?: RequestCredentials; }; diff --git a/packages/media/src/extract-frame-and-audio.ts b/packages/media/src/extract-frame-and-audio.ts index 0584a79ffda..68ccb404530 100644 --- a/packages/media/src/extract-frame-and-audio.ts +++ b/packages/media/src/extract-frame-and-audio.ts @@ -19,6 +19,7 @@ export const extractFrameAndAudio = async ({ trimBefore, fps, maxCacheSize, + credentials, }: { src: string; timeInSeconds: number; @@ -33,6 +34,7 @@ export const extractFrameAndAudio = async ({ trimBefore: number | undefined; fps: number; maxCacheSize: number; + credentials: RequestCredentials | undefined; }): Promise => { try { const [video, audio] = await Promise.all([ @@ -47,6 +49,7 @@ export const extractFrameAndAudio = async ({ trimBefore, fps, maxCacheSize, + credentials, }) : null, includeAudio @@ -62,6 +65,7 @@ export const extractFrameAndAudio = async ({ fps, trimBefore, maxCacheSize, + credentials, }) : null, ]); diff --git a/packages/media/src/get-sink.ts b/packages/media/src/get-sink.ts index f6a0d6e6578..4ebb40f1ccc 100644 --- a/packages/media/src/get-sink.ts +++ b/packages/media/src/get-sink.ts @@ -5,8 +5,13 @@ import {getSinks} from './video-extraction/get-frames-since-keyframe'; export const sinkPromises: Record> = {}; -export const getSink = (src: string, logLevel: LogLevel) => { - let promise = sinkPromises[src]; +export const getSink = ( + src: string, + logLevel: LogLevel, + credentials: RequestCredentials | undefined, +) => { + const cacheKey = credentials ? `${src}::${credentials}` : src; + let promise = sinkPromises[cacheKey]; if (!promise) { Internals.Log.verbose( { @@ -15,8 +20,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, credentials); + sinkPromises[cacheKey] = promise; } return promise; diff --git a/packages/media/src/media-player.ts b/packages/media/src/media-player.ts index 2b49de61be1..eee3aeead13 100644 --- a/packages/media/src/media-player.ts +++ b/packages/media/src/media-player.ts @@ -90,6 +90,7 @@ export class MediaPlayer { onVideoFrameCallback, playing, sequenceOffset, + credentials, }: { canvas: HTMLCanvasElement | OffscreenCanvas | null; src: string; @@ -111,6 +112,7 @@ export class MediaPlayer { onVideoFrameCallback: null | ((frame: CanvasImageSource) => void); playing: boolean; sequenceOffset: number; + credentials: RequestCredentials | undefined; }) { this.canvas = canvas ?? null; this.src = src; @@ -133,9 +135,15 @@ export class MediaPlayer { this.onVideoFrameCallback = onVideoFrameCallback; this.playing = playing; this.sequenceOffset = sequenceOffset; - this.input = new Input({ - source: new UrlSource(this.src), + source: new UrlSource( + this.src, + credentials + ? { + requestInit: {credentials}, + } + : undefined, + ), formats: ALL_FORMATS, }); diff --git a/packages/media/src/test/audio-encoding.test.ts b/packages/media/src/test/audio-encoding.test.ts index df59619e983..7bec8e1293a 100644 --- a/packages/media/src/test/audio-encoding.test.ts +++ b/packages/media/src/test/audio-encoding.test.ts @@ -35,6 +35,7 @@ test('Audio samples from MP3 should produce identical s16 output on Chrome and F maxCacheSize: getMaxVideoCacheSize('info'), durationInSeconds: 1 / 30, audioStreamIndex: 0, + credentials: undefined, }); if (a === 'cannot-decode') { diff --git a/packages/media/src/test/browser.test.ts b/packages/media/src/test/browser.test.ts index 678af15e017..476def6101e 100644 --- a/packages/media/src/test/browser.test.ts +++ b/packages/media/src/test/browser.test.ts @@ -20,6 +20,7 @@ test('Should be able to extract a frame', async () => { trimBefore: undefined, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); if (result.type === 'cannot-decode') { @@ -71,6 +72,7 @@ test('Should be able to extract the last frame', async () => { trimBefore: undefined, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); if (result.type === 'cannot-decode') { @@ -121,6 +123,7 @@ test('Should manage the cache', async (t) => { trimBefore: undefined, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); } @@ -150,6 +153,7 @@ test('Should be apply volume correctly', async () => { trimBefore: undefined, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); if (result.type === 'cannot-decode') { @@ -204,6 +208,7 @@ test('Should be able to loop', async () => { trimBefore: undefined, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); if (result.type === 'cannot-decode') { diff --git a/packages/media/src/test/editlist-offset.test.ts b/packages/media/src/test/editlist-offset.test.ts index 14b8516c64d..70b3f5c3062 100644 --- a/packages/media/src/test/editlist-offset.test.ts +++ b/packages/media/src/test/editlist-offset.test.ts @@ -16,6 +16,7 @@ test('Audio extraction should be correct if there is edit list offset', async () trimBefore: undefined, trimAfter: undefined, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); assert(audio1 !== 'cannot-decode'); assert(audio1 !== 'unknown-container-format'); @@ -36,6 +37,7 @@ test('Audio extraction should be correct if there is edit list offset', async () trimBefore: undefined, trimAfter: undefined, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); assert(audio2 !== 'cannot-decode'); assert(audio2 !== 'unknown-container-format'); @@ -59,6 +61,7 @@ test('Audio extraction should be correct if there is edit list offset', async () trimBefore: undefined, trimAfter: undefined, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); assert(audio3 !== 'cannot-decode'); assert(audio3 !== 'unknown-container-format'); diff --git a/packages/media/src/test/extract-accuracy-uneven.test.ts b/packages/media/src/test/extract-accuracy-uneven.test.ts index 66cc23e252f..756aed1da5c 100644 --- a/packages/media/src/test/extract-accuracy-uneven.test.ts +++ b/packages/media/src/test/extract-accuracy-uneven.test.ts @@ -21,6 +21,7 @@ test('Extract accuracy over 100 frames with playback rate 1.75', async () => { trimBefore: undefined, trimAfter: undefined, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); if (audio === 'cannot-decode') { throw new Error(`Cannot decode at frame ${i}`); diff --git a/packages/media/src/test/extract-accuracy.test.ts b/packages/media/src/test/extract-accuracy.test.ts index eabc06b37f6..5babcb6a177 100644 --- a/packages/media/src/test/extract-accuracy.test.ts +++ b/packages/media/src/test/extract-accuracy.test.ts @@ -21,6 +21,7 @@ test('Extract accuracy over 100 frames with playback rate 2', async () => { trimBefore: undefined, trimAfter: undefined, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); if (audio === 'cannot-decode') { throw new Error(`Cannot decode at frame ${i}`); diff --git a/packages/media/src/test/looping.test.ts b/packages/media/src/test/looping.test.ts index 858c3b8d9c9..08d93cc1af0 100644 --- a/packages/media/src/test/looping.test.ts +++ b/packages/media/src/test/looping.test.ts @@ -41,6 +41,7 @@ test( playbackRate, fps, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); expect(result.type).toBe('success'); assert(result.type === 'success'); diff --git a/packages/media/src/test/media-player.test.ts b/packages/media/src/test/media-player.test.ts index b5907f4be6c..d7f66b9880e 100644 --- a/packages/media/src/test/media-player.test.ts +++ b/packages/media/src/test/media-player.test.ts @@ -53,6 +53,7 @@ test('dispose should immediately unblock playback delays', async () => { onVideoFrameCallback: null, playing: false, sequenceOffset: 0, + credentials: undefined, }); await player.initialize(0, false); diff --git a/packages/media/src/test/trim-change-seek.test.ts b/packages/media/src/test/trim-change-seek.test.ts index dfa217c58e0..224bb59c86d 100644 --- a/packages/media/src/test/trim-change-seek.test.ts +++ b/packages/media/src/test/trim-change-seek.test.ts @@ -23,6 +23,7 @@ test('setTrimBefore and setTrimAfter should update frame when paused', async () onVideoFrameCallback: null, playing: false, sequenceOffset: 0, + credentials: undefined, }); await player.initialize(0, false); diff --git a/packages/media/src/test/video-after-end.test.ts b/packages/media/src/test/video-after-end.test.ts index 2d603e0c5c4..e1bae94d699 100644 --- a/packages/media/src/test/video-after-end.test.ts +++ b/packages/media/src/test/video-after-end.test.ts @@ -15,6 +15,7 @@ test('Should render last frame for timestamps after video end', async () => { playbackRate: 1, fps: 24, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); assert(result.type === 'success'); diff --git a/packages/media/src/test/video-non-zero-start.test.ts b/packages/media/src/test/video-non-zero-start.test.ts index 4e0fb29536f..c557c0a9a61 100644 --- a/packages/media/src/test/video-non-zero-start.test.ts +++ b/packages/media/src/test/video-non-zero-start.test.ts @@ -19,6 +19,7 @@ test('Should render first frame for videos starting after timestamp 0', async () playbackRate: 1, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); // Should successfully extract (no error thrown) diff --git a/packages/media/src/test/video-with-no-frames-in-beginning.test.ts b/packages/media/src/test/video-with-no-frames-in-beginning.test.ts index 352575413f4..8e27d20c5b2 100644 --- a/packages/media/src/test/video-with-no-frames-in-beginning.test.ts +++ b/packages/media/src/test/video-with-no-frames-in-beginning.test.ts @@ -152,6 +152,7 @@ test('in rendering, should also be smart', async (t) => { playbackRate: 1, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); assert(frame.type === 'success'); if (lastFrame) { @@ -174,6 +175,7 @@ test('in rendering, should also be smart', async (t) => { playbackRate: 1, fps: 30, maxCacheSize: getMaxVideoCacheSize('info'), + credentials: undefined, }); assert(firstRealFrame.type === 'success'); diff --git a/packages/media/src/video-extraction/add-broadcast-channel-listener.ts b/packages/media/src/video-extraction/add-broadcast-channel-listener.ts index 99dad4b3ca5..d8a6beb3a4b 100644 --- a/packages/media/src/video-extraction/add-broadcast-channel-listener.ts +++ b/packages/media/src/video-extraction/add-broadcast-channel-listener.ts @@ -53,6 +53,7 @@ export type ExtractFrameRequest = { trimBefore: number | undefined; fps: number; maxCacheSize: number; + credentials: RequestCredentials | undefined; }; // Send to other channels a message to let them know that the @@ -102,6 +103,7 @@ export const addBroadcastChannelListener = () => { trimBefore: data.trimBefore, fps: data.fps, maxCacheSize: data.maxCacheSize, + credentials: data.credentials, }); if (result.type === 'cannot-decode') { diff --git a/packages/media/src/video-extraction/extract-frame-via-broadcast-channel.ts b/packages/media/src/video-extraction/extract-frame-via-broadcast-channel.ts index f936d6d858b..a6285cfef25 100644 --- a/packages/media/src/video-extraction/extract-frame-via-broadcast-channel.ts +++ b/packages/media/src/video-extraction/extract-frame-via-broadcast-channel.ts @@ -39,6 +39,7 @@ export const extractFrameViaBroadcastChannel = async ({ trimBefore, fps, maxCacheSize, + credentials, }: { src: string; timeInSeconds: number; @@ -54,6 +55,7 @@ export const extractFrameViaBroadcastChannel = async ({ trimBefore: number | undefined; fps: number; maxCacheSize: number; + credentials: RequestCredentials | undefined; }): Promise => { if (isClientSideRendering || window.remotion_isMainTab) { return extractFrameAndAudio({ @@ -70,6 +72,7 @@ export const extractFrameViaBroadcastChannel = async ({ trimBefore, fps, maxCacheSize, + credentials, }); } @@ -195,6 +198,7 @@ export const extractFrameViaBroadcastChannel = async ({ trimBefore, fps, maxCacheSize, + credentials, }; window.remotion_broadcastChannel!.postMessage(request); diff --git a/packages/media/src/video-extraction/extract-frame.ts b/packages/media/src/video-extraction/extract-frame.ts index 54ad734a4ec..ce47e042c6c 100644 --- a/packages/media/src/video-extraction/extract-frame.ts +++ b/packages/media/src/video-extraction/extract-frame.ts @@ -25,6 +25,7 @@ type ExtractFrameParams = { playbackRate: number; fps: number; maxCacheSize: number; + credentials: RequestCredentials | undefined; }; const extractFrameInternal = async ({ @@ -37,8 +38,9 @@ const extractFrameInternal = async ({ playbackRate, fps, maxCacheSize, + credentials, }: ExtractFrameParams): Promise => { - const sink = await getSink(src, logLevel); + const sink = await getSink(src, logLevel, credentials); const [video, mediaDurationInSecondsRaw] = await Promise.all([ sink.getVideo(), diff --git a/packages/media/src/video-extraction/get-frames-since-keyframe.ts b/packages/media/src/video-extraction/get-frames-since-keyframe.ts index a7939ec1e26..3141ff9a753 100644 --- a/packages/media/src/video-extraction/get-frames-since-keyframe.ts +++ b/packages/media/src/video-extraction/get-frames-since-keyframe.ts @@ -53,11 +53,15 @@ const getFormatOrNullOrNetworkError = async ( } }; -export const getSinks = async (src: string) => { +export const getSinks = async ( + src: string, + credentials: RequestCredentials | undefined, +) => { const input = new Input({ formats: ALL_FORMATS, source: new UrlSource(src, { getRetryDelay, + ...(credentials ? {requestInit: {credentials}} : undefined), }), }); diff --git a/packages/media/src/video/props.ts b/packages/media/src/video/props.ts index fa29c728d59..6956f8ada37 100644 --- a/packages/media/src/video/props.ts +++ b/packages/media/src/video/props.ts @@ -58,6 +58,7 @@ type OptionalVideoProps = { debugAudioScheduling: boolean; headless: boolean; onError: MediaOnError | undefined; + credentials: RequestCredentials | undefined; }; export type InnerVideoProps = MandatoryVideoProps & diff --git a/packages/media/src/video/video-for-preview.tsx b/packages/media/src/video/video-for-preview.tsx index 19c43244aec..38babbe7d52 100644 --- a/packages/media/src/video/video-for-preview.tsx +++ b/packages/media/src/video/video-for-preview.tsx @@ -64,6 +64,7 @@ type VideoForPreviewProps = { readonly debugAudioScheduling: boolean; readonly headless: boolean; readonly onError: MediaOnError | undefined; + readonly credentials: RequestCredentials | undefined; }; type VideoForPreviewAssertedShowingProps = VideoForPreviewProps & { @@ -95,6 +96,7 @@ const VideoForPreviewAssertedShowing: React.FC< debugAudioScheduling, headless, onError, + credentials, controls, }) => { const src = usePreload(unpreloadedSrc); @@ -229,6 +231,7 @@ const VideoForPreviewAssertedShowing: React.FC< onVideoFrameCallback: initialOnVideoFrameRef.current ?? null, playing: initialPlaying.current, sequenceOffset: initialSequenceOffset.current, + credentials, }); mediaPlayerRef.current = player; @@ -361,6 +364,7 @@ const VideoForPreviewAssertedShowing: React.FC< videoConfig.fps, onError, videoConfig.durationInFrames, + credentials, ]); const classNameValue = useMemo(() => { diff --git a/packages/media/src/video/video-for-rendering.tsx b/packages/media/src/video/video-for-rendering.tsx index 2645703f104..b96a88ccf5e 100644 --- a/packages/media/src/video/video-for-rendering.tsx +++ b/packages/media/src/video/video-for-rendering.tsx @@ -51,6 +51,7 @@ type InnerVideoProps = { readonly trimAfterValue: number | undefined; readonly headless: boolean; readonly onError: MediaOnError | undefined; + readonly credentials: RequestCredentials | undefined; }; type FallbackToOffthreadVideo = { @@ -80,6 +81,7 @@ export const VideoForRendering: React.FC = ({ trimBeforeValue, headless, onError, + credentials, }) => { if (!src) { throw new TypeError('No `src` was passed to