Remove a watermark from a video without losing quality

Answer in the first paragraph Measured on this machine Honest about the cost

Whether you lose quality depends on one thing: does the operation touch the picture, and does the tool copy the other streams or re-encode them. A mark that lives in the container — a C2PA box, a title tag — comes off with zero bytes changed: the picture decodes to identical frames and the file is the same size. A mark painted across the frames has to be re-encoded, and that is where quality is spent — measured below, almost all of it in the fill rather than in the export setting. But the damage people actually report is neither of those: it is a tool that re-encodes the picture and silently drops the audio and subtitle tracks on the way out.

So "without losing quality" is not one question, it is five. The picture, the audio track, the subtitle track, the duration and frame rate, and the alignment between picture and sound are five separate things, and each one can be checked on your own file with a single command. Below is what each method does to each of them, measured, and how to run the checks yourself.

WHAT SURVIVES EACH WAY OF REMOVING A WATERMARK Measured on one test file: 640x360, 30 fps, 4 s, audio + subtitle + 4 tags. WHAT THE FILE CARRIES METADATA STRIP CROP INPAINT RE-ENCODE* The picture identical re-encoded re-encoded kept The audio track byte-identical byte-identical byte-identical DROPPED The subtitle track kept kept kept DROPPED Duration, frame rate, sync unchanged unchanged unchanged unchanged Sync offset in every column above: -2.7 ms, the same as the source file before anything was done to it. *"Re-encode" here means a pipeline that maps only the video. Mapping every stream keeps the audio and the subtitles exactly as the crop and inpaint columns do.

What "quality" actually means for a video file

"Losing quality" is usually said as if it were one thing, but a video file carries five things that can each be damaged separately, and a watermark remover touches them differently:

Keeping all five is the real meaning of "without losing quality". The methods below are judged on all five, not just on whether the mark disappeared — that part is covered on the three layers a mark can live in and on the page about avoiding a blur box.

What each method did to each of the five, measured

Every number below comes from one script on this machine that builds its own fixtures with ffmpeg and prints the table. The test file is 640x360, 30 fps, 4 seconds, 120 frames, H.264 at CRF 20, AAC audio, a mov_text subtitle track and four container tags. A clean copy and a marked copy are made, so the mark's own cost can be separated from the method's cost.

MethodPictureAudio streamSubtitleDuration / fpsSync
Metadata stripidentical framesidentical byteskept4.000000 s / 30 fps-2.7 ms
Crop the marked stripre-encoded, 44.15 dB vs cleanidentical byteskept4.000000 s / 30 fps-2.7 ms
Inpaint the rectanglere-encoded, 38.62 dB vs cleanidentical byteskept4.000000 s / 30 fps-2.7 ms
Re-encode, video onlyre-encodedgonegone4.000000 s / 30 fpsno audio left

Read the audio column first, because that is where the surprises are. In three of the four rows the audio stream is byte-identical: the same compressed bytes went in and came out, which is a stronger statement than "sounds fine". The fourth row has no audio at all. And the sync column never moves from -2.7 ms — including in the row that re-encoded the picture, because the picture and the sound were never re-timed relative to each other.

The metadata strip changes nothing, and that is checkable

When the mark is a record in the container rather than pixels — a C2PA provenance box, a title or artist tag — removing it does not have to touch the media at all. The cleaner on this site rewrites each such box in place as a same-size padding box, so no byte in the file changes position and the media payload is never re-encoded. On the test file:

file size      508778 -> 508778 bytes   (unchanged)
mdat payload   identical md5
video stream   identical md5
audio stream   identical md5
video frames   identical framemd5   (every frame decodes to the same bytes)
streams        video, audio, subtitle   (all still present)
duration       4.000000 s   30/1 fps   120 frames
tags removed   title, artist, encoder, comment

That is the strongest form of "no quality loss" available: not "looks the same", but "decodes to the same bytes". The trade is stated on the free video watermark remover page and it is worth repeating here — because the file does not get smaller, the file size does not drop. That is the price of never moving a byte, and it is why a faststart file keeps playing after the edit.

Cropping and inpainting: the picture is re-encoded, the audio is not

Once the mark is in the pixels, no method can restore the original frames — they were overwritten when the mark was drawn on. What you can choose is which cost to pay, and the measurement shows the audio is not part of it. Cropping the marked strip off and inpainting the rectangle both re-encode the picture, and both leave the audio stream byte-identical, because ffmpeg copies it with -c:a copy while it works on the video:

