status.sh 17 KB

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