先日、EPGStation v2で録画した番組をCMカット&エンコードできるようにしてみました。
Linux上でTSファイルをCMカット&エンコードするために、JoinLogoScpTrialSetLinuxというツールを使いました。
ツール導入およびEPGStation v2との連携手順については、開発者のHPに詳しく解説されています。
開発者に感謝!
今回は、私の記事を参考にしてEPGStationを導入した場合を前提に、JoinLogoScpTrialSetLinuxの導入および録画番組をCMカット&エンコードする方法について解説します。
なお、本手順を実施するとvaapiエンコードがが無効になります。
Contents
前提環境
以下の記事のいずれかを参考にしてEPGStationを導入し、利用できていることを前提としています。
上記の方法じゃなくても、DockerでEPGStationを構築していれば以降の導入手順は参考にできると思います。
なお、docker-composeのプロジェクトは~/docker-mirakurun-epgstation-v2にあることとします。
プロジェクト名をdocker-mirakurun-epgstationで構築した方は、以降の手順ではdocker-mirakurun-epgstationをdocker-mirakurun-epgstation-v2に読み替えて実施してください。
JoinLogoScpTrialSetLinuxの導入手順
録画サーバ環境にJoinLogoScpTrialSetLinuxの導入する手順について解説します。
なお、本手順は開発者の導入手順を参考にしています。
EPGStationを一旦停止する
JoinLogoScpTrialSetLinuxの導入にあたり、EPGStationを停止します。
1 2 |
cd ~/docker-mirakurun-epgstation-v2 docker-compose down |
プロジェクト名docker-mirakurun-epgstation-v2を自身の環境に合わせて読み替えてください。
EPGStationのDockerfileの作成
本手順ではCMカットに必要なツール類をEPGStationコンテナ内に導入するため、gitからリボジトリのクローン&EPGStationのDockerfileを修正します。
大きく内容が書き換わりますが、EPGStationの設定はDBとconfigファイルで保持しているため事前にバックアップを取る必要はありません。
元の環境に戻せるよう、Dockerfileを退避させておきます。
1 2 3 4 5 |
cd ~/docker-mirakurun-epgstation-v2 git clone https://github.com/tobitti0/join_logo_scp_trial cd epgstation mv Dockerfile Dockerfile.org emacs Dockerfile |
以下の内容をコピペします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
FROM ghcr.io/tobitti0/docker-avisynthplus:4.3.1-ubuntu2004 as build ENV DEBIAN_FRONTEND=noninteractive ENV NODE_VERSION=14 ENV EPGSTATION_VERSION=v2.0.8 RUN set -xe && \ apt-get update && \ apt-get install --no-install-recommends -y \ curl git make gcc g++ cmake libboost-all-dev # join_logo_scp_trial build RUN cd /tmp/ && \ git clone --recursive https://github.com/tobitti0/JoinLogoScpTrialSetLinux.git && \ cd /tmp/JoinLogoScpTrialSetLinux && \ git submodule foreach git pull origin master && \ cd /tmp/JoinLogoScpTrialSetLinux/modules/chapter_exe/src && \ make && \ mv chapter_exe /tmp/JoinLogoScpTrialSetLinux/modules/join_logo_scp_trial/bin/ && \ cd /tmp/JoinLogoScpTrialSetLinux/modules/logoframe/src && \ make && \ mv logoframe /tmp/JoinLogoScpTrialSetLinux/modules/join_logo_scp_trial/bin/ && \ cd /tmp/JoinLogoScpTrialSetLinux/modules/join_logo_scp/src && \ make && \ mv join_logo_scp /tmp/JoinLogoScpTrialSetLinux/modules/join_logo_scp_trial/bin/ && \ cd /tmp/JoinLogoScpTrialSetLinux/modules/tsdivider/ && \ mkdir build && \ cd build && \ cmake -DCMAKE_BUILD_TYPE=Release .. && \ make && \ mv tsdivider /tmp/JoinLogoScpTrialSetLinux/modules/join_logo_scp_trial/bin/ && \ mv /tmp/JoinLogoScpTrialSetLinux/modules/join_logo_scp_trial /join_logo_scp_trial && \ cd /join_logo_scp_trial # delogo RUN set -xe && \ git clone https://github.com/tobitti0/delogo-AviSynthPlus-Linux && \ cd delogo-AviSynthPlus-Linux/src && \ make && \ cp libdelogo.so /join_logo_scp_trial # node setup tool RUN set -xe && \ curl -O -sL https://deb.nodesource.com/setup_${NODE_VERSION}.x && \ mv setup_${NODE_VERSION}.x /join_logo_scp_trial/setup_node.x # EPGStation clone RUN set -xe && \ cd /tmp && \ git clone https://github.com/l3tnun/EPGStation.git -b ${EPGSTATION_VERSION} FROM ghcr.io/tobitti0/docker-avisynthplus:4.3.1-ubuntu2004 as release ENV DEBIAN_FRONTEND=noninteractive MAINTAINER Tobitti <mail@tobitti.net> COPY --from=build /join_logo_scp_trial /join_logo_scp_trial COPY --from=build /tmp/EPGStation /app WORKDIR /join_logo_scp_trial RUN bash setup_node.x && \ apt-get update && \ apt-get install --no-install-recommends -y nodejs libboost-filesystem-dev libboost-program-options-dev libboost-system-dev && \ node -v && \ npm --version &&\ mv libdelogo.so /usr/local/lib/avisynth && \ ls /usr/local/lib/avisynth && \ npm install && \ npm link && \ jlse --help # install EPGStation RUN cd /app && \ npm install && \ npm install async && \ npm run all-install && \ npm run build WORKDIR /app ENTRYPOINT ["npm"] CMD ["start"] |
エンコードスクリプトの設定
録画ファイル(TSファイル)をCMカット&エンコードするスクリプトを作成します。
エンコードオプション等にこだわりが無ければ、開発者が作成したものを使えばOKです。
1 2 |
cd ~/docker-mirakurun-epgstation-v2/epgstation/config emacs jlse.js |
以下の内容をコピペします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
const spawn = require('child_process').spawn; const execFile = require('child_process').execFile; const ffmpeg = process.env.FFMPEG; const ffprobe = process.env.FFPROBE; const path = require('path'); const input = process.env.INPUT; const output = process.env.OUTPUT; const videoHeight = parseInt(process.env.VIDEORESOLUTION, 10); const isDualMono = parseInt(process.env.AUDIOCOMPONENTTYPE, 10) == 2; const output_name = path.basename(output, path.extname(output)); const output_dir = path.dirname(output); //FFmpegオプション生成 ここから const args = ['-y']; const preset = 'medium'; const codec = 'libx264'; //libx264でエンコード const crf = 23; const videoFilter = 'yadif'; if (isDualMono) { Array.prototype.push.apply(args, [ '-filter_complex', 'channelsplit[FL][FR]', '-map', '0:v', '-map', '[FL]', '-map', '[FR]', '-metadata:s:a:0', 'language=jpn', '-metadata:s:a:1', 'language=eng', ]); Array.prototype.push.apply(args, ['-c:a ac3', '-ar 48000', '-ab 256k']); } else { // audio dataをコピー Array.prototype.push.apply(args, ['-c:a', 'aac']); } Array.prototype.push.apply(args, ['-ignore_unknown']); // その他設定 Array.prototype.push.apply(args,[ '-vf', videoFilter, '-preset', preset, '-aspect', '16:9', '-c:v', codec, '-crf', crf, '-f', 'mp4', ]); let str = ''; for (let i of args) { str += ` ${i}`; } // console.error(str); //オプションを確認するときコメントアウトを外す //FFmpegオプション生成 ここまで /** * 動画長取得関数 * @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); } }); }); }; //メインの処理 ここから (async () => { // 進捗計算のために動画の長さを取得 const duration = await getDuration(input); //必要な変数 let total_num = 0; let now_num = 0; let avisynth_flag = false; let percent = 0; let update_log_flag = false; let log = ''; const jlse_args = ['-i', input, '-e', '-o', str,'-r','-d', output_dir, '-n', output_name]; console.error(jlse_args); var env = Object.create( process.env ); env.HOME = '/root'; console.error(env); const child = spawn('jlse', jlse_args, {env:env}); let inputfileinfo = false; let outputfileinfo = false; let fileinfolog = ''; /** * エンコード進捗表示用に標準出力に進捗情報を吐き出す * 出力する 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]; switch(str){ case str.startsWith('AviSynth') && str :{ //AviSynth+ const raw_avisynth_data = str.replace(/AviSynth\s/,''); if(raw_avisynth_data.startsWith('Creating')){ const avisynth_reg = /Creating\slwi\sindex\sfile\s(\d+)%/; total_num = 200; now_num = Number(raw_avisynth_data.match(avisynth_reg)[1]); now_num += avisynth_flag ? 100 : 0; avisynth_flag = avisynth_flag ? true : now_num == 100 ? true : false ; } update_log_flag = true; log = `(1/4) AviSynth:Creating lwi index files`; break; } case str.startsWith('chapter_exe') && str :{ //chapter_exe const raw_chapter_exe_data = str.replace(/chapter_exe\s/,''); switch(raw_chapter_exe_data){ case raw_chapter_exe_data.startsWith('\tVideo Frames') && raw_chapter_exe_data :{ //chapter_exeでの総フレーム数取得 const movie_frame_reg = /\tVideo\sFrames:\s(\d+)\s\[\d+\.\d+fps\]/; total_num = Number(raw_chapter_exe_data.match(movie_frame_reg)[1]); update_log_flag = true; break; } case raw_chapter_exe_data.startsWith('mute') && raw_chapter_exe_data :{ //現在のフレーム数取得 const chapter_reg = /mute\s?\d+:\s(\d+)\s\-\s\d+フレーム/; now_num = Number(raw_chapter_exe_data.match(chapter_reg)[1]); update_log_flag = true; break; } case raw_chapter_exe_data.startsWith('end') && raw_chapter_exe_data :{ //chapter_exeの終了検知 now_num = total_num; update_log_flag = true; break; } default :{ break; } } log = `(2/4) Chapter_exe: ${now_num}/${total_num}`; break; } case str.startsWith('logoframe') && str:{ //logoframe const raw_logoframe_data = str.replace(/logoframe\s/,''); switch (raw_logoframe_data){ case raw_logoframe_data.startsWith('checking') && raw_logoframe_data :{ const logoframe_reg = /checking\s*(\d+)\/(\d+)\sended./; const logoframe = raw_logoframe_data.match(logoframe_reg); now_num = Number(logoframe[1]); total_num = Number(logoframe[2]); update_log_flag = true; } default :{ break; } } log = `(3/4) logoframe: ${now_num}/${total_num}`; break; } case str.startsWith('frame') && str:{ //FFmpeg // frame= 2847 fps=0.0 q=-1.0 Lsize= 216432kB time=00:01:35.64 bitrate=18537.1kbits/s speed= 222x const progress = {}; let tmp = (str + ' ').match(/[A-z]*=[A-z,0-9,\s,.,\/,:,-]* /g); if (tmp === null) continue; for (let j = 0; j < tmp.length; j++) { progress[tmp[j].split('=')[0]] = tmp[j].split('=')[1].replace(/\r/g, '').trim(); } progress['frame'] = parseInt(progress['frame']); progress['fps'] = parseFloat(progress['fps']); progress['q'] = parseFloat(progress['q']); 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% now_num = current; total_num = duration; log = '(4/4) FFmpeg: ' + //'frame= ' + //progress.frame + //' fps=' + //progress.fps + //' size=' + //progress.size + ' time=' + progress.time + //' bitrate=' + //progress.bitrate + ' speed=' + progress.speed; update_log_flag = true; break; } default:{ //進捗表示に必要ない出力データを流す console.log(strbyline[i]); break; } } percent = now_num / total_num; if(update_log_flag) console.log(JSON.stringify({ type: 'progress', percent: percent, log: log })); update_log_flag = false; } }); child.on('error', err => { console.error(err); throw new Error(err); }); process.on('SIGINT', () => { child.kill('SIGINT'); }); child.on('close', code => { //終了後にしたい処理があれば書く }); })(); //メインの処理 ここまで |
EPGStationのコンフィグ設定
EPGStationからエンコードスクリプトをキックする設定を加えます。
EPGStationでは録画ファイルの形式がデフォルトではm2tsのため、tsに修正します(本ツールではTSファイルでのみ動作する)。
1 |
emacs ~/docker-mirakurun-epgstation-v2/epgstation/config/config.json |
1 2 3 4 5 6 7 8 9 10 |
: # recordedFileExtension: .m2ts recordedFileExtension: .ts : encode: - name: jlse cmd: '%NODE% %ROOT%/config/jlse.js' suffix: .mp4 rate: 4.0 : |
局ロゴデータの配置
join_logo_scp_trialのlogoフォルダにロゴデータ(.lgd)を配置します。
局ロゴデータは同梱されていないため、自身で用意する必要があります。
局ロゴデータの生成手順についてはここでは割愛します。
1 |
cp -a logodata/* ~/docker-mirakurun-epgstation-v2/join_logo_scp_trial/logo/ |
必要な設定は、これで以上です。
docker-compose.ymlの修正
epgstationのvolumesへ次の内容を追記します。
1 |
emacs ~/docker-mirakurun-epgstation-v2/docker-compose.yml |
1 2 3 4 5 6 7 |
: - ./join_logo_scp_trial/JL:/join_logo_scp_trial/JL - ./join_logo_scp_trial/logo:/join_logo_scp_trial/logo - ./join_logo_scp_trial/result:/join_logo_scp_trial/result - ./join_logo_scp_trial/setting:/join_logo_scp_trial/setting - ./join_logo_scp_trial/src:/join_logo_scp_trial/src : |
EPGStationをビルド&起動
停止したEPGStationをビルドして起動します。
1 2 |
cd ~/docker-mirakurun-epgstation-v2 docker-compose up -d --build |
30分程待って、ブラウザからEPGStationのページが閲覧できればOKです。
EPGStation v2でCMカット&エンコードしてみる
EPGStationから録画番組のCMカット&エンコードする方法を簡単に解説します。
番組表から録画予約する場合
番組表からCMカットしたい番組を選択し、エンコードメニューを選択して予約します。
このとき、jlseを選択すると番組録画終了後に自動でCMカット&エンコードされます。
ル―ル/検索で録画予約する場合
ルールまたは検索メニューからCMカットしたい録画番組を検索して、エンコード方法を指定します。
録画済み番組の場合
録画した番組をエンコードする場合は、ホーム画面の録画一覧メニューから⊕encodeを選択します。
設定からjlseを選択して「追加」します。
上記のいずれかの方法でCMカットとエンコードが始まります。
正常に動作している場合、エンコード途中で以下のような進捗状況を確認することができます。
まとめ
EPGStation v2とJoinLogoScpTrialSetLinuxでCMカット&エンコードする方法について解説してみました。
私が使ってみた感想ですが、CMカット自体はそこそこ正確にできるので素晴らしいですが、エンコードをCPUでやっているので非常に低速&高負荷です。
私のサーバスペックだとCPU360%越えで大体0.4~0.5倍速でエンコード処理されるため、CMカットしようとすると1時間番組で2~2.5時間程掛かってしまい実用的ではありませんでした。
vaapiやqsvでHWエンコードできれば非常に素晴らしいと思いますが、本導入手順だとコンテナ内のffmpegでvaapiが無効になってしまいHWエンコードはできません。
お試しでコンテナ内のffmpegにvaapiを有効化してみたものの、JoinLogoScpTrialSetLinuxのエンコード時にvaapiを使う方法が分かりませんでした。
(↑これはこれで、vaapiとjlseがそれぞれ使える(選択できる)ようになったので便利です)
面白いツールですが、CMカットがそこまで必要が無い私にとっては時間がかかる&サーバ負荷が高すぎて出番がなさそうです。
スペックの高いサーバを使っていて、録画したテレビ番組のCMを飛ばしたい方は幸せになれるかもしれません。
EPGStationと連携してCMカットすることに興味のある方は是非お試しください。
参考になれば幸いです。
コメント
初めまして。
いつも有益な情報ありがとうございます。
下記ページで矛盾点がございましたので質問させてください。
https://komone-life.com/2021/05/09/how-to-install-joinlogoscptrialsetlinux-into-epgstation-container/
以下の分中、★のコマンド行ですが cd に相違がございました。
どちらかのディレクトリに統一する必要があるかと思っています。
お手数をおかけしますが、ご確認のほどよろしくお願いします。
■EPGStationのDockerfileの作成
★cd ~/docker-mirakurun-epgstation-v2/epgstation
git clone https://github.com/tobitti0/join_logo_scp_trial
mv Dockerfile Dockerfile.org
emacs Dockerfile
■EPGStationをビルド&起動
★cd ~/docker-mirakurun-epgstation-v2
docker-compose up -d –build
以上になります。
よろしくお願いします。
>ムツゴロウさん
この記事を参考にしてくださってありがとうございます。
とのことですが、手順通りでうまく行かなかったということでしょうか?
統一する必要はないのと、前者のディレクトリは変更するとうまく動かないと思います。
後者のディレクトリはプロジェクト配下であればどこでもOKです:)
(統一するなら後者のディレクトリを修正でしょうか。)
コメントの内容をはき違えていたら、ご指摘いただけると幸いです。
ご返信ありがとうございます。
はい、手順通り実施しても何故か上手く行きませんでした。
> docker exec -it epgstation-v2 bash で中に入り、jlse –help を入力しても command not found となってしまいます。そのため、CMカットのジョブもすぐに落ちてしまいます。
一方、tobitti0さんの手順通りに構築すると上手く行きました。
私だけの環境でそうなってしまうようです。
コメントいただきありがとうございました。
次回は字幕表示に取り組みたいと思います。
小茂根さんもぜひ取り上げていただけると嬉しいです。
>ムツゴロウさん
jlseのコマンドか無いということは、本記事の手順通りに実施できていない(gitでjlseをダウンロードする際のディレクトリを間違えている等)と思われます。
基本的に本記事では開発者のHPに記載してある手順をベースにしているので、本記事の通り実施していれば結果に差は出ません。
もう少し詳しい状況を知りたいところですが、手順を再実施して頂くことは可能でしょうか?
(別の手順で上手く行っているとのことですので、他の方からも同様のご指摘があれば記事を修正したいと思います。)
よろしくお願いしますm_ _m
こもねさん
ありがとうございます。再現できましたので手順をお知らせします。
お手数ですがご確認のほどよろしくお願いします。
■【完全解説】LinuxとPX-W3U4でEPGStation v2を構築する方法(PX-Q3U4/W3PE4/Q3PE4)
> docker-compose down
> docker rm (docker ps -q -a)
> docker rmi (docker images -q)
> mv ~/docker-mirakurun-epgstation/ ~/docker-mirakurun-epgstation.bak
> cd ~/
※ dockerのディレクトリは作らず、ホームディレクトリ直下でgitを実行。
→ 以降は同一の手順、同一のファイル編集内容で実施し、build後は正常起動を確認。
なお、config.sample.yml は config.yml.template に変更されていた。
■EPGStation v2とJoinLogoScpTrialSetLinuxでCMカット&エンコードする方法
> cd ~/docker-mirakurun-epgstation
> docker-compose down
★ cd ~/docker-mirakurun-epgstation/epgstation
> git clone https://github.com/tobitti0/join_logo_scp_trial
★ mv Dockerfile Dockerfile.org
★ vim Dockerfile
(記事と同一内容)
> cd ~/docker-mirakurun-epgstation/epgstation/config
> vim jlse.js
(記事と同一内容)
> vim ~/docker-mirakurun-epgstation/epgstation/config/config.yml
(記事と同一内容)
★ cd ~/docker-mirakurun-epgstation/epgstation/join_logo_scp_trial/logo/
> wget -r -l1 -A lgd https://down.7086.in/lgd/
> mv down.7086.in/lgd/* ./ && rm -rf down.7086.in/
> vim ~/docker-mirakurun-epgstation/docker-compose.yml
(記事と同一内容)
> cd ~/docker-mirakurun-epgstation
> docker-compose up -d –build
※途中で以下のメッセージも表示される。
/usr/bin/jlse -> /usr/lib/node_modules/join_logo_scp_trial/src/jlse.js
/usr/lib/node_modules/join_logo_scp_trial -> /join_logo_scp_trial
Options:
–version Show version number [boolean]
…
・epgstationの起動後、以下のコマンドで確認。
> docker exec -it epgstation-v2 bash
root@d12a9f7cd6ab:/app# jlse
bash: jlse: command not found
★の箇所を
> cd ~/docker-mirakurun-epgstation
> mv epgstation/Dockerfile epgstation/Dockerfile.org
> vim epgstation/Dockerfile
> cd ~/docker-mirakurun-epgstation/join_logo_scp_trial/logo/
に変更すると jlse がインストールされる。
以上になります。よろしくお願いします。
>ムツゴロウさん
再度コメントありがとうございます。
ご指摘いただいた点について、ディレクトリが間違っていた箇所を修正しました。
(上のご指摘もディレクトリが統一されてないことじゃなく、間違っているというご指摘だったのですねm_ _m)
修正案までご提示いただき、大変参考になりました。
# 局ロゴ(ldg)の取得例も掲載ありがとうございますw
ご対応いただきありがとうございます。
疑問点が解消されて良かったです。また、Dockerの勉強になりました。
今度は字幕関連も取り入れて記事にしていただけると嬉しいです。