Jetson OrinでNVENC(nvmpi)を使ってffmpegで画面収録

タイトルの通りです。Jetsonにはせっかくハードウェアエンコーダがあるのでそれを利用してみよう！という魂胆です。
(筆者の意図としてはROSを実行している際に，CPU側に負担をかけずにrvizの画面を録画したいだけです。しょうじき、Jetson AGX Orin Dev KitではCPUが12コアもあるため、録画にコアを割く余裕はありそうですが…)

環境

- Jetson AGX Orin 32GB Dev Kit
- JetPack 5.1.2 / L4T 35.4.1

以下、クリーンな環境で実行したわけではないので、要所要所でパッケージのインストールが必要かもしれません。
(筆者はこれを行う前にaptでffmpegやobsを入れたため、そのあたりの依存パッケージが必要なんじゃないかなーと思います)

## ハードウェアエンコーダを使えるffmpegのビルド

aptでffmpegを入れてもJetsonのハードウェアエンコーダは利用できないため、カスタムビルドを行います。

(偉大な先人がいっぱいいるので、それに倣います!)

参考:
https://qiita.com/tetsu_koba/items/44a91bc04805b34e297f
https://qiita.com/kitazaki/items/02298356703418450dcc

### jetson-ffmpegのビルド

上記の参考例ではjocover/jetson-ffmpegが利用されていますが、Issueを見ると以下のフォーク版を使うのが良いようです。

https://github.com/Keylost/jetson-ffmpeg

なので、こうなります。(途中までREADMEそのままです)

```bash
git clone https://github.com/Keylost/jetson-ffmpeg.git
cd jetson-ffmpeg
```

ここで、お好きなテキストエディタで`CMakeLists.txt`を開き、以下の行をコメントアウトします。
(2023/11/23時点。今後リポジトリが更新されたら必要なくなるかも)

```cmake
# これを
find_library(LIB_NVBUF nvbuf_utils PATHS /usr/lib/aarch64-linux-gnu/tegra)

# こう
# find_library(LIB_NVBUF nvbuf_utils PATHS /usr/lib/aarch64-linux-gnu/tegra)
```

READMEの続きを実行します。

```bash
mkdir build
cd build
cmake ..
make
sudo make install
sudo ldconfig
```

ここまでで、ffmpegでNVENC(nvmpi)を使うための準備ができました。

### ffmpegのビルド

jetson-ffmpegのREADMEの後半部分です。デスクトップ収録(特に音声)のためにちょっと改変します。

まずはffmpegを持ってきてパッチを当てます。READMEでは3パターン書いてありますが、筆者は6.0にしてみました。

```bash:4.2の場合
git clone git://source.ffmpeg.org/ffmpeg.git -b release/4.2 --depth=1
cd ffmpeg
wget -O ffmpeg_nvmpi.patch https://github.com/Keylost/jetson-ffmpeg/raw/master/ffmpeg_patches/ffmpeg4.2_nvmpi.patch
```

```bash:4.4の場合

git clone git://source.ffmpeg.org/ffmpeg.git -b release/4.4 --depth=1
cd ffmpeg
wget -O ffmpeg_nvmpi.patch https://github.com/Keylost/jetson-ffmpeg/raw/master/ffmpeg_patches/ffmpeg4.4_nvmpi.patch
```

```bash:6.0の場合
git clone git://source.ffmpeg.org/ffmpeg.git -b release/6.0 --depth=1
cd ffmpeg
wget -O ffmpeg_nvmpi.patch https://github.com/Keylost/jetson-ffmpeg/raw/master/ffmpeg_patches/ffmpeg6.0_nvmpi.patch
```

パッチを当てたら、configureしてmakeします。ここで、デスクトップで流れている音声をキャプチャしたいかどうかで分岐します。

とにかくデスクトップ画面を録画したいからNVENCだけ有効ならいいよという場合↓
(単純にビデオエンコードやwebカム映像を保存するだけの場合もこっちですかね?)

```bash
git apply ffmpeg_nvmpi.patch
./configure --enable-nvmpi
make
```

デスクトップ音声もキャプチャする場合↓

```bash
git apply ffmpeg_nvmpi.patch
sudo apt install -y libpulse-dev # 筆者の環境では必要でした
./configure --enable-nvmpi --enable-libpulse
make
```

また、その他諸々のオプションがあるため、必要に応じて https://github.com/FFmpeg/FFmpeg/blob/master/configure を参考に設定してください。

ちなみに、以下で行う画面収録では`x11grab`が有効になっていないとダメなようです。上記の例ではアリでビルドされているみたいです。

ここまで終えたら、必要に応じてPATHを通しておきます。

## いざ録画

ここでも、デスクトップ画面だけで良いのか、デスクトップ音声も収録したいのかで話が変わってきます。

### とりあえずデスクトップ画面をキャプチャ

オプションはよしなに弄ってください。

```bash
ffmpeg -y \
    -video_size 1920x1080 -framerate 30 -f x11grab -i :0.0+0,0 \
    -vf scale=1280:-2 -c:v h264_nvmpi -b:v 2500k \
    -an \
    -pix_fmt yuv420p \
    $HOME/Videos/hoge.mkv
```

