ffmpegvideo-encoding

Kling 4.0 washed out on TikTok? Fix the tag, then the 9:16

··10 min read
Kling 4.0 washed out on TikTok? Fix the tag, then the 9:16

Your Kling 4.0 render looked cinematic in the preview. TikTok finished processing it and now the blacks are grey, the highlights are blown, and the shot sits in a letterbox with bars top and bottom. Two separate things did that, and only one of them has anything to do with HDR.

Quick answer: Kling 4.0 TikTok washed-out uploads have two causes: color metadata the file never wrote, so TikTok's decoder guesses BT.601 and desaturates everything, and a 21:9 or 4K frame that TikTok's ingest re-encodes to fit a 9:16 feed. Run ffprobe first, because most Kling 4.0 output is still 8-bit SDR and needs a correct Rec.709 tag, not a tone map; only genuine PQ or HLG files need the zscale/tonemap chain, and either way you crop to 1080x1920 in the same pass. If you're normalizing dozens of generations instead of one, FFmpeg Micro runs the re-encode as one API call and the Resize for Format blueprint does the reframe in a click.

Check whether the clip is actually HDR before you tone-map anything

Every guide you'll find for washed-out video assumes you have an HDR file. Kling 4.0 went global on October 16, 2026, and Kling's own model release page still lists 10-bit HDR at 1080p and 4K as coming soon, alongside API access for developers. So the odds are good that your file is plain 8-bit SDR and the tone map you were about to run will make it worse.

Read the file before you touch it:

ffprobe -v error -select_streams v:0 \
  -show_entries stream=width,height,pix_fmt,color_space,color_transfer,color_primaries,color_range \
  -of default=noprint_wrappers=1 kling.mp4

If pix_fmt comes back yuv420p and the three color_* fields are unknown, you have an untagged SDR file and your fix is a tag, not a conversion. If pix_fmt is yuv420p10le and color_transfer is smpte2084 (PQ) or arib-std-b67 (HLG), you have real HDR and you need the full chain further down. Those are the only two branches, and taking the wrong one is the single most common reason a "fixed" clip still looks grey.

An untagged H.264 file gets read as BT.601, and that alone looks washed out

Color tags don't change pixel values. They tell the decoder which curve and which primaries those values were encoded against, and when they're missing, players fall back to a guess. For 4:2:0 H.264, that guess is commonly limited-range BT.601, which pulls saturation out of a Rec.709 picture and lifts the blacks. Nothing was converted. The decoder is just reading the right numbers with the wrong instructions.

Writing the tags on an 8-bit SDR Kling export is one pass:

ffmpeg -i kling.mp4 \
  -vf "setparams=colorspace=bt709:color_primaries=bt709:color_trc=bt709:range=tv,format=yuv420p" \
  -c:v libx264 -crf 20 -preset slow -pix_fmt yuv420p \
  -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
  -c:a aac -b:a 192k -movflags +faststart \
  kling-tagged.mp4

The setparams filter matters on FFmpeg 7.1 and newer, where the encoder takes color properties from the frames rather than from the command-line flags alone. Setting both is belt and braces, and it costs you nothing. If you want the longer version of why pixel format and filter negotiation bite here, we wrote up the related failure in FFmpeg "Impossible to Convert": fix the format, not the filter.

The zscale tone-map chain, for real PQ and HLG sources

When ffprobe does show smpte2084 or arib-std-b67, the washout has a different mechanism: the transfer curve was never converted, so an SDR player applies the wrong curve to PQ-encoded values and renders flat, lifted, chalky color. Changing the container and the codec doesn't help, because the pixel values themselves still describe a 1000-nit image.

ffmpeg -i kling-hdr.mp4 -vf "\
zscale=t=linear:npl=1000,\
format=gbrpf32le,\
zscale=p=bt709,\
tonemap=tonemap=hable:desat=0,\
zscale=t=bt709:m=bt709:r=tv,\
format=yuv420p" \
  -c:v libx264 -crf 20 -preset slow \
  -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
  -c:a aac -b:a 192k -movflags +faststart \
  kling-sdr.mp4