crop    audio stream md5   7a4f62b07fea6d77a00f485326b448fc   (same as source)
inpaint audio stream md5   7a4f62b07fea6d77a00f485326b448fc   (same as source)

one extra encode, no mark  48.80 dB   (the encode bill, nothing else)
marked vs clean            23.79 dB   (what the mark itself costs)
crop vs the same crop of the clean file   44.15 dB
inpaint vs clean                          38.62 dB

Read those together. A single extra encode of a clip that had no mark at all already costs 48.80 dB, so cropping at 44.15 dB is spending roughly the price of one encode and inventing nothing — every pixel you keep is a real pixel. Inpainting at 38.62 dB is spending the same encode plus the reconstruction, and it is still far better than leaving the mark (23.79 dB). Neither number is a quality loss in the audio, and neither changes the timeline. If your concern is the fill rather than the file, the logo page scores the same methods in more detail.

The encoder setting is not where the inpaint's loss is

38.62 dB is the figure above for inpainting, and the first instinct is to win it back with a better export setting. The two halves of an inpainted frame answer that question differently: outside the mark's rectangle the only error is compression, and inside it the error is whatever the fill invented. PSNR is mean squared error, so the halves can be separated exactly rather than estimated — the box is measured on its own and the rest of the frame is derived from the whole-frame figure:

mse_outside = (mse_whole x pixels_total - mse_box x pixels_box) / pixels_outside

The same repaired file was then exported six times, CRF 12 through CRF 32, by dev/inpaint_ladder.py, which rebuilds the fixture above and prints the table. Both regions were scored against the clean file at every setting, and higher means closer to it. The whole frame is given twice — once as the YUV average the table above uses, so the CRF 20 row can be checked against its 38.62 dB, and once as luma alone, which is the metric the two region columns are built from:

CRF   file size   whole (YUV)   whole (luma)   inside box   outside box
12      784,337       38.88          41.48         26.25        46.54
16      630,009       38.78          41.30         26.24        46.00
20      493,230       38.62          41.04         26.27        45.21
23      387,411       38.06          40.05         26.28        42.94
28      205,075       36.50          37.61         26.26        38.97
32      136,928       35.09          35.73         26.30        36.51

Read the last two columns down. CRF 12 to CRF 32 is a 5.7 times change in file size, and it moves the picture outside the box by 10.03 dB while moving the inside of the box by 0.05 dB. The fill has already decided that rectangle. No setting reopens it.

THE ENCODER MOVES ONE REGION AND NOT THE OTHER One repaired file, six exports. Decibels against the clean file; higher is closer. outside the mark's box inside the mark's box 25 30 35 40 45 dB 10.03 dB across the range flat: 26.2 to 26.3 dB at every setting CRF 12 16 20 23 28 32 Dashed line: 40 dB, where a repaired box stops reading as a box. The box is 120 x 40 px, 2.08% of the frame.

That column could be measuring encoder noise rather than the mark, so the same box was scored on the marked file with no repair at all. If it were noise, it would climb at a lower CRF. It does not: 6.06 dB at CRF 20 and 6.06 dB at CRF 12, while the whole frame barely moves either, at 22.84 and 22.85 dB. The box column sees the mark and nothing else. In that rectangle the repair is worth 20.19 dB and the encoder is worth 0.00 dB.

The box is 120 x 40 px, 2.08% of a 640 x 360 frame, and it costs the whole-frame luma score 4.17 dB: 45.21 dB outside it against 41.04 dB over the whole picture at CRF 20. A mark that size is still the loudest thing in the file, and it is why the whole-frame figure is the wrong one to optimise — 98% of the pixels can be near-perfect and the score still belongs to the corner.

What follows for the export setting is unglamorous. Land at CRF 20, or at whatever the source was encoded with and no better. CRF 28 saves 58% of the bytes and gives up 6.24 dB outside the box; CRF 12 spends 59% more bytes than CRF 20 to buy 1.33 dB outside it and 0.02 dB less inside it. Bytes spent chasing the box are spent where the outcome was already fixed.

The 26 dB inside the box is visible, and this page does not pretend otherwise. It is also this fixture's own number: a solid 120 x 40 mark over moving synthetic detail. A flat background scores better, texture scores worse, and a wider mark costs more of the frame — the crop arithmetic for a full-width bar is measured on the Snapchat page, and the four pixel routes are scored against each other on the logo page.

