diff --git a/internal/app/playback_gateway.go b/internal/app/playback_gateway.go index 9ca0a37..e5733c4 100644 --- a/internal/app/playback_gateway.go +++ b/internal/app/playback_gateway.go @@ -259,10 +259,16 @@ func (app *UIApp) handlePlaybackMediaAsset(writer http.ResponseWriter, request * return } run, err := strconv.ParseUint(parts[1], 10, 64) + viewer := contextViewer(request.Context()) app.playbackMu.Lock() session := app.playbacks[parts[0]] - if err != nil || !viewerOwnsPlayback(request.Context(), session) || session.run != run || session.media == nil { + denied := err != nil || session == nil || session.run != run || session.media == nil + if !denied && viewer != nil { + denied = !viewerOwnsPlayback(request.Context(), session) + } + if denied { app.playbackMu.Unlock() + app.recordPlaybackAssetDenial(request, "session-unavailable") writer.WriteHeader(http.StatusGone) return } diff --git a/internal/app/ui_playback.go b/internal/app/ui_playback.go index 89cc479..8848078 100644 --- a/internal/app/ui_playback.go +++ b/internal/app/ui_playback.go @@ -94,6 +94,17 @@ func (app *UIApp) registerPlaybackRoutes(mux *http.ServeMux) { mux.HandleFunc("/api/ui/playback/progress", app.handlePlaybackProgress) } +// recordPlaybackAssetDenial 记录媒体资产请求被拒的情况,便于诊断 +// iOS/WebKit 原生媒体管线的拉流失败(区分会话失效与观看者不匹配)。 +func (app *UIApp) recordPlaybackAssetDenial(request *http.Request, reason string) { + agent := request.UserAgent() + if len(agent) > 120 { + agent = agent[:120] + } + app.downloader.recordDiagnostic(diagnosticEvent{Level: "warn", Event: "playback.asset.denied", + HTTPStatus: http.StatusGone, Message: reason + " ua=" + agent}) +} + func playbackRequestAllowed(writer http.ResponseWriter, request *http.Request, method string) bool { writer.Header().Set("Cache-Control", "private, no-store, no-transform") writer.Header().Set("X-Accel-Buffering", "no") diff --git a/internal/app/ui_playback_native.go b/internal/app/ui_playback_native.go index ad06015..d535975 100644 --- a/internal/app/ui_playback_native.go +++ b/internal/app/ui_playback_native.go @@ -95,10 +95,16 @@ func (app *UIApp) handlePlaybackNativeAsset(writer http.ResponseWriter, request writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "播放请求编号无效"}) return } + viewer := contextViewer(request.Context()) app.playbackMu.Lock() session := app.playbacks[query.Get("session")] - if !viewerOwnsPlayback(request.Context(), session) || session.run != run || session.native == nil { + denied := session == nil || session.run != run || session.native == nil + if !denied && viewer != nil { + denied = !viewerOwnsPlayback(request.Context(), session) + } + if denied { app.playbackMu.Unlock() + app.recordPlaybackAssetDenial(request, "stream-unavailable") writeJSON(writer, http.StatusGone, map[string]string{"error": "此播放流已结束,请重新选择分集"}) return } diff --git a/internal/app/ui_viewer.go b/internal/app/ui_viewer.go index c2706ae..a3e505c 100644 --- a/internal/app/ui_viewer.go +++ b/internal/app/ui_viewer.go @@ -207,6 +207,12 @@ func viewerOwnsPlayback(ctx context.Context, session *playbackSession) bool { return viewer != nil && session != nil && session.viewer == viewer && playbackSourceAllowed(ctx, session) } +// playbackCapabilityPath 判断是否为无需 viewer cookie 的媒体资产路径: +// 这些 URL 中已包含不可猜测的播放会话 ID,可作为独立能力凭证。 +func playbackCapabilityPath(path string) bool { + return strings.HasPrefix(path, "/api/ui/playback/media/") || path == "/api/ui/playback/hls/index.m3u8" || path == "/api/ui/playback/hls/segment.ts" +} + func writeViewerError(writer http.ResponseWriter, status int, code, message string) { writer.Header().Set("Cache-Control", "private, no-store") writer.Header().Add("Vary", "Cookie") @@ -234,6 +240,12 @@ func (app *UIApp) withBrowserViewer(next http.Handler) http.Handler { } identity := manager.identity(request) if account.ID == "" && (identity == "" || accountCookie) { + if playbackCapabilityPath(path) { + // AVPlayer 等原生媒体管线可能不携带 cookie;播放会话 ID 本身即能力凭证, + // 由媒体资产接口自行校验会话与运行编号。 + next.ServeHTTP(writer, request) + return + } writeViewerError(writer, http.StatusUnauthorized, "viewer_required", "请刷新页面,恢复此浏览器的独立观看记录") return }