mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(playback): route non-HLS containers to the native source path (#1510)
The HTML5 player chose its engine by exclusion: anything that was not mpd/ts/mp4 went to hls.js, so .mkv (the default Xtream series/VOD container), .webm, .avi, .mov and .m4v were fed to hls.js as manifests. hls.js raised a manifest error and the player showed the network/provider diagnostic over media Chromium plays natively. Add resolvePlaybackUrlSourceKind() to @iptvnator/playback/util as the one URL-to-engine rule (mpd -> dash, m3u8/m3u -> hls, ts/m2ts/extension-less -> mpegts, everything else -> native) and read it from both the HTML5 player and ArtPlayer's getArtPlayerVideoType(), so the two engines agree. ArtPlayer serves every native container through a single ART_PLAYER_NATIVE_SOURCE_TYPE custom type, keeping the source session the owner of teardown and controls binding. The HTML5 native <source> carries the video/mp4 hint only for MP4-family files: a hint the browser's canPlayType() rejects makes it skip the source, and Chromium demuxes containers it does not advertise there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a2f6121f8
commit
5e51c1ea5d
15 files changed
+257
-35
No files matched your search
@@ -0,0 +1,9 @@
|
||||
---
|
||||
type: fix
|
||||
area: playback
|
||||
---
|
||||
|
||||
The built-in HTML5 player now plays `.mkv`, `.webm`, `.avi`, `.mov` and other
|
||||
non-HLS video files directly instead of handing them to the HLS engine, which
|
||||
failed with a "network or provider loading error" on many Xtream episodes and
|
||||
movies. ArtPlayer and the HTML5 player now choose their engine by the same rule.
|
||||
@@ -815,7 +815,14 @@ app as a real argument, so it is not an option.
|
||||
|
||||
**Video Players**:
|
||||
|
||||
- Built-in web players: HTML5+hls.js, Video.js, and ArtPlayer
|
||||
- Built-in web players: HTML5+hls.js, Video.js, and ArtPlayer. The HTML5
|
||||
player and ArtPlayer pick their source engine from one URL rule,
|
||||
`resolvePlaybackUrlSourceKind()` in `libs/playback/util` (`mpd` → Shaka,
|
||||
`m3u8`/`m3u` → hls.js, `ts`/`m2ts`/extension-less → mpegts.js, every other
|
||||
container → native `<video>`), so hls.js never receives an `.mkv`/`.webm`
|
||||
file. The HTML5 native `<source>` carries a `video/mp4` hint only for
|
||||
MP4-family files (`resolveNativeSourceMimeType`); a hint `canPlayType()`
|
||||
rejects would make the browser skip the source.
|
||||
- mpegts.js `1.8.1` errors from all three built-in players cross one
|
||||
version-locked structured evidence boundary in `libs/playback/util`. It
|
||||
retains only exact public type/detail pairs, pair-derived stage/failure,
|
||||
|
||||
@@ -635,12 +635,25 @@ video.
|
||||
|
||||
URL extension metadata is filtered before diagnostics and player selection use it. Web script extensions such as `.php` are not shown as stream containers; explicit media query metadata such as `extension=ts` or `format=m3u8` is preferred when present.
|
||||
|
||||
The HTML5 player and ArtPlayer choose their source engine from one URL rule,
|
||||
`resolvePlaybackUrlSourceKind()` in `@iptvnator/playback/util`, which maps the
|
||||
normalized media extension to `dash` (`mpd`), `hls` (`m3u8`/`m3u`), `mpegts`
|
||||
(`ts`, `m2ts`, and extension-less proxy/script URLs) or `native` (every other
|
||||
container: `mkv`, `webm`, `mp4`, `avi`, `mov`, `m4v`, audio files). hls.js
|
||||
therefore only ever receives HLS manifests; it used to be handed every
|
||||
unlisted container and raised a manifest error over media Chromium plays by
|
||||
itself. ArtPlayer serves the `native` kind through a single
|
||||
`ART_PLAYER_NATIVE_SOURCE_TYPE` custom type so `ArtPlayerSourceSession` keeps
|
||||
owning engine teardown and controls binding; the HTML5 player appends one
|
||||
`<source>` whose `video/mp4` MIME hint is set only for MP4-family files, since
|
||||
a hint the browser's `canPlayType()` rejects makes it skip the source
|
||||
(`resolveNativeSourceMimeType()`).
|
||||
|
||||
MKV sources are attempted through Chromium's native Matroska path. Video.js
|
||||
receives `video/matroska` for `.mkv` URLs and explicit query metadata such as
|
||||
`extension=mkv` or `container=mkv`; ArtPlayer and HTML5 continue to use their
|
||||
native video paths. This is container support rather than a universal codec
|
||||
guarantee: native source or decode failures still produce a diagnostic whose
|
||||
ranked actions may include MPV/VLC.
|
||||
`extension=mkv` or `container=mkv`. This is container support rather than a
|
||||
universal codec guarantee: native source or decode failures still produce a
|
||||
diagnostic whose ranked actions may include MPV/VLC.
|
||||
|
||||
Portal VOD and episode payloads with `contentInfo` are treated as non-live by the inline players unless `isLive` is explicitly set. If Chromium leaves the underlying MediaSource duration at `Infinity` for a finite TS VOD, the Video.js wrapper normalizes its UI duration from the finite `seekable` or `buffered` range. Embedded MPV uses the same live decision rule and shows an unknown duration placeholder for VOD/episode snapshots until MPV reports a finite duration. This removes the misleading `LIVE` control state without changing stream decoding, diagnostics, or external fallback behavior.
|
||||
|
||||
|
||||
@@ -1037,8 +1037,9 @@ player in settings.
|
||||
because those players never receive its keys.
|
||||
Channels with `drm.supported === false` emit a `DrmOrEncryption` diagnostic
|
||||
with a fixed safe detail string and without starting an engine.
|
||||
- HTML5 player: `extension === 'mpd'` branch in `playChannel()`. ArtPlayer:
|
||||
`customType.mpd` in `ArtPlayerSourceSession`. Shared-controls bridge:
|
||||
- HTML5 player: the `dash` branch of `playChannel()` (source kind from the
|
||||
shared `resolvePlaybackUrlSourceKind()`). ArtPlayer: `customType.mpd` in
|
||||
`ArtPlayerSourceSession`. Shared-controls bridge:
|
||||
`WebVideoControlsSource` kind `'shaka'` + `WebVideoShakaControls`
|
||||
(audio/text tracks via the Shaka 5 API — selecting a text track shows it,
|
||||
`selectTextTrack(null)` hides).
|
||||
|
||||
@@ -6,4 +6,5 @@ export type * from './lib/diagnostics/shaka-error.types';
|
||||
export * from './lib/playback-recommendation.model';
|
||||
export * from './lib/playback-recommendation-policy';
|
||||
export * from './lib/playback-session-key';
|
||||
export * from './lib/playback-source-routing';
|
||||
export * from './lib/playback-target-capabilities';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PlaybackSourceKind } from './playback-recommendation.model';
|
||||
import { resolvePlaybackUrlSourceKind } from './playback-source-routing';
|
||||
|
||||
describe('resolvePlaybackUrlSourceKind', () => {
|
||||
it.each([
|
||||
['https://example.test/live/playlist.m3u8', PlaybackSourceKind.Hls],
|
||||
[
|
||||
'https://example.test/live/playlist.M3U8?token=signed',
|
||||
PlaybackSourceKind.Hls,
|
||||
],
|
||||
['https://example.test/live/list.m3u', PlaybackSourceKind.Hls],
|
||||
['https://example.test/play?format=hls', PlaybackSourceKind.Hls],
|
||||
['https://example.test/live.mpd', PlaybackSourceKind.Dash],
|
||||
[
|
||||
'https://example.test/stream.MPD?token=signed#frag',
|
||||
PlaybackSourceKind.Dash,
|
||||
],
|
||||
['https://example.test/raw.ts', PlaybackSourceKind.MpegTs],
|
||||
['https://example.test/disc.m2ts', PlaybackSourceKind.MpegTs],
|
||||
['https://example.test/live/user/pass/1', PlaybackSourceKind.MpegTs],
|
||||
['https://example.test/live.php?id=7', PlaybackSourceKind.MpegTs],
|
||||
['https://example.test/play?ext=mpegts', PlaybackSourceKind.MpegTs],
|
||||
])('routes %s to the %s engine', (url, kind) => {
|
||||
expect(resolvePlaybackUrlSourceKind(url)).toBe(kind);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://example.test/movie/user/pass/2000000.mp4',
|
||||
'https://example.test/series/user/pass/80000.mkv',
|
||||
'https://example.test/movie.MKV?token=signed',
|
||||
'https://example.test/play?extension=mkv',
|
||||
'https://example.test/movie.webm',
|
||||
'https://example.test/movie.avi',
|
||||
'https://example.test/movie.mov',
|
||||
'https://example.test/movie.m4v',
|
||||
'https://example.test/track.mp3',
|
||||
'https://example.test/movie.bin',
|
||||
])('hands the non-HLS container %s to the native element', (url) => {
|
||||
expect(resolvePlaybackUrlSourceKind(url)).toBe(
|
||||
PlaybackSourceKind.Native
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getPlaybackMediaExtensionFromUrl } from '@iptvnator/shared/m3u-utils';
|
||||
import { PlaybackSourceKind } from './playback-recommendation.model';
|
||||
|
||||
/**
|
||||
* Engine family a playback URL is routed to BEFORE any engine runs.
|
||||
*
|
||||
* `resolvePlaybackSourceKind()` answers the opposite question — which family
|
||||
* a diagnostic came out of after an engine failed. Every built-in web player
|
||||
* selects its source engine through this function, so the HTML5 player and
|
||||
* ArtPlayer cannot disagree on what a URL is. A URL never yields `unknown`:
|
||||
* an unrecognized container is exactly what the native `<video>` element is
|
||||
* for.
|
||||
*/
|
||||
export type PlaybackUrlSourceKind = Exclude<
|
||||
PlaybackSourceKind,
|
||||
typeof PlaybackSourceKind.Unknown
|
||||
>;
|
||||
|
||||
const DASH_MANIFEST_EXTENSION = 'mpd';
|
||||
const HLS_MANIFEST_EXTENSIONS: ReadonlySet<string> = new Set(['m3u', 'm3u8']);
|
||||
/**
|
||||
* `.m2ts` (BDAV, 192-byte packets) rides with `.ts`: mpegts.js probes the
|
||||
* packet size itself, so its `mpegts` player type covers both.
|
||||
*/
|
||||
const MPEG_TS_EXTENSIONS: ReadonlySet<string> = new Set(['ts', 'm2ts']);
|
||||
|
||||
export function resolvePlaybackUrlSourceKind(
|
||||
url: string
|
||||
): PlaybackUrlSourceKind {
|
||||
const extension = getPlaybackMediaExtensionFromUrl(url);
|
||||
if (extension === DASH_MANIFEST_EXTENSION) {
|
||||
return PlaybackSourceKind.Dash;
|
||||
}
|
||||
if (HLS_MANIFEST_EXTENSIONS.has(extension)) {
|
||||
return PlaybackSourceKind.Hls;
|
||||
}
|
||||
// Extension-less IPTV proxy/script URLs (`/live/user/pass/1`, `.php`)
|
||||
// are predominantly raw MPEG-TS and keep the established engine.
|
||||
if (!extension || MPEG_TS_EXTENSIONS.has(extension)) {
|
||||
return PlaybackSourceKind.MpegTs;
|
||||
}
|
||||
// Every other container — mkv, webm, mp4, avi, mov, m4v, audio files —
|
||||
// is handed to the browser. hls.js only ever receives HLS manifests: fed
|
||||
// an .mkv it raised a manifest error over media Chromium plays natively.
|
||||
return PlaybackSourceKind.Native;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ART_PLAYER_NATIVE_SOURCE_TYPE,
|
||||
buildArtPlayerChrome,
|
||||
exitOwnedArtPlayerFullscreen,
|
||||
getArtPlayerVideoType,
|
||||
@@ -79,11 +80,25 @@ describe('ArtPlayer setup', () => {
|
||||
'https://example.test/play?extension=m3u8&token=signed'
|
||||
)
|
||||
).toBe('m3u8');
|
||||
expect(getArtPlayerVideoType('https://example.test/movie.mkv')).toBe(
|
||||
'video/matroska'
|
||||
expect(getArtPlayerVideoType('https://example.test/disc.m2ts')).toBe(
|
||||
'ts'
|
||||
);
|
||||
expect(getArtPlayerVideoType('https://example.test/list.m3u')).toBe(
|
||||
'm3u8'
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['mkv', 'webm', 'avi', 'mov', 'm4v', 'mp4'])(
|
||||
'hands .%s files to the session-owned native type, never hls.js',
|
||||
(extension) => {
|
||||
expect(
|
||||
getArtPlayerVideoType(
|
||||
`https://example.test/series/user/pass/80000.${extension}`
|
||||
)
|
||||
).toBe(ART_PLAYER_NATIVE_SOURCE_TYPE);
|
||||
}
|
||||
);
|
||||
|
||||
it('routes DASH manifests to the mpd custom type', () => {
|
||||
expect(getArtPlayerVideoType('https://example.test/live.mpd')).toBe(
|
||||
'mpd'
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { getPlaybackMediaExtensionFromUrl } from '@iptvnator/playback/util';
|
||||
import {
|
||||
PlaybackSourceKind,
|
||||
getPlaybackMediaExtensionFromUrl,
|
||||
resolvePlaybackUrlSourceKind,
|
||||
} from '@iptvnator/playback/util';
|
||||
|
||||
/**
|
||||
* ArtPlayer `type` under which every container that is not an HLS/DASH
|
||||
* manifest or raw MPEG-TS (mkv, webm, mp4, avi, mov, m4v, …) reaches
|
||||
* `ArtPlayerSourceSession`'s native path. One key keeps the session the owner
|
||||
* of that source change (engine teardown plus controls binding) instead of
|
||||
* ArtPlayer's bare `video.src` assignment for an unmatched type.
|
||||
*/
|
||||
export const ART_PLAYER_NATIVE_SOURCE_TYPE = 'native';
|
||||
|
||||
/**
|
||||
* ArtPlayer option overrides for the legacy and shared-control surfaces.
|
||||
@@ -67,23 +80,21 @@ export function resolveArtPlayerIsLive(
|
||||
return extension === 'm3u8' || extension === 'ts' || !extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the shared URL routing decision onto the `customType` keys served by
|
||||
* `ArtPlayerSourceSession`, so ArtPlayer and the HTML5 player always pick the
|
||||
* same engine for a URL.
|
||||
*/
|
||||
export function getArtPlayerVideoType(url: string): string {
|
||||
const extension = getPlaybackMediaExtensionFromUrl(url);
|
||||
switch (extension) {
|
||||
case 'mkv':
|
||||
return 'video/matroska';
|
||||
case 'm3u8':
|
||||
return 'm3u8';
|
||||
case 'mp4':
|
||||
return 'mp4';
|
||||
case 'mpd':
|
||||
switch (resolvePlaybackUrlSourceKind(url)) {
|
||||
case PlaybackSourceKind.Dash:
|
||||
return 'mpd';
|
||||
case 'ts':
|
||||
case PlaybackSourceKind.Hls:
|
||||
return 'm3u8';
|
||||
case PlaybackSourceKind.MpegTs:
|
||||
return 'ts';
|
||||
default:
|
||||
// Extensionless IPTV proxy/script URLs are predominantly raw
|
||||
// MPEG-TS and retain the established ArtPlayer fallback.
|
||||
return extension ? 'auto' : 'ts';
|
||||
return ART_PLAYER_NATIVE_SOURCE_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '../web-video-support/web-video-source-controls.bridge';
|
||||
import { WebVideoSourceTracks } from '../web-video-support/web-video-source-tracks';
|
||||
import { addHlsAudioTrackSettings } from './art-player-audio-tracks';
|
||||
import { ART_PLAYER_NATIVE_SOURCE_TYPE } from './art-player-setup';
|
||||
|
||||
export interface ArtPlayerSourceSessionConfig {
|
||||
sharedControls: boolean;
|
||||
@@ -48,7 +49,8 @@ export class ArtPlayerSourceSession {
|
||||
m3u8: (video, url, art) => this.startHls(video, url, art),
|
||||
mpd: (video, url) => this.startDash(video, url),
|
||||
ts: (video, url) => this.startMpegTs(video, url),
|
||||
'video/matroska': (video, url) => this.startNative(video, url),
|
||||
[ART_PLAYER_NATIVE_SOURCE_TYPE]: (video, url) =>
|
||||
this.startNative(video, url),
|
||||
};
|
||||
|
||||
private player: Artplayer | null = null;
|
||||
|
||||
@@ -101,7 +101,7 @@ export function resetArtPlayerSpecFixtures(): void {
|
||||
}
|
||||
|
||||
export function getCustomType(
|
||||
type: 'm3u8' | 'ts' | 'mkv'
|
||||
type: 'm3u8' | 'mpd' | 'ts' | 'native'
|
||||
): (video: HTMLVideoElement, url: string) => void {
|
||||
const customType = (
|
||||
artPlayerInstances[0].options['customType'] as Record<
|
||||
|
||||
@@ -81,7 +81,13 @@ describe('ArtPlayerComponent', () => {
|
||||
},
|
||||
});
|
||||
|
||||
getCustomType('video/matroska')(
|
||||
// The mkv channel must reach the session's native custom type: an
|
||||
// unregistered `type` would fall to ArtPlayer's bare `video.src`.
|
||||
expect(artPlayerInstances[0].options['type']).toBe('native');
|
||||
expect(
|
||||
Object.keys(artPlayerInstances[0].options['customType'] as object)
|
||||
).toContain('native');
|
||||
getCustomType('native')(
|
||||
artPlayerInstances[0].video,
|
||||
'https://example.com/archive/movie.mkv'
|
||||
);
|
||||
|
||||
+43
@@ -7,8 +7,14 @@ import { PictureInPictureTestEnvironment } from '../player-controls/picture-in-p
|
||||
import { SeriesPlaybackNavigationControlsComponent } from '../portal-inline-player/series-playback-navigation-controls.component';
|
||||
import type { HtmlVideoPlayerComponent as HtmlVideoPlayerComponentInstance } from './html-video-player.component';
|
||||
import {
|
||||
TEST_CHANNEL,
|
||||
cleanupSharedControlsTests,
|
||||
configureSharedControlsTests,
|
||||
hlsInstances,
|
||||
mpegTsInstances,
|
||||
mpegTsIsSupported,
|
||||
observeBridgeSourceBinding,
|
||||
readHtmlPlayerInternals,
|
||||
renderSharedControls,
|
||||
renderSharedControlsDefaults,
|
||||
type SharedControlsFixture,
|
||||
@@ -284,6 +290,43 @@ describe('HtmlVideoPlayerComponent shared controls host', () => {
|
||||
|
||||
expect(tracks[0].mode).toBe('showing');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['mkv', null],
|
||||
['webm', null],
|
||||
['avi', null],
|
||||
['mov', null],
|
||||
['m4v', 'video/mp4'],
|
||||
])(
|
||||
'plays a .%s file through the native element instead of hls.js',
|
||||
(extension, mimeHint) => {
|
||||
// Both MSE engines report support, so a native pick is routing,
|
||||
// not a fallback.
|
||||
mpegTsIsSupported.mockReturnValue(true);
|
||||
const { component, fixture } = renderSharedControls(
|
||||
HtmlVideoPlayerComponent,
|
||||
fixtures,
|
||||
{ isLive: false }
|
||||
);
|
||||
const setSource = observeBridgeSourceBinding(component);
|
||||
const video = fixture.debugElement.query(By.css('video'))
|
||||
.nativeElement as HTMLVideoElement;
|
||||
const url = `https://example.test/series/user/pass/80000.${extension}`;
|
||||
|
||||
component.playChannel({ ...TEST_CHANNEL, url });
|
||||
|
||||
const internals = readHtmlPlayerInternals(component);
|
||||
expect(hlsInstances).toHaveLength(0);
|
||||
expect(mpegTsInstances).toHaveLength(0);
|
||||
expect(internals.hls).toBeNull();
|
||||
expect(internals.mpegtsPlayer).toBeNull();
|
||||
expect(setSource).toHaveBeenCalledWith({ kind: 'native' });
|
||||
expect(internals.controlsSource).toEqual({ kind: 'native' });
|
||||
const sources = Array.from(video.querySelectorAll('source'));
|
||||
expect(sources.map((source) => source.src)).toEqual([url]);
|
||||
expect(sources[0]?.getAttribute('type')).toBe(mimeHint);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function restoreDocumentProperty(
|
||||
|
||||
@@ -20,7 +20,8 @@ import { Channel, createDevLogger } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
InlinePlaybackPlayer,
|
||||
PlaybackDiagnostic,
|
||||
getPlaybackMediaExtensionFromUrl,
|
||||
PlaybackSourceKind,
|
||||
resolvePlaybackUrlSourceKind,
|
||||
} from '@iptvnator/playback/util';
|
||||
import {
|
||||
type LegacyPlayerShortcuts,
|
||||
@@ -35,6 +36,7 @@ import { ShakaVideoSession } from '../shaka-engine/shaka-video-session';
|
||||
import { exitOwnedFullscreen } from '../web-video-support/exit-owned-fullscreen.util';
|
||||
import {
|
||||
clearNativeVideoSources,
|
||||
resolveNativeSourceMimeType,
|
||||
setNativeVideoSource,
|
||||
} from '../web-video-support/web-video-native-source.util';
|
||||
import { WebVideoSourceTracks } from '../web-video-support/web-video-source-tracks';
|
||||
@@ -197,7 +199,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (channel.url) {
|
||||
this.playbackIssue.emit(null);
|
||||
const url = channel.url + (channel.epgParams ?? '');
|
||||
const extension = getPlaybackMediaExtensionFromUrl(channel.url);
|
||||
const sourceKind = resolvePlaybackUrlSourceKind(channel.url);
|
||||
|
||||
// The scoped Electron header override is owned by
|
||||
// WebPlayerViewComponent, which configures the full header set
|
||||
@@ -205,7 +207,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// receives the channel. Re-issuing the three-header call here
|
||||
// would overwrite that richer override.
|
||||
|
||||
if (extension === 'mpd') {
|
||||
if (sourceKind === PlaybackSourceKind.Dash) {
|
||||
debugHtmlPlayer(
|
||||
'Using Shaka Player for DASH stream:',
|
||||
channel.name,
|
||||
@@ -227,7 +229,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.handlePlayOperation();
|
||||
}
|
||||
} else if (
|
||||
(extension === 'ts' || !extension) &&
|
||||
sourceKind === PlaybackSourceKind.MpegTs &&
|
||||
mpegts.isSupported()
|
||||
) {
|
||||
debugHtmlPlayer(
|
||||
@@ -257,11 +259,14 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.mpegtsPlayer.load();
|
||||
this.handlePlayOperation();
|
||||
} else if (
|
||||
extension !== 'mp4' &&
|
||||
extension !== 'mpv' &&
|
||||
sourceKind !== PlaybackSourceKind.Native &&
|
||||
Hls &&
|
||||
Hls.isSupported()
|
||||
) {
|
||||
// HLS manifests, plus raw MPEG-TS when mpegts.js is
|
||||
// unavailable (the historical engine order). Native
|
||||
// containers never reach hls.js: fed an .mkv it raised a
|
||||
// manifest error over media the browser plays by itself.
|
||||
debugHtmlPlayer('Starting HLS playback');
|
||||
const hls = new Hls();
|
||||
this.hls = hls;
|
||||
@@ -280,7 +285,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
setNativeVideoSource(
|
||||
this.videoPlayer.nativeElement,
|
||||
url,
|
||||
'video/mp4'
|
||||
resolveNativeSourceMimeType(channel.url)
|
||||
);
|
||||
this.bindControlsSource({ kind: 'native' });
|
||||
this.videoPlayer.nativeElement.load();
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import { getPlaybackMediaExtensionFromUrl } from '@iptvnator/playback/util';
|
||||
|
||||
/** Helpers for driving the native `<video>` element source list. */
|
||||
|
||||
const MP4_FAMILY_EXTENSIONS: ReadonlySet<string> = new Set(['mp4', 'm4v']);
|
||||
|
||||
export function clearNativeVideoSources(element: HTMLVideoElement): void {
|
||||
element.removeAttribute('src');
|
||||
element.replaceChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* MIME hint for a native `<source>`, given only to MP4-family files.
|
||||
*
|
||||
* The hint is a filter, not a description: the browser skips a `<source>`
|
||||
* whose type its `canPlayType()` rejects, and a container it demuxes (mkv,
|
||||
* webm, mov, avi) is not necessarily one it advertises there. Those sources
|
||||
* stay unhinted so the browser sniffs the bytes instead.
|
||||
*/
|
||||
export function resolveNativeSourceMimeType(url: string): string | undefined {
|
||||
return MP4_FAMILY_EXTENSIONS.has(getPlaybackMediaExtensionFromUrl(url))
|
||||
? 'video/mp4'
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the element's sources with a single `<source>` without loading it;
|
||||
* callers bind their controls source first, then call `element.load()`.
|
||||
@@ -12,11 +30,13 @@ export function clearNativeVideoSources(element: HTMLVideoElement): void {
|
||||
export function setNativeVideoSource(
|
||||
element: HTMLVideoElement,
|
||||
url: string,
|
||||
type: string
|
||||
type?: string
|
||||
): void {
|
||||
clearNativeVideoSources(element);
|
||||
const source = document.createElement('source');
|
||||
source.src = url;
|
||||
source.type = type;
|
||||
if (type) {
|
||||
source.type = type;
|
||||
}
|
||||
element.appendChild(source);
|
||||
}
|
||||
Reference in new issue
Block a user