Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion internal/jellycompat/audio_selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func (m *testCompatSessionManager) SetTranscodeNodeURL(sessionID, url string) er
}

// SetTranscodeStreamDetails records the execution facts supplied by the compatibility handler.
func (m *testCompatSessionManager) SetTranscodeStreamDetails(sessionID, targetVideoCodec, targetAudioCodec string, transcodeAudio bool, hwAccel string, toneMapMode tonemap.Mode) error {
func (m *testCompatSessionManager) SetTranscodeStreamDetails(sessionID, targetVideoCodec, targetAudioCodec string, transcodeAudio bool, hwAccel string, toneMapMode tonemap.Mode, targetResolution string, targetBitrateKbps int) error {
session, ok := m.sessions[sessionID]
if !ok {
return playback.ErrSessionNotFound
Expand All @@ -159,9 +159,44 @@ func (m *testCompatSessionManager) SetTranscodeStreamDetails(sessionID, targetVi
session.TranscodeAudio = transcodeAudio
session.TranscodeHWAccel = hwAccel
session.ToneMapMode = toneMapMode
session.TargetResolution = targetResolution
session.TargetBitrateKbps = targetBitrateKbps
return nil
}

// TestRecordTranscodeStreamDetailsIncludesTargetResolutionAndBitrate covers
// #920: the admin sessions view fell back to displaying the source's own
// resolution/bitrate for any jellycompat transcode, because
// recordTranscodeStreamDetails never forwarded the encode's actual
// TargetResolution/TargetBitrateKbps onto the upstream session — only the
// native (non-compat) playback path did.
func TestRecordTranscodeStreamDetailsIncludesTargetResolutionAndBitrate(t *testing.T) {
mgr := &testCompatSessionManager{
sessions: map[string]*playback.Session{
"upstream-1": {ID: "upstream-1"},
},
}
h := &PlaybackHandler{sessionMgr: mgr}

h.recordTranscodeStreamDetails(context.Background(), "upstream-1", playback.TranscodeOpts{
TargetCodecVideo: "h264",
TargetCodecAudio: "aac",
TargetResolution: "480p",
TargetBitrateKbps: 420,
})

session, err := mgr.GetSession("upstream-1")
if err != nil {
t.Fatalf("GetSession: %v", err)
}
if session.TargetResolution != "480p" {
t.Errorf("TargetResolution = %q, want %q", session.TargetResolution, "480p")
}
if session.TargetBitrateKbps != 420 {
t.Errorf("TargetBitrateKbps = %d, want %d", session.TargetBitrateKbps, 420)
}
}

type testCompatFileResolver struct {
file *models.MediaFile
}
Expand Down
11 changes: 7 additions & 4 deletions internal/jellycompat/handlers_playback.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ type compatProxyNodeLookup interface {
// Optional (like sessionStarterContext) so lightweight test fakes don't have
// to; without it the session keeps transport-level defaults only.
type transcodeStreamDetailsSetter interface {
SetTranscodeStreamDetails(sessionID, targetVideoCodec, targetAudioCodec string, transcodeAudio bool, hwAccel string, toneMapMode tonemap.Mode) error
SetTranscodeStreamDetails(sessionID, targetVideoCodec, targetAudioCodec string, transcodeAudio bool, hwAccel string, toneMapMode tonemap.Mode, targetResolution string, targetBitrateKbps int) error
}

type nodeRoutingAssignmentSetter interface {
Expand Down Expand Up @@ -207,15 +207,18 @@ func (h *PlaybackHandler) recordNodeRoutingAssignment(ctx context.Context, playS
// transcode onto the upstream session. StartSession only records the transport
// method (play_method "transcode", transcodeAudio false), so without this an
// audio-only re-encode — video copied — syncs to session sync and the admin
// activity views as a full video transcode. Shared by the local
// (ensureTranscodeSession) and remote (startRemoteTranscode) paths.
// activity views as a full video transcode, and a MaxStreamingBitrate-capped
// resolution/bitrate never reaches admin observability at all (see #920) — it
// stays whatever StartSession defaulted to (unset), and the admin view falls
// back to displaying the source's own resolution/bitrate as if uncapped. Shared
// by the local (ensureTranscodeSession) and remote (startRemoteTranscode) paths.
func (h *PlaybackHandler) recordTranscodeStreamDetails(ctx context.Context, upstreamSessionID string, opts playback.TranscodeOpts) {
setter, ok := h.sessionMgr.(transcodeStreamDetailsSetter)
if !ok {
return
}
transcodeAudio := playback.TranscodesAudio(opts.TargetCodecAudio)
if err := setter.SetTranscodeStreamDetails(upstreamSessionID, opts.TargetCodecVideo, opts.TargetCodecAudio, transcodeAudio, opts.HWAccel, opts.ToneMapMode); err != nil {
if err := setter.SetTranscodeStreamDetails(upstreamSessionID, opts.TargetCodecVideo, opts.TargetCodecAudio, transcodeAudio, opts.HWAccel, opts.ToneMapMode, opts.TargetResolution, opts.TargetBitrateKbps); err != nil {
slog.WarnContext(ctx, "record transcode stream details failed", "component", "jellycompat",
"error", err, "playback_session_id", upstreamSessionID)
return
Expand Down
4 changes: 4 additions & 0 deletions internal/jellycompat/remote_transcode_reconstruct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ func (m *lockedCompatSessionManager) SetTranscodeStreamDetails(
transcodeAudio bool,
hwAccel string,
mode tonemap.Mode,
targetResolution string,
targetBitrateKbps int,
) error {
m.mu.Lock()
defer m.mu.Unlock()
Expand All @@ -344,6 +346,8 @@ func (m *lockedCompatSessionManager) SetTranscodeStreamDetails(
m.session.TranscodeAudio = transcodeAudio
m.session.TranscodeHWAccel = hwAccel
m.session.ToneMapMode = mode
m.session.TargetResolution = targetResolution
m.session.TargetBitrateKbps = targetBitrateKbps
return nil
}

Expand Down
6 changes: 3 additions & 3 deletions internal/playback/recipecard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,17 @@ func TestSetTranscodeStreamDetails(t *testing.T) {
m := NewSessionManager(0, 0)
m.RegisterReconstructed(&Session{ID: "sess-1", UserID: 7, PlayMethod: PlayTranscode})

if err := m.SetTranscodeStreamDetails("sess-1", "copy", "aac", true, "qsv", tonemap.ModeHardware); err != nil {
if err := m.SetTranscodeStreamDetails("sess-1", "copy", "aac", true, "qsv", tonemap.ModeHardware, "720p", 4_000); err != nil {
t.Fatalf("SetTranscodeStreamDetails: %v", err)
}
s, err := m.GetSession("sess-1")
if err != nil {
t.Fatalf("GetSession: %v", err)
}
if s.TargetVideoCodec != "copy" || s.TargetAudioCodec != "aac" || !s.TranscodeAudio || s.TranscodeHWAccel != "qsv" || s.ToneMapMode != tonemap.ModeHardware {
if s.TargetVideoCodec != "copy" || s.TargetAudioCodec != "aac" || !s.TranscodeAudio || s.TranscodeHWAccel != "qsv" || s.ToneMapMode != tonemap.ModeHardware || s.TargetResolution != "720p" || s.TargetBitrateKbps != 4_000 {
t.Fatalf("details not recorded: %+v", s)
}
if err := m.SetTranscodeStreamDetails("missing", "h264", "aac", true, "none", tonemap.ModeSoftware); err == nil {
if err := m.SetTranscodeStreamDetails("missing", "h264", "aac", true, "none", tonemap.ModeSoftware, "", 0); err == nil {
t.Fatal("expected ErrSessionNotFound for unknown session")
}
}
Expand Down
13 changes: 8 additions & 5 deletions internal/playback/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1186,11 +1186,12 @@ func (m *SessionManager) RollbackReplacement(sessionID string, rollback SessionR
}

// SetTranscodeStreamDetails records the actual encode decisions of a running
// transcode on the session — video copy vs re-encode, and whether audio is
// re-encoded — together with the confirmed hardware and tone-map executors —
// so session sync and the admin activity views describe what ffmpeg is doing
// rather than relying on requested transport defaults.
func (m *SessionManager) SetTranscodeStreamDetails(sessionID, targetVideoCodec, targetAudioCodec string, transcodeAudio bool, hwAccel string, toneMapMode tonemap.Mode) error {
// transcode on the session — video copy vs re-encode, whether audio is
// re-encoded, and the resolution/bitrate ceiling the encode was capped to —
// together with the confirmed hardware and tone-map executors — so session
// sync and the admin activity views describe what ffmpeg is doing rather
// than relying on requested transport defaults.
func (m *SessionManager) SetTranscodeStreamDetails(sessionID, targetVideoCodec, targetAudioCodec string, transcodeAudio bool, hwAccel string, toneMapMode tonemap.Mode, targetResolution string, targetBitrateKbps int) error {
m.mu.Lock()
defer m.mu.Unlock()

Expand All @@ -1204,6 +1205,8 @@ func (m *SessionManager) SetTranscodeStreamDetails(sessionID, targetVideoCodec,
s.TranscodeAudio = transcodeAudio
s.TranscodeHWAccel = hwAccel
s.ToneMapMode = toneMapMode
s.TargetResolution = targetResolution
s.TargetBitrateKbps = targetBitrateKbps
m.touchSessionLocked(s)
return nil
}
Expand Down
Loading