From 02f1ffed1e7146b87b787b86299b15df578f43b0 Mon Sep 17 00:00:00 2001 From: blurbery <271863866+blurbery@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:14:33 +1000 Subject: [PATCH 1/4] fix(playback): normalize transcoded AAC timestamps --- internal/playback/plan_v3_union_test.go | 20 ++++++++++---------- internal/playback/protocol_v3.go | 2 +- internal/playback/remux_dv_test.go | 2 +- internal/playback/transcode.go | 5 ++++- internal/playback/transcode_args_test.go | 2 +- internal/playback/transformations_v3.go | 4 ++-- internal/playback/transformations_v3_test.go | 8 ++++---- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/internal/playback/plan_v3_union_test.go b/internal/playback/plan_v3_union_test.go index 2e0a52073..d68b90884 100644 --- a/internal/playback/plan_v3_union_test.go +++ b/internal/playback/plan_v3_union_test.go @@ -12,7 +12,7 @@ func staticHLSRegistryV3(registry *TransformationRegistryV3) func() *Transformat func TestTransformationRegistryWithAdvertised(t *testing.T) { registry := NewTransformationRegistryV3([]TransformationSpecV3{ - {Name: "audio_to_aac", RecipeVersion: "2"}, + {Name: "audio_to_aac", RecipeVersion: TransformationAudioToAACRecipeVersionV3}, {Name: "video_to_h264", RecipeVersion: "2"}, {Name: "server_dv7_to_hdr10", RecipeVersion: TransformationServerDV7HDR10RecipeVersionV3, Available: true}, }) @@ -20,7 +20,7 @@ func TestTransformationRegistryWithAdvertised(t *testing.T) { t.Fatal("empty advertisement must return the receiver unchanged") } widened := registry.WithAdvertised([]TransformationV3{ - {Name: "Audio_To_AAC", Executor: "server", RecipeVersion: "2"}, + {Name: "Audio_To_AAC", Executor: "server", RecipeVersion: TransformationAudioToAACRecipeVersionV3}, {Name: "video_to_h264", Executor: "server", RecipeVersion: "1"}, {Name: "made_up_transform", Executor: "server", RecipeVersion: "1"}, }) @@ -39,7 +39,7 @@ func TestTransformationRegistryWithAdvertised(t *testing.T) { if registry.Available("audio_to_aac") { t.Fatal("widening must not mutate the receiver") } - clientOnly := registry.WithAdvertised([]TransformationV3{{Name: "audio_to_aac", Executor: "client", RecipeVersion: "2"}}) + clientOnly := registry.WithAdvertised([]TransformationV3{{Name: "audio_to_aac", Executor: "client", RecipeVersion: TransformationAudioToAACRecipeVersionV3}}) if clientOnly.Available("audio_to_aac") { t.Fatal("client-executor advertisements must not widen server availability") } @@ -58,7 +58,7 @@ func TestPlanPlaybackV3TranscodeOffloadsToNodeToolchain(t *testing.T) { req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}} local := NewTransformationRegistryV3([]TransformationSpecV3{ {Name: "video_to_h264", RecipeVersion: "2"}, - {Name: "audio_to_aac", RecipeVersion: "2"}, + {Name: "audio_to_aac", RecipeVersion: TransformationAudioToAACRecipeVersionV3}, }) settings := PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true} @@ -69,7 +69,7 @@ func TestPlanPlaybackV3TranscodeOffloadsToNodeToolchain(t *testing.T) { union := local.WithAdvertised([]TransformationV3{ {Name: "video_to_h264", Executor: "server", RecipeVersion: "2"}, - {Name: "audio_to_aac", Executor: "server", RecipeVersion: "2"}, + {Name: "audio_to_aac", Executor: "server", RecipeVersion: TransformationAudioToAACRecipeVersionV3}, }) withNodes := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: settings, Registry: local, HLSRegistry: staticHLSRegistryV3(union)}) if withNodes.Plan == nil || withNodes.Plan.Delivery != DeliveryTranscodeHLSV3 { @@ -87,7 +87,7 @@ func TestPlanPlaybackV3AudioAdaptationOffloadsToHLSRemux(t *testing.T) { req := validStartRequestV3() req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}} req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true} - local := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "audio_to_aac", RecipeVersion: "2"}}) + local := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "audio_to_aac", RecipeVersion: TransformationAudioToAACRecipeVersionV3}}) settings := PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true} withoutNodes := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: settings, Registry: local}) @@ -95,7 +95,7 @@ func TestPlanPlaybackV3AudioAdaptationOffloadsToHLSRemux(t *testing.T) { t.Fatalf("without nodes = %s", ExplainPlannerResultV3(withoutNodes)) } - union := local.WithAdvertised([]TransformationV3{{Name: "audio_to_aac", Executor: "server", RecipeVersion: "2"}}) + union := local.WithAdvertised([]TransformationV3{{Name: "audio_to_aac", Executor: "server", RecipeVersion: TransformationAudioToAACRecipeVersionV3}}) offloaded := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: settings, Registry: local, HLSRegistry: staticHLSRegistryV3(union)}) if offloaded.Plan == nil || offloaded.Plan.Delivery != DeliveryRemuxHLSV3 || !offloaded.TranscodeAudio || offloaded.TargetAudioCodec != "aac" { t.Fatalf("with nodes = %s", ExplainPlannerResultV3(offloaded)) @@ -103,7 +103,7 @@ func TestPlanPlaybackV3AudioAdaptationOffloadsToHLSRemux(t *testing.T) { // With the toolchain available locally the progressive remux keeps // priority — offloadability must never demote a local-capable route. - localCapable := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "audio_to_aac", RecipeVersion: "2", Available: true}}) + localCapable := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "audio_to_aac", RecipeVersion: TransformationAudioToAACRecipeVersionV3, Available: true}}) preserved := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: settings, Registry: localCapable, HLSRegistry: staticHLSRegistryV3(localCapable)}) if preserved.Plan == nil || preserved.Plan.Delivery != DeliveryRemuxProgressiveV3 { t.Fatalf("local capable = %s", ExplainPlannerResultV3(preserved)) @@ -145,8 +145,8 @@ func TestPlanPlaybackV3NodeToolchainDoesNotMaskTerminalForProgressiveOnlyClient( req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true} delete(req.ClientPlaybackContext.Deliveries, DeliveryClassHLSV3) - local := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "audio_to_aac", RecipeVersion: "2"}}) - union := local.WithAdvertised([]TransformationV3{{Name: "audio_to_aac", Executor: "server", RecipeVersion: "2"}}) + local := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "audio_to_aac", RecipeVersion: TransformationAudioToAACRecipeVersionV3}}) + union := local.WithAdvertised([]TransformationV3{{Name: "audio_to_aac", Executor: "server", RecipeVersion: TransformationAudioToAACRecipeVersionV3}}) result := PlanPlaybackV3(PlannerInputV3{ Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, diff --git a/internal/playback/protocol_v3.go b/internal/playback/protocol_v3.go index 5009da63f..d4aab65c8 100644 --- a/internal/playback/protocol_v3.go +++ b/internal/playback/protocol_v3.go @@ -188,7 +188,7 @@ const ( TransformationHDRToSDRToneMapV3 = "hdr_to_sdr_tonemap" TransformationVideoToH264RecipeVersionV3 = "2" - TransformationAudioToAACRecipeVersionV3 = "2" + TransformationAudioToAACRecipeVersionV3 = "3" TransformationServerDV7HDR10RecipeVersionV3 = "3" TransformationServerDV8BaseRecipeVersionV3 = "2" TransformationHDRToSDRToneMapRecipeVersionV3 = "1" diff --git a/internal/playback/remux_dv_test.go b/internal/playback/remux_dv_test.go index 80ecdbb29..665718f6f 100644 --- a/internal/playback/remux_dv_test.go +++ b/internal/playback/remux_dv_test.go @@ -72,7 +72,7 @@ func TestBuildRemuxArgsHonorsPlannedAACOutput(t *testing.T) { } func TestBuildRemuxArgsBoostsOnlySurroundToStereoAAC(t *testing.T) { - const wantFilter = "aresample=out_chlayout=stereo,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" + const wantFilter = "aresample=out_chlayout=stereo:async=1,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" tests := []struct { name string transcodeAudio bool diff --git a/internal/playback/transcode.go b/internal/playback/transcode.go index 732bb7b30..b53d87e8b 100644 --- a/internal/playback/transcode.go +++ b/internal/playback/transcode.go @@ -1452,13 +1452,16 @@ func IsAudioToAACStereoDownmixV3(sourceChannels int, targetCodecAudio string, ta (targetAudioChannels == 0 || targetAudioChannels == 2) } -const stereoDownmixBoostFilterV3 = "aresample=out_chlayout=stereo,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" +const stereoDownmixBoostFilterV3 = "aresample=out_chlayout=stereo:async=1,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" // appendStereoDownmixBoostArgs applies the playback downmix policy only after // the source is explicitly rematrixed to stereo. The order matters: limiting // the source channels before FFmpeg sums them would still allow the final // stereo signal to clip. The limiter's input gain is +6.0206 dB; its -2 dBFS // sample ceiling leaves headroom for lossy-codec and inter-sample overshoot. +// async=1 removes sub-frame input timestamp jitter while retaining the source +// clock and first packet timestamp. Without it, fixed-duration AAC packets can +// carry small PTS gaps that Firefox renders as audible zero-fill crackle. func appendStereoDownmixBoostArgs(args []string, sourceChannels, outputChannels int) []string { if sourceChannels <= 2 || outputChannels != 2 { return args diff --git a/internal/playback/transcode_args_test.go b/internal/playback/transcode_args_test.go index ab3a72a32..f1ac43a62 100644 --- a/internal/playback/transcode_args_test.go +++ b/internal/playback/transcode_args_test.go @@ -1617,7 +1617,7 @@ func TestIsAudioToAACStereoDownmixV3RequiresExactRecipeShape(t *testing.T) { } func TestAppendAudioArgsBoostsOnlyEncodedSurroundToStereo(t *testing.T) { - const wantFilter = "aresample=out_chlayout=stereo,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" + const wantFilter = "aresample=out_chlayout=stereo:async=1,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" tests := []struct { name string codec string diff --git a/internal/playback/transformations_v3.go b/internal/playback/transformations_v3.go index d99bcfcbd..bc7c3a602 100644 --- a/internal/playback/transformations_v3.go +++ b/internal/playback/transformations_v3.go @@ -66,7 +66,7 @@ func ProbeTransformationRegistryWithToneMapV3Result(ctx context.Context, ffmpegP registry := NewTransformationRegistryV3([]TransformationSpecV3{ {Name: TransformationServerDV7HDR10V3, RecipeVersion: TransformationServerDV7HDR10RecipeVersionV3, Available: bytes.Contains(bsfs, []byte("dovi_rpu")) && bytes.Contains(bsfs, []byte("filter_units")), RequiredCapability: "ffmpeg_bsf:dovi_rpu+filter_units", PromisedDynamicRange: DynamicRangeHDR10V3, ValidatedClaims: DV7ToHDR10ClaimsV3(), TerminalReason: TerminalDVConversionUnsupportedV3}, {Name: TransformationServerDV8BaseV3, RecipeVersion: TransformationServerDV8BaseRecipeVersionV3, Available: bytes.Contains(bsfs, []byte("dovi_rpu")) && bytes.Contains(bsfs, []byte("filter_units")), RequiredCapability: "ffmpeg_bsf:dovi_rpu+filter_units", ValidatedClaims: DV8ToBaseLayerClaimsV3(""), TerminalReason: TerminalDVConversionUnsupportedV3}, - {Name: TransformationAudioToAACV3, RecipeVersion: TransformationAudioToAACRecipeVersionV3, Available: ffmpegErr == nil && bytes.Contains(encoders, []byte(" aac ")) && audioRecipeErr == nil, RequiredCapability: "ffmpeg_encoder:aac+ffmpeg_filter_smoke:stereo_downmix_limiter_v2", ValidatedClaims: []string{ClaimAudioDecodeV3}, TerminalReason: TerminalAudioConversionUnsupportedV3}, + {Name: TransformationAudioToAACV3, RecipeVersion: TransformationAudioToAACRecipeVersionV3, Available: ffmpegErr == nil && bytes.Contains(encoders, []byte(" aac ")) && audioRecipeErr == nil, RequiredCapability: "ffmpeg_encoder:aac+ffmpeg_filter_smoke:stereo_downmix_limiter_v3", ValidatedClaims: []string{ClaimAudioDecodeV3}, TerminalReason: TerminalAudioConversionUnsupportedV3}, {Name: TransformationVideoToH264V3, RecipeVersion: TransformationVideoToH264RecipeVersionV3, Available: ffmpegErr == nil && h264EncoderAvailableV3(encoders), RequiredCapability: "ffmpeg_encoder:h264", PromisedDynamicRange: DynamicRangeSDRV3, ValidatedClaims: []string{ClaimH264DecodeV3}, TerminalReason: TerminalVideoConversionUnsupportedV3}, {Name: TransformationHDRToSDRToneMapV3, RecipeVersion: TransformationHDRToSDRToneMapRecipeVersionV3, Available: len(toneMapCapabilities) > 0, RequiredCapability: "ffmpeg_filter:hdr_to_sdr_tonemap", PromisedDynamicRange: DynamicRangeSDRV3, ValidatedClaims: []string{ClaimHDRMetadataRemovedV3, ClaimSDRBT709OutputV3}, TerminalReason: TerminalHDRTranscodeUnsupportedV3}, }) @@ -74,7 +74,7 @@ func ProbeTransformationRegistryWithToneMapV3Result(ctx context.Context, ffmpegP } // An ordinary non-zero FFmpeg exit means the installed filter graph is not a -// v2 executor; that is a capability result, not a failed inventory. Process +// v3 executor; that is a capability result, not a failed inventory. Process // startup failures and caller cancellation still make the registry uncacheable. func audioRecipeProbeInfrastructureError(err error) error { var exitErr *exec.ExitError diff --git a/internal/playback/transformations_v3_test.go b/internal/playback/transformations_v3_test.go index 4975c49f9..155c9f2b4 100644 --- a/internal/playback/transformations_v3_test.go +++ b/internal/playback/transformations_v3_test.go @@ -120,9 +120,9 @@ func TestProbeTransformationRegistryV3RequiresBothDV7BaseLayerFilters(t *testing } } -func TestProbeTransformationRegistryV3RequiresVersion2AudioFilterGraph(t *testing.T) { +func TestProbeTransformationRegistryV3RequiresVersion3AudioFilterGraph(t *testing.T) { ffmpeg := filepath.Join(t.TempDir(), "ffmpeg") - // Model an older FFmpeg that lists both filters but rejects one of the v2 + // Model an older FFmpeg that lists both filters but rejects one of the v3 // graph options (notably out_chlayout or alimiter latency compensation). script := "#!/bin/sh\ncase \"$2\" in\n-bsfs) : ;;\n-encoders) echo ' A....D aac AAC' ;;\n-filters) echo ' ... aresample A->A'; echo ' T.C alimiter A->A' ;;\nesac\ncase \" $* \" in\n*\" -f lavfi \"*) exit 1 ;;\nesac\n" if err := os.WriteFile(ffmpeg, []byte(script), 0o755); err != nil { @@ -134,7 +134,7 @@ func TestProbeTransformationRegistryV3RequiresVersion2AudioFilterGraph(t *testin t.Fatalf("unsupported graph should be a cacheable capability result: %v", err) } if registry.Available(TransformationAudioToAACV3) { - t.Fatal("audio_to_aac advertised when the exact version 2 graph was rejected") + t.Fatal("audio_to_aac advertised when the exact version 3 graph was rejected") } script = "#!/bin/sh\ncase \"$2\" in\n-bsfs) : ;;\n-encoders) echo ' A....D aac AAC' ;;\n-filters) echo ' ... aresample A->A'; echo ' T.C alimiter A->A' ;;\nesac\n" @@ -150,5 +150,5 @@ func TestProbeTransformationRegistryV3RequiresVersion2AudioFilterGraph(t *testin return } } - t.Fatal("audio_to_aac was not advertised with the complete version 2 toolchain") + t.Fatal("audio_to_aac was not advertised with the complete version 3 toolchain") } From 6d34fccce7370815033f3aa28155b3d23d3e1a4d Mon Sep 17 00:00:00 2001 From: blurbery <271863866+blurbery@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:17:16 +1000 Subject: [PATCH 2/4] test(playback): cover AAC seek anchors --- internal/playback/remux_dv_test.go | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/internal/playback/remux_dv_test.go b/internal/playback/remux_dv_test.go index 665718f6f..bf9f87862 100644 --- a/internal/playback/remux_dv_test.go +++ b/internal/playback/remux_dv_test.go @@ -5,6 +5,8 @@ import ( "io" "os" "path/filepath" + "slices" + "strconv" "strings" "testing" ) @@ -102,6 +104,39 @@ func TestBuildRemuxArgsBoostsOnlySurroundToStereoAAC(t *testing.T) { } } +func TestBuildRemuxArgsNormalizesAACAcrossSeekAnchors(t *testing.T) { + const wantFilter = "aresample=out_chlayout=stereo:async=1,alimiter=level_in=2:limit=0.794328235:attack=5:release=50:level=false:latency=true" + anchors := []struct { + name string + seek float64 + }{ + {name: "initial start", seek: 0}, + {name: "rewind reanchor", seek: 600}, + {name: "saved resume", seek: 2201.111}, + {name: "forward reanchor", seek: 2800}, + } + + for _, anchor := range anchors { + t.Run(anchor.name, func(t *testing.T) { + args := buildRemuxArgsWithAudioV3("/movie.mkv", "mp4", anchor.seek, true, 0, 0, false, false, 6, 2, 192, false) + if !argsContainPair(args, "-af", wantFilter) { + t.Fatalf("AAC timestamp normalization missing at seek %.3f: %s", anchor.seek, strings.Join(args, " ")) + } + if strings.Contains(strings.Join(args, " "), "first_pts") { + t.Fatalf("seek %.3f reset the source clock instead of preserving its anchor: %s", anchor.seek, strings.Join(args, " ")) + } + if anchor.seek > 0 && (!argsContainPair(args, "-ss", strconv.FormatFloat(anchor.seek, 'f', 3, 64)) || !slices.Contains(args, "-noaccurate_seek")) { + t.Fatalf("seek %.3f lost the copy-video reanchor recipe: %s", anchor.seek, strings.Join(args, " ")) + } + }) + } + + codecCopy := buildRemuxArgsWithAudioV3("/movie.mkv", "mp4", 600, false, 0, 0, false, false, 6, 2, 192, false) + if slices.Contains(codecCopy, "-af") { + t.Fatalf("codec-copy remux unexpectedly received an audio filter: %s", strings.Join(codecCopy, " ")) + } +} + func TestBuildRemuxArgsRequiresVideoForVideoPlans(t *testing.T) { args := buildRemuxArgs("/movie.mkv", "mp4", 0, false, -1, 0, false, false) if !argsContainPair(args, "-map", "0:V:0") || argsContainPair(args, "-map", "0:V:0?") { From 864de191c1fab01a73cc1209c9b5d9ed419dcf13 Mon Sep 17 00:00:00 2001 From: blurbery <271863866+blurbery@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:26:08 +1000 Subject: [PATCH 3/4] test(playback): refresh protocol v3 fixtures --- .../fixtures/valid/capability_response.json | 2 +- .../testdata/protocol_v3/attempt_keys.json | 6 ++-- .../protocol_v3/capability_response.json | 2 +- .../protocol_v3/conformance_matrix.json | 36 +++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/design/schemas/playback-v3/v3/fixtures/valid/capability_response.json b/docs/design/schemas/playback-v3/v3/fixtures/valid/capability_response.json index dbb67004f..39a3b1850 100644 --- a/docs/design/schemas/playback-v3/v3/fixtures/valid/capability_response.json +++ b/docs/design/schemas/playback-v3/v3/fixtures/valid/capability_response.json @@ -28,7 +28,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] diff --git a/internal/playback/testdata/protocol_v3/attempt_keys.json b/internal/playback/testdata/protocol_v3/attempt_keys.json index 65cd65129..e7bcdf728 100644 --- a/internal/playback/testdata/protocol_v3/attempt_keys.json +++ b/internal/playback/testdata/protocol_v3/attempt_keys.json @@ -1,10 +1,10 @@ [ { "name": "hls_burn_in_sorted_transformations_and_pcm_mutations", - "server_plan_attempt_key": "v3:27ee8bdc3ff5b1d7", - "replan_echo": "v3:27ee8bdc3ff5b1d7", + "server_plan_attempt_key": "v3:e139629390ef6e20", + "replan_echo": "v3:e139629390ef6e20", "attempted_plan_keys": [ - "v3:27ee8bdc3ff5b1d7" + "v3:e139629390ef6e20" ], "expected_server_action": "reject_already_attempted_plan" }, diff --git a/internal/playback/testdata/protocol_v3/capability_response.json b/internal/playback/testdata/protocol_v3/capability_response.json index dbb67004f..39a3b1850 100644 --- a/internal/playback/testdata/protocol_v3/capability_response.json +++ b/internal/playback/testdata/protocol_v3/capability_response.json @@ -28,7 +28,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] diff --git a/internal/playback/testdata/protocol_v3/conformance_matrix.json b/internal/playback/testdata/protocol_v3/conformance_matrix.json index f0c0af757..3a0e43b51 100644 --- a/internal/playback/testdata/protocol_v3/conformance_matrix.json +++ b/internal/playback/testdata/protocol_v3/conformance_matrix.json @@ -175,8 +175,8 @@ "outcome": "playable", "delivery": "server_transcode_hls", "decision_reason": "quality_original", - "plan_id": "plan:1496ada4f9ff74754e2f14031f86fb21", - "plan_attempt_key": "v3:4db9d7afc07c1655", + "plan_id": "plan:a8beff7df33c6ca6776a093b840cdea3", + "plan_attempt_key": "v3:2cdebc939dc3cf22", "selected_tracks": { "audio": { "id": "file:42:audio:0", @@ -218,7 +218,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] @@ -1862,8 +1862,8 @@ "outcome": "playable", "delivery": "server_transcode_hls", "decision_reason": "quality_fixed_rung", - "plan_id": "plan:e8ddb06ffa481d3561b07cef06a91ec5", - "plan_attempt_key": "v3:4830f767fa704f38", + "plan_id": "plan:c2695f5ba02dba71753615a263d2367e", + "plan_attempt_key": "v3:4b72643d8afc4436", "selected_tracks": { "audio": { "id": "file:42:audio:0", @@ -1905,7 +1905,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] @@ -2573,8 +2573,8 @@ "outcome": "playable", "delivery": "server_transcode_hls", "decision_reason": "quality_fixed_rung", - "plan_id": "plan:23105e95d0363c4627ab0f60ed0bfa96", - "plan_attempt_key": "v3:8a4c0deb6795be69", + "plan_id": "plan:01ff67650a0ef2592ce739121de6d99a", + "plan_attempt_key": "v3:adc66f1d69acda72", "selected_tracks": { "audio": { "id": "file:42:audio:0", @@ -2616,7 +2616,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] @@ -3603,8 +3603,8 @@ "outcome": "playable", "delivery": "server_transcode_hls", "decision_reason": "quality_fixed_rung", - "plan_id": "plan:48fee96902632f7f3b70e8e5dd0669f3", - "plan_attempt_key": "v3:f7602afa81a3eee0", + "plan_id": "plan:5fb510747d6b4314346fc53132de7fe3", + "plan_attempt_key": "v3:00106a7fb5f5abfd", "selected_tracks": { "audio": { "id": "file:42:audio:0", @@ -3646,7 +3646,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] @@ -3928,8 +3928,8 @@ "outcome": "playable", "delivery": "server_remux_progressive", "decision_reason": "audio_adaptation", - "plan_id": "plan:732df3dcf9a89f51dcca8ab88dcf129d", - "plan_attempt_key": "v3:be1f25f8d9ca6807", + "plan_id": "plan:95f8d4582b61f1420657f490266f621e", + "plan_attempt_key": "v3:314df86bd43f5f34", "selected_tracks": { "audio": { "id": "file:42:audio:0", @@ -3963,7 +3963,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] @@ -4902,8 +4902,8 @@ "outcome": "playable", "delivery": "server_transcode_hls", "decision_reason": "subtitle_burn_in_required", - "plan_id": "plan:3fbb45a597dbfeb7ec62888782109a50", - "plan_attempt_key": "v3:09c05142189931a3", + "plan_id": "plan:ffc560da5dd89610e4e672299601c0f2", + "plan_attempt_key": "v3:2156e83ea26e40a0", "selected_tracks": { "audio": { "id": "file:42:audio:0", @@ -4964,7 +4964,7 @@ { "name": "audio_to_aac", "executor": "server", - "recipe_version": "2", + "recipe_version": "3", "validated_claims": [ "audio_decode" ] From 6d6ddb67e4fb7cd1cef7dbf96f45ecbddd0370a7 Mon Sep 17 00:00:00 2001 From: blurbery <271863866+blurbery@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:36:35 +1000 Subject: [PATCH 4/4] test(playback): follow current AAC recipe version --- internal/api/handlers/playback_v3_union_test.go | 2 +- internal/downloadprepare/transport.go | 2 +- internal/downloadprepare/transport_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/api/handlers/playback_v3_union_test.go b/internal/api/handlers/playback_v3_union_test.go index 8550f7d46..46db7ef88 100644 --- a/internal/api/handlers/playback_v3_union_test.go +++ b/internal/api/handlers/playback_v3_union_test.go @@ -124,7 +124,7 @@ func TestHLSPlanningRegistryV3UnionsPooledNodeCapabilities(t *testing.T) { } writeJSON(w, http.StatusOK, playback.HWAccelInfo{Transformations: []playback.TransformationV3{ {Name: "video_to_h264", Executor: "server", RecipeVersion: "2"}, - {Name: "audio_to_aac", Executor: "server", RecipeVersion: "2"}, + {Name: "audio_to_aac", Executor: "server", RecipeVersion: playback.TransformationAudioToAACRecipeVersionV3}, }}) })) defer remote.Close() diff --git a/internal/downloadprepare/transport.go b/internal/downloadprepare/transport.go index d47bf9a33..402077a5d 100644 --- a/internal/downloadprepare/transport.go +++ b/internal/downloadprepare/transport.go @@ -187,7 +187,7 @@ func (r Request) AudioRecipeRequested() bool { } // StereoDownmixBoostRequested reports whether this is the complete, -// source-sensitive audio_to_aac v2 recipe. Prepared encoded audio uses the +// source-sensitive audio_to_aac recipe. Prepared encoded audio uses the // historical stereo default, so only a known surround source qualifies. func (r Request) StereoDownmixBoostRequested() bool { return r.AudioRecipeVersion == playback.TransformationAudioToAACRecipeVersionV3 && diff --git a/internal/downloadprepare/transport_test.go b/internal/downloadprepare/transport_test.go index 33c794e3f..2dcd0be03 100644 --- a/internal/downloadprepare/transport_test.go +++ b/internal/downloadprepare/transport_test.go @@ -148,7 +148,7 @@ func TestRequestExecutionFingerprintBindsRecipeButNotArtifactHandle(t *testing.T t.Fatal("byte-affecting software decode did not change execution fingerprint") } changed = base - changed.AudioRecipeVersion = "3" + changed.AudioRecipeVersion = base.AudioRecipeVersion + "-changed" if got := changed.ExecutionFingerprint(); got == want { t.Fatal("byte-affecting audio recipe version did not change execution fingerprint") }