録画サーバの構築(地デジ・BS・CS録画環境) – Debian Linuxによる自宅サーバ

自宅サーバ
自宅サーバ

パソコンに接続できるTVチューナーを使って、自宅サーバで地デジ・BS・CSを録画できる環境を構築する方法をご紹介します。具体的には、株式会社プレクスが販売しているPX-Q3U4/PX-Q3PE4のTVチューナーを使用し、録画アプリケーションとしてMirakurunとEPGStationをDocker上で動かします。

必要な機器

  • TVチューナー

    • PX-Q3U4 (外付け型)
    • PX-Q3PE4 (内蔵型)

    上記以外の機種でもドライバが対応していれば問題ありませんが、設定が変わる場合もあります。

  • ICカードリーダー
    クレジットカードやマイナンバーカードにはICチップが埋め込まれているのですが、それらを読み取る事ができる装置です。

    ICカードリーダ
    https://www.amazon.co.jp/gp/product/B003XF2JJY

  • B-CASカード

    地デジの情報を復号化するために必要です。
    既存のテレビに付属しているものを使用できますが、その場合、そのテレビでは地デジが見られなくなります。
    B-CASカードにはICチップが埋め込まれていて、ICカードリーダーから読み取ります。

  • アンテナ類
    チューナーに接続するアンテナケーブルやアンテナ分配器が必要になります。

ドライバのインストール

必要なパッケージをインストールします。

sudo apt install -y linux-headers-`uname -r` cmake gcc git g++ make dkms

ドライバをインストールします。

cd /usr/src

sudo git clone https://github.com/nns779/px4_drv

cd px4_drv/fwtool
sudo make
sudo wget http://plex-net.co.jp/plex/pxw3u4/pxw3u4_BDA_ver1x64.zip -O pxw3u4_BDA_ver1x64.zip
sudo unzip -oj pxw3u4_BDA_ver1x64.zip pxw3u4_BDA_ver1x64/PXW3U4.sys
sudo ./fwtool PXW3U4.sys it930x-firmware.bin
sudo mkdir -p /lib/firmware
sudo cp it930x-firmware.bin /lib/firmware/

# dkmsでカーネルドライバを自動的に設定
cd ../
sudo cp -a ./ /usr/src/px4_drv-0.2.1
sudo dkms add px4_drv/0.2.1
sudo dkms install px4_drv/0.2.1

再起動します。

sudo reboot

デバイスが表示されるか確認します。

ls /dev/px4*

下記のようなデバイスが表示されれば大丈夫です。

/dev/px4video0  /dev/px4video1  /dev/px4video2  /dev/px4video3  /dev/px4video4  /dev/px4video5  /dev/px4video6  /dev/px4video7

BIOSのSecure Bootの設定が有効になっているとドライバはインストールできません。
Secure Bootを無効にしてください。

ハードウェアエンコードの設定

動画のエンコードにハードウェアエンコードを行えるCPUがあります。
ハードウェアエンコードを使用する設定になります。

パッケージのインストールを行います。

sudo apt install -y firmware-amd-graphics i965-va-driver-shaders mesa-va-drivers vainfo
  • firmware-amd-graphics
    AMD/ATI グラフィックス チップ用のバイナリファームウェア

  • i965-va-driver-shaders
    Intel CPU用 VA-APIドライバ

  • mesa-va-drivers
    AMD CPU用 VA-APIドライバ

  • vainfo
    VA-APIのサポートをチェック

再起動します。

sudo reboot

VA-APIで使用するデバイスの確認。

ls /dev/dri

renderD128 が表示されれば問題ありません。

by-path  card0  renderD128

vainfo コマンドは、システムがハードウェアアクセラレーションに対応しているかどうか、そしてどのような機能が利用可能かを確認するのに役立ちます。

vainfo

以下のように表示され、サポート状況が確認できます。

