Route Chromium HLS through hls.js
This commit is contained in:
@@ -12,7 +12,8 @@ import {
|
||||
createWebPlaybackErrorReporter,
|
||||
describeHlsError,
|
||||
describeMediaError,
|
||||
isHlsSource,
|
||||
isAppleWebKit,
|
||||
selectWebHlsPlaybackMode,
|
||||
type WebHlsPlaybackMode,
|
||||
} from './webHls';
|
||||
|
||||
@@ -270,6 +271,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
}>();
|
||||
|
||||
const uri = source?.uri as string | undefined;
|
||||
const sourceType = source?.type;
|
||||
|
||||
useEffect(() => {
|
||||
const video = nativeRef.current;
|
||||
@@ -279,10 +281,11 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
|
||||
playbackErrorReporterRef.current?.reset();
|
||||
const nativeHlsSupport = video.canPlayType(HLS_MIME);
|
||||
const mode: WebHlsPlaybackMode =
|
||||
isHlsSource(uri) && !nativeHlsSupport && Hls.isSupported()
|
||||
? 'hls.js'
|
||||
: 'native';
|
||||
const mode = selectWebHlsPlaybackMode(uri, sourceType, {
|
||||
hlsJsSupported: Hls.isSupported(),
|
||||
nativeHlsSupport,
|
||||
preferNativeHls: isAppleWebKit(navigator.userAgent, navigator.vendor),
|
||||
});
|
||||
playbackDiagnosticsRef.current = {mode, nativeHlsSupport};
|
||||
|
||||
if (mode === 'native') {
|
||||
@@ -316,7 +319,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
playbackErrorReporterRef.current?.reset();
|
||||
hls.destroy();
|
||||
};
|
||||
}, [uri]);
|
||||
}, [sourceType, uri]);
|
||||
|
||||
useMediaSession(source?.metadata, nativeRef, showNotificationControls);
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
export type WebHlsPlaybackMode = 'hls.js' | 'native';
|
||||
|
||||
export interface WebHlsPlaybackCapabilities {
|
||||
hlsJsSupported: boolean;
|
||||
nativeHlsSupport: string;
|
||||
preferNativeHls: boolean;
|
||||
}
|
||||
|
||||
export interface HlsErrorDetails {
|
||||
type: string;
|
||||
details: string;
|
||||
@@ -30,6 +36,12 @@ export interface WebVideoError {
|
||||
}
|
||||
|
||||
const HLS_SOURCE_PATTERN = /\.m3u8(\?|#|$)/i;
|
||||
const HLS_SOURCE_TYPES = new Set([
|
||||
'application/vnd.apple.mpegurl',
|
||||
'application/x-mpegurl',
|
||||
'hls',
|
||||
'm3u8',
|
||||
]);
|
||||
|
||||
const MEDIA_ERROR_NAMES: Record<number, string> = {
|
||||
1: 'MEDIA_ERR_ABORTED',
|
||||
@@ -38,8 +50,36 @@ const MEDIA_ERROR_NAMES: Record<number, string> = {
|
||||
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED',
|
||||
};
|
||||
|
||||
export const isHlsSource = (uri: string): boolean =>
|
||||
HLS_SOURCE_PATTERN.test(uri);
|
||||
export const isHlsSource = (uri: string, sourceType?: string): boolean => {
|
||||
const normalizedType = sourceType?.split(';', 1)[0]?.trim().toLowerCase();
|
||||
return (
|
||||
HLS_SOURCE_PATTERN.test(uri) ||
|
||||
(normalizedType !== undefined && HLS_SOURCE_TYPES.has(normalizedType))
|
||||
);
|
||||
};
|
||||
|
||||
export const isAppleWebKit = (userAgent: string, vendor: string): boolean =>
|
||||
/AppleWebKit/i.test(userAgent) && /Apple/i.test(vendor);
|
||||
|
||||
export const selectWebHlsPlaybackMode = (
|
||||
uri: string,
|
||||
sourceType: string | undefined,
|
||||
{
|
||||
hlsJsSupported,
|
||||
nativeHlsSupport,
|
||||
preferNativeHls,
|
||||
}: WebHlsPlaybackCapabilities,
|
||||
): WebHlsPlaybackMode => {
|
||||
if (!isHlsSource(uri, sourceType)) {
|
||||
return 'native';
|
||||
}
|
||||
|
||||
if (preferNativeHls && nativeHlsSupport) {
|
||||
return 'native';
|
||||
}
|
||||
|
||||
return hlsJsSupported ? 'hls.js' : 'native';
|
||||
};
|
||||
|
||||
const compactText = (value: string): string =>
|
||||
value.replace(/\s+/g, ' ').trim();
|
||||
|
||||
@@ -6,13 +6,93 @@ const {
|
||||
createWebPlaybackErrorReporter,
|
||||
describeHlsError,
|
||||
describeMediaError,
|
||||
isAppleWebKit,
|
||||
isHlsSource,
|
||||
sanitizeDiagnosticUrl,
|
||||
selectWebHlsPlaybackMode,
|
||||
} = require('./lib/webHls');
|
||||
|
||||
test('recognizes HLS playlist URLs with query strings', () => {
|
||||
assert.equal(isHlsSource('https://api.example/playlist/42.m3u8?vod=1'), true);
|
||||
assert.equal(isHlsSource('https://cdn.example/video.mp4'), false);
|
||||
assert.equal(
|
||||
isHlsSource('https://api.example/playlist/42', 'application/x-mpegURL'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('uses hls.js in Chromium even when native HLS support reports maybe', () => {
|
||||
const userAgent =
|
||||
'Mozilla/5.0 Chrome/146.0.0.0 Safari/537.36 AppleWebKit/537.36';
|
||||
assert.equal(isAppleWebKit(userAgent, 'Google Inc.'), false);
|
||||
assert.equal(
|
||||
selectWebHlsPlaybackMode(
|
||||
'https://api.example/playlist/42.m3u8',
|
||||
undefined,
|
||||
{
|
||||
hlsJsSupported: true,
|
||||
nativeHlsSupport: 'maybe',
|
||||
preferNativeHls: isAppleWebKit(userAgent, 'Google Inc.'),
|
||||
},
|
||||
),
|
||||
'hls.js',
|
||||
);
|
||||
});
|
||||
|
||||
test('uses hls.js for an extensionless HLS source with an explicit type', () => {
|
||||
assert.equal(
|
||||
selectWebHlsPlaybackMode(
|
||||
'https://api.example/playlist/42',
|
||||
'application/vnd.apple.mpegurl; charset=utf-8',
|
||||
{
|
||||
hlsJsSupported: true,
|
||||
nativeHlsSupport: '',
|
||||
preferNativeHls: false,
|
||||
},
|
||||
),
|
||||
'hls.js',
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves native HLS in Apple WebKit browsers', () => {
|
||||
const userAgent =
|
||||
'Mozilla/5.0 Version/18.6 Safari/605.1.15 AppleWebKit/605.1.15';
|
||||
assert.equal(isAppleWebKit(userAgent, 'Apple Computer, Inc.'), true);
|
||||
assert.equal(
|
||||
selectWebHlsPlaybackMode(
|
||||
'https://api.example/playlist/42.m3u8',
|
||||
undefined,
|
||||
{
|
||||
hlsJsSupported: true,
|
||||
nativeHlsSupport: 'maybe',
|
||||
preferNativeHls: isAppleWebKit(userAgent, 'Apple Computer, Inc.'),
|
||||
},
|
||||
),
|
||||
'native',
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to native playback when hls.js is unavailable', () => {
|
||||
assert.equal(
|
||||
selectWebHlsPlaybackMode(
|
||||
'https://api.example/playlist/42.m3u8',
|
||||
undefined,
|
||||
{
|
||||
hlsJsSupported: false,
|
||||
nativeHlsSupport: 'probably',
|
||||
preferNativeHls: false,
|
||||
},
|
||||
),
|
||||
'native',
|
||||
);
|
||||
assert.equal(
|
||||
selectWebHlsPlaybackMode('https://cdn.example/video.mp4', undefined, {
|
||||
hlsJsSupported: true,
|
||||
nativeHlsSupport: '',
|
||||
preferNativeHls: false,
|
||||
}),
|
||||
'native',
|
||||
);
|
||||
});
|
||||
|
||||
test('includes fatal HLS network and response details', () => {
|
||||
|
||||
Reference in New Issue
Block a user