diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index ef77bc0..7469bd9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -45,6 +45,15 @@ jobs: -pthread -o "$artifacts/mave-media-network.so" sudo install -m 755 "$artifacts/mave-media-broker" "$artifacts/mave-media-sandbox" /usr/local/bin/ sudo install -m 644 "$artifacts/mave-media-network.so" /usr/local/lib/ + - name: Test browser-compatible CPU encoder output + run: | + artifacts=$(mktemp -d) + trap 'sudo rm -rf "$artifacts"' EXIT + docker run --rm \ + -v "$PWD/deploy/encoding-booster/encoder:/src:ro" \ + -v "$artifacts:/out" -w /src golang:1.27.3-alpine3.23 \ + sh -c 'CGO_ENABLED=0 go test -c -o /out/encoder.test' + "$artifacts/encoder.test" -test.run '^TestH264BrowserPixelFormats$' -test.v - name: Exercise confinement and real codecs without root or capabilities run: | # GitHub's checkout parents are not traversable by nobody. diff --git a/Dockerfile b/Dockerfile index 9f028c5..e095623 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,9 +64,15 @@ WORKDIR /tmp/ffmpeg # Bind verification to the pinned release key, even if the downloaded key bundle contains others. # bsdtar also supports extracting this archive under Docker's amd64 emulation. -RUN curl -fsSLo /tmp/ffmpeg.tar.xz "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \ - && curl -fsSLo /tmp/ffmpeg.tar.xz.asc "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz.asc" \ - && curl -fsSLo /tmp/ffmpeg-devel.asc "https://ffmpeg.org/ffmpeg-devel.asc" \ +RUN curl --http1.1 --retry 5 --retry-all-errors --retry-delay 2 \ + --retry-max-time 180 --connect-timeout 20 --max-time 120 \ + -fsSLo /tmp/ffmpeg.tar.xz "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \ + && curl --http1.1 --retry 5 --retry-all-errors --retry-delay 2 \ + --retry-max-time 180 --connect-timeout 20 --max-time 120 \ + -fsSLo /tmp/ffmpeg.tar.xz.asc "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz.asc" \ + && curl --http1.1 --retry 5 --retry-all-errors --retry-delay 2 \ + --retry-max-time 180 --connect-timeout 20 --max-time 120 \ + -fsSLo /tmp/ffmpeg-devel.asc "https://ffmpeg.org/ffmpeg-devel.asc" \ && gpg --batch --import /tmp/ffmpeg-devel.asc \ && gpg --batch --assert-signer FCF986EA15E6E293A5644F10B4322F04D67658D8 \ --verify /tmp/ffmpeg.tar.xz.asc /tmp/ffmpeg.tar.xz \ diff --git a/deploy/encoding-booster/README.md b/deploy/encoding-booster/README.md index 1a9aed7..889064d 100644 --- a/deploy/encoding-booster/README.md +++ b/deploy/encoding-booster/README.md @@ -91,6 +91,14 @@ delaying or rejecting the upload when warmup fails. ## H.264 quality and bitrate +Browser-facing H.264 is encoded as 8-bit 4:2:0 (`yuv420p`), including +10-bit and 4:2:2/4:4:4 sources. HLS packages these compatible renditions. +Uploaded H.264 can only bypass transcoding when inspection confirms a +Baseline, Main or High profile with 8-bit 4:2:0 pixels; missing or unsupported +format information requires transcoding. Inline sources also transcode in `auto` +mode. Local FFmpeg outputs are checked before publication. Explicit `copy` mode +is a fixture/compatibility shortcut and does not normalize input. + The H.264 ladder uses capped CRF 20 with the `faster` x264 preset. Simple scenes can use fewer bits, while complex scenes can use more up to the rate-control limits below. CRF is a quality target, not a promise of a particular file size diff --git a/deploy/encoding-booster/encoder/Dockerfile b/deploy/encoding-booster/encoder/Dockerfile index 682f39f..11ec9ec 100644 --- a/deploy/encoding-booster/encoder/Dockerfile +++ b/deploy/encoding-booster/encoder/Dockerfile @@ -38,7 +38,10 @@ RUN apk add --no-cache \ xz \ zlib-dev WORKDIR /src/ffmpeg -RUN curl -fsSLo /tmp/ffmpeg.tar.xz \ +# The release host can reset Alpine curl connections; keep downloads retryable. +RUN curl --http1.1 --retry 5 --retry-all-errors --retry-delay 2 \ + --retry-max-time 180 --connect-timeout 20 --max-time 120 \ + -fsSLo /tmp/ffmpeg.tar.xz \ "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \ && echo "${FFMPEG_SHA256} /tmp/ffmpeg.tar.xz" | sha256sum -c - \ && tar -xJf /tmp/ffmpeg.tar.xz --strip-components=1 \ diff --git a/deploy/encoding-booster/encoder/Dockerfile.gpu b/deploy/encoding-booster/encoder/Dockerfile.gpu index 7ce2ca3..4626208 100644 --- a/deploy/encoding-booster/encoder/Dockerfile.gpu +++ b/deploy/encoding-booster/encoder/Dockerfile.gpu @@ -31,7 +31,10 @@ RUN apt-get update \ libx264-dev libx265-dev nasm pkg-config xz-utils zlib1g-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /src/ffmpeg -RUN curl -fsSLo /tmp/ffmpeg.tar.xz \ +# Keep transient release-host connection resets from failing the image build. +RUN curl --http1.1 --retry 5 --retry-all-errors --retry-delay 2 \ + --retry-max-time 180 --connect-timeout 20 --max-time 120 \ + -fsSLo /tmp/ffmpeg.tar.xz \ "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \ && echo "${FFMPEG_SHA256} /tmp/ffmpeg.tar.xz" | sha256sum -c - \ && tar -xJf /tmp/ffmpeg.tar.xz --strip-components=1 \ diff --git a/deploy/encoding-booster/encoder/browser_compatibility_test.go b/deploy/encoding-booster/encoder/browser_compatibility_test.go new file mode 100644 index 0000000..9c767d3 --- /dev/null +++ b/deploy/encoding-booster/encoder/browser_compatibility_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func TestH264BrowserPixelFormats(t *testing.T) { + ffmpeg, err := exec.LookPath("ffmpeg") + if err != nil { + t.Skip("ffmpeg is not installed") + } + ffprobe, err := exec.LookPath("ffprobe") + if err != nil { + t.Skip("ffprobe is not installed") + } + probe := func(t *testing.T, path string) (string, string) { + t.Helper() + body, err := exec.Command(ffprobe, "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=codec_name,pix_fmt", "-of", "json", path).Output() + if err != nil { + t.Fatal(err) + } + var result struct { + Streams []struct { + Codec string `json:"codec_name"` + PixelFormat string `json:"pix_fmt"` + } `json:"streams"` + } + if err := json.Unmarshal(body, &result); err != nil || len(result.Streams) != 1 { + t.Fatalf("unexpected probe: %s (%v)", body, err) + } + return result.Streams[0].Codec, result.Streams[0].PixelFormat + } + for _, format := range []string{"yuv420p10le", "yuv422p10le", "yuv444p10le", "yuv444p"} { + t.Run(format, func(t *testing.T) { + dir := t.TempDir() + source := filepath.Join(dir, "source.mp4") + if output, err := exec.Command(ffmpeg, "-v", "error", "-f", "lavfi", "-i", "testsrc2=size=160x96:rate=12", "-t", "1", "-vf", "format="+format, "-c:v", "libx264", "-threads", "1", "-pix_fmt", format, source).CombinedOutput(); err != nil { + t.Fatalf("generate %s source: %v: %s", format, err, output) + } + if _, actual := probe(t, source); actual != format { + t.Fatalf("fixture must retain %s, got %s", format, actual) + } + request := encodeRequest{InputURL: "https://example.test/source.mp4", Operation: "video", Codec: "h264", Width: 160, VideoCRF: 20, Preset: "faster"} + if err := request.validate(); err != nil { + t.Fatal(err) + } + request.InputURL = source + command := exec.Command(ffmpeg, ffmpegArgs(request, "cpu")...) + var encoded, diagnostics bytes.Buffer + command.Stdout, command.Stderr = &encoded, &diagnostics + if err := command.Run(); err != nil { + t.Fatalf("encode: %v: %s", err, diagnostics.String()) + } + output := filepath.Join(dir, "output.mp4") + if err := os.WriteFile(output, encoded.Bytes(), 0600); err != nil { + t.Fatal(err) + } + if out, err := exec.Command(ffmpeg, hlsArgs(output, dir)...).CombinedOutput(); err != nil { + t.Fatalf("package HLS: %v: %s", err, out) + } + for _, path := range []string{output, filepath.Join(dir, "playlist.m3u8")} { + codec, pixels := probe(t, path) + if codec != "h264" || pixels != "yuv420p" { + t.Fatalf("%s is not browser H.264: codec=%s pix_fmt=%s", path, codec, pixels) + } + } + }) + } +} diff --git a/docs/flow_engine.md b/docs/flow_engine.md index a842180..f6a6bca 100644 --- a/docs/flow_engine.md +++ b/docs/flow_engine.md @@ -218,7 +218,9 @@ For deterministic local tests, provide `source_body` with `source_content_type`, `media_probe`, or `transcription_text` / `transcription_vtt` / `transcription_segments` as appropriate. -Media step `*_mode` inputs select `auto`, `ffmpeg` or `copy`. Step-level +Media step `*_mode` inputs select `auto`, `ffmpeg` or `copy`. H.264 steps +transcode inline sources in `auto` mode; use explicit `copy` for opaque test +fixtures. Copy mode does not normalize media for browser playback. Step-level `params.strict` makes errors fail the run; the corresponding run-input `*_strict` override takes precedence. These overrides are for debugging and custom flows. Discover current step options through `/v1/flow/step-types` and diff --git a/lib/mave_core/flow/steps/media_transcode_h264_ladder_step.ex b/lib/mave_core/flow/steps/media_transcode_h264_ladder_step.ex index 659b975..0834f16 100644 --- a/lib/mave_core/flow/steps/media_transcode_h264_ladder_step.ex +++ b/lib/mave_core/flow/steps/media_transcode_h264_ladder_step.ex @@ -187,19 +187,14 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeH264LadderStep do end end - defp maybe_copy_source_variants(%{mode: mode} = context, variants) - when mode in ["auto", "copy"] do - if Map.has_key?(context.run_input, "source_body") or mode == "copy" do - case StepSupport.resolve_source_body( - context.run_input, - context.source_url, - context.dependency_outputs - ) do - {:ok, source_body} -> put_variant_bodies(context, variants, source_body) - {:error, reason} -> {:error, reason} - end - else - :skip + defp maybe_copy_source_variants(%{mode: "copy"} = context, variants) do + case StepSupport.resolve_source_body( + context.run_input, + context.source_url, + context.dependency_outputs + ) do + {:ok, source_body} -> put_variant_bodies(context, variants, source_body) + {:error, reason} -> {:error, reason} end end @@ -1335,6 +1330,8 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeH264LadderStep do [ "-c:v", "libx264", + "-pix_fmt", + "yuv420p", "-preset", encoder_settings.preset ] ++ @@ -2002,7 +1999,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeH264LadderStep do output_paths |> Map.values() |> Enum.reduce_while(:ok, fn output_path, :ok -> - case StepSupport.validate_media_file(output_path, :video) do + case StepSupport.validate_media_file(output_path, :h264) do :ok -> {:cont, :ok} {:error, reason} -> {:halt, {:error, reason}} end diff --git a/lib/mave_core/flow/steps/media_transcode_video_step.ex b/lib/mave_core/flow/steps/media_transcode_video_step.ex index 5b6f25a..4c98d0a 100644 --- a/lib/mave_core/flow/steps/media_transcode_video_step.ex +++ b/lib/mave_core/flow/steps/media_transcode_video_step.ex @@ -3,7 +3,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStep do Produces a video rendition artifact for a configured codec/size step. Default behavior: - - `auto`: uses FFmpeg for normal sources; copy-mode only for inline `source_body`. + - `auto`: transcodes sources, including inline `source_body`. - `copy`: compatibility mode for inline `source_body`. - `ffmpeg`: always attempts FFmpeg transcode. """ @@ -321,11 +321,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStep do end defp build_rendition_body("auto", context) do - if Map.has_key?(context.run_input, "source_body") do - build_rendition_body("copy", context) - else - transcode_with_ffmpeg(context) - end + transcode_with_ffmpeg(context) end defp run_booster_or_delegate(context, profile) do @@ -727,9 +723,11 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStep do end end - defp ffmpeg_input_opts(_context, output_path, progress) do + defp ffmpeg_input_opts(context, output_path, progress) do + kind = if context.codec == "h264", do: :h264, else: :video + [ - validate_output: fn -> StepSupport.validate_media_file(output_path, :video) end, + validate_output: fn -> StepSupport.validate_media_file(output_path, kind) end, command_timeout_ms: video_command_timeout_ms(), progress: progress ] @@ -852,6 +850,11 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStep do normalize_codec(map_get(inspect_output, "video_codec")) != "h264" -> {:error, :source_video_codec_not_h264} + not StepSupport.browser_h264_stream?( + StepSupport.video_stream(map_get(inspect_output, "streams", [])) + ) -> + {:error, :source_video_format_not_browser_compatible} + not source_audio_copy_compatible?(inspect_output) -> {:error, :source_audio_codec_not_copy_compatible} @@ -1110,6 +1113,8 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStep do [ "-c:v", ffmpeg_codec, + "-pix_fmt", + "yuv420p", "-crf", "23", "-g", @@ -1139,6 +1144,8 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStep do [ "-c:v", ffmpeg_codec, + "-pix_fmt", + "yuv420p", "-preset", "faster", "-tune", diff --git a/lib/mave_core/flow/steps/support.ex b/lib/mave_core/flow/steps/support.ex index 2f44464..edfb533 100644 --- a/lib/mave_core/flow/steps/support.ex +++ b/lib/mave_core/flow/steps/support.ex @@ -320,8 +320,8 @@ defmodule MaveCore.Flow.Steps.Support do def report_progress(_progress_reporter, _event), do: :ok - @spec validate_media_file(String.t(), :audio | :video) :: :ok | {:error, term()} - def validate_media_file(path, kind) when kind in [:audio, :video] do + @spec validate_media_file(String.t(), :audio | :video | :h264) :: :ok | {:error, term()} + def validate_media_file(path, kind) when kind in [:audio, :video, :h264] do case do_validate_media_file(path, kind) do :ok -> :ok {:error, reason} -> {:error, {:invalid_media_output, kind, reason}} @@ -331,6 +331,15 @@ defmodule MaveCore.Flow.Steps.Support do def validate_media_file(_path, kind), do: {:error, {:invalid_media_output, kind, :unsupported_media_kind}} + @doc "Checks the codec, profile and pixel format required by browser H.264 renditions." + def browser_h264_stream?(%{} = stream) do + map_value(stream, "codec_name") == "h264" and + map_value(stream, "pix_fmt") in ["yuv420p", "yuvj420p"] and + map_value(stream, "profile") in ["Baseline", "Constrained Baseline", "Main", "High"] + end + + def browser_h264_stream?(_), do: false + @doc false def decode_probe_output(output) when is_binary(output) do case Jason.decode(String.trim(output)) do @@ -386,7 +395,10 @@ defmodule MaveCore.Flow.Steps.Support do "-select_streams", "#{media_stream_selector(kind)}:0", "-show_entries", - "stream=codec_type,codec_name", + if(kind == :h264, + do: "stream=codec_type,codec_name,profile,pix_fmt", + else: "stream=codec_type,codec_name" + ), "-of", "json", path @@ -420,6 +432,12 @@ defmodule MaveCore.Flow.Steps.Support do defp probe_decode_error(%_{} = error), do: Exception.message(error) defp probe_decode_error(error), do: inspect(error) + defp validate_media_stream(%{"streams" => streams}, :h264) when is_list(streams) do + if browser_h264_stream?(video_stream(streams)), + do: :ok, + else: {:error, :incompatible_h264_stream} + end + defp validate_media_stream(%{"streams" => streams}, kind) when is_list(streams) do expected_type = Atom.to_string(kind) @@ -435,6 +453,7 @@ defmodule MaveCore.Flow.Steps.Support do defp media_stream_selector(:audio), do: "a" defp media_stream_selector(:video), do: "v" + defp media_stream_selector(:h264), do: "v" @spec prepare_ffmpeg_input(String.t(), map(), term(), map(), keyword()) :: {:ok, String.t()} | {:error, term()} diff --git a/test/mave_core/flow/steps/h264_browser_compatibility_test.exs b/test/mave_core/flow/steps/h264_browser_compatibility_test.exs new file mode 100644 index 0000000..d392e49 --- /dev/null +++ b/test/mave_core/flow/steps/h264_browser_compatibility_test.exs @@ -0,0 +1,139 @@ +defmodule MaveCore.Flow.Steps.H264BrowserCompatibilityTest do + use ExUnit.Case, async: false + + alias MaveCore.Flow.Steps.{MediaTranscodeH264LadderStep, MediaTranscodeVideoStep, Support} + alias MaveCore.TestSupport.FlowStorageAdapterStub + + setup do + previous = Application.get_env(:mave_core, :flow_storage_adapter) + Application.put_env(:mave_core, :flow_storage_adapter, FlowStorageAdapterStub) + FlowStorageAdapterStub.reset!() + + on_exit(fn -> + FlowStorageAdapterStub.reset!() + + if previous, + do: Application.put_env(:mave_core, :flow_storage_adapter, previous), + else: Application.delete_env(:mave_core, :flow_storage_adapter) + end) + + :ok + end + + test "only known browser H.264 profiles and 8-bit 4:2:0 are reusable" do + for profile <- ["Constrained Baseline", "Baseline", "Main", "High"], + format <- ["yuv420p", "yuvj420p"] do + assert Support.browser_h264_stream?(%{ + "codec_name" => "h264", + "profile" => profile, + "pix_fmt" => format + }) + end + + for {profile, format} <- [ + {"High 10", "yuv420p10le"}, + {"High 4:2:2", "yuv422p10le"}, + {"High 4:4:4 Predictive", "yuv444p"}, + {"High", "yuv444p"}, + {nil, "yuv420p"}, + {"High", nil} + ] do + refute Support.browser_h264_stream?(%{ + "codec_name" => "h264", + "profile" => profile, + "pix_fmt" => format + }) + end + + refute Support.browser_h264_stream?(nil) + + refute Support.browser_h264_stream?(%{ + "codec_name" => "hevc", + "profile" => "Main", + "pix_fmt" => "yuv420p" + }) + end + + test "normalizes real 10-bit and 4:2:2/4:4:4 sources and rejects incompatible output" do + if ffmpeg = System.find_executable("ffmpeg") do + for format <- ["yuv420p", "yuv420p10le", "yuv422p10le", "yuv444p10le", "yuv444p"] do + Support.with_temp_dir("browser_h264", fn dir -> + path = Path.join(dir, "source.mp4") + + assert {_, 0} = + System.cmd( + ffmpeg, + [ + "-v", + "error", + "-f", + "lavfi", + "-i", + "testsrc2=size=160x96:rate=12", + "-t", + "1", + "-vf", + "format=#{format}", + "-c:v", + "libx264", + "-threads", + "1", + "-pix_fmt", + format, + path + ], + stderr_to_stdout: true + ) + + if format == "yuv420p" do + assert :ok = Support.validate_media_file(path, :h264) + else + assert {:error, {:invalid_media_output, :h264, :incompatible_h264_stream}} = + Support.validate_media_file(path, :h264) + end + + context = %{ + run_input: %{ + "space_hash" => "test0", + "embed_hash" => "synthetic0", + "version" => 0, + "source_body" => File.read!(path), + "source_content_type" => "video/mp4", + "media_transcode_video_mode" => "auto", + "media_transcode_h264_ladder_mode" => "auto" + }, + dependency_outputs: %{ + "source" => %{"source_url" => "https://example.com/source.mp4"}, + "inspect_media" => %{ + "has_video" => true, + "has_audio" => false, + "width" => 160, + "height" => 96, + "duration" => 1 + } + } + } + + for {handler, params, key} <- [ + {MediaTranscodeVideoStep, %{"codec" => "h264", "size" => "sd", "strict" => true}, + "synthetic0/h264_sd.mp4"}, + {MediaTranscodeVideoStep, + %{"codec" => "h264", "size" => "sd", "keyframe_interval" => 2, "strict" => true}, + "synthetic0/h264_sd_clip_keyframes.mp4"}, + {MediaTranscodeH264LadderStep, %{"sizes" => ["sd"], "strict" => true}, + "synthetic0/h264_sd.mp4"} + ] do + assert {:ok, output, _} = + handler.run(%{"id" => "encode", "params" => params}, context) + + assert output["mode"] == "ffmpeg" + assert {:ok, body} = FlowStorageAdapterStub.get("space-test0", key, nil) + encoded = Path.join(dir, "encoded.mp4") + File.write!(encoded, body) + assert :ok = Support.validate_media_file(encoded, :h264) + end + end) + end + end + end +end diff --git a/test/mave_core/flow/steps/media_transcode_video_step_test.exs b/test/mave_core/flow/steps/media_transcode_video_step_test.exs index 55f9ee0..41c7a7d 100644 --- a/test/mave_core/flow/steps/media_transcode_video_step_test.exs +++ b/test/mave_core/flow/steps/media_transcode_video_step_test.exs @@ -359,6 +359,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStepTest do "space_hash" => "ubg50", "embed_hash" => "LeDE9v86ye", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", "version" => 0 }, dependency_outputs: %{ @@ -492,6 +493,14 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStepTest do "filetype" => "mp4", "video_codec" => "h264", "audio_codec" => "aac", + "streams" => [ + %{ + "codec_type" => "video", + "codec_name" => "h264", + "profile" => "High", + "pix_fmt" => "yuv420p" + } + ], "width" => 1280, "height" => 720, "size_bytes" => byte_size(original_body) @@ -509,6 +518,37 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStepTest do FlowStorageAdapterStub.get("space-ubg50", "LeDE9v86ye/h264_hd.mp4", "us-east-1") assert FlowStorageAdapterStub.public?("space-ubg50", "LeDE9v86ye/h264_hd.mp4", "us-east-1") + + for stream <- [ + %{ + "codec_type" => "video", + "codec_name" => "h264", + "profile" => "High 10", + "pix_fmt" => "yuv420p10le" + }, + %{ + "codec_type" => "video", + "codec_name" => "h264", + "profile" => "High 4:2:2", + "pix_fmt" => "yuv422p10le" + }, + %{ + "codec_type" => "video", + "codec_name" => "h264", + "profile" => "High 4:4:4 Predictive", + "pix_fmt" => "yuv444p" + }, + %{"codec_type" => "video", "codec_name" => "h264"} + ] do + incompatible_context = + context + |> put_in([:dependency_outputs, "inspect_media", "streams"], [stream]) + |> Map.put(:encoding_booster_dispatch, :direct) + + assert {:ok, output, _} = MediaTranscodeVideoStep.run(step_definition, incompatible_context) + assert output["mode"] == "encoding_booster" + refute output["source_copy"] + end end test "returns unavailable output in non-strict mode on missing source" do @@ -560,6 +600,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStepTest do "space_hash" => "ubg50", "embed_hash" => "LeDE9v86ye", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", "version" => 0 }, dependency_outputs: %{ @@ -589,6 +630,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStepTest do "space_hash" => "ubg50", "embed_hash" => "LeDE9v86ye", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", "version" => 0 }, dependency_outputs: %{ @@ -786,6 +828,7 @@ defmodule MaveCore.Flow.Steps.MediaTranscodeVideoStepTest do "space_hash" => "ubg50", "embed_hash" => "LeDE9v86ye", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", "version" => 0 }, dependency_outputs: %{ diff --git a/test/mave_core/flow_test.exs b/test/mave_core/flow_test.exs index 4404bea..5215117 100644 --- a/test/mave_core/flow_test.exs +++ b/test/mave_core/flow_test.exs @@ -5006,6 +5006,8 @@ defmodule MaveCore.FlowTest do "input_url" => "https://example.com/projected.mp4", "source_url" => "https://example.com/projected.mp4", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", + "media_transcode_h264_ladder_mode" => "copy", "source_content_type" => "video/mp4", "media_package_hls_variant_strict" => false, "media_package_hls_audio_strict" => false, @@ -6233,6 +6235,8 @@ defmodule MaveCore.FlowTest do "input_url" => "https://example.com/ready-renditions.mp4", "source_url" => "https://example.com/ready-renditions.mp4", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", + "media_transcode_h264_ladder_mode" => "copy", "source_content_type" => "video/mp4", "upload_id" => upload_id, "media_probe" => %{ diff --git a/test/mave_core_web/controllers/api/flow/runs_controller_test.exs b/test/mave_core_web/controllers/api/flow/runs_controller_test.exs index fc35fc5..fe44d95 100644 --- a/test/mave_core_web/controllers/api/flow/runs_controller_test.exs +++ b/test/mave_core_web/controllers/api/flow/runs_controller_test.exs @@ -138,6 +138,8 @@ defmodule MaveCoreWeb.Api.Flow.RunsControllerTest do "embed_hash" => "LeDE9v86ye", "input_url" => "https://example.com/video.mp4", "source_body" => "stub-media-body", + "media_transcode_video_mode" => "copy", + "media_transcode_h264_ladder_mode" => "copy", "source_content_type" => "video/mp4", "media_probe" => %{ "format" => %{