From ffddcaee73221b8b4d6e8eb84e5639dbc4e6685f Mon Sep 17 00:00:00 2001 From: Vulcan Date: Sun, 27 Sep 2026 07:07:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(cli):=20CLI=20=E5=9C=A8=20Server=20?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E8=BE=BE/worker=20=E8=A2=AB=E6=9D=80?= =?UTF-8?q?=E6=97=B6=E9=9D=99=E9=BB=98=E5=9B=9E=E9=80=80=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=20ffmpeg=EF=BC=88=E6=97=A0=E5=A4=B1=E8=B4=A5=E6=97=A0=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 能力查询标志(-encoders/-decoders/-codecs/-hwaccels/-filters/-pix_fmts/-formats)在 Server 不可达、令牌错误或返回错误时会静默改跑本地 ffmpeg 且 rc=0:实测 Server 停机时 `rffmpeg -encoders` 输出本机 236 行编码器列表、stderr 无任何提示;对存活 Server 用错 令牌(401)时同样静默输出本机列表。这些标志回答的是**集群**(Worker)的能力,与 -buildconf/-layouts 这类本地查询标志定位不同;用户据此挑编码器时会把本机能力误当成 集群能力。README 与 usage 也都把这类标志描述为「queries server」,认证一节还明确承诺 401 时直接退出、不回退本地 ffmpeg——实现与文档、定位三者不一致。 改为默认 fail-fast:Server 无法应答时打印错误行与一行提示(指向显式 opt-in),以非零 码退出,stdout 不输出任何能力列表;确需本机列表时设置 RFFMPEG_LOCAL_FALLBACK=1,回退 前打印 `[rffmpeg] local fallback: <标志> server query failed (…); listing LOCAL ffmpeg capabilities` 告警行(走 stderr,--json 下 stdout 仍是单份 JSON)。顺带去掉 -codecs 的 goto,改为顺序短路的错误处理。 转码路径未改:提交前健康检查自始 fail-fast,本地 ffmpeg 从不参与转码——Server 停机时 提交立即 rc=1 且无产物,无 worker 时提交被拒(worker_unavailable)。 Co-authored-by: Vulcan --- README.md | 3 + cmd/cli/main.go | 136 +++++++++++++-------- cmd/cli/main_test.go | 282 ++++++++++++++----------------------------- 3 files changed, 184 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index afdcb73..a0fff7f 100644 --- a/README.md +++ b/README.md @@ -448,8 +448,11 @@ CLI 配置文件搜索顺序(优先级从高到低): | `RFFMPEG_MAX_RETRIES` | WS/HTTP 重试次数上限(连接中断后) | `14`(约 5 分钟) | | `RFFMPEG_SERVER_LOSS_TIMEOUT` | 已提交任务等待期间 Server 断连的快速失败预算(`0` = 关闭该预算) | `5s` | | `RFFMPEG_POLL_TIMEOUT` | 等待 Worker(pending)阶段的最长轮询时长(`0` = 不设上限) | `10m` | +| `RFFMPEG_LOCAL_FALLBACK` | 允许能力查询(`-encoders`/`-decoders`/`-codecs`/`-hwaccels`/`-filters`/`-pix_fmts`/`-formats`)在 Server 无法应答时改列本地 ffmpeg 能力(`1`/`true` 开启,并打印 `[rffmpeg] local fallback:` 告警行) | 未设置(Server 无法应答即失败退出) | | `RFFMPEG_LOG_FILE` | 提交前失败诊断日志路径(追加写入,未设置则禁用) | - | +**能力查询在 Server 无法应答时失败退出**:`-encoders`/`-decoders`/`-codecs`/`-hwaccels`/`-filters`/`-pix_fmts`/`-formats` 回答的是**集群**(Worker)的能力,与 `-buildconf`/`-layouts` 等本地查询标志定位不同。因此当 Server 不可达、令牌错误或返回错误时,CLI 打印错误并以非零码退出,**不会静默改用本地 ffmpeg 的能力列表**——否则用户会把本机能力误当成集群能力(例如据此选定一个集群并不支持的编码器)。确需本机列表时:直接运行 `ffmpeg -encoders` 等本地命令,或设置 `RFFMPEG_LOCAL_FALLBACK=1` 显式开启回退(回退时会打印 `[rffmpeg] local fallback: …; listing LOCAL ffmpeg capabilities` 告警行)。 + **未配置 Server URL 时的连接失败提示**:`--server`、`RFFMPEG_SERVER_URL` 与配置文件都没有提供 Server URL 时,CLI 使用默认 `http://localhost:8080`;此时若健康检查失败,除错误信息外会额外打印一行提示(说明当前用的是默认 URL,并提示设置 `RFFMPEG_SERVER_URL` 或 `--server`),避免首次使用只看到 `connection refused` 而无从判断。显式指定过 URL(命令行、环境变量、配置文件任一)时不打印该提示。 **提交前失败诊断日志(`RFFMPEG_LOG_FILE`)**:设置该变量指向一个可写文件后,CLI 会在「提交作业之前」以非零退出码退出时,把一条诊断记录追加写入该文件——包含时间戳、进程 PID、退出码、Server URL、命令行(令牌已脱敏)以及本次运行捕获的 stderr 内容。这用于诊断「CLI 在提交前即退出、Server 与 Worker 均无对应 job 记录」的偶发故障:此类故障无服务端线索,仅靠 CLI 侧的 stderr 才能定位。默认不设置(零开销、不改动 stderr 与文件系统),仅当需要诊断时按需开启。日志文件以 `0600` 权限创建,避免令牌(若未来扩展写入)被其他用户读取。 diff --git a/cmd/cli/main.go b/cmd/cli/main.go index c24dec0..7770d61 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -1734,9 +1734,30 @@ func runProbe(cli *client.Client, input string, quiet bool, sharedFS bool) int { return runProbeRequest(cli, fileID, quiet) } +// localFallbackEnv opts into answering a server-backed capability query from +// local ffmpeg when the server cannot answer it (see localFallbackAllowed). +const localFallbackEnv = "RFFMPEG_LOCAL_FALLBACK" + +// localFallbackAllowed decides what a server-backed capability query does after +// the server failed to answer it. The capability list describes the cluster the +// job will run on, so answering with the local machine's list is a substitution +// the caller must ask for: without the opt-in the query fails, and with it the +// substitution is announced on stderr. Returns true when the caller should run +// local ffmpeg. +func localFallbackAllowed(flag string, serverErr error) bool { + switch os.Getenv(localFallbackEnv) { + case "1", "true": + fmt.Fprintf(os.Stderr, "[rffmpeg] local fallback: %s server query failed (%v); listing LOCAL ffmpeg capabilities\n", flag, serverErr) + return true + } + fmt.Fprintf(os.Stderr, "Error: %s server query failed: %v\n", flag, serverErr) + fmt.Fprintf(os.Stderr, "Hint: this list comes from the cluster; set %s=1 to list the LOCAL ffmpeg's capabilities instead.\n", localFallbackEnv) + return false +} + // runEncoders queries encoders from the server and prints in ffmpeg-compatible format. // When jsonOut is true, outputs JSON instead of text. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runEncoders(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { @@ -1756,8 +1777,11 @@ func runEncoders(serverURL, token string, jsonOut bool) int { } return ExitSuccess } + err = apiErr + } + if !localFallbackAllowed("-encoders", err) { + return ExitError } - // Server unavailable — fall back to local ffmpeg if !jsonOut { return runLocalFfmpegPassthrough("-encoders") } @@ -1772,7 +1796,7 @@ func runEncoders(serverURL, token string, jsonOut bool) int { // runDecoders queries decoders from the server and prints in ffmpeg-compatible format. // When jsonOut is true, outputs JSON instead of text. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runDecoders(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { @@ -1792,8 +1816,11 @@ func runDecoders(serverURL, token string, jsonOut bool) int { } return ExitSuccess } + err = apiErr + } + if !localFallbackAllowed("-decoders", err) { + return ExitError } - // Server unavailable — fall back to local ffmpeg if !jsonOut { return runLocalFfmpegPassthrough("-decoders") } @@ -1818,52 +1845,46 @@ func runDecoders(serverURL, token string, jsonOut bool) int { // runCodecsFromServer queries both encoders and decoders from the server and prints in // ffmpeg-compatible format: encoder list first, then decoder list. // When jsonOut is true, outputs JSON instead of text. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runCodecsFromServer(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { - if jsonOut { - encoders, apiErr1 := cli.ListAllEncoders() - if apiErr1 != nil { - goto fallback - } - decoders, apiErr2 := cli.ListAllDecoders() - if apiErr2 != nil { - goto fallback - } - return outputInfoFlagJSON("codecs", encoders, decoders) + var encoders []protocol.EncoderInfo + var decoders []protocol.DecoderInfo + encoders, err = cli.ListAllEncoders() + if err == nil { + decoders, err = cli.ListAllDecoders() } - - encoders, apiErr := cli.ListAllEncoders() - if apiErr == nil { - decoders, apiErr2 := cli.ListAllDecoders() - if apiErr2 == nil { - printCodecsHeader("Encoders") - if len(encoders) > 0 { - for _, enc := range encoders { - capFlags := encoderCapabilityFlags(enc.Type) - fmt.Printf("%s %-22s %s\n", capFlags, enc.Name, enc.Description) - } - } else { - fmt.Fprintf(os.Stderr, "No encoders available\n") + if err == nil { + if jsonOut { + return outputInfoFlagJSON("codecs", encoders, decoders) + } + printCodecsHeader("Encoders") + if len(encoders) > 0 { + for _, enc := range encoders { + capFlags := encoderCapabilityFlags(enc.Type) + fmt.Printf("%s %-22s %s\n", capFlags, enc.Name, enc.Description) } + } else { + fmt.Fprintf(os.Stderr, "No encoders available\n") + } - printCodecsHeader("Decoders") - if len(decoders) > 0 { - for _, dec := range decoders { - capFlags := decoderCapabilityFlags(dec.Type) - fmt.Printf("%s %-22s %s\n", capFlags, dec.Name, dec.Description) - } - } else { - fmt.Fprintf(os.Stderr, "No decoders available\n") + printCodecsHeader("Decoders") + if len(decoders) > 0 { + for _, dec := range decoders { + capFlags := decoderCapabilityFlags(dec.Type) + fmt.Printf("%s %-22s %s\n", capFlags, dec.Name, dec.Description) } - return ExitSuccess + } else { + fmt.Fprintf(os.Stderr, "No decoders available\n") } + return ExitSuccess } } -fallback: - // Server unavailable — fall back to local ffmpeg + if !localFallbackAllowed("-codecs", err) { + return ExitError + } if !jsonOut { return runLocalFfmpegPassthrough("-codecs") } @@ -1904,7 +1925,8 @@ func runLocalFfmpegInfo(flag string) int { return ExitSuccess } -// === Local ffmpeg fallback helpers for P0/P1 info flags === +// === Local ffmpeg fallback helpers for P0/P1 info flags (opt-in, see +// localFallbackAllowed) === // runLocalFfmpegOutput runs a local ffmpeg with the given flag, capturing stdout // and discarding stderr. Returns the combined output string. @@ -2128,7 +2150,7 @@ func jsonEncodeToStdout(enc *json.Encoder, v interface{}) int { // runHwaccels queries hwaccels from the server and prints in ffmpeg-compatible format (single-column list). // When jsonOut is true, outputs JSON instead of text. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runHwaccels(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { @@ -2146,8 +2168,11 @@ func runHwaccels(serverURL, token string, jsonOut bool) int { } return ExitSuccess } + err = apiErr + } + if !localFallbackAllowed("-hwaccels", err) { + return ExitError } - // Server unavailable — fall back to local ffmpeg if !jsonOut { return runLocalFfmpegPassthrough("-hwaccels") } @@ -2161,7 +2186,7 @@ func runHwaccels(serverURL, token string, jsonOut bool) int { } // runFilters queries filters from the server and prints in ffmpeg-compatible format. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runFilters(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { @@ -2180,8 +2205,11 @@ func runFilters(serverURL, token string, jsonOut bool) int { } return ExitSuccess } + err = apiErr + } + if !localFallbackAllowed("-filters", err) { + return ExitError } - // Server unavailable — fall back to local ffmpeg if !jsonOut { return runLocalFfmpegPassthrough("-filters") } @@ -2195,7 +2223,7 @@ func runFilters(serverURL, token string, jsonOut bool) int { } // runPixFmts queries pixel formats from the server and prints in ffmpeg-compatible format. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runPixFmts(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { @@ -2214,8 +2242,11 @@ func runPixFmts(serverURL, token string, jsonOut bool) int { } return ExitSuccess } + err = apiErr + } + if !localFallbackAllowed("-pix_fmts", err) { + return ExitError } - // Server unavailable — fall back to local ffmpeg if !jsonOut { return runLocalFfmpegPassthrough("-pix_fmts") } @@ -2229,7 +2260,7 @@ func runPixFmts(serverURL, token string, jsonOut bool) int { } // runFormats queries formats from the server and prints in ffmpeg-compatible format. -// Falls back to local ffmpeg if server is unavailable. +// Falls back to local ffmpeg only with the localFallbackEnv opt-in. func runFormats(serverURL, token string, jsonOut bool) int { cli, err := setupClient(serverURL, token) if err == nil { @@ -2248,8 +2279,11 @@ func runFormats(serverURL, token string, jsonOut bool) int { } return ExitSuccess } + err = apiErr + } + if !localFallbackAllowed("-formats", err) { + return ExitError } - // Server unavailable — fall back to local ffmpeg if !jsonOut { return runLocalFfmpegPassthrough("-formats") } @@ -2394,6 +2428,10 @@ rffmpeg options: -filters List all available filters (queries server, no input file required) -pix_fmts List all available pixel formats (queries server, no input file required) -formats List all available muxers/demuxers (queries server, no input file required) + Capability queries (-encoders/-decoders/-codecs/-hwaccels/-filters/-pix_fmts/-formats) + describe the cluster, so they fail when the server cannot answer instead of + silently listing this machine's ffmpeg; RFFMPEG_LOCAL_FALLBACK=1 opts into the + local list (announced on stderr). -buildconf Show build configuration (runs local ffmpeg) -layouts List channel layouts (runs local ffmpeg) -protocols List protocols (runs local ffmpeg) @@ -2482,6 +2520,8 @@ Configuration: RFFMPEG_SERVER_URL Server URL RFFMPEG_TOKEN Auth token RFFMPEG_SHARED_FS Enable shared filesystem mode (set to 1 or true) + RFFMPEG_LOCAL_FALLBACK Allow capability queries to answer from local ffmpeg + when the server cannot (set to 1 or true; default: fail) `) } diff --git a/cmd/cli/main_test.go b/cmd/cli/main_test.go index ebb9e06..28eb591 100644 --- a/cmd/cli/main_test.go +++ b/cmd/cli/main_test.go @@ -760,55 +760,99 @@ func TestPrintEncoderDecoderHeader(t *testing.T) { } } -func TestRunEncoders_FallbackToLocal(t *testing.T) { - // When server is unavailable, should fall back to local ffmpeg - // and return success (ffmpeg is available in test environment) - exitCode := runEncoders("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) - } -} - -func TestRunDecoders_FallbackToLocal(t *testing.T) { - // When server is unavailable, should fall back to local ffmpeg - exitCode := runDecoders("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) +// capabilityQueryCases enumerates every server-backed info flag with its run +// entry point and the JSON key it must emit, so the capability-query tests +// below cover the whole family without repeating one body per flag. +var capabilityQueryCases = []struct { + flag string + jsonKey string + run func(serverURL, token string, jsonOut bool) int +}{ + {"-encoders", "encoders", runEncoders}, + {"-decoders", "decoders", runDecoders}, + {"-codecs", "codecs", runCodecsFromServer}, + {"-hwaccels", "hwaccels", runHwaccels}, + {"-filters", "filters", runFilters}, + {"-pix_fmts", "pix_fmts", runPixFmts}, + {"-formats", "formats", runFormats}, +} + +// TestCapabilityQuery_ServerUnavailableFails pins the contract: the list these +// flags print describes the cluster, so when the server cannot be reached the +// query must fail — printing the local machine's list would have a caller read +// it as the cluster's capabilities. +func TestCapabilityQuery_ServerUnavailableFails(t *testing.T) { + for _, tc := range capabilityQueryCases { + t.Run(tc.flag, func(t *testing.T) { + for _, jsonOut := range []bool{false, true} { + var code int + stderr := "" + stdout := captureStdout(func() { + stderr = captureStderr(func() { code = tc.run("http://127.0.0.1:1", "", jsonOut) }) + }) + if code != ExitError { + t.Errorf("json=%v: exit code = %d, want %d", jsonOut, code, ExitError) + } + if strings.TrimSpace(stdout) != "" { + t.Errorf("json=%v: leaked a capability list without a server: %q", jsonOut, stdout) + } + if !strings.Contains(stderr, tc.flag) || !strings.Contains(stderr, localFallbackEnv) { + t.Errorf("json=%v: stderr must name the query and the opt-in: %q", jsonOut, stderr) + } + } + }) } } -func TestRunEncoders_FallbackJSON(t *testing.T) { - // JSON mode should also fall back and produce valid JSON - output := captureStdout(func() { - exitCode := runEncoders("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - if _, ok := result["encoders"]; !ok { - t.Errorf("expected 'encoders' key in JSON output") +// TestCapabilityQuery_LocalFallbackOptIn covers the other half: with the opt-in +// the degradation is kept, but it is announced on stderr instead of silent. +func TestCapabilityQuery_LocalFallbackOptIn(t *testing.T) { + t.Setenv(localFallbackEnv, "1") + for _, tc := range capabilityQueryCases { + t.Run(tc.flag, func(t *testing.T) { + var code int + stderr := "" + stdout := captureStdout(func() { + stderr = captureStderr(func() { code = tc.run("http://127.0.0.1:1", "", false) }) + }) + if code != ExitSuccess { + t.Fatalf("exit code = %d, want %d (stderr: %s)", code, ExitSuccess, stderr) + } + if !strings.Contains(stderr, "[rffmpeg] local fallback: "+tc.flag) { + t.Errorf("stderr missing local-fallback warning: %q", stderr) + } + if strings.TrimSpace(stdout) == "" { + t.Error("expected the local ffmpeg list on stdout") + } + }) } } -func TestRunDecoders_FallbackJSON(t *testing.T) { - output := captureStdout(func() { - exitCode := runDecoders("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - if _, ok := result["decoders"]; !ok { - t.Errorf("expected 'decoders' key in JSON output") +// TestCapabilityQuery_LocalFallbackJSON pins that the opt-in keeps --json output +// parseable: the warning lands on stderr, stdout stays a single JSON document. +func TestCapabilityQuery_LocalFallbackJSON(t *testing.T) { + t.Setenv(localFallbackEnv, "1") + for _, tc := range capabilityQueryCases { + t.Run(tc.flag, func(t *testing.T) { + var code int + stdout := captureStdout(func() { + captureStderr(func() { code = tc.run("http://127.0.0.1:1", "", true) }) + }) + if code != ExitSuccess { + t.Fatalf("exit code = %d, want %d", code, ExitSuccess) + } + var result map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &result); err != nil { + t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, stdout) + } + items, ok := result[tc.jsonKey].([]interface{}) + if !ok { + t.Fatalf("expected %q array in JSON output, got %T", tc.jsonKey, result[tc.jsonKey]) + } + if len(items) == 0 { + t.Errorf("expected non-empty %q array", tc.jsonKey) + } + }) } } @@ -1110,151 +1154,6 @@ func TestParseFFmpegNameList_RealFormatsOutput(t *testing.T) { } } -// ============================================================================= -// P0/P1 fallback-to-local tests -// ============================================================================= - -func TestRunHwaccels_FallbackToLocal(t *testing.T) { - // When server is unavailable, should fall back to local ffmpeg - exitCode := runHwaccels("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) - } -} - -func TestRunHwaccels_FallbackJSON(t *testing.T) { - output := captureStdout(func() { - exitCode := runHwaccels("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - hwaccels, ok := result["hwaccels"].([]interface{}) - if !ok { - t.Fatalf("expected 'hwaccels' key with array, got %T", result["hwaccels"]) - } - if len(hwaccels) == 0 { - t.Error("expected non-empty hwaccels array in JSON") - } -} - -func TestRunFilters_FallbackToLocal(t *testing.T) { - exitCode := runFilters("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) - } -} - -func TestRunFilters_FallbackJSON(t *testing.T) { - output := captureStdout(func() { - exitCode := runFilters("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - filters, ok := result["filters"].([]interface{}) - if !ok { - t.Fatalf("expected 'filters' key with array, got %T", result["filters"]) - } - if len(filters) == 0 { - t.Error("expected non-empty filters array in JSON") - } -} - -func TestRunPixFmts_FallbackToLocal(t *testing.T) { - exitCode := runPixFmts("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) - } -} - -func TestRunPixFmts_FallbackJSON(t *testing.T) { - output := captureStdout(func() { - exitCode := runPixFmts("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - pixFmts, ok := result["pix_fmts"].([]interface{}) - if !ok { - t.Fatalf("expected 'pix_fmts' key with array, got %T", result["pix_fmts"]) - } - if len(pixFmts) == 0 { - t.Error("expected non-empty pix_fmts array in JSON") - } -} - -func TestRunFormats_FallbackToLocal(t *testing.T) { - exitCode := runFormats("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) - } -} - -func TestRunFormats_FallbackJSON(t *testing.T) { - output := captureStdout(func() { - exitCode := runFormats("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - formats, ok := result["formats"].([]interface{}) - if !ok { - t.Fatalf("expected 'formats' key with array, got %T", result["formats"]) - } - if len(formats) == 0 { - t.Error("expected non-empty formats array in JSON") - } -} - -func TestRunCodecs_FallbackToLocal(t *testing.T) { - exitCode := runCodecsFromServer("http://127.0.0.1:1", "", false) - if exitCode != ExitSuccess { - t.Errorf("expected fallback to local ffmpeg success, got exit code %d", exitCode) - } -} - -func TestRunCodecs_FallbackJSON(t *testing.T) { - output := captureStdout(func() { - exitCode := runCodecsFromServer("http://127.0.0.1:1", "", true) - if exitCode != ExitSuccess { - t.Errorf("expected JSON fallback success, got exit code %d", exitCode) - } - }) - - var result map[string]interface{} - if err := json.Unmarshal([]byte(output), &result); err != nil { - t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, output) - } - codecs, ok := result["codecs"].([]interface{}) - if !ok { - t.Fatalf("expected 'codecs' key with array, got %T", result["codecs"]) - } - if len(codecs) == 0 { - t.Error("expected non-empty codecs array in JSON") - } -} - // ============================================================================= // P2 info flag tests — these run local ffmpeg directly (no server needed) // ============================================================================= @@ -1415,8 +1314,10 @@ func TestJSONOutput_CodecsFields(t *testing.T) { func TestOutputFormat_EncodersVsFfmpeg(t *testing.T) { // The rffmpeg encoder output format should match ffmpeg's // Verify the format: "{flags} {name:22} {description}" + // The local list is the fixture here, so enable the opt-in. + t.Setenv(localFallbackEnv, "1") - // Capture rffmpeg output (via fallback to local ffmpeg) + // Capture rffmpeg output (via the opt-in local ffmpeg fallback) rffmpegOutput := captureStdout(func() { exitCode := runEncoders("http://127.0.0.1:1", "", false) if exitCode != ExitSuccess { @@ -1466,6 +1367,7 @@ func TestOutputFormat_EncodersVsFfmpeg(t *testing.T) { } func TestOutputFormat_DecodersVsFfmpeg(t *testing.T) { + t.Setenv(localFallbackEnv, "1") rffmpegOutput := captureStdout(func() { exitCode := runDecoders("http://127.0.0.1:1", "", false) if exitCode != ExitSuccess { @@ -1501,6 +1403,7 @@ func TestOutputFormat_DecodersVsFfmpeg(t *testing.T) { func TestOutputFormat_HwaccelsVsFfmpeg(t *testing.T) { // ffmpeg -hwaccels output is a simple list + t.Setenv(localFallbackEnv, "1") rffmpegOutput := captureStdout(func() { exitCode := runHwaccels("http://127.0.0.1:1", "", false) if exitCode != ExitSuccess { @@ -1528,6 +1431,7 @@ func TestOutputFormat_HwaccelsVsFfmpeg(t *testing.T) { } func TestOutputFormat_FiltersVsFfmpeg(t *testing.T) { + t.Setenv(localFallbackEnv, "1") rffmpegOutput := captureStdout(func() { exitCode := runFilters("http://127.0.0.1:1", "", false) if exitCode != ExitSuccess {