error: can't connect to X server!
libva info: VA-API version 1.10.0
libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
libva info: va_openDriver() returns -1
libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so
libva info: Found init function __vaDriverInit_1_8
libva info: va_openDriver() returns 0
vainfo: VA-API version: 1.10 (libva 2.10.0)
vainfo: Driver version: Intel i965 driver for Intel(R) Gemini Lake - 2.4.1
vainfo: Supported profile and entrypoints
      VAProfileMPEG2Simple            : VAEntrypointVLD
      VAProfileMPEG2Main              : VAEntrypointVLD
      VAProfileH264ConstrainedBaseline: VAEntrypointVLD
      VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice
      VAProfileH264ConstrainedBaseline: VAEntrypointEncSliceLP
      VAProfileH264Main               : VAEntrypointVLD
      VAProfileH264Main               : VAEntrypointEncSlice
      VAProfileH264Main               : VAEntrypointEncSliceLP
      VAProfileH264High               : VAEntrypointVLD
      VAProfileH264High               : VAEntrypointEncSlice
      VAProfileH264High               : VAEntrypointEncSliceLP
      VAProfileH264MultiviewHigh      : VAEntrypointVLD
      VAProfileH264MultiviewHigh      : VAEntrypointEncSlice
      VAProfileH264StereoHigh         : VAEntrypointVLD
      VAProfileH264StereoHigh         : VAEntrypointEncSlice
      VAProfileVC1Simple              : VAEntrypointVLD
      VAProfileVC1Main                : VAEntrypointVLD
      VAProfileVC1Advanced            : VAEntrypointVLD
      VAProfileNone                   : VAEntrypointVideoProc
      VAProfileJPEGBaseline           : VAEntrypointVLD
      VAProfileJPEGBaseline           : VAEntrypointEncPicture
      VAProfileVP8Version0_3          : VAEntrypointVLD
      VAProfileVP8Version0_3          : VAEntrypointEncSlice
      VAProfileHEVCMain               : VAEntrypointVLD
      VAProfileHEVCMain               : VAEntrypointEncSlice
      VAProfileHEVCMain10             : VAEntrypointVLD
      VAProfileHEVCMain10             : VAEntrypointEncSlice
      VAProfileVP9Profile0            : VAEntrypointVLD
      VAProfileVP9Profile0            : VAEntrypointEncSlice
      VAProfileVP9Profile2            : VAEntrypointVLD

デバイスファイルのアクセス権限を設定します。

echo 'KERNEL=="render*" GROUP="render", MODE="0666"' | sudo tee /etc/udev/rules.d/99-render.rules
sudo udevadm control --reload-rules && sudo udevadm trigger

Mirakurun・EPGStation

MirakurunとEPGStationをDocker上に設定します。

Docker Composeの設定

サンプルがあるのでダウンロードします。

mkdir -p ~/docker/mirakurun-epgstation
cd ~/docker/mirakurun-epgstation
git clone https://github.com/l3tnun/docker-mirakurun-epgstation .

Dockerイメージ chinachu/mirakurun に recpt1 をインストールします。

vi mirakurun/Dockerfile

FROM chinachu/mirakurun
ENV DEV="build-essential git libpcsclite-dev libssl-dev libtool pkg-config"

RUN apt update \
    && apt -y install $DEV \
    #
    # recpt1
    && git clone https://github.com/stz2012/recpt1.git \
    && cd recpt1/recpt1 \
    && ./autogen.sh \
    && ./configure \
    && make \
    && make install \
    && cd

サンプルの Compose ファイルをコピーします。

mv docker-compose-sample.yml compose.yaml

mirakurun では devices を正しく設定します。

epgstation でハードウェアエンコードを行うため devices/dev/dri を設定します。
ハードウェアエンコードが使えない場合はこちらをコメントアウトします。

vi compose.yaml

