Skip to content

Instantly share code, notes, and snippets.

@voluntas
Created March 15, 2018 06:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save voluntas/354ce57e5923c1926cb7f753a04c0f07 to your computer and use it in GitHub Desktop.
Save voluntas/354ce57e5923c1926cb7f753a04c0f07 to your computer and use it in GitHub Desktop.
muxer.sh
#!/bin/bash
# Sora のマルチストリーム機能を使用し、かつ、録画機能を使用した場合に作成される録画情報のメタファイルを元に、映像と音声を 1 ファイルに合成します
# 録画の途中で解像度が変越された場合は、映像や音声が飛んだり、途中で停止等することがあります(頻繁に解像度が変更されるため Chrome 非推奨)
# 合成元の録画ファイルが壊れている場合は、映像や音声が飛んだり、途中で停止等することがあります
# TODO: video のみの映像が含まれる場合の合成
# TODO: audio のみの映像が含まれる場合の合成
# TODO: 合成時の video コーデック指定
# TODO: 合成時の audio コーデック指定
# TODO: 任意のオプションの指定
#video_codec=libopenh264
video_codec=libx264
audio_codec=libmp3lame
usage() {
echo "usage: muxer.sh [-f metadata_file_path] [-c column]" 1>&2
exit 1
}
init() {
metadata=$( cat $metadata_file_path )
created_at=$( echo $metadata | jq .created_at )
channel_id=$( echo $metadata | jq -r .channel_id )
archives=( $( echo $metadata | jq -c ".archives | sort_by(.start_time_offset) | .[]" ) )
video_archives=()
for archive in ${archives[@]}
do
file_path=$( echo $archive | jq -r .file_path )
ffprobe $file_path 2>&1 | grep Video >/dev/null
if [ "$?" = "0" ];
then
video_archives=( ${video_archives[@]} $archive )
fi
done
audio_archives=()
for archive in ${archives[@]}
do
file_path=$( echo $archive | jq -r .file_path )
ffprobe $file_path 2>&1 | grep Audio > /dev/null
if [ "$?" = "0" ];
then
audio_archives=( ${audio_archives[@]} $archive )
fi
done
last_client_id=( $( echo $metadata | jq -c -r ".archives | max_by(.stop_time_offset) | .client_id" ) )
audio_file="${channel_id}.mp3"
video_file="${channel_id}.mp4"
}
# 音声の合成
# 音声と映像の合成を一コマンドで実行すると Buffer queue overflow, dropping. が発生して、映像または音声が飛び飛びになるため、はじめに音声の合成のみをおこない、次に映像と合成済みの音声の合成をおこなう
# ffmpeg
# -i /home/user/sora/_build/dev/rel/sora/archive/3ec96de5-abad-4b19-ae6a-6514010051b1.webm
# -i /home/user/sora/_build/dev/rel/sora/archive/041d743f-3b91-4790-b0b4-8b653bbc1ada.webm
# -i /home/user/sora/_build/dev/rel/sora/archive/e238f67a-55bb-480d-984f-4a03aec25a4a.webm
# -filter_complex "
# [0:a] adelay=10 [a0];
# [1:a] adelay=15000 [a1];
# [2:a] adelay=417000 [a2];
# [a0][a1][a2] amix=inputs=3:duration=longest:dropout_transition=0 [a]
# "
# -c:a libmp3lame
# -map "[a]"
# sora.mp3
mix_audio() {
args=""
index=0
filter_complex=""
tags=""
for archive in ${audio_archives[@]}
do
metadata_file_path=$( echo $archive | jq -r .metadata_file_path )
file_path=$( cat $metadata_file_path | jq -r .file_path )
args="$args -i $file_path"
tag="[audio${index}]"
filter_complex="$filter_complex $( audio_filter_complex $index $tag $archive )"
tags="${tags}${tag}"
index=$(( $index + 1 ))
done
filter_complex="$filter_complex $tags amix=inputs=${#audio_archives[@]}:duration=longest:dropout_transition=0 [audio]"
args="$args -filter_complex \"${filter_complex}\" \
-c:a $audio_codec \
-map \"[audio]\" \
$audio_file"
bash -c "ffmpeg $args"
}
audio_filter_complex() {
index=$1
tag=$2
archive=$3
delay=$( echo $archive | jq .start_time_offset )
tag="[audio${index}]"
if [ "$delay" -eq "0" ];
then
# adelay は 0 以上を指定する必要があるためとりあえず 10 msec を指定
# TODO(yoshida): 10msec の映像とのズレが発生するため調整が必要
echo "[${index}:a] adelay=$(( $delay + 10 )) $tag;"
else
# 開始時間は msec で指定する
echo "[${index}:a] adelay=$(( $delay * 1000 )) $tag;"
fi
}
# 映像と音声の合成
# ffmpeg
# -i /home/user/sora/_build/dev/rel/sora/archive/3ec96de5-abad-4b19-ae6a-6514010051b1.webm
# -i /home/user/sora/_build/dev/rel/sora/archive/041d743f-3b91-4790-b0b4-8b653bbc1ada.webm
# -i /home/user/sora/_build/dev/rel/sora/archive/e238f67a-55bb-480d-984f-4a03aec25a4a.webm
# -i sora.mp3
# -filter_complex "
# [0:v] setpts=PTS-STARTPTS+0/TB, scale=640x480 [v0];
# [1:v] setpts=PTS-STARTPTS+15/TB, scale=640x480 [v1];
# [2:v] setpts=PTS-STARTPTS+417/TB, scale=640x480 [v2];
# color=c=black@0.2:size=1280x960 [b];
# [b][v0] overlay=shortest=0:x=0:y=0 [t0];
# [t0][v1] overlay=shortest=1:x=640:y=0 [t1];
# [t1][v2] overlay=shortest=0:x=0:y=480 [v]
# "
# -c:v libx264
# -c:a copy
# -map "3:a"
# -map "[v]"
# sora.mp4
mux() {
# 解像度の最大値を全体の動画サイズのベースとして採用する
base_width=0
base_height=0
for archive in ${video_archives[@]}
do
metadata_file_path=$( echo $archive | jq -r .metadata_file_path )
width=$( cat $metadata_file_path | jq -r .video.width )
height=$( cat $metadata_file_path | jq -r .video.height )
if [ "$base_width" -lt "$width" ];
then
base_width=$width
fi
if [ "$base_height" -lt "$height" ];
then
base_height=$height
fi
done
# サイズの計算
video_width=$(( $base_width * $column ))
video_height=$(( $base_height * $(( $(( ${#video_archives[@]} + $(( $column - 1 )) )) / $column )) ))
video_size=${video_width}x${video_height}
args=""
filter_complex="color=c=black@0.2:size=${video_size} [base];"
index=0
for archive in ${video_archives[@]}
do
metadata_file_path=$( echo $archive | jq -r .metadata_file_path )
file_path=$( cat $metadata_file_path | jq -r .file_path )
metadata=$( cat $metadata_file_path )
width=$( echo $metadata | jq .video.width )
height=$( echo $metadata | jq .video.height )
args="$args -i $file_path"
delay=$( echo $archive | jq .start_time_offset )
tag="[video${index}]"
if [ "$delay" -eq "0" ];
then
# TODO: 0 のままでは 10msec の音声とのズレが発生するため調整する
filter_complex="$filter_complex [${index}:v] setpts=PTS-STARTPTS/TB, scale=${width}x${height} $tag;"
else
filter_complex="$filter_complex [${index}:v] setpts=PTS-STARTPTS+${delay}/TB, scale=${width}x${height} $tag;"
fi
index=$(( $index + 1 ))
done
index=0
for archive in ${video_archives[@]}
do
tag="[video${index}]"
if [ "$index" -eq "0" ];
then
target_tag="[t${index}]"
next_target_tag="[base]"
else
# 最後だけ shortest と tag が異なる
if [ $(( ${#video_archives[@]} - 1 )) -eq $index ];
then
target_tag="[video]"
else
target_tag="[t${index}]"
fi
fi
x=$(( $base_width * $(( $index % $column )) ))
y=$(( $base_height * $(( $index / $column )) ))
client_id=$( echo $archive | jq -r .client_id )
if [ "$client_id" = "$last_client_id" ];
then
shortest=1
else
shortest=0
fi
filter_complex="$filter_complex ${next_target_tag}${tag} overlay=shortest=${shortest}:x=${x}:y=${y} $target_tag"
# 最後以外は ; が必要
if [ $(( ${#video_archives[@]} - 1 )) -ne $index ];
then
filter_complex="${filter_complex};"
fi
next_target_tag=$target_tag
index=$(( $index + 1 ))
done
if [ -e $audio_file ];
then
args="$args -i $audio_file"
args="$args -filter_complex \"${filter_complex}\" \
-c:v $video_codec \
-c:a copy \
-map \"${#video_archives[@]}:a\" \
-map \"[video]\" \
$video_file"
else
args="$args -filter_complex \"${filter_complex}\" \
-c:v $video_codec \
-map \"[video]\" \
$video_file"
fi
echo $args
bash -c "ffmpeg $args"
}
while getopts f:c:h opt
do
case $opt in
"f" ) metadata_file_path="$OPTARG" ;;
"c" ) column="$OPTARG" ;;
"h" ) usage ;;
esac
done
if [ -z "$metadata_file_path" ];
then
usage
fi
# TODO: 整数以外の値が指定された場合の処理の追加
if [ -z "$column" ];
then
column=2
elif [ "$column" -lt "1" ];
then
usage
fi
init
mix_audio
mux
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment