テレビ録画メディアサーバー構築入門(第21回)
多重化(muxing)
病院や公共施設の待合室でつけっぱなしになっているテレビに、字幕テロップ -- 字幕放送 -- が流れているのをよく目にします。音が聞こえにくい環境でも、手持ち無沙汰な人々の注意を引き、ちょっとした気晴らしになる。なかなか親切な配慮だと思う。
放送波(m2ts)には映像と主音声だけではなく、解説[解]や字幕など、様々な付加的情報が含まれています。個人的に、あまり意識することはありませんでしたが、現在放送されている地上波放送、とくに視聴者層の厚そうな昼間の番組については、ほぼすべてが文字多重放送[字]を含んでいます。まぁ時代の要請なのでしょう。ちょっとした驚きです。
できるなら、mp4に変換するときに、できるだけ忠実に情報を残せたらうれしい。
個人的には、ため込んだ録画番組は、倍速で消化することが多いので、もしかすると文字情報は便利かもしれない、と思った。調べて試行錯誤してみて、なかなか一筋縄ではいかない(最先端な?)問題なことがわかった。
今回のテーマは、第11回の「その場しのぎ」の改善です。「些細な問題」どころではなかった。
方針
EPGStation 内部で使用するエンコード処理スクリプト enc.js のサードパーティによる拡張版があります。
基本部分を参考にしながら、
1.(EPGStationに依存しない)独立なコマンドにして、
2. (特にNHKの)字幕ストリームをうまく取り込めるように、
拡張改変したい。
過去回にて、エンコード部分は
enc0.sh input.m2ts output.mp4
のようにしてきた。けれども、よく考えると
./enc0.sh "$cleaned_filename".m2ts "$cleaned_filename".mp4
のように、同じファイル名を並べた使い方をしてきた。入出力ファイル名を区別しないなら、出力用の第2引数は必要ない。
ということで、録画m2tsを受け取って、
enc.js input.m2ts
で拡張子だけを変えたinput.mp4ができるようにする。
ただし、これだけでは不十分です。上のenc.jsではEPGStationから変数AudioComponentType を引き継いでいます。EPGStationから独立したいので、この変数を別の手段で取得して第2引数として渡すようにする。つまり、
enc.js input.m2ts AudioComponentType
という形で使うことにする。
AudioComponentTypeは、「番組名」$NAMEを引数とするスクリプト act.sh (Audio Component Typeだからact) から得るようにする。
AUDIOCOMPONENTTYPE=`./act.sh $NAME`
ちなみに、この変数が 2 の場合が(トラブルの元になる)デュアルモノとなる。2か否かだけが重要。
enc.jsではffmpegを字幕処理に使うので、ffmpegをコンパイルし直す必要がある。
まとめ
ffmpegを再コンパイル。
act.sh と enc.js を、作業ディレクトリ~/workと ~/work2につくる。
enc01.sh を書き換える。
Jellyfinクライエントの個人設定(字幕)を調整する。
ffmpeg
sudo apt install libaribb24-0 libaribb24-dev
sudo ln -s /usr/lib/x86_64-linux-gnu/pkgconfig/aribb24.pc /usr/lib/x86_64-linux-gnu/pkgconfig/libaribb24.pc
cd ~/ffmpeg_611
make distclean
PKG_CONFIG_PATH="$HOME/build/ffmpeg_build/lib/pkgconfig" ./configure --prefix="/usr/local" --pkg-config-flags="--static" --extra-cflags="-I$HOME/build/ffmpeg_build/include" --extra-ldflags="-L$HOME/build/ffmpeg_build/lib:/usr/lib/x86_64-linux-gnu" --extra-libs="-lpthread -lm" --ld="g++" --bindir="$HOME/bin" --enable-gpl --enable-version3 --enable-gnutls --enable-libaom --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libdav1d --enable-libvorbis --enable-libvpl --enable-libx264 --enable-shared --enable-avisynth --enable-vaapi --enable-libvpx --enable-nonfree --disable-doc --disable-debug --enable-libaribb24 && make -j"$(nproc)"
sudo cp ffmpeg ffprobe /usr/local/bin/
sudo make install第4回との違いは、--enable-libaribb24 の追加
act.sh
説明:EPGStationのデータベースに登録された録画番組のAudioComponentTypeを出力する
使用法: ./act.sh 録画番組名
takya@fearow22:~/work$ cat act.sh
#!/bin/bash
#番組名を引数として受取り、audioComponentTypeを出力
URL='http://localhost:8888/api/recorded?isHalfWidth=false&offset=0&limit=1000'
TARGET_NAME="$1"
curl -s -X 'GET' $URL| jq -r --arg name "$TARGET_NAME" '
.records[]
| select(.name == $name)
| .audioComponentType
' 2>/dev/null
enc.js
説明:地上波m2tsに含まれる多重ストリーム(副音声[二] [解] [多]と文字[字])を保持しつつmp4に変換する
使用法: ./enc.js /path/to/番組.m2ts [AudioComponentType(省略可)]
課題:1. ffprobeで見えない副音声は拾えない. 2. 一部文字化け〓〓
takya@fearow22:~/work$ cat enc.js
#!/usr/bin/env node
// モジュールの読み込み
const { spawn } = require('child_process');
const { execFileSync } = require('child_process');
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
// コマンドライン引数の解析
const args = process.argv.slice(2);
const inputFile = args[0];
const audioComponentType = args[1] || '0';
if (!inputFile) {
console.error('Usage: node enc.js <input_file_path> [audio_component_type]');
console.error('Example: node enc.js /path/to/番組名.m2ts 2');
process.exit(1);
}
// 入力ファイルの存在確認
if (!fs.existsSync(inputFile)) {
console.error(`Error: Input file not found: ${inputFile}`);
process.exit(1);
}
// 出力ファイル名の生成(カレントディレクトリに出力)
const inputFileName = path.parse(inputFile).name;
const outputFile = `./${inputFileName}.mp4`;
// 固定設定
const epgsConfig = {
recordedFileExtension: '.m2ts'
};
// エンコード設定
const ffmpegLogOutOnlyOnError = true;
const progressLogOutMax = 0; // 進捗表示を無効化
// デフォルトコーデック設定
const useCodec = 'h264_qsv'; //libx264, h264_qsv, h264_vaapi
// 固定で3秒カット
const fixedCutSecond = 3;
// 環境変数代替関数
function getEnv(variableName) {
const envs = {
INPUT: inputFile,
OUTPUT: outputFile,
NAME: inputFileName,
AUDIOCOMPONENTTYPE: audioComponentType,
FFMPEG: 'ffmpeg',
FFPROBE: 'ffprobe'
};
return envs[variableName];
}
// メイン処理
(() => {
// デバッグモードを有効にする
const DEBUG_MODE = true;
if (DEBUG_MODE) {
debugAllStreams();
}
const useCodecPreArgs = [];
const useCodecPostArgs = [];
if (useCodec === 'h264_qsv') {
useCodecPreArgs.push('-fflags', '+genpts');
useCodecPostArgs.push('-vf', 'yadif'); //cmcutで最適
useCodecPostArgs.push('-preset', 'fast');
useCodecPostArgs.push('-global_quality', '20');
} else if (useCodec === 'libx264') {
useCodecPreArgs.push('-fflags', '+genpts');
useCodecPostArgs.push('-vf', 'yadif');
useCodecPostArgs.push('-preset', 'veryfast');
useCodecPostArgs.push('-crf', '26');
} else if (useCodec === 'h264_vaapi') {
useCodecPreArgs.push('-hwaccel', 'vaapi');
useCodecPreArgs.push('-hwaccel_device', '/dev/dri/renderD128');
useCodecPostArgs.push('-vf', 'format=nv12,hwupload,deinterlace_vaapi,scale_vaapi=w=1280:h=720');
useCodecPostArgs.push('-compression_level', '1');
useCodecPostArgs.push('-global_quality', '20');
}
// 字幕設定
const sub = getSubTitlesArg();
const audio = getAudioArgs();
// 固定で3秒カット
const cutSecond = fixedCutSecond;
const ss = cutSecond > 0 ? ['-ss', cutSecond.toString()] : [];
// 字幕ストリームが有効かどうかをチェック
const hasValidSubtitles = sub.map.length > 0;
// FFmpeg引数の組み立て
let outputArgs;
if (hasValidSubtitles) {
// 字幕ありでエンコード
outputArgs = [
'-y',
...getAnalyze(),
...sub.fix,
...useCodecPreArgs, // 入力ファイルの前に追加
...ss,
'-i', getEnv('INPUT'),
'-map', '0:v',
'-c:v', useCodec,
...audio.args,
...useCodecPostArgs, // 入力ファイルの後に追加
...sub.map,
getEnv('OUTPUT')
];
console.log('Encoding with subtitles');
} else {
// 字幕なしでエンコード
outputArgs = [
'-y',
...getAnalyze(),
...sub.fix,
...useCodecPreArgs, // 入力ファイルの前に追加
...ss,
'-i', getEnv('INPUT'),
'-map', '0:v',
'-c:v', useCodec,
...audio.args,
...useCodecPostArgs, // 入力ファイルの後に追加
getEnv('OUTPUT')
];
console.log('Encoding without subtitles');
}
console.log('Input file:', getEnv('INPUT'));
console.log('Output file:', getEnv('OUTPUT'));
console.log('FFmpeg command:', 'ffmpeg', outputArgs.join(' '));
// 実行処理
const durationInfo = getFFprobe('-show_format');
const duration = durationInfo && durationInfo.format ? durationInfo.format.duration : 0;
const startTime = process.uptime();
const logBuffers = [];
const child = spawn(getEnv('FFMPEG'), outputArgs, {
stdio: ['ignore', 'pipe', 'pipe']
});
// 標準出力も捕捉
child.stdout.on('data', data => {
logBuffers.push(String(data).trim());
});
child.stderr.on('data', data => {
const dataStr = String(data);
if (!dataStr.startsWith('frame=')) {
logBuffers.push(dataStr.trim());
}
});
child.on('exit', code => {
const isError = code !== 0;
if (!ffmpegLogOutOnlyOnError || isError) {
console.log('FFmpeg messages:', logBuffers.join('\n'));
}
const elapsed = parseFloat(process.uptime() - startTime);
const logs = {
outputArgs: outputArgs.join(' '),
duration: convertSecToTime(duration),
elapsedTime: convertSecToTime(elapsed),
averageSpeed: duration > 0 ? Math.floor(duration / elapsed) + 'x' : 'N/A',
useCodec, cutSecond,
subtitlesIncluded: hasValidSubtitles
};
if (isError) {
console.error('Error code:' + code, logs);
process.exit(code);
} else {
console.log('Successfully encoded:', logs);
}
});
child.on('error', error => {
console.error('Spawn error:', error);
process.exit(1);
});
process.on('SIGINT', () => child.kill('SIGINT'));
})();
// すべての字幕ストリームを検出する包括的な関数
function detectAllSubtitleStreams() {
try {
const options = [
'-v', 'error',
'-select_streams', 's',
'-show_entries', 'stream=index,codec_name,codec_type,tags:stream_tags:stream_tags=language',
'-of', 'json',
getEnv('INPUT')
];
const result = execFileSync(getEnv('FFPROBE'), options, { encoding: 'utf8' });
const info = JSON.parse(result);
const subtitleStreams = [];
if (info.streams && info.streams.length > 0) {
// すべての字幕ストリームを対象とする(コーデック名に関わらず)
for (const stream of info.streams) {
if (stream.codec_type === 'subtitle') {
const lang = stream.tags && stream.tags.language ? stream.tags.language : 'unknown';
subtitleStreams.push(stream.index);
console.log(`Found subtitle stream: index=${stream.index}, codec=${stream.codec_name}, language=${lang}`);
// arib_captionの場合は特別にログ出力
if (stream.codec_name === 'arib_caption') {
console.log(` ARIB caption stream detected: index=${stream.index}`);
}
}
}
}
console.log('All subtitle streams found:', subtitleStreams);
return subtitleStreams;
} catch (error) {
console.error('Error detecting subtitle streams:', error.message);
return [];
}
}
// 字幕の設定を取得
function getSubTitlesArg() {
const fix = [];
const map = [];
const fileName = getEnv('NAME');
const isSub = /\[字\]/.test(fileName);
console.log('Subtitle detection:', { fileName, isSub });
// [字]がある場合のみ字幕処理を実行
if (isSub) {
// libaribb24が利用可能かチェック
const hasLibaribb24 = checkLibaribb24Availability();
if (hasLibaribb24) {
fix.push('-fix_sub_duration');
console.log('libaribb24 is available, using -fix_sub_duration');
} else {
console.log('libaribb24 is not available, will try without special subtitle processing');
}
// 複数の方法で字幕ストリームを検出
const subtitleStreams = detectAllSubtitleStreams();
console.log('All detected subtitle streams:', subtitleStreams);
if (subtitleStreams.length > 0) {
// 検出されたすべての字幕ストリームをマップ
for (let i = 0; i < subtitleStreams.length; i++) {
map.push('-map', `0:${subtitleStreams[i]}?`);
map.push(`-c:s:${i}`, 'mov_text');
map.push(`-metadata:s:${i}`, 'language=jpn');
}
console.log('Mapped subtitle streams:', map);
} else {
console.log('No subtitle streams found, trying fallback method');
// フォールバック: すべての字幕ストリームをマップ
map.push('-map', '0:s?');
map.push('-c:s', 'mov_text');
map.push('-metadata:s:0', 'language=jpn');
}
} else {
console.log('No [字] tag in filename, skipping subtitle processing');
}
return { fix: fix, map: map, isSub: isSub };
}
// libaribb24の利用可能性をチェック
function checkLibaribb24Availability() {
try {
// 方法1: ffmpegのビルド設定を確認
const buildconfOptions = ['-buildconf'];
const buildconfResult = execFileSync(getEnv('FFMPEG'), buildconfOptions, { encoding: 'utf8' });
const hasInBuildconf = buildconfResult.includes('--enable-libaribb24');
// 方法2: コーデックリストを確認
const codecsOptions = ['-codecs'];
const codecsResult = execFileSync(getEnv('FFMPEG'), codecsOptions, { encoding: 'utf8' });
const hasInCodecs = codecsResult.includes('arib_caption') && codecsResult.includes('libaribb24');
console.log(`libaribb24 detection - Buildconf: ${hasInBuildconf}, Codecs: ${hasInCodecs}`);
// どちらかで検出されたら利用可能と判断
return hasInBuildconf || hasInCodecs;
} catch (error) {
console.error('Error checking libaribb24 availability:', error.message);
return false;
}
}
// 補助関数
function isTs() {
const reg = new RegExp(epgsConfig.recordedFileExtension + '$');
return (getEnv('INPUT').match(reg) !== null)
}
function getAnalyze() {
return ['-analyzeduration', '10M'];
}
function getFFprobe(showOptions) {
try {
const options = [].concat(getAnalyze(), '-v', '0', showOptions, '-of', 'json', getEnv('INPUT'));
const stdout = execFileSync(getEnv('FFPROBE'), options);
return JSON.parse(stdout);
} catch (error) {
console.error('getFFprobe error:', error.message);
return null;
}
}
function convertTimeToSec(time) {
const times = time.split(':');
return parseFloat(times[0]) * 3600 + parseFloat(times[1]) * 60 + parseFloat(times[2]);
}
function convertSecToTime(second) {
const date = new Date(0);
date.setSeconds(second);
return date.toISOString().substring(11, 19);
}
// 実際の音声ストリームインデックスを取得する関数
function getActualAudioStreamIndices() {
try {
const options = [
'-v', 'error',
'-select_streams', 'a',
'-show_entries', 'stream=index,codec_name,channels,bit_rate,sample_rate',
'-of', 'json',
getEnv('INPUT')
];
const result = execFileSync(getEnv('FFPROBE'), options, { encoding: 'utf8' });
const info = JSON.parse(result);
const audioIndices = [];
if (info.streams && info.streams.length > 0) {
console.log('All audio streams found:');
for (const stream of info.streams) {
console.log(` Stream #${stream.index}: ${stream.codec_name}, ${stream.channels} channels, ${stream.bit_rate} bitrate, ${stream.sample_rate} Hz`);
audioIndices.push(stream.index);
}
} else {
console.log('No audio streams found, using default stream 0');
audioIndices.push(0);
}
return audioIndices;
} catch (error) {
console.error('Error getting actual audio stream indices:', error.message);
// エラー時は安全策として[0]を返す
return [0];
}
}
// 音声の設定を取得する関数
function getAudioArgs() {
const fileName = getEnv('NAME');
const audioComponentType = parseInt(getEnv('AUDIOCOMPONENTTYPE'), 10);
const isDualMono = audioComponentType == 2;
const isBilingual = /\[二\]/.test(fileName);
const isExplanation = /\[解\]/.test(fileName);
const isMultiAudio = /\[多\]/.test(fileName);
const args = [];
console.log('Audio component type:', audioComponentType, 'isDualMono:', isDualMono, 'isBilingual:', isBilingual, 'isExplanation:', isExplanation);
// 実際の音声ストリームインデックスを取得
let actualAudioIndices = getActualAudioStreamIndices();
// 必要な音声ストリーム数を決定
let needAudioCount = 1;
if ((isBilingual || isExplanation || isMultiAudio) && !isDualMono) {
needAudioCount = 2;
}
// 実際のストリーム数と必要なストリーム数の小さい方を採用
const audioCount = Math.min(actualAudioIndices.length, needAudioCount);
console.log('Using audio streams:', audioCount, 'out of', actualAudioIndices.length, 'available');
// 音声多重放送(デュアルモノ) - audioComponentTypeが2の場合のみ処理
if (isDualMono && actualAudioIndices.length >= 1) {
console.log('Processing as dual mono');
// 左右チャンネルを分割して2つのモノラルストリームを作成
args.push('-filter_complex', `[0:${actualAudioIndices[0]}]channelsplit=channel_layout=stereo[left][right]`);
// 左チャンネル(日本語)
args.push('-map', '[left]');
// 右チャンネル(英語)
args.push('-map', '[right]');
// メタデータ設定(日本語→英語の順序)
args.push('-metadata:s:a:0', 'language=jpn');
args.push('-metadata:s:a:1', 'language=eng');
// 音声コーデック設定(libfdk_aacを維持)
args.push('-c:a', 'libfdk_aac');
args.push('-b:a:0', '192k');
args.push('-b:a:1', '192k');
args.push('-ac:0', '1');
args.push('-ac:1', '1');
return { args: args };
}
// 通常の音声処理
console.log('Processing as normal audio');
// 音声マッピング - 取得したインデックスを使ってマップ(オプショナルマッピング)
for (let i = 0; i < audioCount; i++) {
const streamIndex = actualAudioIndices[i];
args.push('-map', `0:${streamIndex}?`); // オプショナルマッピング
console.log(`Mapping audio stream: 0:${streamIndex}?`);
}
// メタデータ設定 - 実際にマップされたストリームにのみ設定
for (let index = 0; index < audioCount; index++) {
let lang = 'jpn';
if (audioCount > 1) {
// 二ヶ国語放送の場合のみ2番目の音声を英語に設定
if (isBilingual && index === 1) {
lang = 'eng'; // 二ヶ国語放送の副音声は英語
} else if (isExplanation || isMultiAudio) {
lang = 'jpn'; // 解説放送や多重放送は日本語
}
}
args.push(`-metadata:s:a:${index}`, `language=${lang}`);
}
// 音声エンコード設定(libfdk_aacを維持)
args.push('-c:a', 'libfdk_aac');
args.push('-b:a', '192k');
args.push('-ac', '2');
return { args: args };
}
// 入力ファイルのすべてのストリーム情報を表示する関数
function debugAllStreams() {
try {
const options = [
'-v', 'error',
'-show_streams',
'-of', 'json',
getEnv('INPUT')
];
const result = execFileSync(getEnv('FFPROBE'), options, { encoding: 'utf8' });
const info = JSON.parse(result);
console.log('=== DEBUG: All streams in input file ===');
if (info.streams && info.streams.length > 0) {
for (const stream of info.streams) {
const lang = stream.tags && stream.tags.language ? stream.tags.language : 'unknown';
console.log(` Stream #${stream.index}: type=${stream.codec_type}, codec=${stream.codec_name}, lang=${lang}`);
// 詳細情報
if (stream.codec_type === 'video') {
console.log(` Resolution: ${stream.width}x${stream.height}, duration: ${stream.duration}`);
} else if (stream.codec_type === 'audio') {
console.log(` Channels: ${stream.channels}, sample_rate: ${stream.sample_rate}`);
} else if (stream.codec_type === 'subtitle') {
console.log(` Subtitle details available`);
}
}
} else {
console.log(' No streams found');
}
console.log('=== DEBUG END ===');
} catch (error) {
console.error('Error debugging streams:', error.message);
}
}
// https://note.com/leal_walrus5520/n/n74a7c7561d43
// Time stamp: 2025/10/05
第2作業フォルダ~/work2 (第13回)については、act.shとenc.jsはそのままコピーでよい。
enc01.sh
takya@fearow22:~/work$ cat enc01.sh
WORKDIR="$HOME/work/"
OUTDIR="$HOME/media/"
NHK1="GR26"
NHK2="GR27"
NTFY_URL="https://fearow22.taile153a7.ts.net/PX-S1UD"
#CMCUT=false #CMカットしないなら false
trim() {
local INPUT="$1"
local TRIMFILE="$2"
local OUTPUT="$3"
# 地上波のfpsを指定(例: 29.97)
FPS=29.97
# jls_out.txt が空なら入力をそのまま出力
if [ ! -s "$TRIMFILE" ]; then
echo "No trim info. Copying input to output..."
ffmpeg -hide_banner -loglevel error -y \
-i "$INPUT" \
-c copy -map 0 \
"$OUTPUT"
exit 0
fi
TEMPDIR=$(mktemp -d)
PARTS_LIST="$TEMPDIR/parts.txt"
INDEX=0
> "$PARTS_LIST"
bc_calc() {
echo "scale=6; $1" | bc -l | awk '{printf "%.6f", $0}'
}
# 入力に字幕ストリームがあるかチェック
if ffprobe -v error -select_streams s -show_entries stream=index \
-of csv=p=0 "$INPUT" | grep -q .; then
SUB_OPT=(-c:s copy -map 0)
else
SUB_OPT=()
fi
while read -r line; do
while [[ "$line" =~ Trim\(([0-9]+),([0-9]+)\) ]]; do
STARTF="${BASH_REMATCH[1]}"
ENDF="${BASH_REMATCH[2]}"
STARTSEC=$(bc_calc "$STARTF / $FPS")
ENDSEC=$(bc_calc "$ENDF / $FPS")
DURATION=$(bc_calc "$ENDSEC - $STARTSEC")
PART="$TEMPDIR/part_${INDEX}.mp4"
#動けばよいなら
# ffmpeg -hide_banner -loglevel error -y -i "$INPUT" -ss "$STARTSEC" -t "$DURATION" -c:v libx264 -crf 26 -preset veryfast -c:a copy -c:s copy -map 0 "$PART"
#小メモリ環境
ffmpeg -threads 1 -hide_banner -loglevel error -y -i "$INPUT" -ss "$STARTSEC" -t "$DURATION" -c:v h264_qsv -global_quality 20 -preset fast -c:a copy -c:s copy -map 0 "$PART"
#注:vaapiは不向き
echo "file '$PART'" >> "$PARTS_LIST"
INDEX=$((INDEX+1))
line=${line#*"Trim("}
done
done < "$TRIMFILE"
# concat で結合
ffmpeg -hide_banner -loglevel error -y \
-f concat -safe 0 -i "$PARTS_LIST" \
-c copy -map 0 \
"$OUTPUT"
rm -rf "$TEMPDIR"
echo "Done: $OUTPUT"
}
jls() {
local filename="$1"
local output_file="$2"
# 引数チェック
if [[ -z "$filename" || -z "$output_file" ]]; then
echo "Usage: jls FILENAME OUTPUT_FILE" >&2
return 1
fi
# 一時ファイルの作成
local avs_file="join.avs"
# avsファイルの作成
cat > "$avs_file" << EOF
TSFilePath="$filename"
LWLibavVideoSource(TSFilePath, repeat=true, dominance=1)
AudioDub(last,LWLibavAudioSource(TSFilePath, av_sync=true))
EOF
# 各処理の実行
timeout -k 60 1800 chapter_exe -v "$avs_file" -o chap_out.txt || {
echo "chapter_exe failed" >&2
return 1
}
if [ ! -f "$GRSTRING.lgd" ]; then
echo "Error: $GRSTRING.lgd is not found" >&2
return 1
fi
logoframe "$avs_file" -logo "$GRSTRING.lgd" -oa lf_out.txt || {
echo "logoframe failed" >&2
return 1
}
join_logo_scp -inlogo lf_out.txt -inscp chap_out.txt -incmd JL_標準.txt -o "$output_file" || {
echo "join_logo_scp failed" >&2
return 1
}
# 一時ファイルのクリーンアップ(必要に応じてコメントアウト)
# rm -f "$avs_file" chap_out.txt lf_out.txt
}
$WORKDIR/toprocess.py | while IFS= read -r FILE;
do
cd $SOURCEDIR
if [ -f "$FILE" ]; then
start_time=$(date +%s)
FILENAME=${FILE%.*}
GRSTRING=`echo $FILENAME | sed -n 's/.*\(GR[0-9][0-9]\).*/\1/p'`
# $WORKDIR/chkdrp.sh "$FILE" #エラーチェック
cd $WORKDIR
NAME=$(echo "$FILENAME" | sed 's/__.*//')
AUDIOCOMPONENTTYPE=`./act.sh $NAME`
./enc.js $SOURCEDIR/"$FILE" $AUDIOCOMPONENTTYPE
if [ -f "$FILENAME".mp4 ] && [ "$CMCUT" != "false" ] && [ "$GRSTRING" != "$NHK1" ] && [ "$GRSTRING" != "$NHK2" ]; then
jls "$FILENAME".mp4 jls_out.txt
trim "$FILENAME".mp4 jls_out.txt temp$$.mp4
if [ -s temp$$.mp4 ] ; then
rm "$FILENAME".mp4
mv temp$$.mp4 "$FILENAME".mp4
else
echo "Error: trim failed" >&2
curl -H "X-Priority: 3" -d "ERROR: trim failed: $FILENAME" $NTFY_URL
exit 1
fi
rm "$FILENAME".mp4.lwi
fi
if [ -s "$FILENAME".mp4 ] ; then
./mvjf.sh "$FILENAME".mp4 $OUTDIR
curl -H "X-Priority: 2" -d "mp4 created: $FILENAME" $NTFY_URL
else
echo "ERROR: mp4 not created" >&2
curl -H "X-Priority: 3" -d "ERROR: mp4 not created: $FILENAME" $NTFY_URL
exit 1
fi
./processed.py "$FILE" || true
end_time=$(date +%s)
duration=$((end_time - start_time))
minutes=$((duration / 60))
seconds=$((duration % 60))
echo "RUN TIME; $minutes min $seconds sec"
fi
done
こちらは、第2作業フォルダ~/work2にコピーする際に、冒頭の変数を適宜変更する。
CMカットが不要ならば、CMCUT=falseで全局が今回の仕様(多重化)になる。
Jellyfin
右上の人マークの個人設定の「字幕」から文字の大きさ、影付け、垂直位置などを調整する。
おわりに
2段目のトランスコード(trim)は、ハードウェア的に非力なPCでは辛くなる。2段目のせいで作業時間が2〜3倍になるので、encode.sh の変数speedを適宜変更する。
Error while filtering: Cannot allocate memory
Failed to inject frame into filter network: Cannot allocate memory
こんなエラーを吐きながらも、とりあえず動いてはいる。愛機をいじめまくってみて、こうしてハードウェアトランスコードの泥沼にはまっていくのか、と痛感している。
参考メモ:字幕ストリームを filter_complex で切り貼りすることはできない
おまけ:こんな感じ
索引
過去回の概要一覧
第1回:はじめに。準備が必要なもの
おすすめPCなど
第2回:Ubuntu OS インストール。はじめてのlinux
新PCが使えるようになる
第3回:デバイス認識。EPGStation, Mirakurunインストール
テレビ録画できるようになる
第4回:AviSynth, chapter_exe, logoframe, join_logo_scp, ffmpeg
昔風のインストール作業
第5回:CMカット。ロゴファイル作成
市民的自由
第6回:作業フォルダ、メディアフォルダ。Jellyfin インストール
視聴環境が整う
第7回:処理済み判定、番組名でフォルダ整理
随時更新中
第8回:空き時間に編集自動化
全自動化完了
第9回:録画ルール設定、処理済み動画自動削除
コンセプトと使用方法
第10回:スマホに自動通知
ちゃんと動いているか不安になるので
第11回:使用雑感。不具合対策。再起動法
感想とか
第12回:外部データベース連携。TMDB登録方法
見た目がよくなる
第13回:ライブラリ追加「映画」
ますます、カッコよくなる
第14回:ダッシュボード(ホームページ)
スマホで便利に
第15回:ネットダウンローダー
TVerとか、youtubeとか
第16回:ハードウェアアクセラレーション
動画処理効率化
第17回:スマートテレビ
リビングの大画面で
第18回:外部ストレージ増設
録画マニアへの入り口
第19回:トラブルシューティング
戦いは続く
第20回:イントロスキップ
豊富なプラグインで、ますます便利に



コメント