services:
    mirakurun:
        build:
            context: mirakurun
        cap_add:
            - SYS_ADMIN
            - SYS_NICE
        ports:
            - "40772:40772"
            - "9229:9229"
        volumes:
            - ./mirakurun/conf:/app-config
            - ./mirakurun/data:/app-data
        environment:
            TZ: "Asia/Tokyo"
        devices:
            - /dev/bus:/dev/bus
            - /dev/px4video0:/dev/px4video0
            - /dev/px4video1:/dev/px4video1
            - /dev/px4video2:/dev/px4video2
            - /dev/px4video3:/dev/px4video3
            - /dev/px4video4:/dev/px4video4
            - /dev/px4video5:/dev/px4video5
            - /dev/px4video6:/dev/px4video6
            - /dev/px4video7:/dev/px4video7
        restart: always
        logging:
            driver: json-file
            options:
                max-file: "1"
                max-size: 10m

    mysql:
        image: mariadb:10.5
        # image: mysql:8.0 # 囲み文字を使用する場合
        volumes:
            - mysql-db:/var/lib/mysql
        environment:
            MYSQL_USER: epgstation
            MYSQL_PASSWORD: epgstation
            MYSQL_ROOT_PASSWORD: epgstation
            MYSQL_DATABASE: epgstation
            TZ: "Asia/Tokyo"
        command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --performance-schema=false --expire_logs_days=1 # for mariadb
        # command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_ci --performance-schema=false --expire_logs_days=1 --default-authentication-plugin=mysql_native_password # for myql
        restart: always
        logging:
            options:
                max-size: "10m"
                max-file: "3"

    epgstation:
        build:
            context: "./epgstation"
            dockerfile: "debian.Dockerfile"
        volumes:
            - ./epgstation/config:/app/config
            - ./epgstation/data:/app/data
            - ./epgstation/thumbnail:/app/thumbnail
            - ./epgstation/logs:/app/logs
            - ./recorded:/app/recorded
        environment:
            TZ: "Asia/Tokyo"
        depends_on:
            - mirakurun
            - mysql
        ports:
            - "8888:8888"
            - "8889:8889"
        user: "1000:1000"
        devices:
            - /dev/dri:/dev/dri
        restart: always

volumes:
    mysql-db:
        driver: local

Mirakurunの設定

Mirakurunのチューナーの設定です。
デバイスとGR/BS/CSを正しく設定します。

vi mirakurun/conf/tuners.yml

- name: PX4-S1
  types:
    - BS
    - CS
  command: recpt1 --device /dev/px4video0 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-S2
  types:
    - BS
    - CS
  command: recpt1 --device /dev/px4video1 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-S3
  types:
    - BS
    - CS
  command: recpt1 --device /dev/px4video4 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-S4
  types:
    - BS
    - CS
  command: recpt1 --device /dev/px4video5 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-T1
  types:
    - GR
  command: recpt1 --device /dev/px4video2 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-T2
  types:
    - GR
  command: recpt1 --device /dev/px4video3 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-T3
  types:
    - GR
  command: recpt1 --device /dev/px4video6 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

- name: PX4-T4
  types:
    - GR
  command: recpt1 --device /dev/px4video7 <channel> - -
  decoder: arib-b25-stream-test
  isDisabled: false

Mirakurunのチャンネルの設定です。
地域によってチャンネル設定が異なるので、中身を確認して調べて設定してみてください。

curl https://gist.githubusercontent.com/horatjp/6d99040a1c0c1ad79bbb4e5ae39f1730/raw/a994364766d908e0d38d2d9be3c05796c4e2cd9e/Mirakurun_channels.yml --output mirakurun/conf/channels.yml

※gistにアップしてあります。

EPGStationの設定

EPGStationのサンプルの設定をコピー(ファイル移動)します。

mv epgstation/config/enc.js.template epgstation/config/enc.js
mv epgstation/config/config.yml.template epgstation/config/config.yml
mv epgstation/config/operatorLogConfig.sample.yml epgstation/config/operatorLogConfig.yml
mv epgstation/config/epgUpdaterLogConfig.sample.yml epgstation/config/epgUpdaterLogConfig.yml
mv epgstation/config/serviceLogConfig.sample.yml epgstation/config/serviceLogConfig.yml

保存するディレクトリを2か所設定します。
こちらは自分の好みで設定します。

vi epgstation/config/config.yml

recorded:
    - name: original
      path: '%ROOT%/recorded/original'
    - name: program
      path: '%ROOT%/recorded/program'