Two numbers in that chain do most of the damage when they're left alone. tonemap defaults to desat=2.0, which desaturates bright areas hard and is itself a cause of the washed-out look people blame on HDR; set it to 0 and judge from there. And npl is the nominal peak luminance of your source, so npl=100 on a 1000-nit master squashes the entire highlight range into SDR and produces exactly the grey mush you're trying to escape. For HDR10-style content, somewhere between 1000 and 4000 is the right neighborhood.

If zscale isn't in your build, you're not doing anything wrong. Plenty of distro packages and Docker images ship FFmpeg without --enable-libzimg. Check with ffmpeg -filters | grep zscale or ffmpeg -buildconf | grep zimg. The fallback is libplacebo, the Vulkan filter documented in the FFmpeg filters reference, which implements BT.2390 tone mapping and will also do your scaling in the same filter instance.

On ordering: a crop only selects pixels, so it's identical before or after the conversion, but a downscale should happen after you're in linear or Rec.709 space, because averaging PQ-encoded samples averages them in the wrong domain. The thing that actually hurts output quality is encoding twice. Build one filter graph, run one pass.

21:9 to 9:16 keeps about a quarter of your width, so neither crop nor pad works by default

The fraction of source width a center crop keeps is the target ratio divided by the source ratio. Kling's 21:9 is 2.33:1 and 9:16 is 0.5625:1, so a straight center crop to vertical keeps 24% of the frame and throws away three quarters of the shot you prompted for. Pad instead and you get the mirror problem: a 3840x1646 clip scaled to 1080 wide is 463 pixels tall inside a 1920-tall frame, so your subject occupies 24% of the screen height with black above and below.

Here are the real numbers for a 3840x1646 ultrawide source. Read your own dimensions off ffprobe first, since Kling's exact pixel counts vary by tier.

TargetCropScale toWidth kept
9:16 TikTok/Reels`crop=926:1646`1080x192024%
4:5 feed`crop=1316:1646`1080x135034%
1:1 feed`crop=1646:1646`1080x108043%

When the subject isn't centered, the crop filter's x offset is where you earn the shot back. crop=926:1646:(iw-ow)/2+260:0 shifts the window 260 pixels right of center, which on a 3840-wide frame is roughly a quarter of the way toward the right third. Nudge it per clip; it's faster than regenerating.

The option most people miss is the hybrid. Crop to 4:5, which keeps a third of the width instead of a quarter, then pad that into a 9:16 canvas:

ffmpeg -i kling-tagged.mp4 \
  -vf "crop=1316:1646:(iw-ow)/2:0,scale=1080:1350,pad=1080:1920:0:285,format=yuv420p" \
  -c:v libx264 -crf 20 -preset slow -r 30 \
  -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
  -c:a aac -b:a 192k -movflags +faststart \
  kling-vertical.mp4

That leaves 285 pixels of bar top and bottom, which is where hook text and captions go anyway in the shorts format. The same crop-versus-pad tradeoff shows up every time long-form becomes vertical, which we covered in repurposing long-form video to Shorts, and the per-ratio variant math is the same job as cutting all four ad ratios from one master.

Downscale to 1080p yourself rather than letting the platform do it. TikTok's practical target is a 1080x1920 H.264 MP4 with AAC audio at 30fps, somewhere around 8 to 12 Mbps. Hand it 4K and its ingest re-encodes to get there, with no say from you about how.

Doing this for one clip is a command; doing it for forty is a pipeline

Kling 4.0 shipped through MCP and a CLI with no public developer API yet, which means this week's users are exporting by hand, in batches, and then running two filter chains per file on a laptop that may or may not have zimg compiled in. That's the point where the local-FFmpeg approach stops paying for itself. FFmpeg Micro takes the clip as a job instead: no binary, no Vulkan device, no build flags to verify.

