IUrlResolver
Every URL the player touches, whether a media manifest, a subtitle file, a poster image, or a DRM license endpoint, passes through the URL resolver before it reaches a <video> element, a Worker, or a Cast receiver.
The built-in resolver applies auth.transformUrl (your token-injection callback) and then parses the result into a structured ResolvedUrl you can inspect.
That covers most cases.
Where you need more is when different asset types need different treatment.
A CDN might require per-request signed URLs on media and subtitles, but let poster images through unsigned.
A multi-origin setup might route *.m3u8 files to one edge and font assets to another.
The urlResolver port handles all of that from one place, and the rest of the core never knows the difference.
You supply a resolver at setup({ urlResolver }) or swap it at runtime with player.urlResolver(fn).
When you only need to extend the built-in behavior rather than replace it, call ctx.defaultResolve(url) inside your resolver and let the default path handle any category you don’t want to intercept.
Everything you import from this page lives in these two lines:
import type { IUrlResolver, ResolvedUrl, UrlCategory, UrlResolverContext } from '@nomercy-entertainment/nomercy-player-core/adapters/url-resolver';
Built-in adapter
There is no exported class for the default resolver.
When no urlResolver is configured, the player applies auth.transformUrl to the raw URL string and parses the result into a ResolvedUrl internally.
If you only need to add an auth token to every request, auth.transformUrl is the right place for it and you do not need to supply a urlResolver at all.
URL categories
The player core passes a category string alongside every URL so your resolver can branch on asset type. The built-in category constants are:
const URL_CATEGORY = {
MEDIA: 'media',
SUBTITLE: 'subtitle',
FONT: 'font',
POSTER: 'poster',
SPRITE: 'sprite',
LYRICS: 'lyrics',
CAST: 'cast',
LICENSE: 'license',
} as const;
Custom plugins may pass any string. Your resolver should treat unknown categories as a passthrough rather than an error.
The ResolvedUrl shape
Your resolver returns a ResolvedUrl object.
The player hands this directly to the appropriate consumer, so href is what ends up in <video>.src or the Cast receiver.
interface ResolvedUrl {
readonly raw: string; // the original input, unmodified
readonly href: string; // the final URL string, use this
readonly scheme: string;
readonly origin: string;
readonly pathname: string;
readonly ext: string; // lowercase extension, no leading dot, no query params
readonly search: string;
readonly searchParams: URLSearchParams;
readonly hash: string;
readonly relative: boolean; // true when input was relative and baseUrl was unavailable
toString(): string; // returns href, so template strings work
}
Use ext to gate on file type.
It strips query strings for you, so manifest.m3u8?token=abc gives 'm3u8', not 'm3u8?token=abc'.
The UrlResolverContext shape
The second argument to your resolver function carries everything you need to make a routing decision:
interface UrlResolverContext {
readonly auth: AuthConfig | undefined;
readonly baseUrl: string | undefined;
readonly category: UrlCategory;
readonly defaultResolve: (url: string) => Promise<ResolvedUrl>;
}
Call ctx.defaultResolve(url) to delegate back to the built-in pipeline for any category you don’t want to handle yourself.
Interface
interface IUrlResolver {
(url: string, ctx: UrlResolverContext): Promise<ResolvedUrl> | ResolvedUrl;
}
A resolver is a callable interface, not a class. The simplest valid implementation is a plain function.
Custom implementation
The common case is signing media and subtitle URLs with a short-lived CDN token while leaving everything else to the default pipeline:
import { authFetch } from '@nomercy-entertainment/nomercy-player-core';
import type { IUrlResolver } from '@nomercy-entertainment/nomercy-player-core/adapters/url-resolver';
async function fetchSigningToken(): Promise<string> {
const payload = await authFetch<{ token: string }>({
url: 'https://api.example.com/cdn-token',
responseType: 'json',
signal: AbortSignal.timeout(10_000),
});
return payload.token;
}
const cdnResolver: IUrlResolver = async (url, ctx) => {
const signable = ctx.category === 'media'
|| ctx.category === 'subtitle';
if (!signable) {
return ctx.defaultResolve(url);
}
const token = await fetchSigningToken();
const signed = new URL(url);
signed.searchParams.set('token', token);
return ctx.defaultResolve(signed.toString());
};
nmplayer('main').setup({
urlResolver: cdnResolver,
});
The token fetch uses authFetch, the core’s built-in client, so it carries your auth and retry policy automatically.
Inside a plugin the same client is this.fetch, and you should prefer it over the platform fetch by default. See Auth and Fetch.
You can also swap the resolver after setup, for example when the user switches between a public and authenticated session:
nmplayer('main').urlResolver(cdnResolver);
// Revert to the default built-in pipeline:
nmplayer('main').urlResolver(undefined);
For multi-origin routing, where different asset types come from different hosts:
const routingResolver: IUrlResolver = (url, ctx) => {
if (ctx.category === 'font') {
const fontUrl = new URL(url);
fontUrl.hostname = 'fonts.example.com';
return ctx.defaultResolve(fontUrl.toString());
}
return ctx.defaultResolve(url);
};