外部ストレージにディレクトリを作成し、そこにシンボリックリンクを張ります。

mkdir /mnt/share/録画
rm -r recorded
ln -s /mnt/share/録画 recorded

H.264・H.265それぞれでソフトウェアエンコード・ハードウェアエンコードを行えるように設定してあります。
エンコード設定はお好みで設定してください。

vi epgstation/config/enc_multi.js

const spawn = require('child_process').spawn;
const execFile = require('child_process').execFile;
const ffmpeg = process.env.FFMPEG;
const ffprobe = process.env.FFPROBE;
const input = process.env.INPUT;
const output = process.env.OUTPUT;
const name = process.env.NAME;

const isDualMono = parseInt(process.env.AUDIOCOMPONENTTYPE, 10) == 2;
const isCodec265 = process.argv[2] && process.argv[2] == '265'
const isHardwareEncode = process.argv[3] && process.argv[3] == 'hardware'

/**
 * 動画長取得関数
 * @param {string} filePath ファイルパス
 * @return number 動画長を返す (秒)
 */
const getDuration = filePath => {
    return new Promise((resolve, reject) => {
        execFile(ffprobe, ['-v', '0', '-show_format', '-of', 'json', filePath], (err, stdout) => {
            if (err) {
                reject(err);

                return;
            }

            try {
                const result = JSON.parse(stdout);
                resolve(parseFloat(result.format.duration));
            } catch (err) {
                reject(err);
            }
        });
    });
};

const args = [];
Array.prototype.push.apply(args, [
    '-y',
    '-fix_sub_duration',
    '-analyzeduration', "10M",
    '-probesize', '32M',
]);

if (isHardwareEncode) {
    Array.prototype.push.apply(args, [
        '-vaapi_device', '/dev/dri/renderD128',
        '-hwaccel', 'vaapi',
        '-hwaccel_output_format', 'vaapi',
    ]);
}

Array.prototype.push.apply(args, [
    '-i', input,
    '-movflags', '+faststart',
    '-ignore_unknown',
]);

// 音声
if (isDualMono) {
    Array.prototype.push.apply(args, [
        '-filter_complex',
        'channelsplit',
        '-metadata:s:a:0', 'title=main',
        '-metadata:s:a:1', 'title=sub'
    ]);
}

Array.prototype.push.apply(args, ['-map', '0:a', '-c:a', 'aac', '-ac', '2']);

// 映像
Array.prototype.push.apply(args, ['-map', '0:v']);

if (isHardwareEncode) {
    if (isCodec265) {
        Array.prototype.push.apply(args, [
            '-vf', 'format=vaapi,deinterlace_vaapi',
            '-c:v', 'hevc_vaapi',
            '-qp', '27',
            '-tag:v', 'hvc1',
        ]);
    } else {
        Array.prototype.push.apply(args, [
            '-vf', 'format=vaapi,deinterlace_vaapi',
            '-c:v', 'h264_vaapi',
            '-level', '40',
            '-qp', '27',
        ]);
    }
} else {
    if (isCodec265) {
        Array.prototype.push.apply(args, [
            '-vf', 'yadif',
            '-preset', 'veryfast',
            '-c:v', 'libx265',
            '-crf', '22',
            '-f', 'mp4',
            '-tag:v', 'hvc1',
        ]);
    } else {
        Array.prototype.push.apply(args, [
            '-vf', 'yadif',
            '-preset', 'veryfast',
            '-c:v', 'libx264',
            '-crf', '22',
            '-f', 'mp4',
        ]);
    }
}

// その他
Array.prototype.push.apply(args, [
    '-map', '0:s?',
    '-c:s', 'mov_text',
    '-max_muxing_queue_size', '1024',
    output
]);

