@oscnord/get-video-resolution
v2.3.1
Published
Get resolution, codec, audio tracks, subtitles, bit depth, rotation, and more from any video source. Zero dependencies, no ffmpeg.
Maintainers
Readme
Get video resolution
Get resolution, codec, audio tracks, subtitles, bit depth, rotation, and more from any video source. Supports local files (MP4, MOV, WebM, MKV, AVI), HLS streams, DASH manifests, and binary input (Buffer/Blob).
Zero dependencies. No ffmpeg required. Browser-compatible for URL/Blob sources (see Where it runs).
Only reads file headers (not the full file), so it works efficiently on files of any size. For remote URLs, uses HTTP Range requests to fetch just the first 1MB.
Install
npm install @oscnord/get-video-resolutionUsage
Basic usage
import { getVideoResolution } from "@oscnord/get-video-resolution";
// Local file
const info = await getVideoResolution("/path/to/video.mp4");
console.log(info.width, info.height); // 1920 1080
// HLS stream
const hls = await getVideoResolution("https://example.com/stream/master.m3u8");
// DASH manifest
const dash = await getVideoResolution("https://example.com/stream/manifest.mpd");VideoInfo return type
Every call returns a VideoInfo object:
const info = await getVideoResolution("/path/to/video.mp4");
// {
// width: 1920,
// height: 1080,
// duration: 120.5,
// codec: "avc1.640028",
// framerate: 29.97,
// bitrate: undefined, // available for HLS/DASH variants
// aspectRatio: "16:9",
// hdr: false,
// rotation: 0, // degrees (0, 90, 180, 270)
// bitDepth: 8, // 8, 10, or 12
// encrypted: undefined, // true when DRM detected (HLS/DASH)
// audioTracks: [
// { codec: "mp4a.40.2", language: "en", channels: 2 }
// ],
// subtitleTracks: undefined // populated when present (MP4/WebM/MKV/HLS/DASH)
// }HLS/DASH variant metadata
For streaming sources, each variant includes manifest-level metadata:
const variants = await getVideoResolution(
"https://example.com/stream/master.m3u8",
{ pick: "all" },
);
// Each variant includes:
// - audioTracks: available audio languages and codecs
// - subtitleTracks: available subtitle languages
// - encrypted: true if DRM detected
console.log(variants[0].audioTracks);
// [{ codec: "mp4a.40.2", language: "en", channels: 2 },
// { codec: "mp4a.40.2", language: "sv", channels: 2 }]Get lowest resolution
const lowest = await getVideoResolution(
"https://example.com/stream/master.m3u8",
{ pick: "lowest" },
);URL content-type sniffing
When a URL has no recognizable extension, enable sniff to send a HEAD request and detect the content type:
const info = await getVideoResolution("https://cdn.example.com/video/12345", {
sniff: true,
});Custom fetch with auth headers
Pass a custom fetch function for authenticated or proxied requests:
const info = await getVideoResolution(
"https://api.example.com/stream/master.m3u8",
{
fetch: (url, init) =>
globalThis.fetch(url, {
...init,
headers: { Authorization: "Bearer token" },
}),
},
);Timeout and AbortSignal
// Timeout in milliseconds
const info = await getVideoResolution("https://example.com/video.mp4", {
timeout: 5000,
});
// Or use an AbortSignal for manual cancellation
const controller = new AbortController();
const info = await getVideoResolution("https://example.com/video.mp4", {
signal: controller.signal,
});Buffer / Blob / ReadableStream input
Pass binary data directly:
import { readFile } from "node:fs/promises";
// Buffer
const buffer = await readFile("/path/to/video.mp4");
const info = await getVideoResolution(buffer);
// Blob (browser, or Node's File API)
const blob = new Blob([buffer], { type: "video/mp4" });
const fromBlob = await getVideoResolution(blob);
// ReadableStream — e.g. from a fetch response or Node fs stream.
// The library reads only the head/tail it needs (capped at 2 MB) and
// cancels the rest, so streaming a multi-GB file is safe.
const res = await fetch("https://example.com/big-video.mp4");
const fromStream = await getVideoResolution(res.body!);Where it runs
Anywhere with fetch — Node 18+, modern browsers, edge runtimes (Vercel Edge, Cloudflare Workers), Bun, Deno.
| Source | Node | Browser | Edge |
| --- | --- | --- | --- |
| Local path (/path/to/video.mp4) | ✅ | ❌ | ❌ |
| http(s):// URL | ✅ | ✅ | ✅ |
| Buffer / Blob / ReadableStream | ✅ | ✅ | ✅ |
In Next.js App Router, call it from a server component so the fetch happens server-side:
// app/video/[id]/page.tsx
import { getVideoResolution } from "@oscnord/get-video-resolution";
export default async function Page({ params }: { params: { id: string } }) {
const info = await getVideoResolution(`https://cdn.example.com/${params.id}.mp4`);
return <p>{info.width}×{info.height}</p>;
}Recipes
Display dimensions for rotated mobile video
The library reports the stored width/height plus a separate rotation. A portrait iPhone clip stores 1920×1080 with rotation: 90. To get the dimensions you'll actually render:
const info = await getVideoResolution(source);
const isSideways = info.rotation === 90 || info.rotation === 270;
const displayWidth = isSideways ? info.height : info.width;
const displayHeight = isSideways ? info.width : info.height;Detecting DRM-protected streams before playback
HLS/DASH variants populate encrypted: true when the manifest carries #EXT-X-KEY or <ContentProtection>. The library can read the manifest but can't decrypt segments — branch early:
const info = await getVideoResolution("https://example.com/master.m3u8");
if (info.encrypted) {
// Hand off to a DRM-aware player (Shaka, hls.js with EME). Don't try
// to download or transcode the segments yourself.
}Sources where duration is missing
Some MP4s (fragmented, malformed, mid-write) and live HLS/DASH playlists return a VideoInfo without duration. Treat it as unknown rather than zero:
const info = await getVideoResolution(source);
const knownDuration = info.duration ?? null;
if (knownDuration === null) {
// For HLS this often means a live playlist; for MP4, the moov box
// didn't carry an mvhd duration. Decide whether to reject the upload,
// probe with a player, or accept without a duration display.
}API
getVideoResolution(source, options?)
function getVideoResolution(
source: string | Buffer | Blob | ReadableStream,
options: GetVideoResolutionOptions & { pick: "all" },
): Promise<VideoInfo[]>;
function getVideoResolution(
source: string | Buffer | Blob | ReadableStream,
options?: GetVideoResolutionOptions,
): Promise<VideoInfo>;When pick is "all", returns VideoInfo[]. Otherwise returns a single VideoInfo.
VideoInfo
interface VideoInfo {
width: number;
height: number;
duration?: number; // seconds
codec?: string; // e.g. "avc1.640028", "hev1.1.6.L150"
framerate?: number; // frames per second
bitrate?: number; // bits per second (HLS/DASH only)
aspectRatio?: string; // e.g. "16:9", "4:3"
hdr?: boolean; // true for HDR codecs (HLG, HDR10, Dolby Vision)
rotation?: number; // degrees (0, 90, 180, 270)
bitDepth?: number; // 8, 10, or 12
encrypted?: boolean; // DRM detected (HLS/DASH only)
audioTracks?: AudioTrack[];
subtitleTracks?: SubtitleTrack[];
}
interface AudioTrack {
codec?: string; // e.g. "mp4a.40.2", "opus", "ac-3"
language?: string; // e.g. "en", "sv"
channels?: number; // e.g. 2, 6
}
interface SubtitleTrack {
language?: string; // e.g. "en", "sv"
codec?: string; // e.g. "wvtt", "stpp"
}GetVideoResolutionOptions
interface GetVideoResolutionOptions {
timeout?: number; // milliseconds
signal?: AbortSignal; // manual abort
fetch?: typeof globalThis.fetch; // custom fetch implementation
pick?: "highest" | "lowest" | "all"; // variant selection (default: "highest")
sniff?: boolean; // HEAD-request content-type detection
}Auto-detection
The input type is detected automatically by file extension:
| Extension | Parser |
| --------- | ------ |
| .m3u8 | HLS manifest parser |
| .mpd | DASH manifest parser |
| Everything else | Built-in file parser (MP4, MOV, WebM, MKV, AVI) |
When sniff: true and the URL has no recognized extension, a HEAD request inspects the Content-Type:
application/vnd.apple.mpegurl/audio/mpegurl→ HLSapplication/dash+xml→ DASH- A generic type (
application/octet-stream,text/plain,text/xml, or empty) triggers a smallRange: bytes=0-2047GET that inspects the first bytes for#EXTM3U,<MPD, or<?xmlbefore falling back to the file parser.
Error handling
All errors extend VideoResolutionError, so you can catch them with instanceof:
import {
getVideoResolution,
VideoResolutionError,
NetworkError,
ManifestParseError,
UnsupportedSourceError,
MediaParseError,
type AudioTrack,
type SubtitleTrack,
type VideoInfo,
} from "@oscnord/get-video-resolution";
try {
const info = await getVideoResolution(source);
} catch (error) {
if (error instanceof NetworkError) {
// fetch failed, timeout, etc.
} else if (error instanceof ManifestParseError) {
// invalid HLS/DASH manifest
} else if (error instanceof UnsupportedSourceError) {
// invalid source path or URL
} else if (error instanceof MediaParseError) {
// file parsing failed
} else if (error instanceof VideoResolutionError) {
// catch-all for any library error
}
}| Error class | When |
| --- | --- |
| NetworkError | HTTP request failed, timed out, or was aborted |
| ManifestParseError | HLS/DASH manifest could not be parsed or has no resolution |
| UnsupportedSourceError | Source string is not a valid path or URL |
| MediaParseError | File could not be parsed or has no video track |
Structured error context
Every error carries an optional context object so you can branch without parsing message strings:
try {
await getVideoResolution(source);
} catch (error) {
if (error instanceof MediaParseError && error.context?.format === "mp4") {
// we know the file detected as MP4 but parsing failed
}
if (error instanceof NetworkError && error.context?.status === 404) {
// ...
}
}interface VideoResolutionErrorContext {
source?: string; // URL or path being processed
format?: string; // "mp4" | "webm" | "avi" | "hls" | "dash"
byteOffset?: number; // file offset, for parser errors
status?: number; // HTTP status, for network errors
}Errors thrown from the file parser also preserve the underlying cause via error.cause when wrapping a non-VideoResolutionError.
CommonJS
const { getVideoResolution } = require("@oscnord/get-video-resolution");
const info = await getVideoResolution("/path/to/video.mp4");Development
Requires Bun.
bun install
bun test
bun run buildLicense
MIT
