通过解析 ffprobe 输出以 VB 形式填充字段值

Populate field values in a VB form by parsing ffprobe output

好的。我正在尝试从视频文件中获取相关的媒体信息。为此,我从我的脚本中 运行 ffprobe。像这样:

Shell("cmd /c [ffpath]\ffprobe.exe -probesize 1000000 -hide_banner -i ""[path]\[video].mp4"" >""[path]\[video]_probe.log"" 2>&1")

创建一个包含如下内容的文件:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '[path]\[video].mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf57.8.102
  Duration: 00:04:34.41, start: 0.033333, bitrate: 957 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 720x480 [SAR 32:27 DAR 16:9], 820 kb/s, 29.97 fps, 29.97 tbr, 30k tbn, 59.94 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      handler_name    : SoundHandler

我说,"something like this," 因为 Stream #0:0(und): 行中的数据并不总是以相同的顺序排列。我需要从日志中获取以下信息:

在批处理脚本中,我会使用标记和定界符来扫描我的信息,并检查我是否抓取了正确的数据;如果不是,请尝试下一个令牌。

这是我如何在批处理脚本中执行任务的示例:

REM Find frame rate and resolution
set count=0
for /f "tokens=1-18* delims=," %%a in (input.log) do (
    REM we only need to parse the first line
    if !count!==0 (
        set fps=%%e
        echo !fps! >res.tmp
        findstr "fps" res.tmp >nul
        if not !errorlevel!==0 (
            set fps=%%f
        )
        set resolution=%%c
        echo !resolution! >res.tmp
        findstr "x" res.tmp >nul
        rem echo errorlevel = !errorlevel!
        if not !errorlevel!==0 (
            set resolution=%%d
        )
        del null
        set fps=!fps:~1,-4!
    )
    set /A count+=1
)

如何在 VB 中执行此操作?我正在使用 Visual Studio Express 2015 桌面版。

好的,在进行了更多的挖掘和游戏之后,我是这样完成任务的:

 Sub main()
        Dim strDuration As String
        Dim strCodec As String
        Dim strRes As String
        Dim lngWidth, lngHeight As Long
        Dim strAudCodec As String
        Dim dblFPS As Double
        Dim audFreq As Double
        Dim audQual As Double
        Using logReader As New Microsoft.VisualBasic.FileIO.TextFieldParser("..\..\Sirach and Matthew003_probe.log")
            logReader.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited
            logReader.SetDelimiters(",")
            Dim curRow As String()
            While Not logReader.EndOfData
                Try
                    curRow = logReader.ReadFields()
                    'look for and assign duration
                    If Mid(curRow(0), 1, 8) = "Duration" Then
                        strDuration = Mid(curRow(0), 11, 11)
                    End If
                    'look for the video stream row
                    If Mid(curRow(0), 19, 6) = "Video:" Then
                        'Assign the video codec
                        strCodec = Mid(curRow(0), 26, Len(curRow(0)))
                        strCodec = Mid(strCodec, 1, InStr(1, strCodec, " ", CompareMethod.Text) - 1)
                        'Look in each field of current row
                        For i = 0 To 10 Step 1
                            'look for the field containing the resolution ("x" should be the 4th or 5th character)
                            If InStr(1, curRow(i), "x", CompareMethod.Text) = 4 Or InStr(1, curRow(i), "x", CompareMethod.Text) = 5 Then
                                'Assign resolution
                                strRes = Mid(curRow(i), 1, InStr(1, curRow(i), " ", CompareMethod.Text))
                                'Assign Width
                                lngWidth = Mid(strRes, 1, InStr(1, strRes, "x", CompareMethod.Text) - 1)
                                'Assign Heigh
                                lngHeight = Mid(strRes, InStr(1, strRes, "x", CompareMethod.Text) + 1, Len(strRes))
                            End If
                            'loof for fps suffix
                            If Mid(curRow(i), Len(curRow(i)) - 2, 3) = "fps" Then
                                'Assign frame rate
                                dblFPS = Mid(curRow(i), 1, Len(curRow(i)) - 4)
                            End If
                        Next i
                    End If
                    'Look for the audio stream row
                    If Mid(curRow(0), 19, 6) = "Audio:" Then
                        'Assign the audio codec
                        strAudCodec = Mid(curRow(0), 26, Len(curRow(0)))
                        strAudCodec = Mid(strAudCodec, 1, InStr(1, strAudCodec, " ", CompareMethod.Text) - 1)
                        For i = 0 To 10 Step 1
                            'look for the field containing the audio sampling frequency
                            If InStr(1, curRow(i), "Hz", CompareMethod.Text) Then
                                'Assign Audio Sampling Frequency
                                audFreq = Mid(curRow(i), 1, InStr(1, curRow(i), " ", CompareMethod.Text) - 1)
                            End If
                            'look for the field containing the audio quality
                            If InStr(1, curRow(i), "kb/s", CompareMethod.Text) Then
                                'assign audio quality
                                audQual = Mid(curRow(i), 1, InStr(1, curRow(i), " ", CompareMethod.Text) - 1)
                            End If
                        Next
                    End If
                Catch ex As Exception
                End Try
            End While
        End Using
        Dim strMsg As String
        strMsg = "Duration: " & strDuration & Chr(13) _
            & "Codec: " & strCodec & Chr(13) _
            & "Resolution: " & strRes & Chr(13) _
            & "Width: " & lngWidth & Chr(13) _
            & "Height: " & lngHeight & Chr(13) _
            & "Frame Rate: " & dblFPS & " fps" & Chr(13) _
            & "Audio Codec: " & strAudCodec & Chr(13) _
            & "Audio Sampling Freq: " & audFreq & " Hz" & Chr(13) _
            & "Audio Quality: " & audQual & " kb/s" & Chr(13)
        MsgBox(strMsg)
    End Sub

