Recipe: Provide a Custom URL Resolver
authFetch (previous recipe) covers URLs your own code fetches. Not every URL goes through fetch: a <video>.src, a Cast receiver, a Worker, and CSS url() all need a final string handed to them directly, with no chance to attach an Authorization header. IUrlResolver is the extension point for that case.
The task
A CDN needs a signed token query parameter on media URLs specifically, poster images and subtitles are served unsigned from the same host. setup({ urlResolver }) replaces the resolver wholesale; ctx.category tells the custom function which kind of URL it’s looking at, and ctx.defaultResolve is the built-in behavior (auth.transformUrl + structured parse) for the categories the custom logic doesn’t touch.
/**
* Recipe: provide a custom URL resolver.
*
* The default resolver applies `auth.transformUrl` and parses the result with
* the kit's structured URL parser — fine for a single CDN with one signing
* scheme. A resolver that needs to branch per asset category (sign media
* URLs, leave poster images alone, route subtitles to a different host)
* replaces the whole function via `setup({ urlResolver })`, and can still
* delegate to the built-in behavior through `ctx.defaultResolve` for the
* categories it doesn't care about.
*/
import type { IUrlResolver } from '@nomercy-entertainment/nomercy-player-core';
import { tourPlayer } from './tour-player';
const CDN_TOKEN = 'sig-abc123';
const cdnResolver: IUrlResolver = async (url, ctx) => {
if (ctx.category !== 'media') {
return ctx.defaultResolve(url); // poster, subtitle, sprite, ... — untouched
}
const resolved = await ctx.defaultResolve(url);
const signed = new URL(resolved.href);
signed.searchParams.set('token', CDN_TOKEN);
return ctx.defaultResolve(signed.toString());
};
const player = tourPlayer('url-resolver-demo');
player.setup({
logLevel: 'info',
baseUrl: 'https://cdn.example.com',
urlResolver: cdnResolver,
});
await player.ready();
const media = await player.resolveUrl('/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8', 'media');
console.log(media.searchParams.get('token')); // 'sig-abc123' — signed by the custom resolver
const poster = await player.resolveUrl('/w780/poster.jpg', 'poster');
console.log(poster.searchParams.get('token')); // null — passed straight through to defaultResolve
// Swap it again at runtime the same way — urlResolver() also reads the active one back.
player.urlResolver(undefined); // revert to the built-in default
console.log(player.urlResolver()); // undefined — no custom resolver active
await player.dispose();
Reading the result
player.resolveUrl(url, category) is what a plugin or your own UI code calls to get a ResolvedUrl: href for the final string, searchParams for query inspection, ext for gating on file type (prefer it over splitting on ., it survives query strings and fragments). relative is true when the input was relative and no baseUrl was configured to absolutize against, useful for catching a missing baseUrl in a test rather than shipping a broken relative URL to production.
Swapping again later
urlResolver() reads the active custom resolver back (undefined when the built-in pipeline is active); urlResolver(fn) replaces it, and urlResolver(undefined) reverts to the default. This is the runtime escape hatch for CDN credentials that rotate without a full setup() re-run.
Next steps
- Plugins & Adapters: purpose, options, and restrictions for every shared plugin and adapter port the kit ships.