ottijp blog

Google Meetの録画動画にチャットコメントをニコニコ動画っぽく合成する

2020-10-03 Tags: GoogleニコニコSwift

Google Meetで録画した動画に,ニコニコ動画っぽくチャットコメントを合成するスクリプトを書きました. 変換結果はこんな感じになります.

作った動画は60fpsなんでもうちょっとヌルヌルとテキストが動きます. (YouTubeへ60fpsでアップロードするやり方がわかんない.)

環境

  • macOS: 10.15 (Catalina)
  • ffmpeg: 4.3.1

必要なもの

Homebrewで以下のパッケージをインストールします.

$ brew install coreutiles grep gnu-sed ffmpeg

BSD版のcsplitコマンドがヘッポコらしく,GNU版が使いたかったので,grepとsedも合わせてGNU版を使っています. Linux環境ならgcsplit,ggrep,gsedをcsplit,grep,sedに書き直せばそのまま使えるかも?

Google Meetの録画動画とチャットログのフォーマット

実際に録画した動画とチャットログはこんな感じの形式でした. 動画はフォーマット違っても大丈夫かもですが,チャットログはこのフォーマットをパースするようにしているので,他のフォーマットの場合はうまく動きません.

$ ffprobe -hide_banner movie.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'movie.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isommp42
    creation_time   : 2020-10-01T10:37:12.000000Z
  Duration: 00:01:15.74, start: 0.000000, bitrate: 613 kb/s
    Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 32000 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      creation_time   : 2020-10-01T10:37:12.000000Z
      handler_name    : ISO Media file produced by Google Inc. Created on: 10/01/2020.
    Stream #0:1(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720, 480 kb/s, 24.01 fps, 24 tbr, 1k tbn, 48 tbc (default)
    Metadata:
      creation_time   : 2020-10-01T10:37:12.000000Z
      handler_name    : ISO Media file produced by Google Inc. Created on: 10/01/2020.
$ tail chat.txt

00:00:49.779,00:00:52.779
hoge: \(^o^)/

00:00:56.397,00:00:59.397
piyo: こんにちは!

00:01:04.485,00:01:07.485
hoge: どうもどうもw

Meetのチャットログはタイムスタンプが同じ時間で2つ出ているんですが,1つ目のものをコメントを表示する時間として使っています.

やってること

動画の高fps化

自分の環境では,Meetの録画動画のフレームレートは24fpsでした. チャットコメントを動画に合成する際に,このフレームレートでコメントストリームが作られてしまうので,ちょっとガタガタした感じになってしまいます. そこで,コメントをヌルヌル流すために,60fps化しています.

チャットログの分割

チャットログを,各コメントごとに分割するためにgcsplitコマンドを使っています. BSD版のcsplitはイケてないらしいので,Homebrewのcoreutilsパッケージに含まれるGNU版のgscplitを使ってます.

ビデオへのチャットコメントの合成

ffmpegを使い,分割したチャットコメントを合成しています. コメントが多いとコマンドラインが長くなってしまうので,生成した別ファイルから-filter_scriptオプション読み込んでいます.

スクリプト中で定義しているフォントサイズとテキストマージンに応じて,動画に表示できるテキストの行数を計算し, 前のコメントとタイミングが被る場合は,別の行に表示するようにしています.

できないこと

絵文字,特殊文字?(顔文字とかに使われるような全角文字)は表示できません.(フォントに依るかも?)

使い方

フォントファイルの準備

テキストのレンダリングに使用するフォントをfont.ttfとして用意してください.

参考: IPA Font ダウンロード | 一般社団法人 文字情報技術促進協議会

録画動画とチャットログの準備

Google Meetで録画した動画とチャットログを,それぞれmovie.mp4chat.txtとして保存します.

スクリプトの実行

以下で実行します. 上書きされるファイルやディレクトリがあるので,消されたくないファイルやディレクトリがある場合は, 新規作業用ディレクトリでも作ってから実行してください.

$ ./merge

スクリプト

merge
#!/bin/bash -e

