0

我必须提取作为块 blob 存储在 azure 存储中的视频文件(mp4、wmv、mov)的第一帧。我必须在 httphandler 中执行此操作,然后将其作为字节缓冲区存储到我们的 SQL Azure 数据库上的表中。

任何帮助将不胜感激。谢谢

4

1 回答 1

1

您可以使用 Windows Azure 媒体服务来完成此操作。这听起来像是他们可能会为你处理的那种事情,但我对他们没有任何特别的经验。

否则,你必须自己做。看起来像这样:

  1. 将 blob 从存储中取出到可以使用它的地方
  2. 把框架拿出来。
  3. 将框架保存到 SQL Azure。

我假设您可以自己弄清楚#1和#3。对于#2,我建议查看ffmpeg。这有点让人头疼,但它几乎可以从任何东西中抓住一个框架。实际上,我已经为此使用了几年,而且效果还不错。我用于抓帧的参数是:

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

但肯定有更多的方法可以做到这一点。

示例代码: 这是我如何做的(有些简化的)版本。注意我的 ffmpeg 版本有点旧,所以命令行参数可能已经改变。但基本的想法应该有效。

/// <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;
        }
    }
}

这依赖于以下辅助类:

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; }
}
于 2013-05-16T15:13:24.987 回答