Sunday, March 04, 2012

redirecting ffmpeg output, performing calculations is messy

Today, I tried to capture some output from FFmpeg, the media encoder/decoder and had some trouble.  The context is that I wanted to do a sanity check on the total number of frames of video FFmpeg outputs from joining multiple video files:
http://crazedmuleproductions.blogspot.com/2012/03/joining-concatenating-video-files.html

Seems the output from FFmpeg is actually sent via standard error, and not standard output.  The typical output from FFmpeg looks like this:
[sodo@computer tmp]$ ffmpeg -i 1.mpg -an -vcodec copy -f mpeg2video -y NUL 
ffmpeg version 0.7.11-rpmfusion, Copyright (c) 2000-2011 the FFmpeg developers
  built on Feb 25 2012 08:39:28 with gcc 4.6.1 20110908 (Red Hat 4.6.1-9)
  configuration: --prefix=/usr --bindir=/usr/bin --datadir=/usr/share/ffmpeg --incdir=/usr/include/ffmpeg --libdir=/usr/lib64 --mandir=/usr/share/man --arch=x86_64 --extra-cflags='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic' --extra-version=rpmfusion --enable-bzlib --enable-libcelt --enable-libdc1394 --enable-libdirac --enable-libfaac --enable-nonfree --enable-libfreetype --enable-libgsm --enable-libmp3lame --enable-libopenjpeg --enable-librtmp --enable-libschroedinger --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-x11grab --enable-avfilter --enable-postproc --enable-pthreads --disable-static --enable-shared --enable-gpl --disable-debug --disable-stripping --shlibdir=/usr/lib64 --enable-runtime-cpudetect
  libavutil    50. 43. 0 / 50. 43. 0
  libavcodec   52.123. 0 / 52.123. 0
  libavformat  52.111. 0 / 52.111. 0
  libavdevice  52.  5. 0 / 52.  5. 0
  libavfilter   1. 80. 0 /  1. 80. 0
  libswscale    0. 14. 1 /  0. 14. 1
  libpostproc  51.  2. 0 / 51.  2. 0
Input #0, mpeg, from '1.mpg':
  Duration: 00:12:08.26, start: 1.000000, bitrate: 15584 kb/s
    Stream #0.0[0x1e0]: Video: mpeg2video (Main), yuv420p, 1280x720 [PAR 1:1 DAR 16:9], 104857 kb/s, 29.97 fps, 29.97 tbr, 90k tbn, 59.94 tbc
    Stream #0.1[0x1c0]: Audio: mp2, 44100 Hz, stereo, s16, 256 kb/s
Output #0, avi, to 'NUL':
  Metadata:
    ISFT            : Lavf52.111.0
    Stream #0.0: Video: mpeg2video, yuv420p, 1280x720 [PAR 1:1 DAR 16:9], q=2-31, 104857 kb/s, 29.97 tbn, 29.97 tbc
Stream mapping:
  Stream #0.0 -> #0.0
Press [q] to stop, [?] for help
frame=21827 fps=4494 q=-1.0 Lsize= 1357993kB time=00:12:08.26 bitrate=15275.7kbits/s    
video:1357365kB audio:0kB global headers:0kB muxing overhead 0.046304%

All of that is sent to standard error.  The piece I was interested in capturing was the number of frames (bolded, above).  Now, when FFmpeg runs in a terminal, you see the number of frames incrementing; ie, going up.  I can redirect standard error to standard output and save to a file with this command:
ffmpeg -i 1.mpg -an -vcodec copy -f mpeg2video -y NUL 2>&1 | tee test.txt 

If I cat out "test.txt", and look at the last few lines of the file, I see the frame information I want to capture:
Stream mapping:
  Stream #0.0 -> #0.0
Press [q] to stop, [?] for help
frame=21827 fps=4494 q=-1.0 Lsize= 1357993kB time=00:12:08.26 bitrate=15275.7kbits/s    
video:1357365kB audio:0kB global headers:0kB muxing overhead 0.046304%

If I use awk to search for 'frame' to grab the line with the number of frames:
[sodo@computer tmp]$ awk '/frame/ {print $0}' test.txt
frame=21827 fps=4294 q=-1.0 Lsize= 1357365kB time=00:12:08.26 bitrate=15268.6kbits/s

I see my frames value.  However, when I then pipe that command into awk again to grab the second column of data, I get this:
[sodo@computer tmp]$ awk '/frame/ {print $0}' test.txt | awk -F= '{print $2}'
 2195 fps