POST https://api.ffmpeg-micro.com/v1/transcodes
Authorization: Bearer YOUR_API_KEY

{
  "inputs": [{ "url": "gs://your-bucket/kling-4k-ultrawide.mp4" }],
  "outputFormat": "mp4",
  "preset": { "quality": "medium", "resolution": "1080p" }
}

The response comes back with a job id and status: "pending"; you poll GET /v1/transcodes/{id} until it reads completed, then GET /v1/transcodes/{id}/download for a signed URL. The quickstart has the whole loop in curl, Node, and Python, and the same job submits identically from n8n, Make, or Zapier, which is how most people batching generations actually run it.

Cost, since you're doing this at volume: one token buys 10 seconds of input video, metered per second with no rounding up to the minute. Forty 10-second Kling clips is 40 tokens. A new account gets 200 tokens free, about 33 minutes of video, with no card. Failed jobs are never billed, and you're billed on the duration of the video you process rather than how long the encode takes.

Pitfalls that produce a "fixed" file that still looks wrong

Most of these show up only after upload, which is the worst time to find them.

  • Feeding yuv420p10le to libx264 without a format=yuv420p in the chain silently produces High 10 profile, which plenty of phones refuse to decode.
  • Odd crop or scale dimensions fail the encoder's divisible-by-2 requirement. 926 and 1316 are even on purpose; use -2 rather than -1 in scale expressions.
  • Writing bt709 tags onto a file whose pixels are still PQ-encoded makes it look worse, not better. Tags describe, they don't convert.
  • Leaving desat at its 2.0 default, or npl at 100, undoes the tone map you just ran.
  • Skipping -movflags +faststart means the moov atom sits at the end of the file, which breaks progressive playback and some upload previews.
  • Converting at 4K and uploading 4K. Deliver 1080x1920 at 30fps and the platform has nothing left to re-encode.

FAQ

Does Kling 4.0 actually output 10-bit HDR yet?

Kling's model release page lists 10-bit HDR at 1080p and 4K as coming soon rather than shipped, so most Kling 4.0 files this week are 8-bit SDR. Run ffprobe on your own export instead of assuming either way; the pix_fmt and color_transfer fields settle it in one command.

Will tagging Rec.709 fix the washed-out look on its own?

Writing correct Rec.709 tags fixes it completely when the source is 8-bit SDR and was simply untagged, which is the common case. On a genuine PQ or HLG file, tags alone change nothing, because the pixel values still need the zscale/tonemap conversion.

Should I upload 4K to TikTok or downscale first?

Downscale to 1080x1920 before you upload. TikTok re-encodes 4K to fit its feed anyway, and doing the resize yourself with -crf 20 and a chosen bitrate gives you control over the one encode that reaches viewers.

What do I run if my FFmpeg build has no zscale filter?

Use libplacebo, the Vulkan-based filter in FFmpeg 8.0 that implements BT.2390 tone mapping and handles scaling in the same filter. Confirm which you have with ffmpeg -buildconf | grep zimg before you write a script around either one.

How do I run the color fix and the reframe over 40 generations without 40 commands?

Submit each clip as a job to a hosted FFmpeg API and poll for completion, which lets you fan out dozens of files in parallel from a Bash loop or an n8n workflow. Our bulk shorts walkthrough runs exactly that pattern over 100 clips.

The reframe half of this job is literally one click: the Resize for Format blueprint takes your Kling clip, reframes it to 9:16, 1:1, 4:5 or 16:9 with crop or pad, and hands back the finished file. Sign up free and the 200 starter tokens cover about 33 minutes of generations, which is more than a first batch needs.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Skip the command line

The Resize for Format blueprint reframes your video to any platform ratio: upload, pick the format, done.

Run it (free)