mirror of
https://github.com/fireinrain/JHS-enhance.git
synced 2026-09-29 07:56:38 +08:00
266 lines
9.5 KiB
JavaScript
266 lines
9.5 KiB
JavaScript
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
|
|
|
|
const HLS_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js';
|
|
|
|
let hlsLoadPromise = null;
|
|
|
|
const getHlsClass = () => {
|
|
const HlsClass = window.Hls || globalThis.Hls || (typeof Hls !== 'undefined' ? Hls : null);
|
|
return HlsClass && HlsClass.isSupported && HlsClass.isSupported() ? HlsClass : null;
|
|
};
|
|
|
|
const binaryTextToArrayBuffer = (value) => {
|
|
const text = String(value || '');
|
|
const bytes = new Uint8Array(text.length);
|
|
for (let index = 0; index < text.length; index += 1) {
|
|
bytes[index] = text.charCodeAt(index) & 0xff;
|
|
}
|
|
return bytes.buffer;
|
|
};
|
|
|
|
const loadHlsLibrary = () => {
|
|
const readyHls = getHlsClass();
|
|
if (readyHls) return Promise.resolve(readyHls);
|
|
if (hlsLoadPromise) return hlsLoadPromise;
|
|
|
|
const loadByGm = () => new Promise((resolve) => {
|
|
GM_xmlhttpRequest({
|
|
method: 'GET',
|
|
url: HLS_SCRIPT_URL,
|
|
timeout: 15000,
|
|
onload: (r) => {
|
|
if (r.status >= 200 && r.status < 300 && r.responseText) {
|
|
try {
|
|
Function(`${r.responseText}\n//# sourceURL=${HLS_SCRIPT_URL}`).call(globalThis);
|
|
} catch (err) {
|
|
clog.error('HLS: hls.js 执行失败', err);
|
|
}
|
|
}
|
|
resolve(getHlsClass());
|
|
},
|
|
onerror: () => resolve(getHlsClass()),
|
|
ontimeout: () => resolve(getHlsClass()),
|
|
});
|
|
});
|
|
|
|
hlsLoadPromise = new Promise((resolve) => {
|
|
const existing = document.querySelector('script[data-jhs-hls="1"]');
|
|
if (existing) {
|
|
existing.addEventListener('load', () => resolve(getHlsClass()), {once: true});
|
|
existing.addEventListener('error', () => loadByGm().then(resolve), {once: true});
|
|
setTimeout(() => {
|
|
if (!getHlsClass()) loadByGm().then(resolve);
|
|
}, 4000);
|
|
return;
|
|
}
|
|
const hlsScript = document.createElement('script');
|
|
hlsScript.src = HLS_SCRIPT_URL;
|
|
hlsScript.async = true;
|
|
hlsScript.dataset.jhsHls = '1';
|
|
hlsScript.onload = () => resolve(getHlsClass());
|
|
hlsScript.onerror = () => loadByGm().then(resolve);
|
|
document.head.appendChild(hlsScript);
|
|
}).then((HlsClass) => {
|
|
if (!HlsClass) hlsLoadPromise = null;
|
|
return HlsClass;
|
|
});
|
|
return hlsLoadPromise;
|
|
};
|
|
|
|
const createHlsLoader = () => {
|
|
class GMHlsLoader {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.context = null;
|
|
this.callbacks = null;
|
|
this.loader = null;
|
|
this.stats = this.createStats();
|
|
}
|
|
|
|
createStats() {
|
|
return {
|
|
aborted: false,
|
|
loaded: 0,
|
|
retry: 0,
|
|
total: 0,
|
|
chunkCount: 0,
|
|
bwEstimate: 0,
|
|
trequest: 0,
|
|
tfirst: 0,
|
|
tload: 0,
|
|
loading: {start: 0, first: 0, end: 0},
|
|
parsing: {start: 0, end: 0},
|
|
buffering: {start: 0, first: 0, end: 0},
|
|
};
|
|
}
|
|
|
|
destroy() {
|
|
this.abort();
|
|
}
|
|
|
|
abort() {
|
|
if (this.stats) this.stats.aborted = true;
|
|
if (this.loader) {
|
|
try {
|
|
this.loader.abort();
|
|
} catch (e) { /* ignore */
|
|
}
|
|
}
|
|
this.loader = null;
|
|
}
|
|
|
|
load(context, config, callbacks) {
|
|
this.context = context;
|
|
this.callbacks = callbacks;
|
|
const requestUrl = context.url;
|
|
const wantsArrayBuffer =
|
|
context.responseType === 'arraybuffer' ||
|
|
/\.(?:ts|m4s|mp4|key)(?:[?#]|$)/i.test(requestUrl);
|
|
const startedAt = performance.now();
|
|
const stats = (this.stats = this.createStats());
|
|
stats.trequest = startedAt;
|
|
stats.tfirst = startedAt;
|
|
stats.tload = startedAt;
|
|
stats.loading.start = startedAt;
|
|
|
|
this.loader = GM_xmlhttpRequest({
|
|
method: 'GET',
|
|
url: requestUrl,
|
|
responseType: 'text',
|
|
overrideMimeType: wantsArrayBuffer ? 'text/plain; charset=x-user-defined' : undefined,
|
|
timeout: config && config.timeout ? config.timeout : 20000,
|
|
headers: {
|
|
Accept: wantsArrayBuffer
|
|
? '*/*'
|
|
: 'application/vnd.apple.mpegurl, application/x-mpegURL, */*',
|
|
},
|
|
onprogress: (event) => {
|
|
stats.loaded = Number(event && event.loaded ? event.loaded : stats.loaded || 0);
|
|
stats.total = Number(event && event.total ? event.total : stats.total || stats.loaded || 0);
|
|
if (!stats.loading.first && stats.loaded > 0) {
|
|
stats.loading.first = performance.now();
|
|
}
|
|
},
|
|
onload: (r) => {
|
|
const status = Number(r.status || 0);
|
|
const response = {
|
|
code: status,
|
|
text: r.statusText || '',
|
|
url: r.finalUrl || requestUrl,
|
|
};
|
|
stats.tfirst = stats.tfirst || performance.now();
|
|
stats.tload = performance.now();
|
|
stats.loading.first = stats.loading.first || stats.tload;
|
|
stats.loading.end = stats.tload;
|
|
if (status < 200 || status >= 300) {
|
|
callbacks.onError && callbacks.onError(response, context, null, stats);
|
|
return;
|
|
}
|
|
const responseText = r.responseText || r.response || '';
|
|
const data = wantsArrayBuffer
|
|
? binaryTextToArrayBuffer(responseText)
|
|
: responseText;
|
|
stats.loaded = data && data.byteLength ? data.byteLength : (data && data.length ? data.length : stats.loaded || 0);
|
|
stats.total = stats.total || stats.loaded;
|
|
stats.bwEstimate =
|
|
stats.loading.end > stats.loading.first
|
|
? Math.round((stats.total * 8000) / (stats.loading.end - stats.loading.first))
|
|
: 0;
|
|
callbacks.onSuccess && callbacks.onSuccess({data, url: response.url}, stats, context, response);
|
|
},
|
|
onerror: () => {
|
|
callbacks.onError &&
|
|
callbacks.onError({code: 0, text: 'network error', url: requestUrl}, context, null, stats);
|
|
},
|
|
ontimeout: () => {
|
|
stats.tload = performance.now();
|
|
stats.loading.end = stats.tload;
|
|
callbacks.onTimeout && callbacks.onTimeout(stats, context, null);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return GMHlsLoader;
|
|
};
|
|
|
|
const isM3U8Url = (url) => /\.m3u8(?:[?#].*)?$/i.test(String(url || ''));
|
|
|
|
const attachHlsToVideo = (videoEl, src) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (!videoEl || !src) {
|
|
reject(new Error('video element or source missing'));
|
|
return;
|
|
}
|
|
|
|
const HlsClass = getHlsClass();
|
|
if (!HlsClass) {
|
|
videoEl.src = src;
|
|
videoEl.load && videoEl.load();
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
const hls = new HlsClass({
|
|
enableWorker: false,
|
|
lowLatencyMode: true,
|
|
loader: createHlsLoader(),
|
|
autoStartLoad: true,
|
|
startPosition: 0,
|
|
capLevelToPlayerSize: true,
|
|
testBandwidth: false,
|
|
preferManagedMediaSource: false,
|
|
maxBufferLength: 6,
|
|
maxMaxBufferLength: 12,
|
|
backBufferLength: 30,
|
|
maxBufferHole: 0.5,
|
|
nudgeOffset: 0.1,
|
|
manifestLoadingMaxRetry: 2,
|
|
levelLoadingMaxRetry: 2,
|
|
fragLoadingMaxRetry: 2,
|
|
manifestLoadingTimeOut: 12000,
|
|
levelLoadingTimeOut: 12000,
|
|
fragLoadingTimeOut: 12000,
|
|
abrEwmaFastLive: 3,
|
|
abrEwmaSlowLive: 9,
|
|
});
|
|
|
|
hls.on(HlsClass.Events.MANIFEST_PARSED, () => {
|
|
hls.startLoad(0);
|
|
videoEl.play().catch(() => {
|
|
});
|
|
resolve(true);
|
|
});
|
|
|
|
hls.on(HlsClass.Events.ERROR, (_, data) => {
|
|
if (!data || !data.fatal) return;
|
|
clog.error('HLS: 播放失败', data);
|
|
if (data.type === HlsClass.ErrorTypes.MEDIA_ERROR) {
|
|
if (videoEl.readyState >= 2) {
|
|
try {
|
|
hls.recoverMediaError && hls.recoverMediaError();
|
|
} catch (e) { /* ignore */
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
reject(new Error(data.details || 'HLS fatal error'));
|
|
});
|
|
|
|
hls.loadSource(src);
|
|
hls.attachMedia(videoEl);
|
|
videoEl._hls = hls;
|
|
});
|
|
};
|
|
|
|
const destroyHls = (videoEl) => {
|
|
if (videoEl && videoEl._hls) {
|
|
try {
|
|
videoEl._hls.destroy();
|
|
} catch (e) { /* ignore */
|
|
}
|
|
videoEl._hls = null;
|
|
}
|
|
};
|
|
|
|
export {loadHlsLibrary, getHlsClass, createHlsLoader, isM3U8Url, attachHlsToVideo, destroyHls, HLS_SCRIPT_URL}; |