What's going on here?  "2195" is not the expected result.  I should be seeing "21827 fps".  Looking at the file in vi shows me the problem.  Look at all the hidden ^M characters that have turned up in the file:
Press [q] to stop, [?] for help
frame= 2195 fps=  0 q=-1.0 size=  138799kB time=00:01:13.20 bitrate=15532.0kbits/s    ^Mframe= 4288 fps=4287 q=-1.0 size=  279176kB time=00:02:23.04 bitrate=15988.3kbits/s    ^Mframe= 6398 fps=4265 q=-1.0 size=  419307kB time=00:03:33.44 bitrate=16092.8kbits/s    ^Mframe= 8400 fps=4199 q=-1.0 size=  559022kB time=00:04:40.24 bitrate=16341.0kbits/s    ^Mframe=11378 fps=4551 q=-1.0 size=  696798kB time=00:06:19.61 bitrate=15036.8kbits/s    ^Mframe=14230 fps=4743 q=-1.0 size=  836173kB time=00:07:54.77 bitrate=14427.8kbits/s    ^Mframe=16331 fps=4665 q=-1.0 size=  979121kB time=00:09:04.87 bitrate=14720.7kbits/s    ^Mframe=18526 fps=4631 q=-1.0 size= 1117252kB time=00:10:18.11 bitrate=14807.1kbits/s    ^Mframe=20475 fps=4377 q=-1.0 size= 1245505kB time=00:11:23.14 bitrate=14935.5kbits/s    ^Mframe=21827 fps=4294 q=-1.0 Lsize= 1357365kB time=00:12:08.26 bitrate=15268.6kbits/s    ^M
video:1357365kB audio:0kB global headers:0kB muxing overhead 0.000000%

Oh boy.  You can easily see the problem in the output.  The reason for this seems to be that when FFmpeg updates the values of frames in the output to the terminal, the control character ^M gets input into my output file, test.txt.  We cannot see the ^M when doing a "cat" or "grep" on the file, but it is in the file nonetheless.  So how do we fix?

The goal again is to pluck that final number of frames from the output of FFmpeg that is stored in test.txt.  I can use ^M as a field delimiter within awk using the ascii equivalent of ^M, \015.  First, I have to look for the line in the output that has many fields delimited by the ^M, the line that corresponds to when FFmpeg is updating the "frames" line:
[sodo@computer tmp]$ awk -F"\015" 'NF>1 {print $0}' test.txt
frame=21827 fps=4294 q=-1.0 Lsize= 1357365kB time=00:12:08.26 bitrate=15268.6kbits/s

We can see the many entries on this line:
[sodo@computer tmp]$ awk -F"\015" 'NF > 1 {print $1"\n\r"$2"\n\r"$3}' test.txt
frame= 2195 fps=  0 q=-1.0 size=  138799kB time=00:01:13.20 bitrate=15532.0kbits/s    
frame= 4288 fps=4287 q=-1.0 size=  279176kB time=00:02:23.04 bitrate=15988.3kbits/s    
frame= 6398 fps=4265 q=-1.0 size=  419307kB time=00:03:33.44 bitrate=16092.8kbits/s 

I really just want the last field of information on this line that will give me the final number of frames that FFmpeg has output.  What I have to do is print out the second to last field in the line.  

I do this by performing a simple calculation: a variable called "secondtolast" will be set to the number of fields in the line (delimited by each occurrence of ^M) minus one (secondtolast=NF-1).  I then print use that value as the field I want to print out ($secondtolast) :
[sodo@computer tmp]$ awk -F"\015" 'NF > 1 {secondtolast=NF-1;print $secondtolast}' test.txt
frame=21827 fps=4294 q=-1.0 Lsize= 1357365kB time=00:12:08.26 bitrate=15268.6kbits/s 

After getting the correct field, I can then extract the frame number:
[sodo@computer tmp]$ awk -F"\015" 'NF > 1 {secondtolast=NF-1;print $secondtolast}' test.txt | awk '{print $1}' | awk -F= '{print $2}'
21827

Relating this back to my original problem, counting the total number of frames in a multiple video file concatenation, this long command works:
[sodo@computer tmp]$ for i in 1 2 3 4 ;do ffmpeg -i $i.mpg -an -vcodec copy -f mpeg2video -y NUL 2>&1 | awk -F"\015" 'NF > 1 {secondtolast=NF-1;print $secondtolast}' | awk '{print $1}' | awk -F= '{print $2}'; done
21827
42264
34750
33697

Wow.  Messy.  Even more messy is encapsulating the previous output within another awk command to get a total number of frames for the entire concatenation routine:
[sodo@computer tmp]$ !! | awk '{print sum+=$1}'
for i in 1 2 3 4 ;do ffmpeg -i $i.mpg -an -vcodec copy -f mpeg2video -y NUL 2>&1 | awk -F"\015" 'NF > 1 {secondtolast=NF-1;print $secondtolast}' | awk '{print $1}' | awk -F= '{print $2}'; done | awk '{print sum+=$1}'
21827
64091
98841
132538

Update 3/4/12
I realized I didn't catch the case where FFmpeg does NOT output multiple "frame=" lines.  This occurs when the video file is so small that FFmpeg can digest it in one fell swoop and outputs one "frame=" line only.  To catch this condition, I rewrote the script a bit, throwing in an else in the case where the last field separated by ^M contains the one and only count of frames in the video.  Also, I fixed the script to account for a space " " in the value for the number of frames:
[sodo@computer tmp]$ for i in 1 2 3 4 ; do ffmpeg -i /tmp/$i.mpg -an -vcodec copy -f mpeg2video -y NUL 2>&1 | awk -F"\015" '{if (NF > 1) {secondtolast=NF-1; print $secondtolast} else {print $NF}}' | grep 'frame=' | awk -F"fps" '{print $1}' | awk -F= '{print $2}';done
13307 
11313 
10603 
 9324 
 9033 
10660 

I'm sure there is an easier, more compact way to do this, but I'll leave that up to people with better awk chops.  I'd be interesting to hear if someone has a better solution.

cheers,
TAG
Reference
Feel free to drop me a line or ask me a question.