# files
readonly MOVIE_FILE=movie.mp4
readonly CHAT_FILE=chat.txt
readonly FONT_FILE=font.ttf
readonly OUTPUT_FILE=merged.${MOVIE_FILE##*.}

# merge parameter
readonly COMMENT_DURATION=5
readonly FONT_SIZE=40
readonly LINE_MARGIN=10
readonly SHADOW_WIDTH=2

# intermediates
readonly CHATS_DIR=chats
readonly FILTER_FILE=filter.txt
readonly MOVIE_HIFPS_FILE=${MOVIE_FILE%.*}_high_fps.${MOVIE_FILE##*.}

# variables forimerge
readonly MOVIE_HEIGHT=`ffprobe -v error -select_streams v:0 -show_entries stream=width,height movie.mp4 | ggrep 'height' | gsed -r 's/^height=(.*)/\1/'`
readonly LINE_SLOTS_CNT=`echo "scale=0; $MOVIE_HEIGHT / ($LINE_MARGIN + $FONT_SIZE)" | bc`

# echo params
echo "movie file: $MOVIE_FILE"
echo "font file: $FONT_FILE"
echo "movie high fps file: $MOVIE_HIFPS_FILE"
echo "output file: $OUTPUT_FILE"
echo ----
echo "comment duration: $COMMENT_DURATION"
echo "font size: $FONT_SIZE"
echo "line margin: $LINE_MARGIN"
echo "shadow width: $SHADOW_WIDTH"
echo "movie height: $MOVIE_HEIGHT"
echo "line slots count: $LINE_SLOTS_CNT"
echo ----

# initialize line slots
line_slots=()
for ((i = 0; i < LINE_SLOTS_CNT; i++)) {
  line_slots+=(0)
}

# convert movie to high fps
echo converting $MOVIE_FILE to $MOVIE_HIFPS_FILE
echo ----
ffmpeg -loglevel warning -y -i $MOVIE_FILE -r 60 $MOVIE_HIFPS_FILE

# split each chat comments
rm -rf $CHATS_DIR
mkdir $CHATS_DIR
gcsplit -f $CHATS_DIR/chat- $CHAT_FILE '/^[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9],[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9]$/' '{*}' > /dev/null

# generate filter script
rm -f $FILTER_FILE
for f in $CHATS_DIR/*;do
  if [ -s "$f" ]; then
    h=$(cat "$f" | head -1 | gsed -r 's/^([0-9]{2}):([0-9]{2}):([0-9]{2}\.[0-9]{3}),.*/\1/')
    m=$(cat "$f" | head -1 | gsed -r 's/^([0-9]{2}):([0-9]{2}):([0-9]{2}\.[0-9]{3}),.*/\2/')
    s=$(cat "$f" | head -1 | gsed -r 's/^([0-9]{2}):([0-9]{2}):([0-9]{2}\.[0-9]{3}),.*/\3/')
    time=$(echo "$h * 3600 + $m * 60 + $s" | bc)
    endtime=$(echo "$time + $COMMENT_DURATION" | bc)

    text=$(cat $f | gsed 1d | tr -d '\n' | gsed 's/:/\\\:/g')

    # for debug
    # for ((i = 0; i < LINE_SLOTS_CNT; i++)) {
    #   echo "line_slots[$i]=${line_slots[$i]}"
    # }

    # select empty slot
    slot=-1
    for ((i = 0; i < LINE_SLOTS_CNT; i++)) {
      if [ $(echo "${line_slots[$i]} < $time" | bc ) == 1 ]; then
        slot=$i
        break
      fi
    }
    # if all slot is not empty, select ealiest one
    if [ $slot -eq -1 ]; then
      min_slot=0
      for ((i = 1; i < LINE_SLOTS_CNT; i++)) {
        if [ $(echo "${line_slots[$i]} < ${line_slots[$min_slot]}" | bc ) == 1 ]; then
          min_slot=$i
        fi
      }
      slot=$min_slot
    fi

    echo "time=$time, slot=$slot, text=$text"

    echo "drawtext=text='$text':fontfile=$FONT_FILE:y=$LINE_MARGIN/2+$slot*($LINE_MARGIN+$FONT_SIZE):x=w-(t-$time)*(w+text_w)/$COMMENT_DURATION:fontcolor=white:fontsize=$FONT_SIZE:shadowx=$SHADOW_WIDTH:shadowy=$SHADOW_WIDTH," >> $FILTER_FILE
    line_slots[$slot]=$endtime
  fi
done
echo ----

# delete comma of last line
gsed -i -r '$ s/(.*),$/\1/' $FILTER_FILE

# apply chat overlay to the movie
echo converting $MOVIE_HIFPS_FILE to $OUTPUT_FILE
ffmpeg -loglevel warning -y -i $MOVIE_HIFPS_FILE -filter_script:v $FILTER_FILE -acodec copy $OUTPUT_FILE
echo ----

refs


ottijp
Satoshi SAKAO (@ottijp)

都内でアプリケーションエンジニアをしています

...