mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(m3u): unify DASH URL detection with playback extension normalization
isDashStreamUrl() used the simpler getStreamExtensionFromUrl(), so URLs the player engines classify as DASH (stream.MPD, ?ext=mpd, ?format=mpd) were not routed to the Shaka-capable inline player and lost their ClearKey metadata with Video.js or external players configured (Codex P2). The normalized getPlaybackMediaExtensionFromUrl() now lives in @iptvnator/shared/m3u-utils (re-exported unchanged from the playback lib) and both routing and engine selection share it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48d191b0d7
commit
95bd7f7fe0
5 files changed
+145
-122
No files matched your search
@@ -1,5 +1,6 @@
|
||||
export * from './lib/dash.utils';
|
||||
export * from './lib/kodiprop.utils';
|
||||
export * from './lib/playback-media-extension.util';
|
||||
export * from './lib/playlist.utils';
|
||||
export * from './lib/catchup.utils';
|
||||
export * from './lib/strip-country-prefix.util';
|
||||
@@ -3,8 +3,11 @@ import { isDashChannel, isDashStreamUrl } from './dash.utils';
|
||||
describe('dash.utils', () => {
|
||||
it.each([
|
||||
'https://example.com/live/stream.mpd',
|
||||
'https://example.com/live/stream.MPD',
|
||||
'https://example.com/live/stream.mpd?token=abc#frag',
|
||||
'https://example.com/play?extension=mpd',
|
||||
'https://example.com/play?ext=mpd',
|
||||
'https://example.com/play?format=mpd',
|
||||
])('detects %s as DASH', (url) => {
|
||||
expect(isDashStreamUrl(url)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Channel } from '@iptvnator/shared/interfaces';
|
||||
import { getStreamExtensionFromUrl } from './playlist.utils';
|
||||
import { getPlaybackMediaExtensionFromUrl } from './playback-media-extension.util';
|
||||
|
||||
/**
|
||||
* DASH (`.mpd`) detection for M3U channels. DASH streams must always play in
|
||||
* a Shaka-capable built-in web engine: external MPV/VLC cannot receive the
|
||||
* KODIPROP ClearKey configuration, and Video.js has no DASH bridge yet.
|
||||
*
|
||||
* Uses the same normalized extension detection as the player engines, so
|
||||
* routing and engine selection always agree (`stream.MPD`, `?format=mpd`,
|
||||
* `?ext=mpd` and friends included).
|
||||
*/
|
||||
export const isDashStreamUrl = (url: string | undefined): boolean =>
|
||||
!!url && getStreamExtensionFromUrl(url) === 'mpd';
|
||||
!!url && getPlaybackMediaExtensionFromUrl(url) === 'mpd';
|
||||
|
||||
export const isDashChannel = (
|
||||
channel: Pick<Channel, 'url'> | null | undefined
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { getExtensionFromUrl } from './playlist.utils';
|
||||
|
||||
/**
|
||||
* Normalized media-extension detection for playback URLs. Shared between the
|
||||
* player engines (engine selection in `@iptvnator/ui/playback`) and M3U
|
||||
* routing (`isDashChannel`), so both always agree on what a URL is.
|
||||
*/
|
||||
|
||||
export const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([
|
||||
'avi',
|
||||
'asf',
|
||||
'divx',
|
||||
'flv',
|
||||
'm2ts',
|
||||
'm4v',
|
||||
'mkv',
|
||||
'mov',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'rm',
|
||||
'rmvb',
|
||||
'ts',
|
||||
'vob',
|
||||
'wmv',
|
||||
]);
|
||||
|
||||
const EXPLICIT_MEDIA_EXTENSION_QUERY_KEYS = ['extension', 'ext'];
|
||||
|
||||
const DECLARED_MEDIA_FORMAT_QUERY_KEYS = ['format', 'container'];
|
||||
|
||||
const DECLARED_MEDIA_EXTENSION_ALIASES = new Map([
|
||||
['hls', 'm3u8'],
|
||||
['mpegts', 'ts'],
|
||||
['mpeg-ts', 'ts'],
|
||||
]);
|
||||
|
||||
const DECLARED_MEDIA_EXTENSIONS = new Set([
|
||||
...UNSUPPORTED_CONTAINER_EXTENSIONS,
|
||||
'aac',
|
||||
'flac',
|
||||
'm3u',
|
||||
'm3u8',
|
||||
'm4s',
|
||||
'mp3',
|
||||
'mp4',
|
||||
'mpd',
|
||||
'oga',
|
||||
'ogg',
|
||||
'ogv',
|
||||
'webm',
|
||||
]);
|
||||
|
||||
const NON_MEDIA_URL_EXTENSIONS = new Set([
|
||||
'asp',
|
||||
'aspx',
|
||||
'cgi',
|
||||
'jsp',
|
||||
'mpv',
|
||||
'php',
|
||||
'pl',
|
||||
]);
|
||||
|
||||
export function getPlaybackMediaExtensionFromUrl(url: string): string {
|
||||
const explicitQueryExtension = getMediaExtensionFromQuery(
|
||||
url,
|
||||
EXPLICIT_MEDIA_EXTENSION_QUERY_KEYS
|
||||
);
|
||||
if (explicitQueryExtension) {
|
||||
return explicitQueryExtension;
|
||||
}
|
||||
|
||||
const pathExtension = normalizeExtensionToken(getExtensionFromUrl(url));
|
||||
if (DECLARED_MEDIA_EXTENSIONS.has(pathExtension)) {
|
||||
return pathExtension;
|
||||
}
|
||||
|
||||
const formatQueryExtension = getMediaExtensionFromQuery(
|
||||
url,
|
||||
DECLARED_MEDIA_FORMAT_QUERY_KEYS
|
||||
);
|
||||
if (formatQueryExtension) {
|
||||
return formatQueryExtension;
|
||||
}
|
||||
|
||||
if (NON_MEDIA_URL_EXTENSIONS.has(pathExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return pathExtension;
|
||||
}
|
||||
|
||||
function normalizeExtensionToken(value: string | undefined): string {
|
||||
return (value?.trim().toLowerCase() ?? '').replace(/^\.+/, '');
|
||||
}
|
||||
|
||||
function getMediaExtensionFromQuery(
|
||||
url: string,
|
||||
queryKeys: readonly string[]
|
||||
): string {
|
||||
try {
|
||||
const parsedUrl = new URL(url, 'http://iptvnator.local');
|
||||
for (const key of queryKeys) {
|
||||
const declaredExtension = normalizeDeclaredMediaExtension(
|
||||
parsedUrl.searchParams.get(key) ?? undefined
|
||||
);
|
||||
if (declaredExtension) {
|
||||
return declaredExtension;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDeclaredMediaExtension(value: string | undefined): string {
|
||||
const extension = normalizeExtensionToken(value);
|
||||
const normalized =
|
||||
DECLARED_MEDIA_EXTENSION_ALIASES.get(extension) ?? extension;
|
||||
|
||||
if (!normalized || NON_MEDIA_URL_EXTENSIONS.has(normalized)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return DECLARED_MEDIA_EXTENSIONS.has(normalized) ? normalized : '';
|
||||
}
|
||||
Reference in new issue
Block a user