(async () => {
    // 進捗計算のために動画の長さを取得
    const duration = await getDuration(input);

    const child = spawn(ffmpeg, args);

    /**
     * エンコード進捗表示用に標準出力に進捗情報を吐き出す
     * 出力する JSON
     * {"type":"progress","percent": 0.8, "log": "view log" }
     */
    child.stderr.on('data', data => {
        let strbyline = String(data).split('\n');
        for (let i = 0; i < strbyline.length; i++) {
            let str = strbyline[i];
            if (str.startsWith('frame')) {
                // 想定log
                // frame= 5159 fps= 11 q=29.0 size=  122624kB time=00:02:51.84 bitrate=5845.8kbits/s dup=19 drop=0 speed=0.372x
                const progress = {};
                const ffmpeg_reg = /frame=\s*(?<frame>\d+)\sfps=\s*(?<fps>\d+(?:\.\d+)?)\sq=\s*(?<q>[+-]?\d+(?:\.\d+)?)\sL?size=\s*(?<size>\d+(?:\.\d+)?)kB\stime=\s*(?<time>\d+[:\.\d+]*)\sbitrate=\s*(?<bitrate>\d+(?:\.\d+)?)kbits\/s(?:\sdup=\s*(?<dup>\d+))?(?:\sdrop=\s*(?<drop>\d+))?\sspeed=\s*(?<speed>\d+(?:\.\d+)?)x/;
                let ffmatch = str.match(ffmpeg_reg);
                /**
                 * match結果
                 * [
                 *   'frame= 5159 fps= 11 q=29.0 size=  122624kB time=00:02:51.84 bitrate=5845.8kbits/s dup=19 drop=0 speed=0.372x',
                 *   '5159',
                 *   '11',
                 *   '29.0',
                 *   '122624',
                 *   '00:02:51.84',
                 *   '5845.8',
                 *   '19',
                 *   '0',
                 *   '0.372',
                 *   index: 0,
                 *   input: 'frame= 5159 fps= 11 q=29.0 size=  122624kB time=00:02:51.84 bitrate=5845.8kbits/s dup=19 drop=0 speed=0.372x    \r',
                 *   groups: [Object: null prototype] {
                 *     frame: '5159',
                 *     fps: '11',
                 *     q: '29.0',
                 *     size: '122624',
                 *     time: '00:02:51.84',
                 *     bitrate: '5845.8',
                 *     dup: '19',
                 *     drop: '0',
                 *     speed: '0.372'
                 *   }
                 * ]
                 */

                if (ffmatch === null) continue;

                progress['frame'] = parseInt(ffmatch.groups.frame);
                progress['fps'] = parseFloat(ffmatch.groups.fps);
                progress['q'] = parseFloat(ffmatch.groups.q);
                progress['size'] = parseInt(ffmatch.groups.size);
                progress['time'] = ffmatch.groups.time;
                progress['bitrate'] = parseFloat(ffmatch.groups.bitrate);
                progress['dup'] = ffmatch.groups.dup == null ? 0 : parseInt(ffmatch.groups.dup);
                progress['drop'] = ffmatch.groups.drop == null ? 0 : parseInt(ffmatch.groups.drop);
                progress['speed'] = parseFloat(ffmatch.groups.speed);

                let current = 0;
                const times = progress.time.split(':');
                for (let i = 0; i < times.length; i++) {
                    if (i == 0) {
                        current += parseFloat(times[i]) * 3600;
                    } else if (i == 1) {
                        current += parseFloat(times[i]) * 60;
                    } else if (i == 2) {
                        current += parseFloat(times[i]);
                    }
                }

                // 進捗率 1.0 で 100%
                const percent = current / duration;
                const log =
                    'frame= ' +
                    progress.frame +
                    ' fps=' +
                    progress.fps +
                    ' size=' +
                    progress.size +
                    ' time=' +
                    progress.time +
                    ' bitrate=' +
                    progress.bitrate +
                    ' drop=' +
                    progress.drop +
                    ' speed=' +
                    progress.speed;

                console.log(JSON.stringify({ type: 'progress', percent: percent, log: log }));
            }
        }
    });

    child.on('error', err => {
        console.error(err);
        throw new Error(err);
    });

    process.on('SIGINT', () => {
        child.kill('SIGINT');
    });
})();

エンコードの設定を行います。

vi epgstation/config/config.yml

encode:
    - name: H.264
      cmd: '%NODE% %ROOT%/config/enc_multi.js 264'
      suffix: .mp4
      rate: 4.0
    - name: H.265
      cmd: '%NODE% %ROOT%/config/enc_multi.js 265'
      suffix: .mp4
      rate: 4.0
    - name: H.264(HW)
      cmd: '%NODE% %ROOT%/config/enc_multi.js 264 hardware'
      suffix: .mp4
      rate: 4.0
    - name: H.265(HW)
      cmd: '%NODE% %ROOT%/config/enc_multi.js 265 hardware'
      suffix: .mp4
      rate: 4.0

Dockerイメージ l3tnun/epgstation:master-debian を拡張します。
強引に、ソースリストを bullseye にして deb-multimedia.org のFFmpegをインストールしています。
wakasacat は使用していないのですが、念のためインストールしています。

vi epgstation/debian.Dockerfile

FROM l3tnun/epgstation:master-debian

RUN echo "deb http://deb.debian.org/debian/ bullseye main contrib non-free" > /etc/apt/sources.list \
    && apt update \
    && apt -y install golang git gpg \
    #
    # ffmpeg install 
    && gpg --keyserver  keyring.debian.org --recv-keys  5C808C2B65558117 \
    && gpg --armor --export 5C808C2B65558117 | apt-key add - \
    && echo "deb http://www.deb-multimedia.org bullseye main non-free" >> /etc/apt/sources.list \
    && apt update \
    && apt -y install ffmpeg firmware-amd-graphics i965-va-driver-shaders mesa-va-drivers vainfo \
    #
    # wakasacat
    && mkdir /usr/local/go \
    && export GOPATH=/usr/local/go \
    && go get github.com/kteru/wakasacat \
    && go install github.com/kteru/wakasacat \
    && cp /usr/local/go/bin/wakasacat /usr/local/bin/wakasacat \
    && cd

FFmpegをパッケージからインストールするとパスが変わるので設定を変更します。

vi epgstation/config/config.yml

ffmpeg: /usr/bin/ffmpeg
ffprobe: /usr/bin/ffprobe

番組表で非表示にする番組を設定します。
お好みで設定してください。

vi epgstation/config/config.yml

excludeChannels:
    - 3273701034

番組表の順番を変更します。
お好みで設定してください。

vi epgstation/config/config.yml

channelOrder:
    - 3273601024
    - 3273701032
    - 3273801040
    - 3274101064
    - 3273901048
    - 3274201072
    - 3274001056

起動

起動します。

docker compose up -d

ステータス確認

docker compose ps

ログ確認

docker compose logs -f

Mirakurunの確認

下記URLからMirakurunが表示できます。

http://[ IPアドレス ]:40772

Mirakurunの機能のみで、TVTestというWindows用のソフトからTVの視聴も可能です。

EPGStation確認

下記URLからEPGStationが表示できます。
初回起動から40分程度たつとMirakurunが収集したデータをもとに番組表が表示されるようになります。

http://[ IPアドレス ]:8888

ReadyMedia

DLNAサーバのReadyMediaをインストールします。
DLNA対応家電でサーバー上の動画が再生できます。

mkdir -p ~/docker/minidlna
cd ~/docker/minidlna

vi compose.yaml

services:
    minidlna:
        image: vladgh/minidlna
        network_mode: "host"
        restart: always
        volumes:
            - /mnt/share/録画:/media/recorder
        environment:
            - MINIDLNA_MEDIA_DIR=V,/media/recorder
            - MINIDLNA_FRIENDLY_NAME=share

起動します。

docker compose up -d

下記URLから情報が確認できます。

http://[ IPアドレス ]:8200

おわりに

以上で、Debian Linuxによる自宅サーバでの録画環境構築の手順の紹介を終わります。
必要な設定が多くありますが、1つ1つ確認しながら進めていけば、構築できます。
今回紹介した手順を参考に、自宅サーバでの録画環境を構築してみてはいかがでしょうか。

コメント

タイトルとURLをコピーしました