分佈式FFMPEG轉碼集羣

分佈式FFMPEG轉碼集羣

思路

  • 分佈式轉碼集羣
  • 暫時的目標: FFMPEG實現任意格式轉MP4。
  • 1控制節點,3計算節點。
  • 存儲暫時採用單節點的共享存儲(NFS),可嘗試分佈式存儲。
  • 所有節點對CPU架構無要求,暫時用x86進行測試,後期加入ARM架構。
  • 控制節點和計算節點通信暫時使用SSH。
  • 思路: 控制節點收到請求,將文件傳到共享存儲,計算視頻總幀數,然後發送命令給計算節點,不同節點按照各自權重(手動設置,可加性能測試功能)處理一定的連續幀,輸出到共享存儲,全部節點轉碼完畢後交由存儲節點進行合併,並清理共享臨時文件,最後控制節點返回轉碼後的視頻鏈接。

安裝與配置

  • 測試組網:三臺公網VPS cn.gcc.ac.cn, hk.gcc.ac.cn, us.gcc.ac.cn
  • 存儲節點:hk.gcc.ac.cn
  • 控制節點:cn.gcc.ac.cn
  • 計算節點:cn.gcc.ac.cn,hk.gcc.ac.cn,us.gcc.ac.cn

存儲節點

  • 存儲節點系統是Debian
# 安裝NFS
apt-get install nfs-kernel-server
# 新建共享文件夾,用於放渲染前上傳的文件、渲染後的分片文件、渲染後的完整文件。
mkdir -p /srv/distributed_ffmpeg_transcoding_shared_files
mkdir /srv/distributed_ffmpeg_transcoding_shared_files/upload
mkdir /srv/distributed_ffmpeg_transcoding_shared_files/tmp
mkdir /srv/distributed_ffmpeg_transcoding_shared_files/download
chmod -R 777 /srv/distributed_ffmpeg_transcoding_shared_files
  • 修改文件/etc/exports,將目錄共享出去
  • upload目錄,控制節點有讀寫權限,計算節點有隻讀權限
  • tmp,計算節點有讀寫權限
  • download目錄,存儲計算節點有讀寫權限(由於這裏只有單節點存儲,就不需要共享了)
# /etc/exports: the access control list for filesystems which may be exported
#               to NFS clients.  See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes       hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4        gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
# /srv/nfs4/homes  gss/krb5i(rw,sync,no_subtree_check)
#
/srv/distributed_ffmpeg_transcoding_shared_files/upload cn.gcc.ac.cn(ro,insecure) us.gcc.ac.cn(ro,insecure)
/srv/distributed_ffmpeg_transcoding_shared_files/tmp cn.gcc.ac.cn(rw,insecure) us.gcc.ac.cn(rw,insecure)
  • 注:特別注意要用insecure,否則會掛載不上,顯示access denied。這個坑了我好久。
  • 修改完後用exportfs -arv生效。可以使用showmount -e查看。

計算節點

# 新建目錄
mkdir -p /srv/distributed_ffmpeg_transcoding_shared_files/upload
mkdir -p /srv/distributed_ffmpeg_transcoding_shared_files/tmp
# 掛載NFS,這裏只是臨時掛載,可以修改fstab或開機啓動腳本進行自動掛載
mount hk.gcc.ac.cn:/srv/distributed_ffmpeg_transcoding_shared_files/upload /srv/distributed_ffmpeg_transcoding_shared_files/upload
mount hk.gcc.ac.cn:/srv/distributed_ffmpeg_transcoding_shared_files/tmp /srv/distributed_ffmpeg_transcoding_shared_files/tmp
# 安裝ffmpeg
apt-get install ffmpeg
  • 注:現在jessie要在source.list添加deb http://ftp.debian.org/debian jessie-backports main才能找到這個包。

控制節點

