remotion-dev/remotion · error · Error

getAudioDuration() is only available in the browser.

Error message

getAudioDuration() is only available in the browser.

What it means

Thrown by getAudioDurationInSeconds() (and its deprecated alias getAudioDuration) when `document` is undefined (line 14). The implementation creates a real <audio> element via document.createElement to read loadedmetadata, so it cannot run outside a DOM. Note: this API is deprecated in favor of Mediabunny's metadata API.

Source

Thrown at packages/media-utils/src/get-audio-duration-in-seconds.ts:15

/* eslint-disable @typescript-eslint/no-use-before-define */
import {onMediaError} from './media-tag-error-handling';
import {pLimit} from './p-limit';

const limit = pLimit(3);

const metadataCache: {[key: string]: number} = {};

const fn = (src: string): Promise<number> => {
	if (metadataCache[src]) {
		return Promise.resolve(metadataCache[src]);
	}

	if (typeof document === 'undefined') {
		throw new Error('getAudioDuration() is only available in the browser.');
	}

	const audio = document.createElement('audio');
	audio.src = src;
	return new Promise<number>((resolve, reject) => {
		const onError = () => {
			onMediaError({
				error: audio.error!,
				src,
				cleanup,
				reject,
				api: 'getAudioDurationInSeconds()',
			});
		};

		const onLoadedMetadata = () => {
			metadataCache[src] = audio.duration;
			resolve(audio.duration);

View on GitHub (pinned to 78fe4bb3fd)

Solutions

  1. Call getAudioDurationInSeconds() only from inside Remotion client components or other browser code.
  2. For server-side or test-time duration, migrate to Mediabunny's metadata API which is not DOM-bound (this also removes the deprecation warning).
  3. If you must use this API in tests, set test environment to jsdom or happy-dom and ensure the <audio> element can reach the URL.
  4. Gate the call with typeof document !== 'undefined' to avoid invoking it during SSR.

Example fix

// before (server-side call, throws)
import {getAudioDurationInSeconds} from '@remotion/media-utils';
export async function loader() {
  return {duration: await getAudioDurationInSeconds('/a.mp3')};
}

// after (browser-only via hook)
import {useAudioData} from '@remotion/media-utils';
const audio = useAudioData(staticFile('a.mp3'));
const duration = audio?.durationInSeconds;
Defensive patterns

Strategy: validation

Validate before calling

import {getAudioDurationInSeconds} from '@remotion/media-utils';

async function safeGetAudioDurationInSeconds(src: string): Promise<number | null> {
  if (typeof document === 'undefined') {
    return null; // not in a browser; caller can fall back to ffprobe/Mediabunny
  }
  return getAudioDurationInSeconds(src);
}

Type guard

const canUseDomAudio = (): boolean => typeof document !== 'undefined';

Try / catch

try {
  const duration = await getAudioDurationInSeconds(src);
} catch (err) {
  if ((err as Error).message.includes('only available in the browser')) {
    // fall back to a non-DOM path (Mediabunny metadata or ffprobe)
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling getAudioDurationInSeconds() during SSR, in a Node script, in Node-based unit tests, or in a Cloudflare/Vercel edge function. Importing the module eagerly and invoking at top-level on the server.

Common situations: Server-side pre-render of a Remotion composition; Next.js server component trying to compute clip length; Jest test under Node env without jsdom; build-time data fetching that needs the duration.

Related errors


AI-assisted analysis of remotion-dev/remotion@78fe4bb3fd (2026-08-12). Data as JSON: /api/errors/ea31265b973ba6d8. Report an issue: GitHub.