Route Chromium HLS through hls.js
This commit is contained in:
@@ -12,7 +12,8 @@ import {
|
|||||||
createWebPlaybackErrorReporter,
|
createWebPlaybackErrorReporter,
|
||||||
describeHlsError,
|
describeHlsError,
|
||||||
describeMediaError,
|
describeMediaError,
|
||||||
isHlsSource,
|
isAppleWebKit,
|
||||||
|
selectWebHlsPlaybackMode,
|
||||||
type WebHlsPlaybackMode,
|
type WebHlsPlaybackMode,
|
||||||
} from './webHls';
|
} from './webHls';
|
||||||
|
|
||||||
@@ -270,6 +271,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const uri = source?.uri as string | undefined;
|
const uri = source?.uri as string | undefined;
|
||||||
|
const sourceType = source?.type;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = nativeRef.current;
|
const video = nativeRef.current;
|
||||||
@@ -279,10 +281,11 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
|
|
||||||
playbackErrorReporterRef.current?.reset();
|
playbackErrorReporterRef.current?.reset();
|
||||||
const nativeHlsSupport = video.canPlayType(HLS_MIME);
|
const nativeHlsSupport = video.canPlayType(HLS_MIME);
|
||||||
const mode: WebHlsPlaybackMode =
|
const mode = selectWebHlsPlaybackMode(uri, sourceType, {
|
||||||
isHlsSource(uri) && !nativeHlsSupport && Hls.isSupported()
|
hlsJsSupported: Hls.isSupported(),
|
||||||
? 'hls.js'
|
nativeHlsSupport,
|
||||||
: 'native';
|
preferNativeHls: isAppleWebKit(navigator.userAgent, navigator.vendor),
|
||||||
|
});
|
||||||
playbackDiagnosticsRef.current = {mode, nativeHlsSupport};
|
playbackDiagnosticsRef.current = {mode, nativeHlsSupport};
|
||||||
|
|
||||||
if (mode === 'native') {
|
if (mode === 'native') {
|
||||||
@@ -316,7 +319,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
playbackErrorReporterRef.current?.reset();
|
playbackErrorReporterRef.current?.reset();
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
};
|
};
|
||||||
}, [uri]);
|
}, [sourceType, uri]);
|
||||||
|
|
||||||
useMediaSession(source?.metadata, nativeRef, showNotificationControls);
|
useMediaSession(source?.metadata, nativeRef, showNotificationControls);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export type WebHlsPlaybackMode = 'hls.js' | 'native';
|
export type WebHlsPlaybackMode = 'hls.js' | 'native';
|
||||||
|
|
||||||
|
export interface WebHlsPlaybackCapabilities {
|
||||||
|
hlsJsSupported: boolean;
|
||||||
|
nativeHlsSupport: string;
|
||||||
|
preferNativeHls: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HlsErrorDetails {
|
export interface HlsErrorDetails {
|
||||||
type: string;
|
type: string;
|
||||||
details: string;
|
details: string;
|
||||||
@@ -30,6 +36,12 @@ export interface WebVideoError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HLS_SOURCE_PATTERN = /\.m3u8(\?|#|$)/i;
|
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> = {
|
const MEDIA_ERROR_NAMES: Record<number, string> = {
|
||||||
1: 'MEDIA_ERR_ABORTED',
|
1: 'MEDIA_ERR_ABORTED',
|
||||||
@@ -38,8 +50,36 @@ const MEDIA_ERROR_NAMES: Record<number, string> = {
|
|||||||
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED',
|
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isHlsSource = (uri: string): boolean =>
|
export const isHlsSource = (uri: string, sourceType?: string): boolean => {
|
||||||
HLS_SOURCE_PATTERN.test(uri);
|
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 =>
|
const compactText = (value: string): string =>
|
||||||
value.replace(/\s+/g, ' ').trim();
|
value.replace(/\s+/g, ' ').trim();
|
||||||
|
|||||||
@@ -6,13 +6,93 @@ const {
|
|||||||
createWebPlaybackErrorReporter,
|
createWebPlaybackErrorReporter,
|
||||||
describeHlsError,
|
describeHlsError,
|
||||||
describeMediaError,
|
describeMediaError,
|
||||||
|
isAppleWebKit,
|
||||||
isHlsSource,
|
isHlsSource,
|
||||||
sanitizeDiagnosticUrl,
|
sanitizeDiagnosticUrl,
|
||||||
|
selectWebHlsPlaybackMode,
|
||||||
} = require('./lib/webHls');
|
} = require('./lib/webHls');
|
||||||
|
|
||||||
test('recognizes HLS playlist URLs with query strings', () => {
|
test('recognizes HLS playlist URLs with query strings', () => {
|
||||||
assert.equal(isHlsSource('https://api.example/playlist/42.m3u8?vod=1'), true);
|
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://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', () => {
|
test('includes fatal HLS network and response details', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user