# 生成ssh公鑰並拷貝到計算節點
ssh-keygen -t rsa
ssh-copy-id -i ~/.ssh/id_rsa.pub hk.gcc.ac.cn # 拷貝公鑰到計算節點
ssh-copy-id -i ~/.ssh/id_rsa.pub us.gcc.ac.cn -p 10022 # 拷貝公鑰到計算節點,這個節點的ssh端口是10022
# 創建並掛載upload目錄
mkdir -p /srv/distributed_ffmpeg_transcoding_shared_files/upload
mount hk.gcc.ac.cn:/srv/distributed_ffmpeg_transcoding_shared_files/upload /srv/distributed_ffmpeg_transcoding_shared_files/upload

保存下面這個腳本並加執行權限,名字隨意,我保存爲dffmpeg.sh。

#!/bin/bash

# 分佈式FFMPEG轉碼 v1.2
# 支持任意格式視頻轉成MP4
# Usage:dffmpeg.sh [input_file] [ffmpeg_output_parameter]
# Usage:dffmpeg.sh test.mp4
# Usage:dffmpeg.sh test.mp4 -c mpeg4
# Usage:dffmpeg.sh test.mp4 -c mpeg4 -b:v 1M
# TODO:?

###配置項
storage_node="hk.gcc.ac.cn"
storage_node_ssh_port=22
compute_node=("us.gcc.ac.cn" "hk.gcc.ac.cn" "cn.gcc.ac.cn") # 計算節點
compute_node_ssh_port=(10022 22 22) # 計算節點的ssh端口
compute_node_weight=(10 50 15) # 計算節點的權重
nfs_path=/srv/distributed_ffmpeg_transcoding_shared_files #共享目錄
###配置項結束

upload_path=$nfs_path/upload
tmp_path=$nfs_path/tmp
download_path=$nfs_path/download
input_file=$1
ffmpeg_output_parameter=${@:2}

# display函數,輸出彩色
ECHO=`which echo`
display(){
    local type=$1
    local msg=${@:2}
    if [[ $type = "[Info]" ]]; then
        $ECHO -e "\\033[1;36;40m[Info] $msg \\033[0m"
    elif [[ $type = "[Error]" ]]; then
        $ECHO -e "\\033[1;31;40m[Error] $msg \\033[0m"
    elif [[ $type = "[Exec]" ]]; then
        $ECHO -e "\\033[1;33;40m[Exec] $msg \\033[0m"
    elif [[ $type = "[Success]" ]]; then
        $ECHO -e "\\033[1;32;40m[Success] $msg \\033[0m"
    else
        $ECHO -e $@
    fi
}

### 開始函數重載
# 重載cp,記錄log
CP=`which cp`
cp(){
    local src=$1
    local dst=$2
    display [Exec] cp ${@:1}
    $CP ${@:1}
}
# 重載rm,記錄log
RM=`which rm`
rm(){
    display [Exec] rm ${@:1}
    $RM ${@:1}
}
# 重載ssh,記錄log
SSH=`which ssh`
ssh(){
    display [Exec] ssh ${@:1}
    $SSH ${@:1}
}
### 函數重載完畢

# 檢查輸入文件
if [ -f $input_file ]
then
   display [Info] Input: $input_file
   display [Info] FFmpeg output parameter: $ffmpeg_output_parameter
   filename=$(date +%s) # 用時間戳做文件名
   display [Info] Uploading file, please wait a while. Temporary filename: $filename
   cp $input_file $upload_path/$filename
else
   display [Error] Input error!
   exit
fi

# 計算計算節點總權重
total_weight=0
for i in ${compute_node_weight[*]}
do
    total_weight=$[$total_weight + $i]
done
display [Info] Compute node total weight: $total_weight

