Allan Chua
Allan Chua

Reputation: 10175

How to get the first frame of a video file stored in azure storage

I had to extract the first frame of a video files(mp4, wmv, mov) which is stored on azure storage as block blobs. I had to do this inside a httphandler then store it as byte buffer to a table on our SQL Azure Database.

Any help would be greatly appreciated. Thanks

Upvotes: 0

Views: 1880

Answers (1)

Brian Reischl
Brian Reischl

Reputation: 7356

You might be able to do this with Windows Azure Media Services. It sounds like the sort of thing they might handle for you, but I don't have any particular experience with them.

Otherwise, you'd have to do it yourself. That would look something like this:

  1. Get the blob out of storage to somewhere you can work with it
  2. Grab the frame out.
  3. Save the frame to SQL Azure.

I'm assuming you can figure out #1 & #3 by yourself. For #2, I would recommend looking into ffmpeg. It's kind of a pain in the butt, but it will grab a frame out of nearly anything. Actually, I've used it for that for a couple years now and it works reasonably well. The arguments I use for framegrabbing are:

ffmpeg -i <inputFileName> -frames:v 1 -ss <video duration in seconds / 2> -f image2 <output file name>

But there are certainly more ways to do it.

Example code: This is a (somewhat simplified) version of how I'm doing it. Note my ffmpeg version is a bit old, so the command line args may have changed. But the basic idea should work.

/// <summary>
/// Extract a thumbnail from the middle (by duration) of a video file
/// </summary>
/// <param name="inputFileName">Path to the video file on the local filesystem</param>
/// <param name="duration">Duration of the video</param>
/// <returns></returns>
public Image ExtractThumbnail(string inputFileName)
{
    if (string.IsNullOrEmpty(inputFileName))
    {
        throw new ArgumentNullException("inputFileName", "Input file is null");
    }


    TimeSpan duration = GetVideoDuration(inputFileName);

    const string framegrabTemplate = @"-i ""{0}"" -frames:v 1 -ss {2:##0.0##} -f image2 {1}";

    string framegrabArgs = string.Format(framegrabTemplate, inputFileName, OutputFileName, duration.TotalSeconds / 2);
    WindowsProcessResult result = null;

    try
    {
        result = WindowsProcessUtil.RunProcess(ExePath, framegrabArgs);
    }
    catch (Exception ex)
    {
        log.Error("Framegrab process failed with exception {0}.", ex);
        return null;
    }

    if (result.ExitCode != 0)
    {
        log.Error("Framegrab process failed with exitCode {0}. Process output:\r\n{1}\r\nProcess Error: {2}", result.ExitCode, result.StandardOutput, result.StandardError);
        return null;
    }

    var img = Image.FromFile(OutputFileName);

    //Certain video sources (primarily iOS v5 and below) will give you videos rotated to one side with embedded metadata telling you what that rotation is
    //If you need to deal with that, you can set rotationAngle from that metadata, but that's a whole other issue
    //Leaving it here for now as an FYI
    //int rotationAngle = 0;
    //if (rotationAngle != 0)
    //{
    //    if (rotationAngle == 90)
    //    {
    //        img.RotateFlip(RotateFlipType.Rotate90FlipNone);
    //    }
    //    else if (rotationAngle == 180)
    //    {
    //        img.RotateFlip(RotateFlipType.Rotate180FlipNone);
    //    }
    //    else if (rotationAngle == 270)
    //    {
    //        img.RotateFlip(RotateFlipType.Rotate270FlipNone);
    //    }
    //}

    return img;
}