The second encode: whose loss is it when the upload looks softer

Every number above describes the file on disk. By the time a viewer sees it, a second encoder has run: the delivery ladder. The complaint that follows — "it looked fine before I uploaded it" — is usually read as proof that the removal was bad. It can be measured instead of argued, because mean squared error adds when two errors are independent, so the removal's error outside the mark and the ladder's error outside it can be computed separately and then added back to predict the file that comes out of the platform.

The method, stated before the numbers: nothing was uploaded and no platform's encoder was used. The cleaned file was put through libx264 at a bitrate matched in bits per pixel to a published ladder — 0.129 bpp, which is 1920 x 1080 at 30 fps and 8 Mbps, and 0.064 bpp, the same frame at 4 Mbps — and the untouched clean file went through the identical encode as the control. dev/generation_two.py rebuilds the fixture, runs both and prints every table below. Matching bits per pixel is the part that makes a 640 x 360 test stand in for a 1080p upload: the encoder is handed the same budget per pixel either way.

                      kbps     bytes  whole luma  outside box
gen 1, on disk           -   493,230       41.04        45.21
ladder A, 0.129 bpp    891   436,834       40.09        43.01
ladder B, 0.064 bpp    442   218,331       37.52        38.82

The second encode moves the picture outside the mark by 2.20 dB at the generous ladder and 6.39 dB at the tight one. Both of those are larger errors than the removal's own export left, which is the first surprising thing: the step nobody thinks of as lossy is the more lossy of the two.

Because the two errors are separable, the split can be computed rather than estimated:

              removal MSE  ladder MSE  removal share  ladder share
ladder A           1.9592      1.6986          53.6%         46.4%
ladder B           1.9592      7.6750          20.3%         79.7%

Two honest qualifications. The shares are not an exact decomposition: adding the two errors predicts 42.50 dB outside the box at the generous ladder and 38.29 dB at the tight one, while the files measured 43.01 and 38.82 — half a decibel better than the sum in both cases, so the second encode absorbs part of the first one's error instead of stacking on it. Treat each share as an upper bound. What the arithmetic does establish is the ordering, and the ordering is the answer to the question: at a tight ladder the second encode is four times the removal's error; at a generous one the two are close to even.

Inside the mark's box the second encode does nothing at all. The repaired box scored 26.27 dB, and 26.29 and 26.34 dB after the two ladders — +0.02 and +0.07 dB. The same encode applied to the clean file scores 54.03 and 49.26 dB in that rectangle, roughly 23 dB better than the repair, so what is left in the box is the fill's error and no ladder reaches it. Uploading does not hide a bad repair and does not repair one either.

That leaves the one knob the reader controls, the export setting. The ladder above says CRF 12 is worth 7.57 dB outside the box over CRF 28. Put all three settings through the tight delivery encode and most of it disappears:

CRF    on disk     after upload  bytes after upload
12       46.54            38.89             219,095
20       45.21            38.80             217,870
28       38.97            37.78             209,066

15% of the advantage survives. CRF 12 and CRF 20 land 0.09 dB apart after the upload — a tie, for 59% more bytes on disk — and only CRF 28 is still measurably worse, by 1.02 dB. The mechanism is not mysterious: a bitrate-targeted encode has a quality ceiling set by the bitrate it is handed, and a better source runs into that ceiling and stops improving. So the export setting is worth choosing once, at CRF 20 or at whatever the source was encoded with, and not worth optimising past that for a file that is going to a platform. Keep the low-CRF master for the copy that stays on a disk and in an editor, where nothing re-encodes it a second time — that is the case the section above measures.

Two limits belong in the same breath. The ladder here is modelled, not observed: a real platform's encoder, its scene analysis and its per-title settings will differ, and one that spends more bits will take less. And the fixture is 640 x 360 synthetic detail under a solid mark; a soft mark over flat sky gives an encoder less to spend and less to lose. What transfers is the direction and the order of magnitude, not the third decimal.

WHERE THE DAMAGE OUTSIDE THE MARK COMES FROM Squared error outside the mark, split between the removal's own export and the second encode. generous ladder 891 kbps, 0.129 bpp 54% 46% tight ladder 442 kbps, 0.064 bpp 20% 80% the removal's own export the upload's second encode A lower CRF keeps 15% of its on-disk advantage through the same encode: 7.57 dB becomes 1.11 dB. Inside the mark's box the second encode moves the score by 0.07 dB or less, either ladder.

The bit depth is the one loss no CRF buys back

Everything above is a compression loss, and compression loss is bought back with bytes. There is a third axis that no bitrate setting touches: how many luminance levels the frame is carried in, and how many the fill is handed. A 10-bit source has 1024 of them. If the fill is handed 256, three quarters of the gradient is gone before the mark has even been looked at, and no CRF, no bitrate and no "maximum quality" preset puts a single level of it back.

The mechanism is one line in the filter graph, and it is worth reading rather than trusting. ffmpeg 9.0.1's delogo declares 8-bit pixel formats only, so on a 10-bit input the graph silently inserts a conversion before the fill and another one after it:

ffmpeg -v verbose -i ten_bit.mkv -vf "delogo=x=1000:y=40:w=140:h=80" -frames:v 1 -f null -

[Parsed_delogo_0] auto-inserting filter 'auto_scale_0' between the filter
  'graph -1 input from stream 0:0' and the filter 'Parsed_delogo_0'
[auto_scale_0] w:1280 h:720 fmt:yuv420p10le -> w:1280 h:720 fmt:yuv420p

The measurement follows from that line. One 1280 x 720 fixture, a luminance ramp from 0 to 1023 across the width, a solid 140 x 80 logo plate in the top right, twelve frames — and every file stored as FFV1 lossless, so no encoder is in the chain and a difference between two outputs cannot be a compression difference. dev/bit_depth.py rebuilds the fixture and prints the table.

                                     levels  max step    step %
clean 10-bit, no mark                  1024         1    0.098%
clean + logo, 10-bit                   1024         1    0.098%
delogo, 10-bit in, 10-bit out           256         4    0.391%
boxblur, 10-bit in, 10-bit out         1009         1    0.098%
eq, 10-bit in, 10-bit out               256         4    0.391%
8-bit in, delogo, 8-bit out             256         1    0.392%
10-bit in, delogo, 8-bit out            256         1    0.392%
8-bit, no delogo (mark kept)            256         1    0.392%
8-bit, no mark, nothing removed         256         1    0.392%

Read the second and third rows together. The frame arrives with 1024 levels and leaves the fill with 256, and the largest step between two neighbouring pixels on a smooth ramp goes from 1 to 4 of 1023 — from 0.098% of full scale to 0.391%. That is banding stated in the only unit an eye cares about: how far apart two adjacent pixels are allowed to be. A step four times wider means four times fewer steps across the same gradient, and on a sky or a wall that is exactly what a band is.

The boxblur row is the control that decides whose fault this is. Same input, same 10-bit output, but a filter that does declare 10-bit formats: 1009 of 1024 levels survive and the step stays at 1. The container, the codec and the pipeline can all carry ten bits, so the loss belongs to the filter's declared format list, not to the file. The eq row says the same thing from the other side — it drops to 256 as well, so this is not a quirk of delogo but a property of a whole class of common filters, and it stays invisible until something counts.

The third result is the one that changed this page's mind. If a 10-bit filter chain helped at all, then "10-bit in, 10-bit out" should beat "10-bit in, 8-bit out". Measured against the clean 10-bit frame, in one domain so depth is not counted as error, they are not close — they are identical:

                                        box PSNR  whole frame
10-bit in, 10-bit out                   58.24 dB     56.06 dB
8-bit in,  delogo, 8-bit out            58.24 dB     56.06 dB
10-bit in, delogo, 8-bit out            58.24 dB     56.06 dB

Same pixels to the second decimal, because they are the same pixels: all three went through an 8-bit fill. Keeping the result in a 10-bit container afterwards is a labelling decision, not a quality one. It costs bytes, buys nothing, and the levels column had already said so — 256 in all three rows.

The honest boundary is grain. Repeat the comparison with plus or minus 8 of noise added to the ramp, 0.8% of full scale, and the level count collapses on both sides: 738 for the 10-bit frame against 254 for the 8-bit one, while the largest step is set by the noise itself, 1.662% against 1.955%. Dither does what dither does — it hides banding by destroying the smoothness that made it visible. This is a loss you will see on a gradient and not see on foliage, and a page claiming otherwise would be selling a metric rather than a picture.

What follows is a trade, not a fix. Keeping ten bits means the fill has to be built from filters that carry them, and the obvious 10-bit-capable fill in ffmpeg's default set is a blur — the one fill this site has already measured as keeping the mark's average brightness instead of removing it (measured on the blur page). So the choice is an 8-bit interpolated fill that bands on a smooth gradient, or a 10-bit blur that does not band and does not remove the mark either. There is no free 10-bit fill in the default filter set, and saying so is more use than implying that a 10-bit project setting solved it.

One limit on the word HDR: bit depth is what is measured here, and HDR adds a transfer function — PQ or HLG — on top of it. This page measured depth, not tone mapping, and a file that was tone-mapped down to SDR on the way in will have lost more than the four-to-one above before any filter ran.

THE FILL IS HANDED 8 BITS, THEN STORED AS 10 Luminance levels across a smooth ramp, before the fill, during it, and in the file that comes out. the source 10-bit, as it arrives 1024 levels largest step 1 of 1023 8-bit scale the fill delogo runs here 256 levels largest step 4 of 1023 scale back the file 10-bit container 256 levels identical pixels, more bytes Control: boxblur on the identical 10-bit input keeps 1009 of 1024 levels, step still 1. So the container and the pipeline carry ten bits; the fill's format list is what drops them. The three paths score 58.24 dB in the box and 56.06 dB over the frame, to the second decimal. With plus or minus 8 of grain on the ramp the count falls to 738 against 254, and the step is the noise.

The failure that actually loses you the file: dropped tracks

The damage people report is not a soft picture. It is a video that comes back with no sound, or no captions — because the pipeline re-encoded the picture and never mapped the other streams. That is not hypothetical: it is what happens when a re-encode maps only the video, which is the default shape of a great many one-click tools. Measured on the same test file:

map every stream     streams: video, audio, subtitle
map only the video   streams: video

                     (the audio and the subtitle are not degraded - they are absent)

There is no warning for this. The file plays, the picture looks right, and the audio is simply gone. It is also the reason "does removing a watermark remove the audio" is worth answering with a command rather than an assurance: the stream list is the first thing to read, and it takes one line.

Sync: why the offset does not move

To measure sync rather than assume it, the test file is built with a sound and a picture event at the same instant: a 1 kHz tone gated to a burst at exactly 1.000 s, and a white flash at exactly 1.000 s. The tone's first loud sample and the flash's frame are then located in each output. The source file itself measures the tone at 1.0027 s against the flash at 1.0000 s — a residual 2.7 ms that the AAC encode left in the file before anything was done to it. That same -2.7 ms offset appears after the metadata strip, after cropping and after inpainting:

source file              tone 1.0027 s   flash 1.0000 s   offset -2.7 ms
after metadata strip     tone 1.0027 s   flash 1.0000 s   offset -2.7 ms
after cropping           tone 1.0027 s   flash 1.0000 s   offset -2.7 ms
after inpainting         tone 1.0027 s   flash 1.0000 s   offset -2.7 ms

The offset does not move because nothing re-timed it. Copying the audio stream cannot change the alignment — the bytes are the same bytes — and re-encoding the picture does not touch the audio at all. This is why the audio column in the table above is the one to watch: as long as the audio is copied, sync is preserved by construction, and the moment a tool re-encodes the audio it has introduced a variable that was not there before.

Check your own file in three commands

None of this needs the tool that made the file, or this site. On the file you have:

# 1. Is everything still there, and is the timeline right?
ffprobe -v error -show_format -show_streams cleaned.mp4
#    look for: one video stream, one audio stream, any subtitle stream,
#    and duration / r_frame_rate / nb_frames matching the original

# 2. Is the audio stream the same bytes as before?
ffmpeg -i original.mp4 -map 0:a -c copy -f md5 -
ffmpeg -i cleaned.mp4  -map 0:a -c copy -f md5 -
#    identical output means the audio was copied, not re-encoded

# 3. Did the picture change at all?
ffmpeg -i original.mp4 -map 0:v -f framemd5 - > before.txt
ffmpeg -i cleaned.mp4  -map 0:v -f framemd5 - > after.txt
diff before.txt after.txt
#    no output means every frame decodes to identical bytes

If the mark was in the container, check three gives no output and the file is provably untouched. If the mark was in the pixels, check three will differ on every frame — that is expected, because the picture was re-encoded — and checks one and two are the ones that catch a broken file. The metadata-strip method this site's browser tool performs is the case where all three pass at once, and it is described on the page about removing metadata from a video.

Frequently asked questions

Does removing a watermark reduce video quality?

