mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-09 17:18:45 +00:00
fix(transcoding): honor player forced format on the WebUI transcode flow (#5613)
Some checks failed
POEditor export / push-translations (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Some checks failed
POEditor export / push-translations (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
* feat(stream): add ClientInfo.ForceFormat for browser-aware forced format Restricts the client to a forced transcoding format and suppresses direct play, but only when the client declares it supports that format. Part of #5583. * fix(transcoding): honor player forced format on getTranscodeDecision When the WebUI player has a forced transcoding format configured and the browser declares it can play that format, transcode to it (suppressing direct play). Fall back to normal negotiation with a warning when the format is unsupported. The MaxBitRate cap still applies on top. Fixes #5583. * test(e2e): cover player forced format on getTranscodeDecision Forced format honored when the client supports it, falls back to negotiation otherwise, and the MaxBitRate cap still applies on top. Part of #5583. * feat(ui): remove obsolete 'format ignored' helper text on player form The web player now honors the forced transcoding format, so the caveat added in #5611 no longer applies. Reverts the Transcoding field to a plain selector. Part of #5583.
This commit is contained in:
parent
c4c70519b5
commit
08a027dbcc
9 changed files with 234 additions and 86 deletions
|
|
@ -159,11 +159,22 @@ var _ = Describe("Transcode Endpoints", Ordered, func() {
|
|||
Expect(ds.Player(ctx).Put(player)).To(Succeed())
|
||||
}
|
||||
|
||||
setPlayerForcedFormat := func(format string) {
|
||||
doReq("ping")
|
||||
player, err := ds.Player(ctx).FindMatch(adminUser.ID, "test-client", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
trc, err := ds.Transcoding(ctx).FindByFormat(format)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
player.TranscodingId = trc.ID
|
||||
Expect(ds.Player(ctx).Put(player)).To(Succeed())
|
||||
}
|
||||
|
||||
AfterEach(func() {
|
||||
// Reset player MaxBitRate to 0 after each test
|
||||
player, err := ds.Player(ctx).FindMatch(adminUser.ID, "test-client", "")
|
||||
if err == nil {
|
||||
player.MaxBitRate = 0
|
||||
player.TranscodingId = ""
|
||||
_ = ds.Player(ctx).Put(player)
|
||||
}
|
||||
})
|
||||
|
|
@ -509,6 +520,43 @@ var _ = Describe("Transcode Endpoints", Ordered, func() {
|
|||
Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(192000)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("player forced format", func() {
|
||||
It("transcodes a FLAC to the forced opus format when the client supports it", func() {
|
||||
setPlayerForcedFormat("opus")
|
||||
|
||||
resp := doPostReq("getTranscodeDecision", opusTranscodeClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||
Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("opus"))
|
||||
})
|
||||
|
||||
It("falls back to negotiation when the client does not support the forced format", func() {
|
||||
setPlayerForcedFormat("opus")
|
||||
|
||||
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||
Expect(resp.TranscodeDecision.TranscodeStream.Container).To(Equal("mp3"))
|
||||
})
|
||||
|
||||
It("applies maxBitRate on top of the forced format", func() {
|
||||
setPlayerForcedFormat("opus")
|
||||
setPlayerMaxBitRate(96)
|
||||
|
||||
resp := doPostReq("getTranscodeDecision", opusTranscodeClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||
Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("opus"))
|
||||
Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(96000)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getTranscodeStream", func() {
|
||||
|
|
|
|||
|
|
@ -279,6 +279,19 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
|
|||
return stream.IsAACCodec(p.Container)
|
||||
})
|
||||
|
||||
// Honor the player's forced transcoding format, falling back to normal
|
||||
// negotiation when the client can't play it (issue #5583).
|
||||
if trc, ok := request.TranscodingFrom(ctx); ok && trc.TargetFormat != "" {
|
||||
if !clientInfo.ForceFormat(trc.TargetFormat) {
|
||||
clientName := clientInfo.Name
|
||||
if player, ok := request.PlayerFrom(ctx); ok && player.Client != "" {
|
||||
clientName = player.Client
|
||||
}
|
||||
log.Debug(ctx, "Player forced format not supported by client; falling back to negotiation",
|
||||
"forcedFormat", trc.TargetFormat, "client", clientName)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the player's MaxBitRate as a ceiling on the client's declared
|
||||
// limits (issue #5583). Both fields are capped because the client sends
|
||||
// them independently here; capping only MaxAudioBitrate would let an
|
||||
|
|
|
|||
|
|
@ -305,6 +305,73 @@ var _ = Describe("Transcode endpoints", func() {
|
|||
Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(320))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("player forced format", func() {
|
||||
withForcedFormat := func(r *http.Request, format string, maxBitRate int) *http.Request {
|
||||
ctx := r.Context()
|
||||
ctx = request.WithTranscoding(ctx, model.Transcoding{TargetFormat: format})
|
||||
if maxBitRate > 0 {
|
||||
ctx = request.WithPlayer(ctx, model.Player{Client: "NavidromeUI", MaxBitRate: maxBitRate})
|
||||
}
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
mockMFRepo.SetData(model.MediaFiles{
|
||||
{ID: "song-1", Suffix: "flac", Codec: "FLAC", BitRate: 900, Channels: 2, SampleRate: 44100},
|
||||
})
|
||||
mockTD.decision = &stream.TranscodeDecision{MediaID: "song-1", CanTranscode: true}
|
||||
mockTD.token = "token"
|
||||
})
|
||||
|
||||
It("forces a supported format and clears direct play", func() {
|
||||
body := `{"directPlayProfiles":[{"containers":["flac"],"audioCodecs":["flac"],"protocols":["http"]}],
|
||||
"transcodingProfiles":[{"container":"ogg","audioCodec":"opus","protocol":"http"},
|
||||
{"container":"mp3","audioCodec":"mp3","protocol":"http"}]}`
|
||||
r := withForcedFormat(newJSONPostRequest("mediaId=song-1&mediaType=song", body), "opus", 0)
|
||||
|
||||
_, err := router.GetTranscodeDecision(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(1))
|
||||
Expect(mockTD.capturedClient.TranscodingProfiles[0].AudioCodec).To(Equal("opus"))
|
||||
Expect(mockTD.capturedClient.DirectPlayProfiles).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("falls back to negotiation when the forced format is unsupported", func() {
|
||||
// Forced format is opus, but the client only declares mp3 and flac.
|
||||
// Should fall back to negotiating among the client's own profiles.
|
||||
body := `{"directPlayProfiles":[{"containers":["flac"],"audioCodecs":["flac"],"protocols":["http"]}],
|
||||
"transcodingProfiles":[
|
||||
{"container":"flac","audioCodec":"flac","protocol":"http"},
|
||||
{"container":"mp3","audioCodec":"mp3","protocol":"http"}]}`
|
||||
r := withForcedFormat(newJSONPostRequest("mediaId=song-1&mediaType=song", body), "opus", 0)
|
||||
|
||||
_, err := router.GetTranscodeDecision(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Profiles left intact for normal negotiation (forced format not applied).
|
||||
Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(2))
|
||||
Expect(mockTD.capturedClient.DirectPlayProfiles).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("applies the maxBitRate cap on top of the forced format", func() {
|
||||
// Client supports opus + mp3; forced format opus must be selected,
|
||||
// and the maxBitRate cap applied on top.
|
||||
body := `{"transcodingProfiles":[
|
||||
{"container":"ogg","audioCodec":"opus","protocol":"http"},
|
||||
{"container":"mp3","audioCodec":"mp3","protocol":"http"}]}`
|
||||
r := withForcedFormat(newJSONPostRequest("mediaId=song-1&mediaType=song", body), "opus", 128)
|
||||
|
||||
_, err := router.GetTranscodeDecision(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(1))
|
||||
Expect(mockTD.capturedClient.TranscodingProfiles[0].AudioCodec).To(Equal("opus"))
|
||||
Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(128))
|
||||
Expect(mockTD.capturedClient.MaxTranscodingAudioBitrate).To(Equal(128))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetTranscodeStream", func() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue