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::download_volcano_video,
|
||||
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| {
|
||||
// 初始化日志系统
|
||||
|
|
|
|||
|
|
@ -276,10 +276,26 @@ pub async fn download_video_to_directory(
|
|||
Err(_) => return Err("文件选择对话框超时".to_string()),
|
||||
};
|
||||
|
||||
// 下载视频文件
|
||||
let client = reqwest::Client::new();
|
||||
// 创建带有防盗链绕过的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")
|
||||
.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) => {
|
||||
if response.status().is_success() {
|
||||
match response.bytes().await {
|
||||
|
|
@ -371,3 +387,59 @@ pub async fn batch_download_volcano_videos(
|
|||
|
||||
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 [error, setError] = useState<string | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [proxiedVideoUrl, setProxiedVideoUrl] = useState<string | null>(null);
|
||||
const [isLoadingProxy, setIsLoadingProxy] = useState(false);
|
||||
|
||||
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改变时
|
||||
useEffect(() => {
|
||||
if (videoUrl) {
|
||||
|
|
@ -166,10 +191,33 @@ export const VideoPreviewModal: React.FC<VideoPreviewModalProps> = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const handleVideoError = useCallback(() => {
|
||||
const handleVideoError = useCallback(async () => {
|
||||
console.log('视频加载失败,尝试使用代理服务');
|
||||
|
||||
// 如果还没有尝试过代理,则尝试使用代理服务
|
||||
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('视频加载失败');
|
||||
}, []);
|
||||
setError('视频加载失败,请检查网络连接或视频链接');
|
||||
} finally {
|
||||
setIsLoadingProxy(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setError('视频加载失败,请检查网络连接或视频链接');
|
||||
}
|
||||
}, [videoUrl, proxiedVideoUrl]);
|
||||
|
||||
const handleTimeUpdate = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
|
|
@ -243,9 +291,10 @@ export const VideoPreviewModal: React.FC<VideoPreviewModalProps> = ({
|
|||
onPlay={handlePlay}
|
||||
onPause={handlePause}
|
||||
controls={false}
|
||||
crossOrigin="anonymous"
|
||||
>
|
||||
{videoUrl && (
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
{proxiedVideoUrl && (
|
||||
<source src={proxiedVideoUrl} type="video/mp4" />
|
||||
)}
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
|
|
|
|||
Loading…
Reference in New Issue