private static readonly Regex durationRegex = new Regex(@"Duration\: (?<duration>[\d\:\.]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
private TimeSpan GetVideoDuration(string inputFileName)
{
    Process getDurationProcess = null;
    try
    {
        const string fileInfoTemplate = @"-i ""{0}""";
        ProcessStartInfo psi = new ProcessStartInfo(ExePath, string.Format(fileInfoTemplate, inputFileName));
        psi.UseShellExecute = false;
        psi.RedirectStandardOutput = true;
        psi.RedirectStandardError = true;
        psi.CreateNoWindow = true;
        //psi.WorkingDirectory = Path.GetDirectoryName(ExePath);
        //psi.EnvironmentVariables["Path"] = psi.EnvironmentVariables["Path"] + ";" + Path.GetDirectoryName(ExePath);

        getDurationProcess = new Process();
        getDurationProcess.StartInfo = psi;
        getDurationProcess.EnableRaisingEvents = true;

        StringBuilder processOutput = new StringBuilder();
        StringBuilder processError = new StringBuilder();
        getDurationProcess.OutputDataReceived += (o, args) =>
        {
            processOutput.AppendLine(args.Data);
        };
        getDurationProcess.ErrorDataReceived += (o, args) =>
        {
            processError.AppendLine(args.Data);
        };

        getDurationProcess.Start();
        getDurationProcess.BeginOutputReadLine();
        getDurationProcess.BeginErrorReadLine();

        getDurationProcess.WaitForExit();

        //Don't do this - ffmpeg errors out because we didn't give it an output file
        //if (getDurationProcess.ExitCode != 0)
        //{
        //    log.Error("Get video duration process failed with exitCode {0}. Process output:\r\n{1}\r\nProcess Error: {2}", getDurationProcess.ExitCode, processOutput, processError);
        //}

        //Now we need to parse output
        #region Sample output
        /*
            ffmpeg version git-N-29946-g27614b1, Copyright (c) 2000-2011 the FFmpeg developers
            built on May 15 2011 15:07:09 with gcc 4.5.3
            configuration: --enable-gpl --enable-version3 --enable-memalign-hack --enable-
        runtime-cpudetect --enable-avisynth --enable-bzlib --enable-frei0r --enable-libo
        pencore-amrnb --enable-libopencore-amrwb --enable-libfreetype --enable-libgsm --
        enable-libmp3lame --enable-libopenjpeg --enable-librtmp --enable-libschroedinger
            --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enabl
        e-libx264 --enable-libxavs --enable-libxvid --enable-zlib --pkg-config=pkg-confi
        g
            libavutil    51.  2. 1 / 51.  2. 1
            libavcodec   53.  5. 0 / 53.  5. 0
            libavformat  53.  0. 3 / 53.  0. 3
            libavdevice  53.  0. 0 / 53.  0. 0
            libavfilter   2.  5. 0 /  2.  5. 0
            libswscale    0. 14. 0 /  0. 14. 0
            libpostproc  51.  2. 0 / 51.  2. 0

        Seems stream 0 codec frame rate differs from container frame rate: 180000.00 (18
        0000/1) -> 30.00 (30/1)
        Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '..\fromQuicktime.mp4':
            Metadata:
            major_brand     : mp42
            minor_version   : 0
            compatible_brands: mp42isomavc1
            creation_time   : 2011-04-22 16:36:45
            encoder         : HandBrake 0.9.5 2011010300
            Duration: 00:00:22.13, start: 0.000000, bitrate: 712 kb/s
            Stream #0.0(und): Video: h264 (High), yuv420p, 480x272, 578 kb/s, 30 fps, 30
            tbr, 90k tbn, 180k tbc
            Metadata:
                creation_time   : 2011-04-22 16:36:45
            Stream #0.1(und): Audio: aac, 44100 Hz, mono, s16, 127 kb/s
            Metadata:
                creation_time   : 2011-04-22 16:36:45
        At least one output file must be specified
            * */
        #endregion

        processOutput.Append(processError.ToString());

        Match m = durationRegex.Match(processOutput.ToString());
        Group durationGroup = m.Groups["duration"];

        TimeSpan duration;
        if (!TimeSpan.TryParse(durationGroup.Value, out duration))
        {
            log.Error("Failed to parse duration from FFMpeg output:\r\n{0}", processOutput);
            return TimeSpan.Zero;
        }
        else
        {
            return duration;
        }
    }
    finally
    {
        if (getDurationProcess != null)
        {
            getDurationProcess.Dispose();
            getDurationProcess = null;
        }
    }
}

That relies on the following helper classes:

public static class WindowsProcessUtil
{
    /// <summary>
    /// Spawn a Windows process, capture StandardOut and StandardError, and wait for it to complete
    /// </summary>
    public static WindowsProcessResult RunProcess(string exePath, string cmdLineArgs, TimeSpan? timeout = null)
    {
        Process p = null;
        StringBuilder processOutput = new StringBuilder();
        StringBuilder processError = new StringBuilder();
        int exitCode = 0;

        try
        {
            ProcessStartInfo psi = new ProcessStartInfo(exePath, cmdLineArgs);
            psi.UseShellExecute = false;
            psi.RedirectStandardOutput = true;
            psi.RedirectStandardError = true;
            psi.CreateNoWindow = true;
            psi.WorkingDirectory = Path.GetDirectoryName(exePath);
            psi.EnvironmentVariables["Path"] = psi.EnvironmentVariables["Path"] + ";" + Path.GetDirectoryName(exePath);
            psi.LoadUserProfile = true;

            p = new Process();
            p.StartInfo = psi;
            p.EnableRaisingEvents = true;

            p.OutputDataReceived += (o, args) =>
            {
                processOutput.AppendLine(args.Data);
            };
            p.ErrorDataReceived += (o, args) =>
            {
                processError.AppendLine(args.Data);
            };

            p.Start();
            p.BeginOutputReadLine();
            p.BeginErrorReadLine();

            if (timeout.HasValue)
            {
                bool processExited = p.WaitForExit((int)timeout.Value.TotalMilliseconds);
                if (!processExited)
                {
                    p.Kill();
                    throw new TimeoutException("Process did not complete after " + timeout.Value.TotalMilliseconds + " msec");
                }
            }
            else
            {
                p.WaitForExit();
            }

            exitCode = p.ExitCode;
        }
        finally
        {
            if (p != null)
            {
                p.Dispose();
                p = null;
            }
        }

        return new WindowsProcessResult()
        {
            ExitCode = exitCode,
            StandardError = processError.ToString(),
            StandardOutput = processOutput.ToString()
        };
    }
}

public class WindowsProcessResult
{
    public int ExitCode { get; set; }
    public string StandardOutput { get; set; }
    public string StandardError { get; set; }
}

Upvotes: 1

Related Questions