diff --git a/internal/handler/system_meta.go b/internal/handler/system_meta.go index 9f97157..a488b14 100644 --- a/internal/handler/system_meta.go +++ b/internal/handler/system_meta.go @@ -24,21 +24,14 @@ var startedAt = time.Now() func systemInfoHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { - directOnly := false - if svc.Repo != nil && svc.Repo.Setting != nil { - if v, err := svc.Repo.Setting.Get(c.Request.Context(), service.PlaybackDirectOnlySettingKey); err == nil { - directOnly = service.ParseBoolSetting(v, false) - } - } c.JSON(http.StatusOK, gin.H{ - "name": "MediaStationGo", - "version": "0.1.0", - "go": runtime.Version(), - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "data_dir": svc.Cfg.App.DataDir, - "cache_dir": svc.Cfg.Cache.CacheDir, - "direct_play_only": directOnly, + "name": "MediaStationGo", + "version": "0.1.0", + "go": runtime.Version(), + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "data_dir": svc.Cfg.App.DataDir, + "cache_dir": svc.Cfg.Cache.CacheDir, }) } } diff --git a/internal/service/emby_compat.go b/internal/service/emby_compat.go index b1e6215..b1800bd 100644 --- a/internal/service/emby_compat.go +++ b/internal/service/emby_compat.go @@ -36,11 +36,6 @@ import ( // 该 id 后会把所有派生数据(cookie/收藏/历史)和它绑定。 const embyServerID = "mediastation-go-001" -// PlaybackDirectOnlySettingKey 控制「客户端直连解码」模式:开启后宿主机 -// 不再提供转码,所有播放交给第三方客户端本地解码(direct play / 302 直链), -// 以释放宿主机 CPU 资源。 -const PlaybackDirectOnlySettingKey = "playback.direct_only" - // EmbyService produces Emby-shaped JSON. type EmbyService struct { cfg *config.Config @@ -649,7 +644,7 @@ func (e *EmbyService) itemPayload(m *model.Media, fav bool, posMs int64) map[str "Played": played, "PlayedPercentage": pct, }, - "MediaSources": []map[string]any{e.mediaSource(m, true, false)}, + "MediaSources": []map[string]any{e.mediaSource(m, true)}, } } @@ -1154,24 +1149,11 @@ func (e *EmbyService) PlaybackInfo(ctx context.Context, mediaID, userID string) return nil, err } return map[string]any{ - "MediaSources": []map[string]any{e.mediaSource(m, false, e.directPlayOnly(ctx))}, + "MediaSources": []map[string]any{e.mediaSource(m, false)}, "PlaySessionId": fmt.Sprintf("%s-%d", m.ID, time.Now().Unix()), }, nil } -// directPlayOnly reports whether the admin enabled「客户端直连解码」mode. -// In that mode the host never transcodes; clients must direct-play. -func (e *EmbyService) directPlayOnly(ctx context.Context) bool { - if e.repo == nil || e.repo.Setting == nil { - return false - } - v, err := e.repo.Setting.Get(ctx, PlaybackDirectOnlySettingKey) - if err != nil { - return false - } - return parseBoolSetting(v, false) -} - func (e *EmbyService) playableMedia(ctx context.Context, id, userID string) (*model.Media, error) { if season, ok, err := e.findSeasonGroup(ctx, id, userID); err != nil { return nil, err @@ -1198,7 +1180,7 @@ func (e *EmbyService) playableMedia(ctx context.Context, id, userID string) (*mo // asEmbedded=true:嵌在 /Items 列表里,不包含完整 stream URL(避免暴露 // 直链给搜索接口)。/PlaybackInfo 走 false 路径,URL 指向 Emby 兼容 // /Videos/{id}/stream(客户端会继续携带 X-Emby-Token 或 append api_key)。 -func (e *EmbyService) mediaSource(m *model.Media, asEmbedded, directOnly bool) map[string]any { +func (e *EmbyService) mediaSource(m *model.Media, asEmbedded bool) map[string]any { src := map[string]any{ "Id": m.ID, "Name": m.Title, @@ -1208,7 +1190,7 @@ func (e *EmbyService) mediaSource(m *model.Media, asEmbedded, directOnly bool) m "Protocol": "Http", "Type": "Default", "IsRemote": false, - "SupportsTranscoding": !directOnly, + "SupportsTranscoding": true, "SupportsDirectStream": true, "SupportsDirectPlay": true, "SupportsProbing": true, @@ -1217,11 +1199,7 @@ func (e *EmbyService) mediaSource(m *model.Media, asEmbedded, directOnly bool) m } if !asEmbedded { src["DirectStreamUrl"] = "/Videos/" + m.ID + "/stream" - // 直连解码模式下不下发 TranscodingUrl,迫使客户端本地解码直连, - // 宿主机不参与转码。 - if !directOnly { - src["TranscodingUrl"] = "/Videos/" + m.ID + "/stream" - } + src["TranscodingUrl"] = "/Videos/" + m.ID + "/stream" } if strings.TrimSpace(m.STRMURL) != "" { // STRM 重定向:客户端直接拉远端,跳过我们这一层。 diff --git a/internal/service/emby_compat_test.go b/internal/service/emby_compat_test.go index 8676119..bb4cad8 100644 --- a/internal/service/emby_compat_test.go +++ b/internal/service/emby_compat_test.go @@ -202,50 +202,6 @@ func TestEmbyHidesAdultLibrariesForUserLock(t *testing.T) { } } -func TestEmbyPlaybackInfoRespectsDirectPlayOnly(t *testing.T) { - svc := newTestEmbyService(t) - lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true} - if err := svc.repo.Library.Create(t.Context(), &lib); err != nil { - t.Fatalf("create library: %v", err) - } - media := model.Media{Base: model.Base{ID: "m-1"}, LibraryID: lib.ID, Title: "Inception", Path: `/media/movies/inception.mkv`} - if err := svc.repo.DB.Create(&media).Error; err != nil { - t.Fatalf("create media: %v", err) - } - - // 默认(关闭):宿主机可转码,下发 TranscodingUrl。 - pb, err := svc.PlaybackInfo(t.Context(), "m-1", "user-1") - if err != nil { - t.Fatalf("playback info: %v", err) - } - src := pb["MediaSources"].([]map[string]any)[0] - if src["SupportsTranscoding"] != true { - t.Fatalf("expected SupportsTranscoding=true by default, got %#v", src["SupportsTranscoding"]) - } - if _, ok := src["TranscodingUrl"]; !ok { - t.Fatalf("expected TranscodingUrl present by default: %#v", src) - } - - // 开启「客户端直连解码」:不再下发转码能力 / TranscodingUrl,仍保留 DirectStream。 - if err := svc.repo.Setting.Set(t.Context(), PlaybackDirectOnlySettingKey, "true"); err != nil { - t.Fatalf("enable direct-only: %v", err) - } - pb, err = svc.PlaybackInfo(t.Context(), "m-1", "user-1") - if err != nil { - t.Fatalf("playback info (direct-only): %v", err) - } - src = pb["MediaSources"].([]map[string]any)[0] - if src["SupportsTranscoding"] != false { - t.Fatalf("expected SupportsTranscoding=false in direct-only mode, got %#v", src["SupportsTranscoding"]) - } - if _, ok := src["TranscodingUrl"]; ok { - t.Fatalf("expected no TranscodingUrl in direct-only mode: %#v", src) - } - if src["SupportsDirectPlay"] != true || src["DirectStreamUrl"] != "/Videos/m-1/stream" { - t.Fatalf("direct-only must still allow direct play: %#v", src) - } -} - func newTestEmbyService(t *testing.T) *EmbyService { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) diff --git a/internal/service/runtime_settings.go b/internal/service/runtime_settings.go index fb77cca..8ffea2a 100644 --- a/internal/service/runtime_settings.go +++ b/internal/service/runtime_settings.go @@ -75,12 +75,6 @@ func ApplyRuntimeSetting(cfg *config.Config, key, value string) { } } -// ParseBoolSetting is the exported variant of parseBoolSetting for handlers -// that need to interpret persisted on/off settings (accepts zh + en tokens). -func ParseBoolSetting(value string, fallback bool) bool { - return parseBoolSetting(value, fallback) -} - func parseBoolSetting(value string, fallback bool) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "yes", "on", "enabled", "启用", "开启": diff --git a/internal/service/stream.go b/internal/service/stream.go index 296e866..1596511 100644 --- a/internal/service/stream.go +++ b/internal/service/stream.go @@ -54,20 +54,6 @@ func NewStreamService(cfg *config.Config, log *zap.Logger, repo *repository.Cont // ErrMediaNotFound is returned when the media row or its file is missing. var ErrMediaNotFound = errors.New("media not found") -// directPlayOnly reports whether the admin enabled「客户端直连解码」mode, -// in which the host never transcodes (HLS is refused) and all playback is -// handled by the client (direct play / 302 redirect). -func (s *StreamService) directPlayOnly(ctx context.Context) bool { - if s.repo == nil || s.repo.Setting == nil { - return false - } - v, err := s.repo.Setting.Get(ctx, PlaybackDirectOnlySettingKey) - if err != nil { - return false - } - return parseBoolSetting(v, false) -} - // ServeFile streams the file backing the given media ID using // http.ServeContent so HEAD / Range / If-Modified-Since are handled for free. // @@ -105,11 +91,6 @@ func (s *StreamService) ServeFile(w http.ResponseWriter, r *http.Request, mediaI // ServeHLSPlaylist makes sure a transcode is running and writes the m3u8. // We block (with a 30s timeout) until the playlist file shows up. func (s *StreamService) ServeHLSPlaylist(w http.ResponseWriter, r *http.Request, mediaID string) error { - // 「客户端直连解码」模式下宿主机不提供转码,HLS 一律拒绝, - // 迫使播放器走 direct play 本地解码。 - if s.directPlayOnly(r.Context()) { - return ErrTranscodeDisabled - } if _, err := s.transcoder.EnsureJob(r.Context(), mediaID); err != nil { return err } diff --git a/web/src/api/system.ts b/web/src/api/system.ts index e6506dc..14e1566 100644 --- a/web/src/api/system.ts +++ b/web/src/api/system.ts @@ -13,7 +13,6 @@ export const systemAPI = { arch: string data_dir: string cache_dir: string - direct_play_only?: boolean }>('/system/info') .then((r) => r.data), diff --git a/web/src/pages/PlayerPage.tsx b/web/src/pages/PlayerPage.tsx index 352ea3f..563b09a 100644 --- a/web/src/pages/PlayerPage.tsx +++ b/web/src/pages/PlayerPage.tsx @@ -8,7 +8,6 @@ import { mediaAPI } from '../api/library' import { api, hlsURL, streamURL } from '../api/client' import { playbackAPI } from '../api/playback' import { subtitlesAPI, type SubtitleTrack } from '../api/subtitles' -import { systemAPI } from '../api/system' import type { Media } from '../types' type Mode = 'direct' | 'hls' @@ -38,16 +37,6 @@ export function PlayerPage() { const [subs, setSubs] = useState([]) const [hlsUnavailable, setHlsUnavailable] = useState(false) const [playerError, setPlayerError] = useState('') - // 「客户端直连解码」模式:宿主机不转码,播放器强制 direct play、隐藏 HLS 切换。 - const [directOnly, setDirectOnly] = useState(false) - - // 读取宿主机的「直连解码」开关。开启时全程 direct play,不走 HLS。 - useEffect(() => { - systemAPI - .info() - .then((info) => setDirectOnly(Boolean(info.direct_play_only))) - .catch(() => setDirectOnly(false)) - }, []) // Load metadata and pick a default mode. useEffect(() => { @@ -56,15 +45,14 @@ export function PlayerPage() { setMedia(m) const forced = params.get('mode') as Mode | null const auto = pickMode(m) - // 直连解码模式下忽略 ?mode=hls 与自动判定,始终 direct play。 - setMode(directOnly ? 'direct' : (forced ?? auto)) + setMode(forced ?? auto) setPlayerError('') }) subtitlesAPI .list(id) .then((tracks) => setSubs(tracks ?? [])) .catch(() => setSubs([])) - }, [id, params, directOnly]) + }, [id, params]) // Wire up the actual