- `-video_size`: 取得する画像サイズ。デスクトップ全体ならそれに合わせる(上記の例では1920x1080)。
- `-framerate`: フレームレート。30とか60とか。
- `-f x11grab`: スクリーン録画。
- `-i :0.0+0,0`: 画面`:0.0`の左上`0,0`から録画する。(ここで、`:0.0+10,100`とか指定すると左上から10px横に、100px縦にずれた位置から`-video_size`で指定したエリアを録画します。たぶん。)
- `-vf scale=1280:-2`: 横が1280pxになるようにスケーリング。縦はアス比を可能な限り保つように切り捨てだか切り上げだかする。今回は1920x1080が1280x720にスケールされる。
- `-c:v h264_nvmpi`: コーデック指定。**Jetsonのハードウェアエンコーダでh264エンコード。**
- `-b:v 2500k`: ビデオビットレートを2500kbpsにする。クオリティ指定の方が良いかも？
- `-an`: オーディオを含めない。
- `-pix_fmt yuv420p`: つけてると再生できるソフトが増えるらしい。
- `$HOME/Videos/hoge.mkv`: ホームディレクトリのVideosディレクトリに、hoge.mkvという名前で出力する。

### 音声付きでキャプチャ

上記の例とほとんど同じです。音声に関する部分が変わっています。

```bash
ffmpeg -y \
    -video_size 1920x1080 -framerate 30 -f x11grab -i :0.0+0,0 \
    -f pulse -ac 2 -i alsa_output.platform-3510000.hda.hdmi-stereo.monitor -c:a aac -ar 44100 \
    -vf scale=1280:-2 -c:v h264_nvmpi -b:v 2500k \
    -pix_fmt yuv420p \
    $HOME/Videos/hoge.mkv
```

増えたオプションだけ:

- `-f pulse`: pulseaudioで収録
- `-ac 2`: オーディオチャネル数。2なのでステレオ．
- `-i alsa_output.platform-3510000.hda.hdmi-stereo.monitor`: 使うデバイス(※)
- `-c:a aac`: オーディオコーデック。この場合はaacを使う。
- `-ar 44100`: オーディオサンプルレート。44100か48000を指定しておけばいいと思う。

※: これを確認するためには`pactl list short sources`を実行します。↓のような感じで出てくるので、今使っているものを選択してください。HDMIオーディオを使っているなら、`hdmi-stereo.monitor`と入っているやつでいいと思います。

```bash
$ pactl list short sources
0	alsa_output.platform-sound.analog-stereo.monitor	module-alsa-card.c	s16le 2ch 44100Hz	SUSPENDED
1	alsa_input.platform-sound.analog-stereo	module-alsa-card.c	s16le 2ch 44100Hz	SUSPENDED
2	alsa_output.platform-3510000.hda.hdmi-stereo.monitor	module-alsa-card.c	s16le 2ch 44100Hz	IDLE
```

このあたりも参考:
http://underpop.online.fr/f/ffmpeg/help/x11grab.htm.gz
http://trac.ffmpeg.org/wiki/Capture/Desktop

ちなみに変な解像度のビデオを食わせるとnvmpiがエラーを出すので、1920とか3840とか食いやすそうな値を入れてあげてください。
また、ハードウェアエンコーダが働いているかどうかを見るには、以下の`jtop`が便利だと思います。

https://github.com/rbonghi/jetson_stats

## テキトーにまとめたスクリプト

以下のスクリプトを実行すると，勝手にデスクトップ解像度を取得して録画してくれます。

スケーリングとオーディオ収録でそれぞれオプションをつけておきました。デフォルトでは1080pでオーディオ無しを収録しますが、`./screen_record.sh --scale 720p --audio`とすると、720pでオーディオ付きで画面収録します。

```bash:screen_record.sh
# get screen resolution
RESOLUTION=`xdpyinfo | grep dimensions | sed -r 's/^[^0-9]*([0-9]+x[0-9]+).*$/\1/'`

# get current date and time, like 20231123-123456
CURRENT_DT=`date "+%Y%m%d-%H%M%S"`
# output file path
OUT_PATH=$HOME/Videos/record_$CURRENT_DT.mkv

# default output scaling and audio record option
SCALE="1080p" # scales are written below
AUDIO_OPT="-an"

# command line args treatment
while (( $# > 0 ))
do
  case $1 in
    # to check the input, run: pactl list short sources
    # TODO: automatically decide input source
    -a | --audio)
      AUDIO_OPT="-f pulse -ac 2 -i alsa_output.platform-3510000.hda.hdmi-stereo.monitor -c:a aac -ar 44100"
      ;;
    -s | --scale | --scale=*)
      if [[ "$1" =~ ^--scale= ]]; then
        SCALE=$(echo $1 | sed -e 's/^--scale=//')
      elif [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]; then
        echo "'option' requires an argument." 1>&2
        exit 1
      else
        SCALE="$2"
        shift
      fi
      ;;
  esac
  shift
done

echo "AUDIO OPTOIN IS $AUDIO_OPT"

echo "OUTPUT SCALE SELECTION IS $SCALE"
# I did not check whether the resolutions below are OK
if [ $SCALE = "2160p" ]; then
  SCALE="-2:2160"
elif [ $SCALE = "1440p" ]; then
  SCALE="-2:1440"
elif [ $SCALE = "1080p" ]; then
  SCALE="-2:1080"
elif [ $SCALE = "720p" ]; then
  SCALE="-2:720"
elif [ $SCALE = "540p" ]; then
  SCALE="-2:540"
elif [ $SCALE = "480p" ]; then
  SCALE="-2:480"
fi
echo "SO THE SCALE OPTION OF ffmpeg will be $SCALE"

sleep 2 # wait for 2 secs

ffmpeg -y \
    -video_size $RESOLUTION -framerate 30 -f x11grab -i $DISPLAY.0+0,0 \
    $AUDIO_OPT -vf scale=$SCALE -c:v h264_nvmpi -b:v 2500k -pix_fmt yuv420p $OUT_PATH
```