feat(playback): client direct-play decode mode (release host transcoding)

Add an admin toggle playback.direct_only that offloads all decoding to the
client. When enabled:
- Emby PlaybackInfo no longer advertises SupportsTranscoding nor a
  TranscodingUrl, forcing Emby/Infuse/Yamby clients to direct play.
- HLS endpoints refuse (ErrTranscodeDisabled) so the host never spawns ffmpeg.
- Web player forces direct play, hides the HLS toggle, and surfaces a hint.
Exposed via /system/info (direct_play_only) and the Settings page.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
soldosluka857
2026-05-30 11:08:59 +00:00
committed by Shuke
parent 48aa08fc94
commit e50282178d
8 changed files with 166 additions and 36 deletions
+44
View File
@@ -202,6 +202,50 @@ 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{})