Making a YouTube Short
Tags: bash, imagemagick, ruby, ffmpeg
Published: 2023-09-28 13:07:26

Every year myself and my wife go to Essen, Germany for the annual Spiel, this year it's 5th - 8th October. She runs her own board game [YouTube channel as I Play Red](youtube.com/iplayred). I help her film and stuff. 

So, this year we decided to create a fun video before the Spiel, which coincidentally crosses over with [Rails World](https://rubyonrails.org/world) which I am sad about not attending, maybe next year.

As a fun side-project I created [MySpiel](https://myspiel.co.uk) which gathers information about all of the new board game releases at Spiel including grabbing images of the board game box art. It's a Rails project with the images stored with ActiveStorage, there are about 1,000 images so what came we make with that.

We wanted to make a [YouTube Short](https://www.youtube.com/hashtag/shorts), the criteria for that is a video with a ratio of 16:9 that is 60 seconds long at the most. Ok, it actually got to be vertical, so the ratio is actually 9:16. So I think we're going to smash all of those images together into a video just to demonstrate how huge 1,000 new releases really is. So whatever images we've got they have to be resized to that ratio before we do anything else.

[ImageMagick](https://imagemagick.org/index.php) is a pretty standard tool for image manipulation and it's got a pretty powerful command line interface which honestly is often overwhelming but fortunately there are plenty of forums, stack overflow, etc to get good examples.

Firstly though I want to find all the images on my local disk I'm using local storage for ActiveStorage. We can grab the path to all of the files and whack that in an external file. You can extract the path name to the file using `ActiveStorage::Blob.service#path_for`

```ruby
class Game < ApplicationRecord
  has_one_attached :cover

  def on_disk
    ActiveStorage::Blob.service.path_for(self.cover.key)
  end
end
```

Then we dump all the paths to a file.

```ruby
File.open(Rails.root.join("images.txt"), "w") do |f| 
  f.puts Game.all.map(&:on_disk).join("\n")
end
```

Now that we have all the paths, I'm going to use ImageMagick to resize and crop all the images to 9:16 in bash. It will read in the files from `images.txt` and output each of them to a .png

```bash
#!/bin/bash
for i in `cat images.txt`;
do 
  file $i;
  o=`basename $i`
  magick $i -gravity center -extent 9:16 -resize 1440x2560 out/$o.png;
done;
```

> [Celtae](https://boardgamegeek.com/boardgame/374200/celtae)  from [Pythagoras Games](https://pythagoras.pt/en/) is a "worker swapping" game powered by a rondel in which players choose actions to perform during their turn. 

#### Celtae 1:1

![Celtae 1:1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5im17djvp5y9bfxbhfq.png)


#### Celtae 9:16

![Celtae 9:16](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/solxf6yw6vtv6rq34gvf.png)


Ok, that took a while but now we have almost 1,100 images resized and cropped. We want to collect these altogether into a video. We can use [FFMpeg](https://ffmpeg.org/) for that, but pausing a bit. We want to fit 1,100 images into 60 seconds. That 18 images per second. I'm going to take a guess that smashing that many images together in close succession is going to be quite jarring. How can we make make it less jarring. Well for fun we could order the images by colour (or approximate colour).

This was a nice little snippet.

https://www.imagemagick.org/discourse-server/viewtopic.php?t=29789

Effectively it resizes the image in memory to 1px by 1px, then extracts the RGB value

```bash
convert celtae.png -scale 1x1\! txt:- | tail -n 1
0,0: (76,30,13)  #4C1E0D  srgb(29.6631%,11.7692%,4.93835%)
```

The full script here effectively renames each file to extracted RGB value.

```bash
#!/bin/bash
for file in *.png
do
  filename=`convert $file -scale 1x1\! txt:- | tail -n 1 | awk -F\( '{print $2}'|cut -d\) -f1|awk -F\, '{print $1$2$3}'`

  extension=".png"
  while [ -f "$filename$extension" ]
  do
    random=`echo $RANDOM % 10 + 1 | bc`
    filename=$filename$random
  done

  mv $file $filename$extension
done
```

So now I have a directory of images that are ordered by the filename (RGB value) by default. We can now use `ffmpeg` to convert our images to into a video, each image represents a frame and we can set our framerate to 18 in order to fit everything into 60 seconds.

```bash
cat *.png | ffmpeg -framerate 18 -f image2pipe -i - -c:v libx264 -r 30 -pix_fmt yuv420p output.mp4
```

The result is our `output.mp4`.

The final version needed a few tweaks, so I imported the final mp4 into [DaVinci Resolve](https://www.blackmagicdesign.com/uk/products/davinciresolve) added a few overlay images but here it is.

{% youtube Y6iSFTCtb4I %}

That was a fun, can you spot any games you are looking forward to? I can pick a few of them out and recognise but 18 frames per second is a bit unsettling. 

If you like boardgames this is [I Play Red](youtube.com/iplayred?sub_confirmation=1), if you're in Germany and not at [Rails World](https://rubyonrails.org/world) then you could do worse than to check out [Spiel](https://www.spiel-essen.de/en/). It's a little bit enormous, and a spectacle and with 1,100 releases this year you'll find something that appeals.

I'd love to see how other people are programmatically creating video content for some further inspiration.