#!/data/data/com.termux/files/usr/bin/bash # 远程ffmpeg转码 # @todo 断点续传失败,文件自动从0开始,需要仔细研究下rsync的相关参数 # @todo 需要dashboard跟踪每个任务的文件绝对路径、日志、状态。能识别是否因网络不稳定等原因导致某个循环代码僵住无法跳出 # @todo 支持多个位置插入干预命令(用途: 改变休眠时长,跳出循环,提前终止,改变行为等) # @todo 提供命令, 停止指定任务, 并清理残留垃圾文件 # 基础支持 CUR_DIR="$(dirname "$(readlink -f "$0")")" . "${CUR_DIR}"/lib/get_abs_filename.lib . "${CUR_DIR}"/lib/echolog.lib . "${CUR_DIR}"/lib/monitor.lib # 参数 # 接受最后一个参数,作为本地视频文件路径,转码完成后的结果文件为 [输入路径再追加"{.SERV}{.CRF}.mkv"] (此参数必须写在最尾) # -c 参数可定制 ffmpeg 的crf参数值 (可选) # -s 参数指定配置文件的简称,例如 -s mm 会指定 rh265.mm.conf (可选) ARGS=("$@") if [[ $# = 0 ]]; then less "${CUR_DIR}/readme.md"; exit; fi VDPATH=${ARGS[$(($#-1))]} # 输入文件名(一般取最后一个参数作为输入视频文件名。但如果倒数第二个参数也是文件名,则会先将 [倒数第二个文件名] 所指文件,移动到 [最后一个参数值] 所指文件路径,再开始转码) CRF="" # ffmpeg命令的crf残片 CRF_SUFFIX="" # 本地生成文件名后缀的crf部分 SERV="" # 带有服务器简称的文件名中缀残片 PRESET="" # ffmpeg命令的preset残片 PRESET_SUFFIX="" # 本地生成文件名后缀的preset部分 while getopts "c:s:p:" optname; do case "$optname" in c) CRF="-crf ${OPTARG}" CRF_SUFFIX=".crf${OPTARG}" ;; s) SERV=".${OPTARG}" ;; p) PRESET="-preset ${OPTARG}" PRESET_SUFFIX=".pr${OPTARG}" ;; *) echo "error arg option: -${optname}." exit ;; esac done # 文件参数拦截 # 允许: # rh265 [..options..] rawfile_exists.mp4 # rh265 [..options..] rawfile_exists.mp4 movetofile_not_exists.mp4 # 禁止: # rh265 [..options..] rawfile_not_exists.mp4 # rh265 [..options..] rawfile_exists.mp4 movetofile_exists.mp4 if ! [[ -e "${VDPATH}" ]]; then if [[ $# -eq 1 ]]; then echo "错误:输入的文件不存在" exit fi # 可能采用了 【rh265 [...] rawfile.mp4 movetofile.mp4】的调用形式(先改名,再转码) # MOVETO=$VDPATH # VDPATH=${ARGS[$(($#-2))]} if ! [[ -e "${ARGS[$(($#-2))]}" ]]; then echo "错误:输入的文件不存在" exit else mv "${ARGS[$(($#-2))]}" "${VDPATH}" fi else if [[ $# -ge 2 ]]; then if [[ -e "${ARGS[$(($#-2))]}" ]]; then echo "错误:转码前文件改新名失败,因新名所指文件在以前就已存在" exit fi fi fi # 根据选择的服务器,装载配置文件 conf="${CUR_DIR}"/conf/rh265${SERV}.conf if ! [[ -e "$conf" ]]; then echo '配置文件 ${conf} 不存在.' exit fi . $conf # 本地生成文件统一用的完整中缀,如 ".mm.crf23",生成某文件的具体名称为 xxxxxxx.mm.crf23.finished GENF_SUFFIX=${SERV}${CRF_SUFFIX}${PRESET_SUFFIX} # 生成PID标记文件,便于跟踪 PID_FILE="${VDPATH}"${GENF_SUFFIX}.pid.$$ touch "$PID_FILE" # 准备好本地日志文件及归档目录 @todo 改为存储json,方便共享读取 LOG_FILE="${CUR_DIR}"/logs/${REMOTE_TMPFILE}-pid-$$.log LOG_FILE_END="${CUR_DIR}"/logs/end MONITOR_DIR="${CUR_DIR}"/logs/monitor MONITOR_FILE="${CUR_DIR}"/logs/monitor/$$.json mkdir -p "${LOG_FILE_END}" mkdir -p "${MONITOR_DIR}" # 保存任务明细到监控中心 (注明下,此处CMD位置由于用了双引号括住,故$@要改成$*,不然报坑爹的Argument `×××' is neither k=v nor k@v 错误) MONITOR_JSON=$(jo \ serv=${SERV/./} \ cwd="`ls -l /proc/$$/cwd`" \ cmd="$0 $*" \ relatepids="`ls /proc/$$/task`" \ local="$VDPATH" \ localfull="`lib_get_abs_filename "$VDPATH"`" \ remote="${REMOTEDIR}/${REMOTE_TMPFILE}.input" \ loging="${CUR_DIR}/logs/${REMOTE_TMPFILE}-pid-$$.log" \ logend="${CUR_DIR}/logs/end/ ${REMOTE_TMPFILE}-pid-$$.log" \ logremote="sshpass -p '${PASSWD}' ssh -l $USER -p $PORT $HOST 'tail -f -n 100 ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup'" \ state="START" \ ) echo "$MONITOR_JSON" > "${MONITOR_FILE}" echo "======== PID=$$ ======== $(echo "${MONITOR_JSON}"|jq -r) " >> "${LOG_FILE_END}/log.map" # 生成便于查看本地日志的脚本文件 LOG_SHOTCUT="${VDPATH}"${GENF_SUFFIX}.locallog.sh echo "tail -f -n 100 '${LOG_FILE}' " > "$LOG_SHOTCUT" # 生成便于查看远程日志的脚本文件(等开始转码时再写) RMLOG_SHOTCUT="${VDPATH}${GENF_SUFFIX}.remotelog.sh" # 控制错误输出 # exec 2>> "`getLogPath`" echo "────────────── PID=$$ ──────────────" { # <<<<<<<<<<<<<<<<< 主逻辑开始 <<<<<<<<<<<<<<<<< # 上传 echolog "上传中... ${VDPATH} => 远程目录${REMOTEDIR}/${REMOTE_TMPFILE}.input" rm -f "${VDPATH}${GENF_SUFFIX}.input.md5" uploadTo(){ sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "mkdir -p ${REMOTEDIR}" # 创建远程目录 sshpass -p "${PASSWD}" rsync -avP -e "ssh -p ${PORT}" "${VDPATH}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.input" # 上传 (注意,多个了P参数,支持断点续传) rsyncResult=$? #if [[ "no input" = $(sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "if ! [[ -e ${REMOTEDIR}/${REMOTE_TMPFILE}.input ]]; then echo 'no input'; else echo 'uploaded'; fi") ]]; then # return 0 # 暂时不需要这个判断,有下方while中的input.md5有效性检测足矣 #fi if [[ "0" = "${rsyncResult}" ]]; then sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "md5sum ${REMOTEDIR}/${REMOTE_TMPFILE}.input |awk '{print \$1}' > ${REMOTEDIR}/${REMOTE_TMPFILE}.input.md5" # 上传后写md5 sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.input.md5" "${VDPATH}${GENF_SUFFIX}.input.md5" # 下载md5到本地用于验证 fi # return 1 } while true; do if [[ -e "${VDPATH}${GENF_SUFFIX}.input.md5" ]]; then if [[ $(cat "${VDPATH}${GENF_SUFFIX}.input.md5") = '' ]]; then echolog "检测到远程无效的input.md5文件,可能rsync上传被中断,现在重试)" monitor_set '.state = "UPLOAD_RETRY"' "${MONITOR_FILE}" uploadTo continue fi if [[ $(cat "${VDPATH}${GENF_SUFFIX}.input.md5") = $(md5sum "${VDPATH}"|awk '{print $1}') ]]; then echolog "已确认完整上传: ${VDPATH}" monitor_set '.state = "UPLOAD_SUCCESS"' "${MONITOR_FILE}" break # 确保完整上传后,才可跳出重试的循环 else echolog "上传失败,现在重试..." monitor_set '.state = "UPLOAD_RETRY"' "${MONITOR_FILE}" uploadTo fi else echolog C monitor_set '.state = "UPLOAD_ING"' "${MONITOR_FILE}" uploadTo fi sleep 1 done # 后台远程转码 @todo: 一定要确保网络不稳定时,正确完整执行(观察到的情况:在服务器准备转码时,提示input文件不存在。不知道是怎么到这一步的) # @todo: 通过网络检测ffmpeg进程和mkv文件的过程,其实是不可信的,因为会遇到网络波动的情况,造成误判,进而重复提交ffmpeg命令。在遇到确实已转出mkv文件时,会因无法答复系统的[是否覆盖文件]的提问,造成一直提交失败的假象 echolog '上传完毕,开始转码...' RM_COUNT=0 tracingTranscode(){ # @todo: 已有mkv文件,但是被重复提交,提示是否覆盖 mkv already exists. Overwrite ? [y/N] Not overwriting - exiting pnlist=$(sshpass -p "$PASSWD" ssh -l $USER -p $PORT $HOST "ps -ef|grep '${REMOTE_TMPFILE}'|grep ffmpeg|grep -vw grep|awk '{print \$8}'") for pn in $pnlist; do if [[ "$pn" = "ffmpeg" ]]; then return 1 fi done #检测不到ffmpeg进程,可能视频文件太小,ffmpeg快速完成了,那么检测mkv或output文件是否存在 remoteMkvFileExists=$(sshpass -p "$PASSWD" ssh -l $USER -p $PORT $HOST "if [[ -e ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv ]]; then echo 1; else echo 2; fi") if [[ 1 -eq "$remoteMkvFileExists" ]]; then return 1 fi remoteOutputFileExists=$(sshpass -p "$PASSWD" ssh -l $USER -p $PORT $HOST "if [[ -e ${REMOTEDIR}/${REMOTE_TMPFILE}.output ]]; then echo 1; else echo 2; fi") if [[ 1 -eq "$remoteOutputFileExists" ]]; then return 1 fi #进程和文件都找不到,可能转码失败,或ffmpeg命令提交失败 return 0 } while [ $RM_COUNT -lt 5 ]; do RM_COUNT=$((RM_COUNT+1)) echolog "第${RM_COUNT}次提交转码命令.." monitor_set '.state = "FFMPEG_PREPARE"' "${MONITOR_FILE}" # sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "nohup sh -c 'ffmpeg -i ${REMOTEDIR}/${REMOTE_TMPFILE}.input -c:v libx265 -c:a copy $CRF -movflags +faststart ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv; md5sum ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv|awk \"{print \\\$1}\" > ${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5; touch ${REMOTEDIR}/${REMOTE_TMPFILE}.finished' > ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup 2>&1 &" # 上面这条命令太复杂,以后可能还需要添加更多逻辑,有必要拆行,故采用传递远程脚本文件的形式 echo " inputfile=${REMOTEDIR}/${REMOTE_TMPFILE}.input outputfile=${REMOTEDIR}/${REMOTE_TMPFILE}.mkv pnlist=\`ps -ef|grep \"${REMOTE_TMPFILE}\"|grep ffmpeg|grep -vw grep|awk '{print \$8}'\` for pn in \$pnlist; do if [[ \"\$pn\" = \"ffmpeg\" ]]; then echo \"已检测到ffmpeg进程,不必再提交\" exit fi done if [[ -e \"\$outputfile\" ]]; then echo \"已有mkv文件,不必重复提交\" exit fi ffmpeg -i \$inputfile -c:v libx265 -c:a copy $CRF $PRESET -movflags +faststart \$outputfile ffmpegResult=\$? md5sum \$outputfile|awk \"{print \\\$1}\" > ${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5 if [[ -e \"\$outputfile\" ]] && [[ \"0\" -eq \"\$ffmpegResult\" ]]; then # 确认ffmpeg已成功执行完毕,写入finished标记文件 touch ${REMOTEDIR}/${REMOTE_TMPFILE}.finished # 修改结果文件的后缀名,规避审查(可能行吧) mv ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv ${REMOTEDIR}/${REMOTE_TMPFILE}.output fi " > "${VDPATH}${GENF_SUFFIX}.ffmpeg" sshpass -p "${PASSWD}" rsync -avP -e "ssh -p ${PORT}" "${VDPATH}${GENF_SUFFIX}.ffmpeg" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.ffmpeg" sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "nohup bash ${REMOTEDIR}/${REMOTE_TMPFILE}.ffmpeg > ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup 2>&1 &" rm -f "${VDPATH}${GENF_SUFFIX}.ffmpeg" TRACE_NUM=0 while [ $TRACE_NUM -lt 20 ]; do TRACE_NUM=$((TRACE_NUM+1)) tracingTranscode if [[ $? -eq 1 ]]; then echolog "已确认成功提交转码命令,现进入循环等待阶段..." break 2; fi echolog "无法确认是否完整提交转码命令,可能网络不稳定,现等待${TRACE_NUM}秒后再次检查(第${RM_COUNT}轮/第${TRACE_NUM}次)" sleep $TRACE_NUM done done if [ $RM_COUNT -ge 3 ]; then echolog "三次机会提交命令结果均失败,程序被迫中止,请自行清理垃圾文件" touch "${VDPATH}${GENF_SUFFIX}.ffmpegfail" exit fi echo "sshpass -p '${PASSWD}' ssh -l $USER -p $PORT $HOST 'tail -f -n 100 ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup'; " > "${RMLOG_SHOTCUT}" # 提供一条看远程日志的命令 # 检查转码是否完成(1、如果完成则在服务端写入标记文件*.finished; 2、下载*.finished标记文件到本地;3、当本地检测到*.finished文件时则确认转码完成) WAIT_FF=0 while true; do if [[ $WAIT_FF -lt 30 ]]; then WAIT_FF=$((WAIT_FF+1)); fi echolog "waiting for ${WAIT_FF}s ..." monitor_set '.state = "FFMPEG_ING"' "${MONITOR_FILE}" sleep $WAIT_FF # 获取远程结果文件的大小 sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "ls -sh ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv|awk '{print \$1}' > ${REMOTEDIR}/${REMOTE_TMPFILE}.output.size" sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.output.size" "${VDPATH}${GENF_SUFFIX}.output.size" > /dev/null 2>&1 if [[ -e "${VDPATH}${GENF_SUFFIX}.output.size" ]]; then echolog '进行中, 远程结果文件大小:'`cat "${VDPATH}${GENF_SUFFIX}.output.size"` fi # 检查是否完成 sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.finished" "${VDPATH}${GENF_SUFFIX}.finished" > /dev/null 2>&1 if [[ -e "${VDPATH}${GENF_SUFFIX}.finished" ]]; then monitor_set '.state = "FFMPEG_FINISH"' "${MONITOR_FILE}" break fi done # 此步远程命令,已转移至 *.ffmpeg # 取回前先改名,减少在审查方面的麻烦(这里有大问题:网络不稳定导致执行失败,进而导致后面无法下载) # sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "mv ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv ${REMOTEDIR}/${REMOTE_TMPFILE}.output" # 下载会确认完整性,并最后清理垃圾 dlPath="${VDPATH}${GENF_SUFFIX}.mkv" echolog "转码完毕,开始下载结果... ${USER}@${HOST}:${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5 => ${dlPath} " downloadResult(){ sshpass -p "${PASSWD}" rsync -avP -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.output" "${dlPath}" # 注意,多个了P参数,支持断点续传 sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5" "${VDPATH}${GENF_SUFFIX}.output.md5" } #while true; do DL_NUM=0 while [ $DL_NUM -lt 1000 ]; do DL_NUM=$((DL_NUM+1)) monitor_set '.state = "DOWNLOAD_ING"' "${MONITOR_FILE}" if [[ -e "${dlPath}" ]]; then if [[ $(cat "${VDPATH}${GENF_SUFFIX}.output.md5") = $(md5sum "${dlPath}"|awk '{print $1}') ]]; then echolog D # 确认已下载,开始清理垃圾 sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST " rm ${REMOTEDIR}/${REMOTE_TMPFILE}.input ${REMOTEDIR}/${REMOTE_TMPFILE}.output ${REMOTEDIR}/${REMOTE_TMPFILE}.input.md5 ${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5 ${REMOTEDIR}/${REMOTE_TMPFILE}.output.size -f" rm "${VDPATH}${GENF_SUFFIX}.input.md5" "${VDPATH}${GENF_SUFFIX}.output.md5" "${VDPATH}${GENF_SUFFIX}.output.size" "${VDPATH}${GENF_SUFFIX}.finished" -f echolog "取回完毕: ${dlPath}" monitor_set '.state = "DOWNLOAD_SUCCESS"' "${MONITOR_FILE}" break else downloadResult fi else downloadResult fi # @todo: 由于网络环境切换或波动,下载不一定成功,故给定一些下载的机会;每失败一次,等待时间会增加;当次数用完,则终止程序 echolog "已尝试第${DL_NUM}次下载,下次确认需等待${DL_NUM}秒..." sleep $DL_NUM done # 失败会写标志文件 if ! [[ -e "${dlPath}" ]]; then touch "${VDPATH}${GENF_SUFFIX}.downloadfail" monitor_set '.state = "DOWNLOAD_FAIL"' "${MONITOR_FILE}" fi } 2>&1 | tee -a "${LOG_FILE}" # >>>>>>>>>>>>>>> 主逻辑结束 >>>>>>>>>>>>>>> # 结束时,清理PID和日志捷径;并将日志归档 rm -f "$PID_FILE" rm -f "$LOG_SHOTCUT" rm -f "$RMLOG_SHOTCUT" mv "${LOG_FILE}" "${LOG_FILE_END}" #rm "${MONITOR_FILE}" -f # 等监控功能正常了,这一句就可以放开执行了