Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/playback_v3_union_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion internal/downloadprepare/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
2 changes: 1 addition & 1 deletion internal/downloadprepare/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
20 changes: 10 additions & 10 deletions internal/playback/plan_v3_union_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ 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},
})
if got := registry.WithAdvertised(nil); got != registry {
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"},
})
Expand All @@ -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")
}
Expand All @@ -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}

Expand All @@ -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 {
Expand All @@ -87,23 +87,23 @@ 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})
if withoutNodes.Terminal == nil || withoutNodes.Terminal.Reason != "audio_conversion_unsupported" {
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))
}

// 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))
Expand Down Expand Up @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion internal/playback/protocol_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const (
TransformationHDRToSDRToneMapV3 = "hdr_to_sdr_tonemap"

TransformationVideoToH264RecipeVersionV3 = "2"
TransformationAudioToAACRecipeVersionV3 = "2"
TransformationAudioToAACRecipeVersionV3 = "3"
TransformationServerDV7HDR10RecipeVersionV3 = "3"
TransformationServerDV8BaseRecipeVersionV3 = "2"
TransformationHDRToSDRToneMapRecipeVersionV3 = "1"
Expand Down
37 changes: 36 additions & 1 deletion internal/playback/remux_dv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"io"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"testing"
)
Expand Down Expand Up @@ -72,7 +74,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
Expand Down Expand Up @@ -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?") {
Expand Down
6 changes: 3 additions & 3 deletions internal/playback/testdata/protocol_v3/attempt_keys.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down
36 changes: 18 additions & 18 deletions internal/playback/testdata/protocol_v3/conformance_matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -218,7 +218,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1905,7 +1905,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -2616,7 +2616,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -3646,7 +3646,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -3963,7 +3963,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -4964,7 +4964,7 @@
{
"name": "audio_to_aac",
"executor": "server",
"recipe_version": "2",
"recipe_version": "3",
"validated_claims": [
"audio_decode"
]
Expand Down
5 changes: 4 additions & 1 deletion internal/playback/transcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/playback/transcode_args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading