mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -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 : '';
|
||||
}
|
||||
@@ -1,27 +1,17 @@
|
||||
import { getExtensionFromUrl } from '@iptvnator/shared/m3u-utils';
|
||||
import {
|
||||
UNSUPPORTED_CONTAINER_EXTENSIONS,
|
||||
getPlaybackMediaExtensionFromUrl,
|
||||
} from '@iptvnator/shared/m3u-utils';
|
||||
|
||||
import type {
|
||||
PlaybackSourceMetadata,
|
||||
PlaybackSourceMetadataInput,
|
||||
} from './playback-diagnostics.model';
|
||||
|
||||
const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([
|
||||
'avi',
|
||||
'asf',
|
||||
'divx',
|
||||
'flv',
|
||||
'm2ts',
|
||||
'm4v',
|
||||
'mkv',
|
||||
'mov',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'rm',
|
||||
'rmvb',
|
||||
'ts',
|
||||
'vob',
|
||||
'wmv',
|
||||
]);
|
||||
// Media-extension normalization lives in @iptvnator/shared/m3u-utils so M3U
|
||||
// routing (isDashChannel) and engine selection always agree; re-exported here
|
||||
// to keep the playback-lib import surface stable.
|
||||
export { getPlaybackMediaExtensionFromUrl };
|
||||
|
||||
const UNSUPPORTED_CONTAINER_NAMES = new Set([
|
||||
...UNSUPPORTED_CONTAINER_EXTENSIONS,
|
||||
@@ -31,42 +21,6 @@ const UNSUPPORTED_CONTAINER_NAMES = new Set([
|
||||
'x-msvideo',
|
||||
]);
|
||||
|
||||
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',
|
||||
]);
|
||||
|
||||
const BROWSER_LIMITED_CODEC_PATTERNS: ReadonlyArray<{
|
||||
readonly label: string;
|
||||
readonly pattern: RegExp;
|
||||
@@ -95,35 +49,6 @@ export function createPlaybackSourceMetadata(
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function getLikelyBrowserUnsupportedCodecLabels(
|
||||
metadata: Pick<PlaybackSourceMetadata, 'audioCodecs' | 'videoCodecs'>
|
||||
): string[] {
|
||||
@@ -188,40 +113,3 @@ function normalizeCodecs(codecs: readonly string[] | undefined): string[] {
|
||||
function normalizeToken(value: string | undefined): string {
|
||||
return value?.trim().toLowerCase() ?? '';
|
||||
}
|
||||
|
||||
function normalizeExtensionToken(value: string | undefined): string {
|
||||
return normalizeToken(value).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