From bccebbd006226f334bf64be60cd441abc44ff62a Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sun, 7 Jun 2026 04:35:14 +0330 Subject: [PATCH] feat(render #36): real per-tier output height (360/540/720/1080/4K) r_height was hardcoded 1080. render-svc now derives r_height from the resolution (ResolutionHeight) on job create; node-agent ffmpeg downscales to the tier height (scale=-2:H). Quality picker now actually changes output size. Co-Authored-By: Claude Opus 4.8 --- services/node-agent/internal/runner/runner.go | 30 +++++++++++++++++-- services/render/internal/db/db.go | 24 +++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/services/node-agent/internal/runner/runner.go b/services/node-agent/internal/runner/runner.go index 2e2f792..9da9661 100644 --- a/services/node-agent/internal/runner/runner.go +++ b/services/node-agent/internal/runner/runner.go @@ -187,7 +187,25 @@ func ffmpegPath() string { // transcodeToMP4 converts a lossless AE render (AVI/MOV) to a web-playable H.264 // MP4 using ffmpeg. Returns the .mp4 path. Errors if ffmpeg is unavailable. -func transcodeToMP4(ctx context.Context, src, requested string) (string, error) { +// resolutionHeight maps a quality-tier label to its output height (mirrors render-svc). +func resolutionHeight(resolution string) int { + switch strings.ToLower(strings.TrimSpace(resolution)) { + case "360p": + return 360 + case "540p": + return 540 + case "720p": + return 720 + case "1080p", "fullhd": + return 1080 + case "4k", "2160p": + return 2160 + default: + return 0 // unknown → no scaling + } +} + +func transcodeToMP4(ctx context.Context, src, requested string, height int) (string, error) { ff := ffmpegPath() if ff == "" { return "", fmt.Errorf("ffmpeg not found (set FFMPEG_PATH or place ffmpeg.exe next to the agent)") @@ -196,10 +214,16 @@ func transcodeToMP4(ctx context.Context, src, requested string) (string, error) args := []string{ "-y", "-i", src, "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", + } + // Downscale (or up) to the selected quality tier. -2 keeps width even & aspect. + if height > 0 { + args = append(args, "-vf", fmt.Sprintf("scale=-2:%d", height)) + } + args = append(args, "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", dst, - } + ) log.Printf("[ffmpeg] %s %v", ff, args) cmd := exec.CommandContext(ctx, ff, args...) cmd.Stdout = os.Stdout @@ -281,7 +305,7 @@ func aeRender(ctx context.Context, aePath string, job *Job, outputPath string, o } // Transcode the lossless render → H.264 MP4 (much smaller, web-playable). _ = onProgress(ctx, 92, "Transcoding to MP4…") - mp4, terr := transcodeToMP4(ctx, actual, outputPath) + mp4, terr := transcodeToMP4(ctx, actual, outputPath, resolutionHeight(job.Resolution)) if terr != nil { // ffmpeg missing/failed — fall back to the raw render so the job // still delivers a file (large, but valid). diff --git a/services/render/internal/db/db.go b/services/render/internal/db/db.go index 9907090..6dc0125 100644 --- a/services/render/internal/db/db.go +++ b/services/render/internal/db/db.go @@ -3,6 +3,7 @@ package db import ( "context" "fmt" + "strings" "time" "github.com/flatrender/render-svc/internal/models" @@ -410,6 +411,25 @@ func (s *Store) ListActiveJobs(ctx context.Context, userID uuid.UUID) ([]*models return out, rows.Err() } +// ResolutionHeight maps a quality-tier label to its output height in pixels. +// Used for r_height (stored) and the node's ffmpeg downscale. +func ResolutionHeight(resolution string) int { + switch strings.ToLower(strings.TrimSpace(resolution)) { + case "360p": + return 360 + case "540p": + return 540 + case "720p": + return 720 + case "1080p", "fullhd": + return 1080 + case "4k", "2160p": + return 2160 + default: + return 1080 + } +} + func (s *Store) CreateJob(ctx context.Context, userID, tenantID uuid.UUID, req *models.RenderJobCreateRequest) (*models.RenderJob, error) { priceType := "Free" if req.PriceType != nil { @@ -436,12 +456,12 @@ func (s *Store) CreateJob(ctx context.Context, userID, tenantID uuid.UUID, req * -- to the saved-project id only if the lookup is somehow null. COALESCE((SELECT original_project_id FROM studio.saved_projects WHERE id = $3), $3), 'paid'::render_priority_queue, 'Queued'::render_step, $4::price_kind, - $5::render_quality, $6, 1080, $7, COALESCE($8, FALSE), + $5::render_quality, $6, $11, $7, COALESCE($8, FALSE), 0, 'FIX', $9, $10) RETURNING id`, tenantID, userID, req.SavedProjectID, priceType, req.Quality, req.Resolution, frameRate, req.Is60FPS, - tellMe, req.PreferredRegion, + tellMe, req.PreferredRegion, ResolutionHeight(req.Resolution), ).Scan(&id) if err != nil { return nil, err