Created
March 15, 2018 06:26
-
-
Save voluntas/354ce57e5923c1926cb7f753a04c0f07 to your computer and use it in GitHub Desktop.
muxer.sh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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