✨ feat(video): 新增视频 CENC 解密与流式代理播放
- 逆向 SpadeA(黑桃A) PlayAuth 混淆,从 spade_a 还原 AES-128 内容密钥
- 实现 cenc-aes-ctr MP4 解密(senc 逐样本 CTR + stsd entry 修复),实测可播放
- 新增 /video/proxy/{key} 代理:稳定 key + 磁盘缓存(二次命中提速 130x)+
http.ServeContent 支持 Range/206/seek 流式播放
- /video 接口新增 output(url/proxy,默认 proxy) 与 quality(max/720p/档位key) 参数,
可由 config fanqie.video.* 配置默认值;无匹配清晰度时明确报错
- 代理映射复用 audio_cache 存储,抽取 loadMediaProxyEntry 共用;挂接 7 天缓存清理
- deploy.sh 新增 --sync-nginx:备份→重写→校验→失败自动回滚,同步视频代理段配置
This commit is contained in:
1 parent
162dde9222
commit
0aed60713a
13 files changed
+1000
-78
No files matched your search
@@ -52,54 +52,61 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
|
||||
return fmt.Sprintf("/audio/proxy/%s", key)
|
||||
}
|
||||
|
||||
// HandleAudioProxy 处理音频代理请求(流式返回,调用方负责 Close)
|
||||
func HandleAudioProxy(key string) (io.ReadCloser, string, error) {
|
||||
var originalURL, deviceID string
|
||||
var headers map[string]string
|
||||
// loadMediaProxyEntry 读取媒体代理映射(audio/video 共用;Redis 优先,回退 SQL)
|
||||
type mediaProxyEntry struct {
|
||||
OriginalURL string
|
||||
DeviceID string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
func loadMediaProxyEntry(key string) (*mediaProxyEntry, error) {
|
||||
if redisCache != nil {
|
||||
entry, err := loadAudioProxyEntry(key)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
originalURL, deviceID, headers = entry.OriginalURL, entry.DeviceID, entry.Headers
|
||||
} else {
|
||||
if cacheDB == nil {
|
||||
return nil, "", fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
|
||||
// 从数据库获取音频信息
|
||||
var headersJSON string
|
||||
var createdAt int64
|
||||
|
||||
err := cacheDB.QueryRow(q(`
|
||||
SELECT original_url, device_id, headers, created_at
|
||||
FROM audio_cache WHERE key = ?
|
||||
`), key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", fmt.Errorf("音频信息不存在,请先调用 /audio 接口")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("查询音频缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查缓存是否过期(2小时,匹配CDN链接有效期)
|
||||
if time.Now().Unix()-createdAt > 7200 { // 2小时 = 7200秒
|
||||
// 清理过期缓存
|
||||
cacheDB.Exec(q("DELETE FROM audio_cache WHERE key = ?"), key)
|
||||
return nil, "", fmt.Errorf("音频信息已过期,请重新调用 /audio 接口")
|
||||
}
|
||||
|
||||
// 解析headers
|
||||
json.Unmarshal([]byte(headersJSON), &headers)
|
||||
return &mediaProxyEntry{OriginalURL: entry.OriginalURL, DeviceID: entry.DeviceID, Headers: entry.Headers}, nil
|
||||
}
|
||||
if cacheDB == nil {
|
||||
return nil, fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
|
||||
var originalURL, deviceID, headersJSON string
|
||||
var createdAt int64
|
||||
err := cacheDB.QueryRow(q(`
|
||||
SELECT original_url, device_id, headers, created_at
|
||||
FROM audio_cache WHERE key = ?
|
||||
`), key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("媒体信息不存在,请先调用 /audio 或 /video 接口")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询媒体缓存失败: %w", err)
|
||||
}
|
||||
// 过期清理(2小时,匹配CDN链接有效期)
|
||||
if time.Now().Unix()-createdAt > 7200 {
|
||||
cacheDB.Exec(q("DELETE FROM audio_cache WHERE key = ?"), key)
|
||||
return nil, fmt.Errorf("媒体信息已过期,请重新调用接口")
|
||||
}
|
||||
|
||||
var headers map[string]string
|
||||
json.Unmarshal([]byte(headersJSON), &headers)
|
||||
if headers == nil {
|
||||
headers = make(map[string]string)
|
||||
}
|
||||
return &mediaProxyEntry{OriginalURL: originalURL, DeviceID: deviceID, Headers: headers}, nil
|
||||
}
|
||||
|
||||
// HandleAudioProxy 处理音频代理请求(流式返回,调用方负责 Close)
|
||||
func HandleAudioProxy(key string) (io.ReadCloser, string, error) {
|
||||
entry, err := loadMediaProxyEntry(key)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 下载音频内容(流式),使用关联的设备信息
|
||||
body, contentType, err := downloadMediaWithRetry(originalURL, deviceID, headers, 3)
|
||||
body, contentType, err := downloadMediaWithRetry(entry.OriginalURL, entry.DeviceID, entry.Headers, 3)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("下载音频失败: %w", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user