mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 00:44:41 +08:00
248 lines
7.4 KiB
TypeScript
248 lines
7.4 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||
|
||
import Hls from 'hls.js';
|
||
|
||
/**
|
||
* 获取图片代理 URL 设置
|
||
*/
|
||
export function getImageProxyUrl(): string | null {
|
||
if (typeof window === 'undefined') return null;
|
||
|
||
// 本地未开启图片代理,则不使用代理
|
||
const enableImageProxy = localStorage.getItem('enableImageProxy');
|
||
if (enableImageProxy !== null) {
|
||
if (!JSON.parse(enableImageProxy) as boolean) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const localImageProxy = localStorage.getItem('imageProxyUrl');
|
||
if (localImageProxy != null) {
|
||
return localImageProxy.trim() ? localImageProxy.trim() : null;
|
||
}
|
||
|
||
// 如果未设置,则使用全局对象
|
||
const serverImageProxy = (window as any).RUNTIME_CONFIG?.IMAGE_PROXY;
|
||
return serverImageProxy && serverImageProxy.trim()
|
||
? serverImageProxy.trim()
|
||
: null;
|
||
}
|
||
|
||
/**
|
||
* 处理图片 URL,如果设置了图片代理则使用代理
|
||
*/
|
||
export function processImageUrl(originalUrl: string): string {
|
||
if (!originalUrl) return originalUrl;
|
||
|
||
const proxyUrl = getImageProxyUrl();
|
||
if (!proxyUrl) return originalUrl;
|
||
|
||
return `${proxyUrl}${encodeURIComponent(originalUrl)}`;
|
||
}
|
||
|
||
/**
|
||
* 获取豆瓣代理 URL 设置
|
||
*/
|
||
export function getDoubanProxyUrl(): string | null {
|
||
if (typeof window === 'undefined') return null;
|
||
|
||
// 本地未开启豆瓣代理,则不使用代理
|
||
const enableDoubanProxy = localStorage.getItem('enableDoubanProxy');
|
||
if (enableDoubanProxy !== null) {
|
||
if (!JSON.parse(enableDoubanProxy) as boolean) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const localDoubanProxy = localStorage.getItem('doubanProxyUrl');
|
||
if (localDoubanProxy != null) {
|
||
return localDoubanProxy.trim() ? localDoubanProxy.trim() : null;
|
||
}
|
||
|
||
// 如果未设置,则使用全局对象
|
||
const serverDoubanProxy = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY;
|
||
return serverDoubanProxy && serverDoubanProxy.trim()
|
||
? serverDoubanProxy.trim()
|
||
: null;
|
||
}
|
||
|
||
/**
|
||
* 处理豆瓣 URL,如果设置了豆瓣代理则使用代理
|
||
*/
|
||
export function processDoubanUrl(originalUrl: string): string {
|
||
if (!originalUrl) return originalUrl;
|
||
|
||
const proxyUrl = getDoubanProxyUrl();
|
||
if (!proxyUrl) return originalUrl;
|
||
|
||
return `${proxyUrl}${encodeURIComponent(originalUrl)}`;
|
||
}
|
||
|
||
export function cleanHtmlTags(text: string): string {
|
||
if (!text) return '';
|
||
return text
|
||
.replace(/<[^>]+>/g, '\n') // 将 HTML 标签替换为换行
|
||
.replace(/\n+/g, '\n') // 将多个连续换行合并为一个
|
||
.replace(/[ \t]+/g, ' ') // 将多个连续空格和制表符合并为一个空格,但保留换行符
|
||
.replace(/^\n+|\n+$/g, '') // 去掉首尾换行
|
||
.replace(/ /g, ' ') // 将 替换为空格
|
||
.trim(); // 去掉首尾空格
|
||
}
|
||
|
||
/**
|
||
* 从m3u8地址获取视频质量等级和网络信息
|
||
* @param m3u8Url m3u8播放列表的URL
|
||
* @returns Promise<{quality: string, loadSpeed: string, pingTime: number}> 视频质量等级和网络信息
|
||
*/
|
||
export async function getVideoResolutionFromM3u8(m3u8Url: string): Promise<{
|
||
quality: string; // 如720p、1080p等
|
||
loadSpeed: string; // 自动转换为KB/s或MB/s
|
||
pingTime: number; // 网络延迟(毫秒)
|
||
}> {
|
||
try {
|
||
// 直接使用m3u8 URL作为视频源,避免CORS问题
|
||
return new Promise((resolve, reject) => {
|
||
const video = document.createElement('video');
|
||
video.muted = true;
|
||
video.preload = 'metadata';
|
||
|
||
// 测量网络延迟(ping时间) - 使用m3u8 URL而不是ts文件
|
||
const pingStart = performance.now();
|
||
let pingTime = 0;
|
||
|
||
// 测量ping时间(使用m3u8 URL)
|
||
fetch(m3u8Url, { method: 'HEAD', mode: 'no-cors' })
|
||
.then(() => {
|
||
pingTime = performance.now() - pingStart;
|
||
})
|
||
.catch(() => {
|
||
pingTime = performance.now() - pingStart; // 记录到失败为止的时间
|
||
});
|
||
|
||
// 固定使用hls.js加载
|
||
const hls = new Hls();
|
||
|
||
// 设置超时处理
|
||
const timeout = setTimeout(() => {
|
||
hls.destroy();
|
||
video.remove();
|
||
reject(new Error('Timeout loading video metadata'));
|
||
}, 4000);
|
||
|
||
video.onerror = () => {
|
||
clearTimeout(timeout);
|
||
hls.destroy();
|
||
video.remove();
|
||
reject(new Error('Failed to load video metadata'));
|
||
};
|
||
|
||
let actualLoadSpeed = '未知';
|
||
let hasSpeedCalculated = false;
|
||
let hasMetadataLoaded = false;
|
||
|
||
let fragmentStartTime = 0;
|
||
|
||
// 检查是否可以返回结果
|
||
const checkAndResolve = () => {
|
||
if (
|
||
hasMetadataLoaded &&
|
||
(hasSpeedCalculated || actualLoadSpeed !== '未知')
|
||
) {
|
||
clearTimeout(timeout);
|
||
const width = video.videoWidth;
|
||
if (width && width > 0) {
|
||
hls.destroy();
|
||
video.remove();
|
||
|
||
// 根据视频宽度判断视频质量等级,使用经典分辨率的宽度作为分割点
|
||
const quality =
|
||
width >= 3840
|
||
? '4K' // 4K: 3840x2160
|
||
: width >= 2560
|
||
? '2K' // 2K: 2560x1440
|
||
: width >= 1920
|
||
? '1080p' // 1080p: 1920x1080
|
||
: width >= 1280
|
||
? '720p' // 720p: 1280x720
|
||
: width >= 854
|
||
? '480p'
|
||
: 'SD'; // 480p: 854x480
|
||
|
||
resolve({
|
||
quality,
|
||
loadSpeed: actualLoadSpeed,
|
||
pingTime: Math.round(pingTime),
|
||
});
|
||
} else {
|
||
// webkit 无法获取尺寸,直接返回
|
||
resolve({
|
||
quality: '未知',
|
||
loadSpeed: actualLoadSpeed,
|
||
pingTime: Math.round(pingTime),
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// 监听片段加载开始
|
||
hls.on(Hls.Events.FRAG_LOADING, () => {
|
||
fragmentStartTime = performance.now();
|
||
});
|
||
|
||
// 监听片段加载完成,只需首个分片即可计算速度
|
||
hls.on(Hls.Events.FRAG_LOADED, (event: any, data: any) => {
|
||
if (
|
||
fragmentStartTime > 0 &&
|
||
data &&
|
||
data.payload &&
|
||
!hasSpeedCalculated
|
||
) {
|
||
const loadTime = performance.now() - fragmentStartTime;
|
||
const size = data.payload.byteLength || 0;
|
||
|
||
if (loadTime > 0 && size > 0) {
|
||
const speedKBps = size / 1024 / (loadTime / 1000);
|
||
|
||
// 立即计算速度,无需等待更多分片
|
||
const avgSpeedKBps = speedKBps;
|
||
|
||
if (avgSpeedKBps >= 1024) {
|
||
actualLoadSpeed = `${(avgSpeedKBps / 1024).toFixed(1)} MB/s`;
|
||
} else {
|
||
actualLoadSpeed = `${avgSpeedKBps.toFixed(1)} KB/s`;
|
||
}
|
||
hasSpeedCalculated = true;
|
||
checkAndResolve(); // 尝试返回结果
|
||
}
|
||
}
|
||
});
|
||
|
||
hls.loadSource(m3u8Url);
|
||
hls.attachMedia(video);
|
||
|
||
// 监听hls.js错误
|
||
hls.on(Hls.Events.ERROR, (event: any, data: any) => {
|
||
console.error('HLS错误:', data);
|
||
if (data.fatal) {
|
||
clearTimeout(timeout);
|
||
hls.destroy();
|
||
video.remove();
|
||
reject(new Error(`HLS播放失败: ${data.type}`));
|
||
}
|
||
});
|
||
|
||
// 监听视频元数据加载完成
|
||
video.onloadedmetadata = () => {
|
||
hasMetadataLoaded = true;
|
||
checkAndResolve(); // 尝试返回结果
|
||
};
|
||
});
|
||
} catch (error) {
|
||
throw new Error(
|
||
`Error getting video resolution: ${
|
||
error instanceof Error ? error.message : String(error)
|
||
}`
|
||
);
|
||
}
|
||
}
|