feat: 实现防盗链绕过机制解决403错误
问题分析: - 火山云CDN配置了防盗链保护,直接访问返回403 Forbidden - 需要特定的Referer和User-Agent头部才能正常访问 - 跨域访问限制导致浏览器无法直接播放视频 解决方案: 1. 添加crossOrigin='anonymous'属性到video标签 2. 创建代理服务绕过防盗链限制 3. 实现fallback机制:直接访问失败时自动使用代理 技术实现: - 新增get_video_stream_base64命令,返回Base64编码的视频数据 - 添加完整的HTTP头部模拟真实浏览器请求 - 包含Referer、Origin、User-Agent等关键头部信息 - 实现自动fallback:直接播放失败时转为代理模式 用户体验: - 透明的错误处理,用户无感知切换 - 保持原有的播放控制功能 - 支持大文件的Base64编码传输 - 提供详细的错误日志便于调试 HTTP头部配置: - User-Agent: 模拟Chrome浏览器 - Referer: https://www.volcengine.com/ - Origin: https://www.volcengine.com - Accept: 视频MIME类型 - Sec-Fetch-*: 安全策略头部
This commit is contained in:
parent
fb8b5b90ee
commit
d3713e54c7
|
|
@ -497,7 +497,8 @@ pub fn run() {
|
||||||
commands::volcano_video_commands::batch_delete_volcano_video_generations,
|
commands::volcano_video_commands::batch_delete_volcano_video_generations,
|
||||||
commands::volcano_video_commands::download_volcano_video,
|
commands::volcano_video_commands::download_volcano_video,
|
||||||
commands::volcano_video_commands::download_video_to_directory,
|
commands::volcano_video_commands::download_video_to_directory,
|
||||||
commands::volcano_video_commands::batch_download_volcano_videos
|
commands::volcano_video_commands::batch_download_volcano_videos,
|
||||||
|
commands::volcano_video_commands::get_video_stream_base64
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// 初始化日志系统
|
// 初始化日志系统
|
||||||
|
|
|
||||||
|
|
@ -276,10 +276,26 @@ pub async fn download_video_to_directory(
|
||||||
Err(_) => return Err("文件选择对话框超时".to_string()),
|
Err(_) => return Err("文件选择对话框超时".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 下载视频文件
|
// 创建带有防盗链绕过的HTTP客户端
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("创建HTTP客户端失败: {}", e))?;
|
||||||
|
|
||||||
match client.get(&video_url).send().await {
|
// 构建请求,添加必要的头部信息绕过防盗链
|
||||||
|
let request = client
|
||||||
|
.get(&video_url)
|
||||||
|
.header("Referer", "https://www.volcengine.com/")
|
||||||
|
.header("Origin", "https://www.volcengine.com")
|
||||||
|
.header("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
|
||||||
|
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
.header("Pragma", "no-cache")
|
||||||
|
.header("Sec-Fetch-Dest", "video")
|
||||||
|
.header("Sec-Fetch-Mode", "no-cors")
|
||||||
|
.header("Sec-Fetch-Site", "cross-site");
|
||||||
|
|
||||||
|
match request.send().await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
match response.bytes().await {
|
match response.bytes().await {
|
||||||
|
|
@ -371,3 +387,59 @@ pub async fn batch_download_volcano_videos(
|
||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取视频流的Base64数据,绕过防盗链限制
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_video_stream_base64(
|
||||||
|
video_url: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
info!("获取视频流Base64数据: {}", video_url);
|
||||||
|
|
||||||
|
// 创建带有防盗链绕过的HTTP客户端
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
.timeout(std::time::Duration::from_secs(60))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("创建HTTP客户端失败: {}", e))?;
|
||||||
|
|
||||||
|
// 构建请求,添加必要的头部信息
|
||||||
|
let request = client
|
||||||
|
.get(&video_url)
|
||||||
|
.header("Referer", "https://www.volcengine.com/")
|
||||||
|
.header("Origin", "https://www.volcengine.com")
|
||||||
|
.header("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
|
||||||
|
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
.header("Pragma", "no-cache")
|
||||||
|
.header("Sec-Fetch-Dest", "video")
|
||||||
|
.header("Sec-Fetch-Mode", "no-cors")
|
||||||
|
.header("Sec-Fetch-Site", "cross-site");
|
||||||
|
|
||||||
|
match request.send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
match response.bytes().await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
|
let base64_data = general_purpose::STANDARD.encode(&bytes);
|
||||||
|
let data_url = format!("data:video/mp4;base64,{}", base64_data);
|
||||||
|
info!("成功获取视频流Base64数据,大小: {} bytes", bytes.len());
|
||||||
|
Ok(data_url)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("读取视频流数据失败: {}", e);
|
||||||
|
Err(format!("读取视频流数据失败: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error_msg = format!("获取视频流失败,HTTP状态码: {}", response.status());
|
||||||
|
error!("{}", error_msg);
|
||||||
|
Err(error_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("获取视频流请求失败: {}", e);
|
||||||
|
Err(format!("获取视频流请求失败: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,34 @@ export const VideoPreviewModal: React.FC<VideoPreviewModalProps> = ({
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const [proxiedVideoUrl, setProxiedVideoUrl] = useState<string | null>(null);
|
||||||
|
const [isLoadingProxy, setIsLoadingProxy] = useState(false);
|
||||||
|
|
||||||
const { addNotification } = useNotifications();
|
const { addNotification } = useNotifications();
|
||||||
|
|
||||||
|
// 加载代理视频
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoUrl || !isOpen) return;
|
||||||
|
|
||||||
|
const loadProxiedVideo = async () => {
|
||||||
|
setIsLoadingProxy(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 首先尝试直接加载
|
||||||
|
setProxiedVideoUrl(videoUrl);
|
||||||
|
setIsLoading(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载视频失败:', error);
|
||||||
|
setError('视频加载失败');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingProxy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadProxiedVideo();
|
||||||
|
}, [videoUrl, isOpen]);
|
||||||
|
|
||||||
// 重置状态当视频URL改变时
|
// 重置状态当视频URL改变时
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
|
|
@ -166,10 +191,33 @@ export const VideoPreviewModal: React.FC<VideoPreviewModalProps> = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleVideoError = useCallback(() => {
|
const handleVideoError = useCallback(async () => {
|
||||||
setIsLoading(false);
|
console.log('视频加载失败,尝试使用代理服务');
|
||||||
setError('视频加载失败');
|
|
||||||
}, []);
|
// 如果还没有尝试过代理,则尝试使用代理服务
|
||||||
|
if (proxiedVideoUrl === videoUrl) {
|
||||||
|
try {
|
||||||
|
setIsLoadingProxy(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const proxiedData = await invoke<string>('get_video_stream_base64', {
|
||||||
|
videoUrl: videoUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
setProxiedVideoUrl(proxiedData);
|
||||||
|
console.log('成功获取代理视频数据');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('代理视频加载失败:', error);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError('视频加载失败,请检查网络连接或视频链接');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingProxy(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
|
setError('视频加载失败,请检查网络连接或视频链接');
|
||||||
|
}
|
||||||
|
}, [videoUrl, proxiedVideoUrl]);
|
||||||
|
|
||||||
const handleTimeUpdate = useCallback(() => {
|
const handleTimeUpdate = useCallback(() => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
|
|
@ -243,9 +291,10 @@ export const VideoPreviewModal: React.FC<VideoPreviewModalProps> = ({
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
onPause={handlePause}
|
onPause={handlePause}
|
||||||
controls={false}
|
controls={false}
|
||||||
|
crossOrigin="anonymous"
|
||||||
>
|
>
|
||||||
{videoUrl && (
|
{proxiedVideoUrl && (
|
||||||
<source src={videoUrl} type="video/mp4" />
|
<source src={proxiedVideoUrl} type="video/mp4" />
|
||||||
)}
|
)}
|
||||||
您的浏览器不支持视频播放。
|
您的浏览器不支持视频播放。
|
||||||
</video>
|
</video>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue