There’s been a lot of controversy over the White House Press Secretary using a potentially-doctored video to justify banning CNN journalist Jim Acosta from the White House, with the Associated Press, Buzzfeed, Motherboard, and an endless cavalcade of Twitter users weighing in. Too much of it is wrong. Let’s recreate all the odd video artifacts for ourselves, starting from the unmodified source, and talk about what this means.
First, we need copies of both the unmodified video frames and the ones from the suspect video. youtube-dl and ffmpeg make this easy:
youtube-dl -f 136+140 https://www.youtube.com/watch?v=4BZ8ck_7cqk ffmpeg -ss 00:01:29 -t 00:00:05 -i Exchange\ between\ President\ Trump\ and\ CNN\'s\ Jim\ Acosta\ \(C-SPAN\)-4BZ8ck_7cqk.mp4 cspn%04d.png youtube-dl https://twitter.com/PressSec/status/1060374680991883265 ffmpeg -i Sarah\ Sanders\ -\ We\ stand\ by\ our\ decision\ to\ revoke\ \ this\ individual’s\ hard\ pass.\ We\ will\ not\ tolerate\ the\ inappropriate\ behavior\ clearly\ documented\ in\ this\ video.-1060374680991883265.mp4 whps%04d.png
The first odd artifact is that every second frame is replaced with a weird double-blurred frame (top is original, bottom is suspect video):
Now, the AP’s expert says in their video that this is the result of the speed being altered, but since the speed seems to match let’s forget about that for now. The video’s creator claims he made it from this Daily Wire GIF using Sony Vegas. Turns out the GIF was reduced to 15 FPS by dropping every second frame (fairly standard), the mangled frames replace ones it’s missing, and Sony Vegas apparently uses blending for frame rate conversion by default. Let’s try replicating this:
from PIL import Image in_frame = 17 count = 106 for i in range(1, count+1): if i % 2 != 0: # Copy frame directly img = Image.open("cspn%04i.png" % in_frame) prev = img; in_frame += 2 else: # Every second frame is replaced with a blend of the previous and next next = Image.open("cspn%04i.png" % in_frame) img = Image.blend(prev, next, 0.5) img.save("recr%04i.png" % i)
Sure enough, this mostly works! Top is odd video, bottom is our replica. The video still has a bunch more artifacts, but they’re normal video compression ones.
There’s only one other issue. Some frames are also duplicated in the dubious video. 21, 22, and 23 are duplicates of each other, as are 63, 64 and 65, and 105 and 106. This causes the video to slowly move out of sync with the original. This is very nearly explained by the “GIF” having an erroneous framerate of 14.566, but correcting that would require duplicating every 38th frame rather than every 40th and you’d only need one duplicate of each. In any case, it’s a regular pattern (duplicate frame 21 of every 40-frame block twice, making it a 42-frame block) and so easy to replicate:
from PIL import Image in_frame = 17 count = 106 for i in range(1, count+1): if i % 42 in (22, 23): # Duplicate these frames img = prev elif i % 2 != 0: # Copy frame directly img = Image.open("cspn%04i.png" % in_frame) prev = img; in_frame += 2 else: # Every second frame is replaced with a blend of the previous and next next = Image.open("cspn%04i.png" % in_frame) img = Image.blend(prev, next, 0.5) img.save("recr%04i.png" % i)
That’s all it takes to exactly reproduce, frame for frame, all the artifacts AP pointed to as proof it’s been doctored. Really. There are no speed-ups or slow-downs other than the duplicated frames, just a pair of dirt-simple frame rate conversions that can be imitated with a few lines of Python. You can even try it for yourself.
Note that Buzzfeed come out of all this pretty well – they guessed the frame rate of the GIF too low, and the blending means that the result isn’t as jerky as the original GIF, but their conclusions still hold up. (It’s still not clear if those duplicate frames are intentional!) AP not so much. The double-blurred frames they point to as evidence of speed manipulation aren’t, the duplicate frames they claim are an attempt to bring the video back into sync afterwards are actually the only thing causing it to fall out of sync, and the idea that something sourced from a GIF lacks audio because of some malicious intent is laughable. The Twitter users, oh god the Twitter users. The verified tweet with 88,000 retweets was the worst, but I’m too sober to deal with that one tonight.
Technically we need to replicate the zooms too I guess – here’s the first one, the rest are left as an exercise for the reader:
out_frame = count+1 for i in range(1, count+1): img = Image.open("recr%04i.png" % i) img = img.crop((146, 188, 146+483, 188+275)) img = img.resize((1280, 720), Image.BICUBIC) img.save("recr%04i.png" % out_frame) out_frame += 1
I was going to do a video about this, but that’s entirely too much work.