用于执行完整性/损坏检测的mpv PS1脚本在当前视频卡住时停止
我有大量视频文件,需要一种深入分析它们是否损坏的方法,因此我写了一个脚本来完成这件事,但我发现了一组特别损坏的文件,它们让我不得不不断手动关闭窗口。
有时非常损坏的文件会让mpv直接卡住,阻止其余文件被“分析”,直到用户输入,也就是手动关闭mpv的终端。为防止这种情况,我试图从它的终端读取mpv的时间线,如果在合理的时间内它停止,就结束进程让下一个文件播放。
这是当前的脚本:
$root = Get-Location # Scan video files in the same folder
$logDir = Join-Path $PSScriptRoot "mpv_logs" # Folder of logs for each video
$summaryLogFile = "log_short.txt"
if (-not (Test-Path $logDir)) {
New-Item $logDir -ItemType Directory | Out-Null
}
# Formats
$extensions = '*.mp4','*.mkv','*.avi','*.mov','*.webm','*.flv'
# Get files recursively
$playedVideos = @()
$videoFiles = Get-ChildItem $root -Recurse -File -Include $extensions
# Scan
foreach ($file in $videoFiles) {
Write-Host "Checking: $($file.FullName)"
# Unneccessary block perhaps?
$safeName = [IO.Path]::GetFileNameWithoutExtension($file.Name) `
-replace '[^\w\-\.]', '_'
if ($safeName.Length -gt 120) {
$safeName = $safeName.Substring(0,120)
}
$logFile = Join-Path $logDir "$safeName`_mpv_log.txt"
# Delete old logs
if (Test-Path $logFile) {
Remove-Item $logFile -Force
}
# MPV
$args = @(
'--no-config'
"--log-file=""$logFile"""
'--msg-level=all=debug'
'--vo=null'
'--ao=null'
'--stop-screensaver=yes'
"""$($file.FullName)"""
)
$process = Start-Process mpv `
-ArgumentList $args `
-Wait `
-PassThru
#Summary of scanned file results so I don't need to read them
$retries = 15
while (-not (Test-Path $logFile) -and $retries-- -gt 0) {
Start-Sleep -Milliseconds 200
}
if (-not (Test-Path $logFile)) {
$playedVideos += "$($file.Name)`r`n*_*_Log file not found_*_*"
continue
}
$logContent = Get-Content -Path $logFile -ErrorAction SilentlyContinue
$startMatch = $logContent | Select-String "Starting playback" | Select-Object -First 1
if (-not $startMatch) {
$playedVideos += "$($file.Name)`r`n*_*_Playback did not start_*_*"
continue
}
$startIndex = $startMatch.LineNumber
$postPlayback = $logContent[$startIndex..($logContent.Count-1)]
$errorFound = $postPlayback -match `
"(
error while decoding|
Invalid data found when processing input|
moov atom not found|
demuxer error|
packet corrupt|
Could not find codec parameters|
Cannot open file|
Error opening file|
File is invalid or corrupted|
Unable to read the file|
File access error|
Error while reading the file|
Unsupported codec|
Decoder not found|
Failed to initialize codec|
Unable to decode stream|
HTTP error|
Connection failed|
Failed to open stream|
Network error|
Timeout error|
Invalid URL|
Audio/video sync issue|
Dropping video frame|
Audio buffering error|
Video output error|
Cannot seek to position|
Duration mismatch|
Seek failed|
Failed to decode video|
Failed to decode audio|
Out of memory|
Memory allocation failed|
Unable to allocate buffer|
Failed to allocate memory for stream|
Demuxer not found|
Stream not found|
Invalid stream|
Corrupted container|
Failed to demux stream|
Failed to initialize demuxer|
Failed to start playback|
Playback failed|
Could not find file)"
if ($errorFound -or $process.ExitCode -ne 0) {
$playedVideos += "$($file.Name)`r`n*_*_This file is corrupt_*_*"
}
else {
$playedVideos += "$($file.Name)`r`n*_*_This file is correct_*_*"
}
}
# Verbose to let me know it's finished
$playedVideos | Out-File -FilePath $summaryLogFile -Encoding UTF8
Write-Host "====================================="
Write-Host "Summary log created at: $summaryLogFile"
Write-Host "MPV logs stored in: $logDir"
Write-Host "====================================="
这是我尝试实现这个的版本,目前这是修订版(缺少注释):
$root = Get-Location
$txtFiles = Get-ChildItem $root -Recurse -File -Filter "*.txt"
foreach ($oldLogFile in $txtFiles) {
Remove-Item $oldLogFile.FullName -Force
}
$extensions = '*.mp4','*.mkv','*.avi','*.mov','*.webm','*.flv'
$videoFiles = Get-ChildItem $root -Recurse -File -Include $extensions
$summaryLogFile = "log_short.txt"
$logDir = Join-Path $PSScriptRoot "mpv_logs"
if (-not (Test-Path $logDir)) {
New-Item $logDir -ItemType Directory | Out-Null
}
$playedVideos = @()
foreach ($file in $videoFiles) {
$baseName = $file.BaseName
$logFileName = "$baseName" + "_log.txt"
$logFile = Join-Path $logDir $logFileName
Write-Host "Checking: $baseName"
Write-Host "Created: $logFile"
$command = "mpv --msg-level=all=debug --osd-status-msg=""AV: %p / %L (%B) A-V: %a"" --no-config --vo=null --ao=null --stop-screensaver=yes `"$($file.FullName)`" | tee -a `"$logFile`""
Invoke-Expression $command
$retries = 15
while (-not (Test-Path $logFile) -and $retries-- -gt 0) {
Start-Sleep -Milliseconds 200
}
if (-not (Test-Path $logFile)) {
$playedVideos += "$($file.Name)`r`n*_*_Log file not found_*_*"
continue
}
$logContent = Get-Content -Path $logFile -ErrorAction SilentlyContinue
$startMatch = $logContent | Select-String "Starting playback" | Select-Object -First 1
if (-not $startMatch) {
$playedVideos += "$($file.Name)`r`n*_*_Playback did not start_*_*"
continue
}
$startIndex = $startMatch.LineNumber
$postPlayback = $logContent[$startIndex..($logContent.Count-1)]
$errorFound = $postPlayback -match `
"(
error while decoding|
Invalid data found when processing input|
moov atom not found|
demuxer error|
packet corrupt|
Could not find codec parameters|
Cannot open file|
Error opening file|
File is invalid or corrupted|
Unable to read the file|
File access error|
Error while reading the file|
Unsupported codec|
Decoder not found|
Failed to initialize codec|
Unable to decode stream|
HTTP error|
Connection failed|
Failed to open stream|
Network error|
Timeout error|
Invalid URL|
Audio/video sync issue|
Dropping video frame|
Audio buffering error|
Video output error|
Cannot seek to position|
Duration mismatch|
Seek failed|
Failed to decode video|
Failed to decode audio|
Out of memory|
Memory allocation failed|
Unable to allocate buffer|
Failed to allocate memory for stream|
Demuxer not found|
Stream not found|
Invalid stream|
Corrupted container|
Failed to demux stream|
Failed to initialize demuxer|
Failed to start playback|
Playback failed|
Could not find file)"
if ($errorFound) {
$playedVideos += "$($file.Name)`r`n*_*_This file is corrupt_*_*"
} else {
$playedVideos += "$($file.Name)`r`n*_*_This file is correct_*_*"
}
}
$playedVideos | Out-File -FilePath $summaryLogFile -Encoding UTF8
Write-Host "Summary log saved to: $summaryLogFile"
到目前为止,仍然缺少以下几点:
- 以某种方式在mpv播放该文件的同时,实时读取上述日志,以检查它是否仍在继续……
- …以及如果在某个时间/刻度没有继续,则结束当前mpv进程让下一个播放
我目前唯一的想法是每30秒获取一次最后修改日期,如果自上次以来没有变化,就把它关掉。以下是更新后的区块
```$baseName = $file.BaseName
$logFileName = "$baseName" + "_log.txt"
$logFile = Join-Path $logDir $logFileName
Write-Host "Checking: $baseName"
New-Item -Path $logFile -ItemType File | Out-Null
Write-Host "Created: $logFile"
$currentProgress = (Get-Item $logFile).LastWriteTime
Write-Host "Starting Progress verification..."
$command = "mpv --msg-level=all=debug --osd-status-msg=""AV: %p / %L (%B) A-V: %a"" --no-config --vo=null --ao=null --stop-screensaver=yes `"$($file.FullName)`" | tee -a `"$logFile`""
$job = Start-Job -ScriptBlock {
while ($true) {
Start-Sleep -Seconds 30
if ($global:lastProgress -eq $global:currentProgress) {
Write-Host "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! DEBUG: $global:lastProgress and $global:currentProgress are exactly the same !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" -ForegroundColor Red
} else {
$global:lastProgress = $global:currentProgress
$global:currentProgress = (Get-Item $global:logFile).LastWriteTime
Write-Host "Progress Updated: $global:currentProgress"
}
}
}´´´
Third Edit, after removing wait, currently has some redundancy and It's labeling the mpv command itself as corruption but I'm pretty much there:
$root = Get-Location
$txtFiles = Get-ChildItem $root -Recurse -File -Filter "*.txt"
foreach ($oldLogFile in $txtFiles) {
Remove-Item $oldLogFile.FullName -Force
}
$extensions = '*.mp4','*.mkv','*.avi','*.mov','*.webm','*.flv'
$videoFiles = Get-ChildItem $root -Recurse -File -Include $extensions
$summaryLogFile = "log_short.txt"
$logDir = Join-Path $PSScriptRoot "mpv_logs"
if (-not (Test-Path $logDir)) {
New-Item $logDir -ItemType Directory | Out-Null
}
$playedVideos = @()
foreach ($file in $videoFiles) {
$baseName = $file.BaseName
$logFileName = "$baseName" + "_log.txt"
$logFile = Join-Path $logDir $logFileName
Write-Host "Checking: $baseName"
New-Item -Path $logFile -ItemType File | Out-Null
Write-Host "Created: $logFile"
$command = "mpv --msg-level=all=debug --osd-status-msg=""AV: %p / %L (%B) A-V: %a"" --no-config --vo=null --ao=null --stop-screensaver=yes `"$($file.FullName)`" | tee -a `"$logFile`""
$job = Start-Job -ScriptBlock {
param($logFile)
$lastProgress = $null
$mpvProc = Get-Process | Where-Object { $_.Path -like "*mpv*" } | Sort-Object StartTime -Descending | Select-Object -First 1
while ($true) {
Start-Sleep -Seconds 30
if ($lastProgress -eq (Get-Item $global:logFile).LastWriteTime ) {
if ($mpvProc) {
Stop-Process -Id $mpvProc.Id -Force
}
break
}
else {
$lastProgress = (Get-Item $global:logFile).LastWriteTime
}
}
} -ArgumentList $logFile
Invoke-Expression $command
$retries = 15
while (-not (Test-Path $logFile) -and $retries-- -gt 0) {
Start-Sleep -Milliseconds 200
}
if (-not (Test-Path $logFile)) {
$playedVideos += "$($file.Name)`r`n*_*_Log file not found_*_*"
continue
}
$logContent = Get-Content -Path $logFile -ErrorAction SilentlyContinue
$startMatch = $logContent | Select-String "Starting playback" | Select-Object -First 1
if (-not $startMatch) {
$playedVideos += "$($file.Name)`r`n*_*_Playback did not start_*_*"
continue
}
$startIndex = $startMatch.LineNumber
$postPlayback = $logContent[$startIndex..($logContent.Count-1)]
$errorPattern = @"
error while decoding|
Invalid data found when processing input|
moov atom not found|
demuxer error|
packet corrupt|
Could not find codec parameters|
Cannot open file|
Error opening file|
File is invalid or corrupted|
Unable to read the file|
File access error|
Error while reading the file|
Unsupported codec|
Decoder not found|
Failed to initialize codec|
Unable to decode stream|
HTTP error|
Connection failed|
Failed to open stream|
Network error|
Timeout error|
Invalid URL|
Audio/video sync issue|
Dropping video frame|
Audio buffering error|
Video output error|
Cannot seek to position|
Duration mismatch|
Seek failed|
Failed to decode video|
Failed to decode audio|
Out of memory|
Memory allocation failed|
Unable to allocate buffer|
Failed to allocate memory for stream|
Demuxer not found|
Stream not found|
Invalid stream|
Corrupted container|
Failed to demux stream|
Failed to initialize demuxer|
Failed to start playback|
Playback failed|
Could not find file|
Failed to open file|
[Ff]ile not found|
Invalid file format|
Invalid video stream|
[Cc]orrupt video|
"@
Write-Host "Checking file: $($file.Name)"
if (-not (Test-Path $logFile)) {
Write-Host "Log file not found: $logFile"
$playedVideos += "$($file.Name)`r`n*_*_Log file not found_*_*"
continue
}
$logContent = Get-Content -Path $logFile -ErrorAction SilentlyContinue
$X = 20
Write-Host "Log Content Preview (first $X lines):"
$logContent | Select-Object -First $X | ForEach-Object { Write-Host $_ }
$startMatch = $logContent | Select-String "Starting playback" | Select-Object -First 1
if (-not $startMatch) {
Write-Host "Playback did not start in the log for: $($file.Name)"
$playedVideos += "$($file.Name)`r`n*_*_Playback did not start_*_*"
continue
}
$startIndex = $startMatch.LineNumber
$postPlayback = $logContent[$startIndex..($logContent.Count-1)]
Write-Host "Checking for known error patterns in the log..."
$errorFound = $false
$firstErrorLine = ""
foreach ($line in $postPlayback) {
if ($line -match $errorPattern) {
$firstErrorLine = $line
$errorFound = $true
break
}
}
if ($errorFound) {
Write-Host "Error detected in the log after playback started, marking file as corrupt."
Write-Host "First line triggering error label: $firstErrorLine"
$playedVideos += "$($file.Name)`r`n*_*_This file is corrupt_*_*"
} else {
Write-Host "No error detected, file is considered correct: $($file.Name)"
$playedVideos += "$($file.Name)`r`n*_*_This file is correct_*_*"
}
}
$playedVideos | Out-File -FilePath $summaryLogFile -Encoding UTF8
Write-Host "Summary log saved to: $summaryLogFile"
解决方案
This answer by itself will likely not provide a final solution to your problem, but should serve as guidance while you test.
First, let me point out a big issue with your regex pattern, if you are going to use multiple new-lines with white space to separate each pattern in your capturing group, then you will need to use the (?x) option otherwise your pattern will most likely never match.
Simplified example:
'moov atom not found' -match
'(
error while decoding|
Invalid data found when processing input|
moov atom not found|
demuxer error|
packet corrupt)'
# Outputs: False
However when using the (?x) option you also need to consider the following (from MS .NET docs):
Unescaped white space in the regular expression pattern is ignored. To be part of a regular expression pattern, white-space characters must be escaped (for example, as
\sor\).
Meaning that you need to do this:
'moov atom not found' -match
'(?x)(
error\ while\ decoding|
Invalid\ data\ found\ when\ processing\ input|
moov\ atom\ not\ found|
demuxer\ error|
packet\ corrupt)'
# Outputs: True
And this will be tedious task, so in this case it's much easier to dynamically construct your pattern using and array and -join:
$pattern = @(
'error while decoding'
'Invalid data found when processing input'
'moov atom not found'
'demuxer error'
'packet corrupt') -join '|'
'moov atom not found' -match $pattern
# Outputs: True
Then, focusing on your 2 questions, as stated in comments, I think your first snippet is the one that made more sense as long as you remove -Wait... This way your mpv process can run concurrently with your script. The main issue is that you will need to read the log file over and over which can can be expensive but there is no easy workaround, if you wanted to tail the file there must be something we can use as a stop signal (e.g.: the log file content always ends with a specific sentence) otherwise we will create an infinite loop that will require human intervention to stop it (e.g.: via CTRL+C). If you're fine with reading the file over and over, here is how the code inside your loop should look (as simplified as possible):
```bash
the code before this loop can remain the same, however as recommendation,
define the regex pattern here using the -join approach shown before:
$pattern = @( 'error while decoding' 'Invalid data found when processing input' 'moov atom not found' 'demuxer error' 'packet corrupt' # etc... more should be added here ) -join '|'
we also need to define a timespan to address one of your questions:
>> ...and in case it's not for a X amount of time/ticks, end current mpv process to let the next one play
i'm not sure about how long you want to wait, change this accordingly, this class also has different From*
helper methods for you to use, e.g. FromSeconds, etc..
$timeout = [timespan]::FromMinutes(10)
$playedVideos = foreach ($file in $videoFiles) { # the code before this can remain the same... # ...
# notice we remove `-Wait` here, we don't want powershell to block
$startProcessSplat = @{
PassThru = $true
FilePath = 'mpv'
ArgumentList = @(
'--no-config'
"--log-file=""$logFile"""
'--msg-level=all=debug'
'--vo=null'
'--ao=null'
'--stop-screensaver=yes'
"""$($file.FullName)""")
}
$process = Start-Process @startProcessSplat
# this is a fine way to wait for the log file to be created
$retries = 15
while (-not (Test-Path $logFile) -and $retries-- -gt 0) {
Start-Sleep -Milliseconds 200
}
# here we remove all instances of `$playedVideos +=`, instead we can assign the
# loop expression directly to a variable: `$playedVideos = foreach (....) { ... }`
if (-not (Test-Path $logFile)) {
"$($file.Name)`r`n*_*_Log file not found_*_*"
# before continuing, we should kill the process here
$process.Kill()
continue
}
# in here we know the process started correctly and the log file exist, we can start
# reading it over and over until:
# - either the process ends
# - the timeout is reached
# - the regex pattern is matched
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# we will use this flag to determine if we need to `continue` after the `while` loop
# we could also use a loop label instead... less known up to you
$matchedPattern = $false
# NOTE: also use `-Raw` to get the content, enabling the cmdlet to read all content
# as a single multi-line string. this is mainly for efficiency...
while (-not $process.HasExited) {
# if timeout is reached
if ($stopwatch.Elapsed -gt $timeout) {
"$($file.Name)`r`n*_*_Timeout reached_*_*"
# kill the process
$process.Kill()
# `break` out this `while` loop
break
}
$content = Get-Content $logFile -Raw
# if the file is corrupt because it matched the regex pattern
if ($matchedPattern = $content -match $pattern) {
"$($file.Name)`r`n*_*_This file is corrupt_*_*"
# kill the process
$process.Kill()
# `break` out this `while` loop
break
}
Start-Sleep 1
}
# if the loop ended due to pattern matching
# we don't need to do anything else here, go next
if ($matchedPattern) {
continue
}
# in this instance, we know that the `MPV` process ended and
# the pattern was NOT matched so we just need to determine if it ended OK or not
if ($process.ExitCode -ne 0) {
"$($file.Name)`r`n*_*_This file is corrupt_*_*"
continue
}
"$($file.Name)`r`n*_*_This file is correct_*_*"
}