rh265.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. #!/data/data/com.termux/files/usr/bin/bash
  2. # 远程ffmpeg转码
  3. # @todo 断点续传失败,文件自动从0开始,需要仔细研究下rsync的相关参数
  4. # @todo 需要dashboard跟踪每个任务的文件绝对路径、日志、状态。能识别是否因网络不稳定等原因导致某个循环代码僵住无法跳出
  5. # 基础支持
  6. CUR_DIR="$(dirname "$(readlink -f "$0")")"
  7. . "${CUR_DIR}"/lib/get_abs_filename.lib
  8. . "${CUR_DIR}"/lib/echolog.lib
  9. # 参数
  10. # 接受最后一个参数,作为本地视频文件路径,转码完成后的结果文件为 [输入路径再追加"{.SERV}{.CRF}.mkv"] (此参数必须写在最尾)
  11. # -c 参数可定制 ffmpeg 的crf参数值 (可选)
  12. # -s 参数指定配置文件的简称,例如 -s mm 会指定 rh265.mm.conf (可选)
  13. ARGS=("$@")
  14. if [[ $# = 0 ]]; then less "${CUR_DIR}/readme.md"; exit; fi
  15. VDPATH=${ARGS[$(($#-1))]} # 输入文件名
  16. CRF="" # ffmpeg命令的crf残片
  17. CRF_SUFFIX="" # 本地生成文件名后缀的crf部分
  18. SERV="" # 带有服务器简称的文件名中缀残片
  19. while getopts "c:s:" optname; do
  20. case "$optname" in
  21. c)
  22. CRF="-crf ${OPTARG}"
  23. CRF_SUFFIX=".crf${OPTARG}"
  24. ;;
  25. s)
  26. SERV=".${OPTARG}"
  27. ;;
  28. *)
  29. echo 'error arg option: -${optname}.'
  30. exit
  31. ;;
  32. esac
  33. done
  34. # 参数拦截
  35. if ! [[ -e "${VDPATH}" ]]; then
  36. echo "错误:输入的文件不存在"
  37. exit
  38. fi
  39. # 根据选择的服务器,装载配置文件
  40. conf="${CUR_DIR}"/conf/rh265${SERV}.conf
  41. if ! [[ -e "$conf" ]]; then
  42. echo '配置文件 ${conf} 不存在.'
  43. exit
  44. fi
  45. . $conf
  46. # 本地生成文件统一用的完整中缀,如 ".mm.crf23",生成某文件的具体名称为 xxxxxxx.mm.crf23.finished
  47. GENF_SUFFIX=${SERV}${CRF_SUFFIX}
  48. # 生成PID标记文件,便于跟踪
  49. PID_FILE="${VDPATH}"${GENF_SUFFIX}.pid.$$
  50. touch "$PID_FILE"
  51. # 准备好本地日志文件及归档目录 @todo 改为存储json,方便共享读取
  52. LOG_FILE="${CUR_DIR}"/logs/${REMOTE_TMPFILE}-pid-$$.log
  53. LOG_FILE_END="${CUR_DIR}"/logs/end
  54. mkdir -p "${LOG_FILE_END}"
  55. echo "======== PID=$$ ========
  56. cwd: `ls -l /proc/$$/cwd`
  57. CMD: $0 $@
  58. relate pids: `ls /proc/$$/task`
  59. local: $VDPATH
  60. local(full): `lib_get_abs_filename "$VDPATH"`
  61. remote: ${REMOTEDIR}/${REMOTE_TMPFILE}.input
  62. log-ing: ${CUR_DIR}/logs/${REMOTE_TMPFILE}-pid-$$.log
  63. log-end: ${CUR_DIR}/logs/end/${REMOTE_TMPFILE}-pid-$$.log
  64. log-remote: sshpass -p '${PASSWD}' ssh -l $USER -p $PORT $HOST 'tail -f -n 100 ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup'
  65. " >> "${LOG_FILE_END}/log.map"
  66. # 生成便于查看本地日志的脚本文件
  67. LOG_SHOTCUT="${VDPATH}"${GENF_SUFFIX}.locallog.sh
  68. echo "tail -f -n 100 '${LOG_FILE}' " > "$LOG_SHOTCUT"
  69. # 生成便于查看远程日志的脚本文件(等开始转码时再写)
  70. RMLOG_SHOTCUT="${VDPATH}${GENF_SUFFIX}.remotelog.sh"
  71. # 控制错误输出
  72. # exec 2>> "`getLogPPath`"
  73. { # <<<<<<<<<<<<<<<<< 主逻辑开始 <<<<<<<<<<<<<<<<<
  74. # 上传
  75. echolog "上传中... ${VDPATH} => 远程目录${REMOTEDIR}/${REMOTE_TMPFILE}.input"
  76. rm -f "${VDPATH}${GENF_SUFFIX}.input.md5"
  77. uploadTo(){
  78. sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "mkdir -p ${REMOTEDIR}" # 创建远程目录
  79. sshpass -p "${PASSWD}" rsync -avP -e "ssh -p ${PORT}" "${VDPATH}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.input" # 上传 (注意,多个了P参数,支持断点续传)
  80. rsyncResult=$?
  81. #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
  82. # return 0 # 暂时不需要这个判断,有下方while中的input.md5有效性检测足矣
  83. #fi
  84. if [[ "0" = "${rsyncResult}" ]]; then
  85. sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "md5sum ${REMOTEDIR}/${REMOTE_TMPFILE}.input |awk '{print \$1}' > ${REMOTEDIR}/${REMOTE_TMPFILE}.input.md5" # 上传后写md5
  86. sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.input.md5" "${VDPATH}${GENF_SUFFIX}.input.md5" # 下载md5到本地用于验证
  87. fi
  88. # return 1
  89. }
  90. while true; do
  91. if [[ -e "${VDPATH}${GENF_SUFFIX}.input.md5" ]]; then
  92. if [[ $(cat "${VDPATH}${GENF_SUFFIX}.input.md5") = '' ]]; then
  93. echolog "检测到远程无效的input.md5文件,可能rsync上传被中断,现在重试)"
  94. uploadTo
  95. continue
  96. fi
  97. if [[ $(cat "${VDPATH}${GENF_SUFFIX}.input.md5") = $(md5sum "${VDPATH}"|awk '{print $1}') ]]; then
  98. echolog "已确认完整上传: ${VDPATH}"
  99. break # 确保完整上传后,才可跳出重试的循环
  100. else
  101. echolog "上传失败,现在重试..."
  102. uploadTo
  103. fi
  104. else
  105. echolog C
  106. uploadTo
  107. fi
  108. sleep 1
  109. done
  110. # 后台远程转码 @todo: 一定要确保网络不稳定时,正确完整执行(观察到的情况:在服务器准备转码时,提示input文件不存在。不知道是怎么到这一步的)
  111. # @todo: 通过网络检测ffmpeg进程和mkv文件的过程,其实是不可信的,因为会遇到网络波动的情况,造成误判,进而重复提交ffmpeg命令。在遇到确实已转出mkv文件时,会因无法答复系统的[是否覆盖文件]的提问,造成一直提交失败的假象
  112. echolog '上传完毕,开始转码...'
  113. RM_COUNT=0
  114. tracingTranscode(){ # @todo: 已有mkv文件,但是被重复提交,提示是否覆盖 mkv already exists. Overwrite ? [y/N] Not overwriting - exiting
  115. pnlist=$(sshpass -p "$PASSWD" ssh -l $USER -p $PORT $HOST "ps -ef|grep '${REMOTE_TMPFILE}'|grep ffmpeg|grep -vw grep|awk '{print \$8}'")
  116. for pn in $pnlist; do
  117. if [[ "$pn" = "ffmpeg" ]]; then
  118. return 1
  119. fi
  120. done
  121. #检测不到ffmpeg进程,可能视频文件太小,ffmpeg快速完成了,那么检测mkv文件是否存在
  122. remoteMkvFileExists=$(sshpass -p "$PASSWD" ssh -l $USER -p $PORT $HOST "if [[ -e ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv ]]; then echo 1; else echo 2; fi")
  123. if [[ 1 -eq "$remoteOutputFileExists" ]]; then
  124. return 1
  125. fi
  126. #进程和文件都找不到
  127. return 0
  128. }
  129. while [ $RM_COUNT -lt 5 ]; do
  130. RM_COUNT=$((RM_COUNT+1))
  131. echolog "第${RM_COUNT}次提交转码命令.."
  132. # 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 &"
  133. # 上面这条命令太复杂,以后可能还需要添加更多逻辑,有必要拆行,故采用传递远程脚本文件的形式
  134. echo "
  135. inputfile=${REMOTEDIR}/${REMOTE_TMPFILE}.input
  136. outputfile=${REMOTEDIR}/${REMOTE_TMPFILE}.mkv
  137. pnlist=\`ps -ef|grep \"${REMOTE_TMPFILE}\"|grep ffmpeg|grep -vw grep|awk '{print \$8}'\`
  138. for pn in \$pnlist; do
  139. if [[ \"\$pn\" = \"ffmpeg\" ]]; then
  140. echo \"已检测到ffmpeg进程,不必再提交\"
  141. exit
  142. fi
  143. done
  144. if [[ -e \"\$outputfile\" ]]; then
  145. echo \"已有mkv文件,不必重复提交\"
  146. exit
  147. fi
  148. ffmpeg -i \$inputfile -c:v libx265 -c:a copy $CRF -movflags +faststart \$outputfile
  149. ffmpegResult=\$?
  150. md5sum \$outputfile|awk \"{print \\\$1}\" > ${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5
  151. # 必须确保finished文件一定是成功转码完成后写入的
  152. if [[ -e \"\$outputfile\" ]] && [[ \"0\" -eq \"\$ffmpegResult\" ]]; then
  153. touch ${REMOTEDIR}/${REMOTE_TMPFILE}.finished
  154. fi
  155. " > "${VDPATH}${GENF_SUFFIX}.ffmpeg"
  156. sshpass -p "${PASSWD}" rsync -avP -e "ssh -p ${PORT}" "${VDPATH}${GENF_SUFFIX}.ffmpeg" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.ffmpeg"
  157. sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "nohup bash ${REMOTEDIR}/${REMOTE_TMPFILE}.ffmpeg > ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup 2>&1 &"
  158. rm -f "${VDPATH}${GENF_SUFFIX}.ffmpeg"
  159. TRACE_NUM=0
  160. while [ $TRACE_NUM -lt 20 ]; do
  161. TRACE_NUM=$((TRACE_NUM+1))
  162. tracingTranscode
  163. if [[ $? -eq 1 ]]; then
  164. echolog "已确认成功提交转码命令,现进入循环等待阶段..."
  165. break 2;
  166. fi
  167. echolog "无法确认是否完整提交转码命令,可能网络不稳定,现等待${TRACE_NUM}秒后再次检查(第${RM_COUNT}轮/第${TRACE_NUM}次)"
  168. sleep $TRACE_NUM
  169. done
  170. done
  171. if [ $RM_COUNT -ge 3 ]; then
  172. echolog "三次机会提交命令结果均失败,程序被迫中止,请自行清理垃圾文件"
  173. touch "${VDPATH}${GENF_SUFFIX}.ffmpegfail"
  174. exit
  175. fi
  176. echo "sshpass -p '${PASSWD}' ssh -l $USER -p $PORT $HOST 'tail -f -n 100 ${REMOTEDIR}/${REMOTE_TMPFILE}.nohup'; " > "${RMLOG_SHOTCUT}" # 提供一条看远程日志的命令
  177. # 检查转码是否完成(1、如果完成则在服务端写入标记文件*.finished; 2、下载*.finished标记文件到本地;3、当本地检测到*.finished文件时则确认转码完成)
  178. WAIT_FF=0
  179. while true; do
  180. if [[ $WAIT_FF -lt 30 ]]; then WAIT_FF=$((WAIT_FF+1)); fi
  181. echolog "waiting for ${WAIT_FF}s ..."
  182. sleep $WAIT_FF
  183. # 获取远程结果文件的大小
  184. sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "ls -sh ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv|awk '{print \$1}' > ${REMOTEDIR}/${REMOTE_TMPFILE}.output.size"
  185. 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
  186. if [[ -e "${VDPATH}${GENF_SUFFIX}.output.size" ]]; then
  187. echolog '进行中, 远程结果文件大小:'`cat "${VDPATH}${GENF_SUFFIX}.output.size"`
  188. fi
  189. # 检查是否完成
  190. sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.finished" "${VDPATH}${GENF_SUFFIX}.finished" > /dev/null 2>&1
  191. if [[ -e "${VDPATH}${GENF_SUFFIX}.finished" ]]; then
  192. break
  193. fi
  194. done
  195. # 取回前先改名,减少在审查方面的麻烦
  196. sshpass -p "${PASSWD}" ssh -l $USER -p $PORT $HOST "mv ${REMOTEDIR}/${REMOTE_TMPFILE}.mkv ${REMOTEDIR}/${REMOTE_TMPFILE}.output"
  197. # 下载会确认完整性,并最后清理垃圾
  198. dlPath="${VDPATH}${GENF_SUFFIX}.mkv"
  199. echolog "转码完毕,开始下载结果... ${USER}@${HOST}:${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5 => ${dlPath} "
  200. downloadResult(){
  201. sshpass -p "${PASSWD}" rsync -avP -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.output" "${dlPath}" # 注意,多个了P参数,支持断点续传
  202. sshpass -p "${PASSWD}" rsync -av -e "ssh -p ${PORT}" ${USER}@${HOST}:"${REMOTEDIR}/${REMOTE_TMPFILE}.output.md5" "${VDPATH}${GENF_SUFFIX}.output.md5"
  203. }
  204. #while true; do
  205. DL_NUM=0
  206. while [ $DL_NUM -lt 1000 ]; do
  207. DL_NUM=$((DL_NUM+1))
  208. if [[ -e "${dlPath}" ]]; then
  209. if [[ $(cat "${VDPATH}${GENF_SUFFIX}.output.md5") = $(md5sum "${dlPath}"|awk '{print $1}') ]]; then
  210. echolog D
  211. # 确认已下载,开始清理垃圾
  212. 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"
  213. rm "${VDPATH}${GENF_SUFFIX}.input.md5" "${VDPATH}${GENF_SUFFIX}.output.md5" "${VDPATH}${GENF_SUFFIX}.output.size" "${VDPATH}${GENF_SUFFIX}.finished" -f
  214. echolog "取回完毕: ${dlPath}"
  215. break
  216. else
  217. downloadResult
  218. fi
  219. else
  220. downloadResult
  221. fi
  222. # @todo: 由于网络环境切换或波动,下载不一定成功,故给定一些下载的机会;每失败一次,等待时间会增加;当次数用完,则终止程序
  223. echolog "已尝试第${DL_NUM}次下载,下次确认需等待${DL_NUM}秒..."
  224. sleep $DL_NUM
  225. done
  226. # 失败会写标志文件
  227. if ! [[ -e "${dlPath}" ]]; then
  228. touch "${VDPATH}${GENF_SUFFIX}.downloadfail"
  229. fi
  230. } 2>&1 | tee -a "${LOG_FILE}" # >>>>>>>>>>>>>>> 主逻辑结束 >>>>>>>>>>>>>>>
  231. # 结束时,清理PID和日志捷径;并将日志归档
  232. rm -f "$PID_FILE"
  233. rm -f "$LOG_SHOTCUT"
  234. rm -f "$RMLOG_SHOTCUT"
  235. mv "${LOG_FILE}" "${LOG_FILE_END}"