It depends on which layer the mark is in. If the mark is a record in the container — a C2PA provenance box, a title or an artist tag — then removing it does not touch a single pixel: on this site's test file the picture decoded to identical bytes before and after, and the file did not change size. If the mark is painted across the frames, then removing it means re-encoding the picture, and that is where quality is spent, on every pixel in the frame rather than only the ones under the mark. Measured here, one extra encode of a clip that had no mark at all cost 48.80 dB; inpainting the marked rectangle reached 38.62 dB against the clean original. So the honest answer is: metadata removal costs nothing, pixel removal costs one generation.

Can a higher export quality bring back what the fill removed?

No, and the split is measurable. The fill's error sits inside the mark's rectangle; the encoder's error sits outside it. Across CRF 12 to CRF 32 the inside of the box moves from 26.25 dB to 26.30 dB, 0.05 dB in total, while the picture outside it moves 10.03 dB. The files behind those two ends are 784,337 and 136,928 bytes. A better export setting buys back compression, and compression is not what was lost in that rectangle.

Does removing a watermark from a video remove the audio?

It should not, and there is a way to prove it did not. The audio is a separate stream in the file, so a tool can copy it byte for byte while it rewrites the picture. On this site's test file, the audio stream's MD5 was identical after the metadata strip, after cropping and after inpainting — the same compressed bytes went in and came out. What does lose the audio is a pipeline that re-encodes the picture and forgets to map the other streams: the same command with only -map 0:v produced a file whose stream list was video and nothing else. That is the failure to look for, and it is visible in one line of ffprobe output.

Will my subtitles survive watermark removal?

If they are a real subtitle track inside the file — mov_text in an MP4, for instance — they survive anything that copies the streams, including the metadata strip, cropping and inpainting, as long as the tool maps them. On the test file the subtitle stream was still present after all three. The risk is the same one that loses the audio: a re-encode that maps only the video drops the subtitle track silently, and you find out when the file plays with no captions. Burned-in subtitles are a different case entirely — those are pixels, and they come off only the way the mark does.

Does removing a watermark change the video's duration or frame rate?

Not on any method measured here. The test file was 4.000000 seconds, 30/1 frames per second and 120 frames, and those three numbers were identical after the metadata strip, after cropping the marked strip off and after inpainting the rectangle. Removing a watermark is not a trim and not a speed change, so there is no reason for the timeline to move — but it is one of the first things to check, because a tool that re-times the file to its own container will show it here. ffprobe prints duration, r_frame_rate and nb_frames in one call.

Can watermark removal put the audio out of sync?

Only if the tool re-encodes the audio instead of copying it. When the audio stream is copied, its bytes are unchanged, so the alignment cannot move — that is a proof, not a promise. On the test file the beep was authored at exactly 1.000 s; it measured at 1.0027 s in the source, a residual 2.7 ms from the AAC encode that was already there before anything was done to the file, and it measured at exactly the same 1.0027 s after the metadata strip, after cropping and after inpainting. The offset never moved. Re-encoding the audio produces a different stream, which is where a tool can introduce a delay that was not in the original.

How do I check that a cleaned video is still intact?

Three commands, and none of them needs the original tool. First, ffprobe -v error -show_format -show_streams cleaned.mp4 to confirm the stream list still contains the video, the audio and any subtitle track, and that duration and frame rate are what you expect. Second, ffmpeg -i original.mp4 -map 0:a -c copy -f md5 - against the same command on the cleaned file, to confirm the audio stream is byte-identical. Third, ffmpeg -i original.mp4 -f framemd5 - against the cleaned file, to see whether the picture changed at all — no output from a diff means every frame decodes to the same bytes. If the tool re-encoded the picture, that last comparison will differ everywhere, which is expected; the first two are the ones that catch a broken file.

Why does my video look worse after I upload it?

Because a second encode happens after yours. The cleaned file is re-encoded by the delivery ladder at a bitrate the platform chooses, and that step can be measured on its own: putting a repaired file through an encode matched to a 4 Mbps 1080p ladder moved the picture outside the mark by 6.39 dB, while the removal's own export still scored 45.21 dB against the clean original. Outside the mark, 79.7% of the squared error at that bitrate belongs to the second encode and 20.3% to the removal. A platform that spends more bits takes less — at a ladder matched to 8 Mbps the split was 46.4% against 53.6% — so the exact figure is not fixed, but the direction is, and it is why a file can look clean on disk and soft on the web.