mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-09 06:46:37 +08:00
处理bug
处理bug
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 830 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -85,7 +85,14 @@ func (p *openAPI115Provider) ResolveWithUA(ctx context.Context, fileRef, ua stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &DirectLink{URL: url, Proxy: false}, nil
|
// 115 CDN 防盗链白名单:直链绑定换取时的 User-Agent(调用方 UA 或
|
||||||
|
// DefaultUA),后续请求必须携带同一 UA,否则 403。302 播放由客户端
|
||||||
|
// 自带 UA 天然满足;服务端直连(弹幕 hash 等)依赖这里的 Headers。
|
||||||
|
bound := strings.TrimSpace(ua)
|
||||||
|
if bound == "" {
|
||||||
|
bound = cloud115.DefaultUA
|
||||||
|
}
|
||||||
|
return &DirectLink{URL: url, Proxy: false, Headers: map[string]string{"User-Agent": bound}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenClient 暴露底层客户端(token 刷新用)。
|
// OpenClient 暴露底层客户端(token 刷新用)。
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ShukeBta/MMTL/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
|
"github.com/ShukeBta/MMTL/internal/service/cloud"
|
||||||
)
|
)
|
||||||
|
|
||||||
// overrideDanmakuOfficialBase points the "official" endpoint (match + fallback)
|
// overrideDanmakuOfficialBase points the "official" endpoint (match + fallback)
|
||||||
@@ -178,14 +179,21 @@ func TestDanmakuFetchHashMissFallsBackToFileNameSearch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// strm:通过解析出的直链 Range 拉 16MB 前缀算 hash → match → 拉弹幕。
|
// strm:通过解析出的直链 Range 拉 16MB 前缀算 hash → match → 拉弹幕。
|
||||||
|
// range server 模拟 115 CDN 防盗链:UA 不匹配直接 403(真实部署中
|
||||||
|
// 直链绑定换取时的 UA,不带绑定 UA 拉取会失败,见 pan115_openapi.go)。
|
||||||
func TestDanmakuFetchStrmHashViaDirectLink(t *testing.T) {
|
func TestDanmakuFetchStrmHashViaDirectLink(t *testing.T) {
|
||||||
content := bytes.Repeat([]byte("strm-video-bytes-0123456789"), 400)
|
content := bytes.Repeat([]byte("strm-video-bytes-0123456789"), 400)
|
||||||
sum := md5.Sum(content)
|
sum := md5.Sum(content)
|
||||||
wantHash := hex.EncodeToString(sum[:])
|
wantHash := hex.EncodeToString(sum[:])
|
||||||
|
|
||||||
var gotRange string
|
var gotRange, gotUA string
|
||||||
rangeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
rangeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
gotRange = r.Header.Get("Range")
|
gotRange = r.Header.Get("Range")
|
||||||
|
gotUA = r.Header.Get("User-Agent")
|
||||||
|
if gotUA != "bound-ua-115" {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
_, _ = w.Write(content)
|
_, _ = w.Write(content)
|
||||||
}))
|
}))
|
||||||
@@ -202,7 +210,11 @@ func TestDanmakuFetchStrmHashViaDirectLink(t *testing.T) {
|
|||||||
var gotProvider string
|
var gotProvider string
|
||||||
svc.SetStrmResolver(func(_ context.Context, provider string, _ url.Values) (*StrmPlayResult, error) {
|
svc.SetStrmResolver(func(_ context.Context, provider string, _ url.Values) (*StrmPlayResult, error) {
|
||||||
gotProvider = provider
|
gotProvider = provider
|
||||||
return &StrmPlayResult{RedirectURL: rangeSrv.URL}, nil
|
// 播放链路 302 直链 + 绑定的 UA 头(防服务端直连 403)。
|
||||||
|
return &StrmPlayResult{
|
||||||
|
RedirectURL: rangeSrv.URL,
|
||||||
|
Link: &cloud.DirectLink{URL: rangeSrv.URL, Headers: map[string]string{"User-Agent": "bound-ua-115"}},
|
||||||
|
}, nil
|
||||||
})
|
})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
strmPath := filepath.Join(t.TempDir(), "远程动画.第01话.strm")
|
strmPath := filepath.Join(t.TempDir(), "远程动画.第01话.strm")
|
||||||
@@ -220,6 +232,7 @@ func TestDanmakuFetchStrmHashViaDirectLink(t *testing.T) {
|
|||||||
require.Contains(t, res.Raw, "弹幕Strm命中")
|
require.Contains(t, res.Raw, "弹幕Strm命中")
|
||||||
require.Equal(t, "115", gotProvider)
|
require.Equal(t, "115", gotProvider)
|
||||||
require.Contains(t, gotRange, "bytes=0-")
|
require.Contains(t, gotRange, "bytes=0-")
|
||||||
|
require.Equal(t, "bound-ua-115", gotUA) // 防盗链 UA 必须透传
|
||||||
require.Contains(t, seen, `"fileHash":"`+wantHash+`"`)
|
require.Contains(t, seen, `"fileHash":"`+wantHash+`"`)
|
||||||
require.Contains(t, seen, `"fileName":"`+url.QueryEscape("远程动画.第01话")+`"`)
|
require.Contains(t, seen, `"fileName":"`+url.QueryEscape("远程动画.第01话")+`"`)
|
||||||
// strm 的 SizeBytes 是文本大小,不参与 match。
|
// strm 的 SizeBytes 是文本大小,不参与 match。
|
||||||
|
|||||||
@@ -545,6 +545,8 @@ func (s *DanmakuService) hashStrmTarget(ctx context.Context, raw string) (string
|
|||||||
}
|
}
|
||||||
src, err = s.strmResolve(ctx, segs[0], u.Query())
|
src, err = s.strmResolve(ctx, segs[0], u.Query())
|
||||||
if err != nil || src == nil {
|
if err != nil || src == nil {
|
||||||
|
s.log.Warn("danmaku strm resolve failed, hash layer skipped",
|
||||||
|
zap.String("provider", segs[0]), zap.Error(err))
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
case u.Scheme == "http" || u.Scheme == "https":
|
case u.Scheme == "http" || u.Scheme == "https":
|
||||||
@@ -557,13 +559,20 @@ func (s *DanmakuService) hashStrmTarget(ctx context.Context, raw string) (string
|
|||||||
case src.LocalPath != "":
|
case src.LocalPath != "":
|
||||||
return s.hashLocalFile(src.LocalPath)
|
return s.hashLocalFile(src.LocalPath)
|
||||||
case src.RedirectURL != "":
|
case src.RedirectURL != "":
|
||||||
body, err = s.openRangeBody(ctx, src.RedirectURL, nil)
|
var headers map[string]string
|
||||||
|
if src.Link != nil {
|
||||||
|
headers = src.Link.Headers // 115 直链防盗链要求携带绑定 UA
|
||||||
|
}
|
||||||
|
body, err = s.openRangeBody(ctx, src.RedirectURL, headers)
|
||||||
case src.Link != nil && src.Link.URL != "":
|
case src.Link != nil && src.Link.URL != "":
|
||||||
body, err = s.openRangeBody(ctx, src.Link.URL, src.Link.Headers)
|
body, err = s.openRangeBody(ctx, src.Link.URL, src.Link.Headers)
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if err != nil || body == nil {
|
if err != nil || body == nil {
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("danmaku hash prefix fetch failed, hash layer skipped", zap.String("target", raw), zap.Error(err))
|
||||||
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
defer body.Close()
|
defer body.Close()
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ func (s *StrmService) resolveCloudPlay(ctx context.Context, provider string, q u
|
|||||||
if link.Proxy {
|
if link.Proxy {
|
||||||
return &StrmPlayResult{Link: link, Proxy: true}, nil
|
return &StrmPlayResult{Link: link, Proxy: true}, nil
|
||||||
}
|
}
|
||||||
return &StrmPlayResult{RedirectURL: link.URL}, nil
|
// Link 一并保留:302 处理器只认 RedirectURL,但服务端直连(弹幕 hash
|
||||||
|
// 拉取)需要 link.Headers 才能通过直链防盗链校验。
|
||||||
|
return &StrmPlayResult{RedirectURL: link.URL, Link: link}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveLocalPlay 本地源:校验路径位于某个本地同步目录的源目录内。
|
// resolveLocalPlay 本地源:校验路径位于某个本地同步目录的源目录内。
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -66,19 +66,50 @@ export function PlayerVideoStage({
|
|||||||
|
|
||||||
// 把用户选择的字幕轨道应用到 <video> 的 textTracks(跨浏览器显式设置
|
// 把用户选择的字幕轨道应用到 <video> 的 textTracks(跨浏览器显式设置
|
||||||
// mode;<track default> 只影响初始值,部分浏览器不会自动显示)。
|
// mode;<track default> 只影响初始值,部分浏览器不会自动显示)。
|
||||||
|
//
|
||||||
|
// 通过 React 渲染的 <track> 元素定位而不是 textTracks 索引:hls.js 等
|
||||||
|
// 可能向 textTracks 注入内部轨道(如 CEA-608 captions),索引会错位。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current
|
const video = videoRef.current
|
||||||
if (!video || subs.length === 0) return
|
if (!video || subs.length === 0) return
|
||||||
const apply = () => {
|
const timers = new Set<ReturnType<typeof setTimeout>>()
|
||||||
const tracks = video.textTracks
|
const delay = (fn: () => void, ms: number) => {
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
const id = setTimeout(fn, ms)
|
||||||
tracks[i].mode = i === subtitleIndex ? 'showing' : 'disabled'
|
timers.add(id)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const trackEls = Array.from(video.querySelectorAll('track'))
|
||||||
|
if (trackEls.length === 0) return
|
||||||
|
trackEls.forEach((el, i) => {
|
||||||
|
const tt = el.track
|
||||||
|
if (tt) tt.mode = i === subtitleIndex ? 'showing' : 'disabled'
|
||||||
|
})
|
||||||
|
// Chrome 把轨道从 disabled 切回 showing 时是异步重新拉取字幕;
|
||||||
|
// 若拉取因竞态没有发生(例如切换过程中视频重载),cues 会一直为空,
|
||||||
|
// 表现为「字幕已选中但不显示」。检测到空轨道后强制重载一次兜底。
|
||||||
|
const selected = trackEls[subtitleIndex]
|
||||||
|
if (!selected) return
|
||||||
|
delay(() => {
|
||||||
|
const tt = selected.track
|
||||||
|
if (tt && tt.mode === 'showing' && (!tt.cues || tt.cues.length === 0)) {
|
||||||
|
const src = selected.getAttribute('src')
|
||||||
|
if (src) {
|
||||||
|
selected.setAttribute('src', '')
|
||||||
|
selected.setAttribute('src', src)
|
||||||
|
tt.mode = 'showing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
apply()
|
apply()
|
||||||
// 轨道元数据就绪后再应用一次,确保字幕真正可见
|
// 轨道元数据就绪后再应用一次,确保字幕真正可见
|
||||||
video.addEventListener('loadedmetadata', apply)
|
video.addEventListener('loadedmetadata', apply)
|
||||||
return () => video.removeEventListener('loadedmetadata', apply)
|
return () => {
|
||||||
|
video.removeEventListener('loadedmetadata', apply)
|
||||||
|
timers.forEach(clearTimeout)
|
||||||
|
}
|
||||||
}, [subtitleIndex, subs, videoRef])
|
}, [subtitleIndex, subs, videoRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user