UbuntuのEPGStation_v2でCMカットエンコードする。
2021-01-23
はじめに
EPGStation v2が先日公開された。
EPGSstationのおかげでTVをいつでもどこからでも視聴できる上、気が向いたときに録画予約を入れる事ができ大変便利である。
私はEPGSstation v2が公開された日にアップグレードした。
すると、エンコード進捗を表示できる機能が新たに実装されており、感動した。
そこで私の移植したLinuxで動くJoinLogoScpTrialSetLinuxを用いてエンコード進捗を表示できるようにした。下記のように表示される。
そこで、環境構築情報をここに記しておく。
(というか自分用のメモだが。)
もし、Linux環境にてEPGStation v2でCMカットしたいという人がいれば参考にしていただければ幸いである。
ただし、私の利用しているのはDockerを用いた環境であるため、直接導入している人は異なると思われるので、参考に頑張ってもらいたい。
EPGStationのDockerにJoinLogoScpTrialSetLinuxの導入
どうでもいい前置きが長くなったが、導入手順を記載していく。
とりあえず、docker-mirakurun-epgstationを動作できるようにしておく。
動作できたことが確認できたら、一度downしておく。
docker-mirakurun-epgstation/epgstation/Dockerfile
を次の内容に置き換える。
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"] |
Dockerfileの中で使っている移植したJoin_logo_scp_trial_setとdelogoはGitHubにおいてある。
次に、ホスト側のファイルを用意する。
以前からJoinLogoScpTrialSetLinuxを使っている方はlogoやJLやSettingを一旦避難させて、docker-mirakurun-epgstation/join_logo_scp_trial
を削除する。
つかってない人や、過去のものを削除したらdocker-mirakurun-epgstation
の中でgit clone https://github.com/tobitti0/join_logo_scp_trial
を実行する。
以前から使っていた人は実行したら避難させたものを戻す。
次にdocker-composeを修正する。
docker-mirakurun-epgstation/docker-compose.yml
内のepgstationのvolumesへ次の内容を追記する。
- ./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
ここまでできたらdocker-compose up -d --build
でビルドする。
以前の解説と違い、FFmpegやl-smashなどを含んだdocker-avisynthplusというPackageをGithubにて公開したのでFFmpegなどのビルド時間がかからずに構築できるようになった。
nvencのためのタグもあるのでGPUエンコードにしたい人はベースイメージのタグをそれに変更し、nvidia-container-runtime等を入れて構築することもできる。 必要であれば解説を追加したいと思うが、とりあえずは書かないでおく。
ビルドできたらjoin_logo_scp_trialのlogoフォルダにロゴデータを入れておく。
エンコードスクリプトの設定
次にdocker-mirakurun-epgstation/epgstation/config
にjlse.jsを作成する。中身は次の通り。
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 => { | |
//終了後にしたい処理があれば書く | |
}); | |
})(); | |
//メインの処理 ここまで |
docker-mirakurun-epgstation/epgstation/config/config.json
のencodeには次のような感じで記載する。
encode:
- name: jlse
cmd: '%NODE% %ROOT%/config/jlse.js'
suffix: .mp4
rate: 4.0
これでエンコードのところにjlsの選択が現れるので、選択しエンコードすれば問題なく利用できると思う。
進捗も表示されるはずだ。
注意としては以前からJoinLogoScpTrialSetLinuxを利用していた場合はdocker-mirakurun-epgstation/join_logo_scp_trial
の中身を最新のものにしなければ、進捗は表示されない。
jlsの設定などはwindowsとかわらないのでwindowsの記事を参考に設定すれば問題ないはず。
jlsの設定ファイルなどはdocker-mirakurun-epgstation/join_logo_scp_trial/
の中をいじればよい。
おわりに
雑ですがとりあえず一通り導入方法を記載した。
導入方法に万一問題があればTwitterまで。
移植版に問題があればGitHubのissueに。
現状私が使用できているので、問題なく使用できるはずですが、暇があれば問題には対応します。たぶん。。。
謝辞
EPGStationやJoinLogoScpTrialなどを制作された様々な偉大なる先人達に感謝いたします。