Populate field values in a VB form by parsing ffprobe output

人盡茶涼 提交于 2019-12-24 12:51:39

问题


Ok. I am attempting to grab pertinent media information from video files. To do this, I run ffprobe from my script. Something like this:

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

Which creates a file containing something like this:

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

I say, "something like this," since the data on the Stream #0:0(und): line is not always in the same order. I need to grab the following information from the log:

  • Duration (00:04:34.41)
  • Codec (h264)
  • Resolution (720x480)
  • Width (from Resolution before the x)
  • Height (from Resolution after the x)
  • Frame rate (29.97 fps)
  • Audio Codec (aac)
  • Audio Sampling Freq (48 KHz)
  • Audio Quality (192 kb/s)

In a batch script, I would use tokens and delimiters to scan for my information, and check to see that I grabbed the correct data; if not, try the next token.

Here is an example of how I performed the task in batch script:

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
)

How can I do this in VB? I am using Visual Studio Express 2015 for desktop.

Ok, after doing some more digging and playing, here is how I managed to accomplish the task:

 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

Which yields the result:

All I need to do now is to populate my form's fields using the variables in the form's code: Me.[field].text = [variable] and it should be good to go.

As you can see, I took advantage of the curRow() array to narrow down the fields using mid() and InStr() functions. Though it seems to have accomplished the task, I'm not sure if it was the best way to do it, or if there might be another way.

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


回答1:


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

There is a more direct way to get the info rather than using the old Shell command, and a slightly more organized way to access the info. The following will read the result from ffprobe directly from standard output. To do this, use the shiny new Net Process class instead of the legacy Shell.

FFProbe also supports json output (and xml, I think), which will allow you to query a JObject to fetch back params by "name". The command line argument for this is -print_format json.

The following will get the same properties you show but will put them in a spiffy ListView rather than a MsgBox. Because of the encoding, the references can be a bit long, but I'll show how to shorten them. Most important, there is no string parsing involved which will be more direct. You will need Newtonsoft's JSON.NET for this:

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

The resulting json looks like this (partial display!):

{
    "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" is a 4 element array; apparently Video is element 0 and Audio is 1. I have no idea what the other 2 are. Each of those can be deserialized to a Dictionary of Name-Value pairs representing the media properties.

After Parsing it, the video codec would be: myJObj("streams")(0)("codec_name").ToString():

  • ("streams")(0) references the 0th streams array element
  • ("codec_name") is the key or name of that property. Most values are string, but a few are numeric.

The following code (continued from above), will shorten those references by creating a reference to the desired property set. As I said, I am putting these in a ListView and using Groups.

' 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")

Almost everything is returned as string, and many need some calculation or formatting performed. For instance, avg frame rate comes back as "2997/100" so you can see where I split it, convert to integer and divide. Also the audio bit rate appears to be decimal not metric and can be something like 127997.

You will be able to see the various keys and values in the debugger to locate other values. You can also paste the resulting json string to jsonlint to better understand the structure and read the keys.

The LV helper is simple:

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

The references can be dense or "wordy" but it seems far less tedious and involved than chopping up strings. The result:


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

Compared to what? I did a quick test on 6 files and MediaInfo matched both Explorer and ffprobe. It too has multiple entries and there is some issue with CBR/VBR.



来源:https://stackoverflow.com/questions/34153970/populate-field-values-in-a-vb-form-by-parsing-ffprobe-output

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!