产生结果:

我现在需要做的就是使用表单代码中的变量填充我的表单字段:Me.[field].text = [variable] 应该可以开始了。

如您所见,我利用 curRow() 数组使用 mid()InStr() 函数缩小了字段的范围。虽然它似乎已经完成了任务,但我不确定这是不是最好的方法,或者是否还有其他方法。

如果您有任何改进建议,请告诉我。

Please let me know if you have any suggestions for improvement.

有一种比使用旧的 Shell 命令更直接的方式来获取信息,并且有一种稍微更有条理的方式来访问信息。下面将直接从标准输出中读取 ffprobe 的结果。为此,请使用闪亮的新网络 Process class 而不是旧版 Shell.

FFProbe 还支持 json 输出(我认为 xml),这将允许您查询 JObject 以通过 "name" 取回参数。命令行参数是 -print_format json.

以下将获得与您显示的相同的属性,但会将它们放在漂亮的 ListView 而不是 MsgBox 中。由于编码的原因,参考文献可能有点长,但我将展示如何缩短它们。最重要的是,没有涉及到更直接的字符串解析。为此,您需要 Newtonsoft's JSON.NET

Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
'...
' the part to run ffprobe and read the result to a string:

Dim Quote = Convert.ToChar(34)
Dim json As String
Using p As New Process
    p.StartInfo.FileName = "...ffprobe.exe"  ' use your path
    p.StartInfo.Arguments = String.Format(" -v quiet -print_format json -show_streams {0}{1}{0}",
                                          Quote, theFileName)
    p.StartInfo.RedirectStandardOutput = True
    p.StartInfo.CreateNoWindow = True
    p.StartInfo.UseShellExecute = False

    p.Start()

    json = p.StandardOutput.ReadToEnd()
End Using

结果 json 看起来像这样(部分显示!):

{
    "streams": [{
        "index": 0,
        "codec_name": "h264",
        "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
        "profile": "High",
        "codec_type": "video",
        "codec_time_base": "50/2997",
        "codec_tag_string": "avc1",
        "codec_tag": "0x31637661",
        "width": 720,
        "height": 400,
        ...

"streams" 是一个 4 元素数组;显然,视频是元素 0,音频是 1。我不知道其他两个是什么。其中每一个都可以反序列化为表示媒体属性的名称-值对字典。

解析后,视频编解码器为:myJObj("streams")(0)("codec_name").ToString():

  • ("streams")(0) 引用第 0 个流数组元素
  • ("codec_name") 是 属性 的键或名称。大多数值是字符串,但少数是数字。

以下代码(接上文)将通过创建对所需 属性 集的引用来缩短这些引用。正如我所说,我将它们放在 ListView 中并使用组。

' parse to a temp JObject
Dim jobj= JObject.Parse(json)
' get a list/array of dictionary of prop name/value pairs
Dim medprops = JsonConvert.DeserializeObject( _
            Of List(Of Dictionary(Of String, Object)))(jobj("streams").ToString)

' get Video list:
Dim Props = medprops(0)

AddNewLVItem("Codec", Props("codec_name").ToString, "Video")

Dim secs As Double = Convert.ToDouble(Props("duration").ToString)
Dim ts = TimeSpan.FromSeconds(secs)
AddNewLVItem("Duration", ts.ToString("hh\:mm\:ss"), "Video")

AddNewLVItem("Fr Width", Props("width").ToString, "Video")
AddNewLVItem("Fr Height", Props("height").ToString, "Video")

' get avg fr rate text
Dim afr = Props("avg_frame_rate").ToString
Dim AvgFrRate As String = "???"
' split on "/"
If afr.Contains("/") Then
    Dim split = afr.Split("/"c)
    ' calc by dividing (0) by (1)
    AvgFrRate = (Convert.ToDouble(split(0)) / Convert.ToDouble(split(1))).ToString
End If
AddNewLVItem("Avg Frame Rate", AvgFrRate, "Video")

' NB: audio stream values come from element (1):
Props = medprops(1)
AddNewLVItem("Audio Codec", Props("codec_name").ToString, "Audio")
AddNewLVItem("Audio Sample Rate", Props("sample_rate").ToString, "Audio")

' avg bit rate is apparently decimal (1000) not metric (1024)
Dim abr As Integer = Convert.ToInt32(Props("bit_rate").ToString)
abr = Convert.ToInt32(abr / 1000)
AddNewLVItem("Audio Bit Rate", abr.ToString, "Audio")

几乎所有内容都以字符串形式返回,许多需要执行一些计算或格式化。例如,平均帧速率返回为 "2997/100",因此您可以看到我将其拆分的位置,转换为整数并进行除法。此外,音频比特率似乎是十进制而不是公制,可能类似于 127997

您将能够在调试器中看到各种键和值以定位其他值。您还可以将生成的 json 字符串粘贴到 jsonlint 以更好地理解结构和阅读键。

LV助手很简单:

Private Sub AddNewLVItem(text As String, value As String, g As String)
    Dim LVI = New ListViewItem
    LVI.Text = text
    LVI.SubItems.Add(value)
    LVI.Group = myLV.Groups(g)
    myLV.Items.Add(LVI)
End Sub

引用可以是密集的或 "wordy" 但它似乎远没有切碎字符串那么乏味和复杂。结果:


I find MediaInfo.dll often fails to return the frame rate, and almost always provides an inaccurate frame count

与什么相比?我对 6 个文件进行了快速测试,MediaInfo 与 Explorer 和 ffprobe 都匹配。它也有多个条目并且 CBR/VBR.

存在一些问题