feat(render #36): real per-tier output height (360/540/720/1080/4K)
Build backend images / build content-svc (push) Failing after 50s
Build backend images / build file-svc (push) Failing after 57s
Build backend images / build gateway (push) Failing after 50s
Build backend images / build identity-svc (push) Failing after 58s
Build backend images / build notification-svc (push) Failing after 48s
Build backend images / build render-svc (push) Failing after 53s
Build backend images / build studio-svc (push) Failing after 1m2s

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 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 04:35:14 +03:30
parent c6766b18a1
commit bccebbd006
2 changed files with 49 additions and 5 deletions
+27 -3
View File
@@ -187,7 +187,25 @@ func ffmpegPath() string {
// transcodeToMP4 converts a lossless AE render (AVI/MOV) to a web-playable H.264 // 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. // 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() ff := ffmpegPath()
if ff == "" { if ff == "" {
return "", fmt.Errorf("ffmpeg not found (set FFMPEG_PATH or place ffmpeg.exe next to the agent)") 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{ args := []string{
"-y", "-i", src, "-y", "-i", src,
"-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-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", "-c:a", "aac", "-b:a", "192k",
"-movflags", "+faststart", "-movflags", "+faststart",
dst, dst,
} )
log.Printf("[ffmpeg] %s %v", ff, args) log.Printf("[ffmpeg] %s %v", ff, args)
cmd := exec.CommandContext(ctx, ff, args...) cmd := exec.CommandContext(ctx, ff, args...)
cmd.Stdout = os.Stdout 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). // Transcode the lossless render → H.264 MP4 (much smaller, web-playable).
_ = onProgress(ctx, 92, "Transcoding to MP4…") _ = onProgress(ctx, 92, "Transcoding to MP4…")
mp4, terr := transcodeToMP4(ctx, actual, outputPath) mp4, terr := transcodeToMP4(ctx, actual, outputPath, resolutionHeight(job.Resolution))
if terr != nil { if terr != nil {
// ffmpeg missing/failed — fall back to the raw render so the job // ffmpeg missing/failed — fall back to the raw render so the job
// still delivers a file (large, but valid). // still delivers a file (large, but valid).
+22 -2
View File
@@ -3,6 +3,7 @@ package db
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/flatrender/render-svc/internal/models" "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() 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) { func (s *Store) CreateJob(ctx context.Context, userID, tenantID uuid.UUID, req *models.RenderJobCreateRequest) (*models.RenderJob, error) {
priceType := "Free" priceType := "Free"
if req.PriceType != nil { 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. -- 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), COALESCE((SELECT original_project_id FROM studio.saved_projects WHERE id = $3), $3),
'paid'::render_priority_queue, 'Queued'::render_step, $4::price_kind, '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) 0, 'FIX', $9, $10)
RETURNING id`, RETURNING id`,
tenantID, userID, req.SavedProjectID, priceType, tenantID, userID, req.SavedProjectID, priceType,
req.Quality, req.Resolution, frameRate, req.Is60FPS, req.Quality, req.Resolution, frameRate, req.Is60FPS,
tellMe, req.PreferredRegion, tellMe, req.PreferredRegion, ResolutionHeight(req.Resolution),
).Scan(&id) ).Scan(&id)
if err != nil { if err != nil {
return nil, err return nil, err