; HLS_Batch_Converter_Multi.au3 ; 支持最多 8 个并发转码 + 实时显示 ffmpeg 日志 ; ✅ 新增:递归处理子目录中的视频文件 #include #include #include #include #include ; ========== 配置 ========== Global Const $MAX_CONCURRENT = 8 ; ========== 全局变量 ========== Global $g_sSourceDir = "" Global $g_hLogEdit, $g_hBtnStart, $g_hBtnSelect, $g_hLabelStatus ; 任务队列和运行中任务 Global $g_aPendingFiles[0] ; 待处理文件列表 (完整路径) Global $g_aRunningTasks[0][4] ; [PID, FileName, OutputDir, LogHandle] Global $g_iSuccess = 0 Global $g_bConverting = False ; ========== 创建主窗口 ========== GUICreate("HLS 批量转码工具 - 多线程版", 700, 550) GUICtrlCreateLabel("1. 请选择包含视频文件的源目录:", 10, 10, 400, 20) $g_hBtnSelect = GUICtrlCreateButton("选择目录...", 10, 35, 100, 30) $g_hLabelStatus = GUICtrlCreateLabel("", 120, 40, 570, 20) GUICtrlSetColor(-1, 0x0000FF) GUICtrlCreateLabel("2. 点击“开始转码”(最多 " & $MAX_CONCURRENT & " 个并发):", 10, 75, 400, 20) $g_hBtnStart = GUICtrlCreateButton("开始转码", 10, 100, 100, 30) GUICtrlSetState($g_hBtnStart, $GUI_DISABLE) GUICtrlCreateLabel("实时日志(格式: [视频名] 日志内容):", 10, 140, 300, 20) $g_hLogEdit = GUICtrlCreateEdit("", 10, 165, 680, 370, BitOR($ES_AUTOVSCROLL, $ES_READONLY, $WS_VSCROLL)) GUICtrlSetFont($g_hLogEdit, 9, 400, 0, "Consolas") GUISetState(@SW_SHOW) ; ========== 消息循环 ========== While 1 Switch GUIGetMsg() Case $GUI_EVENT_CLOSE ; 清理子进程 _KillAllRunningTasks() Exit Case $g_hBtnSelect _SelectSourceDir() Case $g_hBtnStart _StartConversion() EndSwitch ; 如果正在转码,定期检查任务状态(模拟异步) If $g_bConverting Then _PollRunningTasks() Sleep(50) ; 减轻 CPU 占用 EndIf WEnd ; ========== 函数定义 ========== Func _SelectSourceDir() Local $sDir = FileSelectFolder("请选择视频所在的源目录", "") If Not @error And $sDir <> "" Then $g_sSourceDir = $sDir GUICtrlSetData($g_hLabelStatus, "已选择: " & $g_sSourceDir) GUICtrlSetState($g_hBtnStart, $GUI_ENABLE) _Log("✅ 目录已选择:" & $g_sSourceDir) Else $g_sSourceDir = "" GUICtrlSetData($g_hLabelStatus, "未选择目录") GUICtrlSetState($g_hBtnStart, $GUI_DISABLE) EndIf EndFunc Func _StartConversion() If $g_sSourceDir = "" Or $g_bConverting Then Return $g_bConverting = True GUICtrlSetState($g_hBtnStart, $GUI_DISABLE) GUICtrlSetState($g_hBtnSelect, $GUI_DISABLE) _Log(@CRLF & "🚀 开始批量转码(最多 " & $MAX_CONCURRENT & " 个并发)..." & @CRLF) ; 初始化 $g_iSuccess = 0 Dim $g_aPendingFiles[0] Dim $g_aRunningTasks[0][4] ; 递归获取所有子目录中的视频文件(返回完整路径) Local $aFiles = _FileListToArrayRec($g_sSourceDir, "*.mp4;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.mpeg;*.mpg;*.webm", $FLTAR_FILES, $FLTAR_RECUR, $FLTAR_SORT, $FLTAR_FULLPATH) If @error Then _Log("❌ 目录中没有找到任何支持的视频文件。") _FinishConversion() Return EndIf ; 填充待处理队列($aFiles[1] 到 $aFiles[$aFiles[0]] 是完整路径) For $i = 1 To $aFiles[0] _ArrayAdd($g_aPendingFiles, $aFiles[$i]) Next ; 启动第一批任务 _LaunchNextTasks() EndFunc Func _LaunchNextTasks() While UBound($g_aRunningTasks, 1) < $MAX_CONCURRENT And UBound($g_aPendingFiles) > 0 Local $sFilePath = $g_aPendingFiles[0] _ArrayDelete($g_aPendingFiles, 0) ; 提取文件名(不含路径和扩展名) Local $sFileName = StringRegExpReplace($sFilePath, "^.*\\", "") ; 如 "video.mp4" Local $sBaseName = StringRegExpReplace($sFileName, "\.[^.\\]+$", "") ; 如 "video" ; 获取视频所在目录(关键!) Local $sVideoDir = StringLeft($sFilePath, StringInStr($sFilePath, "\", 0, -1) - 1) ; 输出目录 = 视频所在目录 \ 视频名(无扩展名) Local $sOutputDir = $sVideoDir & "\" & $sBaseName If DirCreate($sOutputDir) = 0 Then _Log("[系统] ⚠️ 无法创建目录: " & $sOutputDir) ContinueLoop EndIf Local $sM3U8 = $sOutputDir & "\playlist.m3u8" Local $sTS = $sOutputDir & "\segment_%05d.ts" Local $sCmd = 'ffmpeg -i "' & $sFilePath & '" ' & _ '-c:v libx264 -crf 23 -preset fast -profile:v baseline -level 3.0 ' & _ '-c:a aac -b:a 128k ' & _ '-hls_time 2 ' & _ '-hls_playlist_type vod ' & _ '-hls_segment_filename "' & $sTS & '" ' & _ '"' & $sM3U8 & '"' ; 启动进程并捕获合并输出 Local $iPID = Run($sCmd, "", @SW_HIDE, $STDERR_MERGED) If @error Then _Log("[" & $sBaseName & "] ❌ 启动失败") ContinueLoop EndIf ; 记录任务 Local $iIndex = UBound($g_aRunningTasks, 1) ReDim $g_aRunningTasks[$iIndex + 1][4] $g_aRunningTasks[$iIndex][0] = $iPID $g_aRunningTasks[$iIndex][1] = $sBaseName $g_aRunningTasks[$iIndex][2] = $sOutputDir $g_aRunningTasks[$iIndex][3] = "" _Log("[" & $sBaseName & "] 🔄 开始转码...") WEnd EndFunc Func _PollRunningTasks() If UBound($g_aRunningTasks, 1) = 0 Then If UBound($g_aPendingFiles) = 0 Then _FinishConversion() EndIf Return EndIf Local $aToRemove[0] For $i = 0 To UBound($g_aRunningTasks, 1) - 1 Local $iPID = $g_aRunningTasks[$i][0] Local $sName = $g_aRunningTasks[$i][1] ; 读取新日志 Local $sNewOutput = StdoutRead($iPID) If $sNewOutput <> "" Then Local $aLines = StringSplit(StringStripCR($sNewOutput), @LF, $STR_NOCOUNT) For $j = 0 To UBound($aLines) - 1 If $aLines[$j] <> "" Then _Log("[" & $sName & "] " & $aLines[$j]) EndIf Next EndIf ; 检查是否结束 If Not ProcessExists($iPID) Then Local $iExitCode = @extended If $iExitCode = 0 Then $g_iSuccess += 1 _Log("[" & $sName & "] ✅ 成功完成") Else _Log("[" & $sName & "] ❌ 失败 (退出码: " & $iExitCode & ")") EndIf _ArrayAdd($aToRemove, $i) EndIf Next ; 移除已完成的任务(从后往前删) For $i = UBound($aToRemove) - 1 To 0 Step -1 Local $iIndex = $aToRemove[$i] _ArrayDelete($g_aRunningTasks, $iIndex) Next ; 启动新任务填补空缺 If UBound($aToRemove) > 0 Then _LaunchNextTasks() EndIf EndFunc Func _FinishConversion() $g_bConverting = False _Log(@CRLF & "🎉 所有转码任务完成!成功处理 " & $g_iSuccess & " 个视频。") GUICtrlSetState($g_hBtnStart, $GUI_ENABLE) GUICtrlSetState($g_hBtnSelect, $GUI_ENABLE) EndFunc Func _KillAllRunningTasks() If Not IsArray($g_aRunningTasks) Then Return For $i = 0 To UBound($g_aRunningTasks, 1) - 1 ProcessClose($g_aRunningTasks[$i][0]) Next EndFunc Func _Log($sText) Local $sCurrent = GUICtrlRead($g_hLogEdit) Local $sNew = $sCurrent & $sText & @CRLF If StringLen($sNew) > 2000 Then $sNew = StringRight($sNew, 2000) EndIf GUICtrlSetData($g_hLogEdit, $sNew) GUICtrlSendMsg($g_hLogEdit, $EM_LINESCROLL, 0, GUICtrlSendMsg($g_hLogEdit, $EM_GETLINECOUNT, 0, 0)) EndFunc