Files
LunaTV/src/lib/utils.ts
2025-07-26 15:39:29 +08:00

248 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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(/&nbsp;/g, ' ') // 将 &nbsp; 替换为空格
.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)
}`
);
}
}