# 分發任務
video_length=$(ffprobe -show_format $input_file -loglevel error| grep duration | awk -F = '{printf $2}')
part_start=0
part_end=0
node_number=${#compute_node[*]}
# for i in {0..${#compute_node[*]}} # 不知爲啥不能這樣寫
for ((i=0; i<$node_number; i++))
do
    # echo ${compute_node[$i]},${compute_node_weight[$i]} # 顯示計算節點及其權重
    part_end=$(echo "scale=2; $part_start + $video_length * ${compute_node_weight[$i]} / $total_weight" | bc )
    display [Info] Compute node ["${compute_node[$i]}"] : start[$part_start] , end[$part_end]
    # ssh ${compute_node[$i]} -p ${compute_node_ssh_port[$i]} "ffmpeg -ss $part_start -i $upload_path/$filename -to $part_end $ffmpeg_output_parameter $tmp_path/${filename}_$i.mp4 -loglevel error; touch $tmp_path/${filename}_$i.txt" & # -ss在前面,The input will be parsed using keyframes, which is very fast. 但可能會造成視頻部分片段重複。
    ssh ${compute_node[$i]} -p ${compute_node_ssh_port[$i]} "ffmpeg -i $upload_path/$filename -ss $part_start -to $part_end $ffmpeg_output_parameter $tmp_path/${filename}_$i.mp4 -loglevel error; touch $tmp_path/${filename}_$i.txt" & # -ss在後面,速度會變慢,但是不會造成視頻片段重複
    part_start=$part_end
    echo "file '${filename}_$i.mp4'" >> $tmp_path/${filename}_filelist.txt
done 

# 不斷檢查任務是否完成
display [Info] Checking if the tasks are completed.
while :
do
    for ((i=0; i<$node_number; i++))
    do
        if [ -f $tmp_path/${filename}_$i.txt ]
        then
            if [ $i==$[$node_number - 1] ] # 如果全部完成了
            then
                break 2
            else
                continue
            fi
        else
            break
        fi
    done
    sleep 1
    display ".\c"
done
display !

# 進行視頻拼接
display [Info] Tasks all completed! Start to join them.
ssh $storage_node -p $storage_node_ssh_port "ffmpeg -f concat -i $tmp_path/${filename}_filelist.txt -c copy $download_path/$filename.mp4 -loglevel error"

#清除臨時文件和上傳的文件
display [Info] Clean temporary files.
rm -r $tmp_path/${filename}*
rm $upload_path/${filename}

display [Success] Mission complete! Output path: [$download_path/$filename.mp4]


# ffmpeg常用命令。參考 https://www.cnblogs.com/frost-yen/p/5848781.html

#ffprobe -v error -count_frames -select_streams v:0  -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 test.mp4 # 計算幀數量,參考 https://stackoverflow.com/questions/2017843/fetch-frame-count-with-ffmpeg

# 截取視頻,參考 https://trac.ffmpeg.org/wiki/Seeking
#ffmpeg -ss 00:01:00 -i test.mp4 -to 00:02:00 -c copy cut.mp4
#ffmpeg -ss 1 -i test.mp4 -to 2 -c copy cut.mp4
#ffmpeg -ss 1 -i test.mp4 -t 1 -c copy -copyts cut.mp4
# 獲取視頻長度
#ffprobe -show_format test_video.mp4 | grep duration | awk -F = '{printf $2}' > /tmp/1.txt

# 合併視頻 https://blog.csdn.net/doublefi123/article/details/47276739

使用方法和測試

  • 在控制節點運行:dffmpeg.sh test.mp4 -c mpeg4 -b:v 1M
  • 其中test.mp4爲需要轉碼的文件,mpeg4是編碼格式,1M是視頻碼率。
  • 最後會輸出一個mp4文件在download目錄。
  • 由於我的三個服務器分佈在公網不同地域,瓶頸在NFS的讀寫速度,所以最終轉碼速度會比較慢。如果服務器先緩存了要轉碼的文件,那麼最終轉碼速度是比一臺服務器轉碼快的。
  • 代碼運行效果如下圖
    代碼運行效果圖

結束語

  • 覺得這個好玩,所以我就寫了。本來是打算用幾個樹莓派做測試的,不過樹莓派現在只有一個,先用VPS測試一下。
  • 腳本中有一段註釋了的代碼for i in {0..${#compute_node[*]}},這是我一開始的寫法,但是發現不能用。我不知道爲什麼不能那樣寫,所以寫成for ((i=0; i<$node_number; i++))。如果知道答案的還請指點一下。
  • 我覺得不斷檢查任務是否完成那部分的判斷代碼不大好看,不知道有沒有更簡潔的方法。

更新日誌

  • v1.1:修復問題:最後的視頻長度會比原來長。原因在於-ss參數的位置,詳見代碼註釋。
  • v1.2:新增內容:支持FFmpeg輸出參數。輸出彩色詳細信息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章