65.9K
CodeProject 正在变化。 阅读更多。
Home

实时视频转码和流式传输

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (6投票s)

2016年3月14日

CPOL

20分钟阅读

viewsIcon

50125

downloadIcon

2154

将视频实时转码和流式传输到所有主要 Internet 浏览器(仅限于 video 标签)的实验,无需 Flash 或 Silverlight

引言

在这篇文章中,我想开启关于视频转码和流媒体传输到互联网浏览器(无需插件,直接使用原生视频HTML控件)的讨论,并展示我在这个主题上的实验。我将感谢任何关于文章的建议和评论。基本上,这个想法来源于创建一个类似于Plex(https://plex.tv/)的解决方案的需求。

问题在于按需转码媒体文件(目前是文件,但也可以应用于网络摄像头流,例如),使其能够通过互联网浏览器的视频标签播放,并且如果可能的话,实时进行,无需任何临时文件。对于现代浏览器来说,这个问题可以很容易地解决,因为谷歌引入了webm容器,并且大多数浏览器都支持它,可能除了IE。因此,任务变成了将媒体文件转码为这种容器并进行流媒体传输。这听起来比实际要简单。因为实际上这还不够,为了获得正常的用户体验,例如用户在预转码文件上可以拥有的体验,允许他们通过文件进行搜索、查看剩余时间、总时间和流畅播放。要实现这种功能,你需要一个提供基本功能的自定义播放器。当然,这可以使用基于Flash或Silverlight的播放器来实现,并实现RTSPRTMP协议。但正如我上面所说,这不是我们的解决方案,因为主要要求是所有功能都无需浏览器扩展。最接近的解决方案可能是基于HTTP的动态自适应流媒体(MPEG DASH)。在GitHub上我们可以找到客户端实现,例如:
https://github.com/Dash-Industry-Forum/dash.jshttps://github.com/google/shaka-player。此类播放器可以在必要时切换流,以确保连续播放或通过监控CPU利用率和/或缓冲区状态来改善体验。通常,服务器端已经准备好内容并轻松地引导客户端获取适当的流。实际上,我将创建一个基于视频标签、覆盖原生控制的简单JavaScript播放器,实现播放器的一些功能,设计一些对服务器的调用,并在服务器端设计转码并将内容流式传输到客户端。目前,仅进行转码和流式传输,不分析播放,因此所有转码示例都将基于静态配置。

正如我所说,在现代环境中实现这种功能非常简单,但对于不支持webm的旧浏览器和Internet Explorer又如何呢?对于Windows 8及更高版本上的Internet Explorer,可以使用mp4容器,并在每个视频关键帧处启动一个新片段来解决。关于旧浏览器,我稍后会讨论。现在我想说一件重要的事情,以及如何以及使用什么进行转码。可能最著名且适用于所有主要平台的工具是FFMPEG,因此在本文中,我将讨论FFMPEG并提供基于FFMPEG的示例。

目录

 

概念

现在从整体概念开始。

以下是格式列表及其支持者。

浏览器 MP4 WebM Ogg
Internet Explorer
Chrome
Firefox
Safari
Opera 是 (从 Opera 25 开始)

复制自 https://w3schools.org.cn/HTML/html5_video.asp 资源。

所以,正如我们所看到的,所有主流浏览器都支持mp4容器。这里几乎没有什么可说的,除了为了在用户选择源后立即开始播放这些文件,文件应该预先准备好并在文件开头有moov原子。这个操作很快,甚至可以直接在播放文件之前完成。这可以通过在ffmpeg中使用-movflags faststart选项来实现(当然,如果容器包含适当的视频和音频流,我的意思是它是编码为H.264视频和AAC音频,并且不需要转码)。

更有趣的情况是文件必须在播放前进行转码。

首先,我将讨论现代浏览器(目前不包括IE),它们基于通用WebKit并支持webm容器。Chrome、Firefox和Opera可以播放转码为webm格式的流,现在这就是我发现对这些浏览器有用的工作方式。以下是webm格式转码的基本配置:-vcodec libvpx -acodec libopus -deadline realtime -speed 4 -tile-columns 6 -frame-parallel 1 -f webm。这些选项的含义可以在https://ffmpeg.net.cn/资源中找到。Chrome也可以播放转码为matroska配置的流:-vcodec libx264 -preset ultrafast -acodec aac -strict -2 -b:a 96k -threads 0 -f matroska。基本上,webm容器是基于Matroska的一个配置。但根据我的测试,实际上它的处理器效率更高。

对于Windows 8及以上版本的Internet Explorer,媒体应该转码为mp4容器,并在每个视频关键帧处启动一个新片段。这是如何做到的:-vcodec libx264 -preset ultrafast -acodec aac -strict -2 -b:a 96k -threads 0 -f mp4 -movflags frag_keyframe

我并不认为我是在重新发明轮子,我所说的一切都是众所周知的技术,我只是将它们整理到一篇文章中。

让我们谈谈旧浏览器,例如Windows 7及以下版本的Internet Explorer,它们无法播放这些格式。

基本上,问题在于,要在Windows 7上的IE中播放文件,文件必须是mp4格式,并且在开头包含moov原子;否则,如果文件在末尾包含该原子,则文件必须完全下载到客户端,之后才能播放。(该原子包含有关视频文件、时间刻度、持续时间、显示特性等信息。)对于小文件来说这不是大问题,但如果你想在浏览器中播放DVD视频(为什么在浏览器中,你可以自己找到原因 :-)),这可能会不舒服。此时,视频文件必须转码为mp4,理想情况下在开头包含moov原子,转码本身需要一段时间,可能需要等待半小时才能开始观看有趣的电影。因此,解决方案可以将文件转码成小块并依次播放。类似于HLSMPEG-DASH。关于如何平滑播放这些块、如何在视频中搜索以及其他方面,我将在下一章讨论。

详细说明

在本章中,我将详细阐述我对概念的看法和代码示例。

您可以在文章的下载部分找到源代码,该解决方案是在Visual Studio 2008下开发的,但可以轻松转换为最新版本。这些概念已在Windows 7和8上针对IE 10-11、Chrome v48、FF v44和Opera v35浏览器进行了测试,并且仅针对.MOV、.mp4和.mkv文件。

客户端

首先,我将从HTML5播放器在现代浏览器上的自定义开始。

现代浏览器和旧浏览器的播放器略有不同,但基本上具有共同的概念,并以MVC方式设计。

由于这只是一个概念,所以它只包含显示其工作原理的基本功能,没有所有必要的功能,并且仍需要大量工作才能完成。因此,公共接口非常简单,并且有几组公共方法。

PlayerCntr 类接口(\sources\JsSource\js\modern\playercntr.js):

该类提供初始化播放器媒体源的基本功能和控制播放的方法。它还封装了表示播放器用户界面和管理播放数据的对象。

var PlayerCntr = (function()
{
    //constructor, renderTo - parent element to render control
    return function(renderTo)
    {
        var _data = null;
        var _view = null;

        this.start = function()
        {
            //...
        }

        this.stop = function()
        {
            //...
        }

        //source - path to media source
        this.initSource = function(source)
        {
            //...
        }

        this.volume = function(value)
        {
            //...
        }
        
        //...
    }

})();
  • start/stop - 开始/停止媒体播放。
  • initSource - 初始化播放器的媒体源。
  • volume - 调整音量。
接口PlayerView 类 (\sources\JsSource\js\modern\playerview.js):

该类代表播放器的用户界面。

var PlayerView = (function()
{
    return function()
    {
        //public methods
        this.control = function()
        {
            //...
        }

        this.updateDuration = function(duration_sec)
        {
            //...
        }

        this.updateTimeLine = function(time_ratio)
        {
            //...
        }

        this.updateVolumeBar = function(volume_ratio)
        {
            //...
        }

        this.isPlayed = function()
        {
            //...
        }

        this.render = function(element)
        {
            //...
        }
        
        //...
    }
})();
  • render - 将此组件渲染到传入的HTML元素中,并初始化事件处理程序。
  • isPlayed - 返回播放状态。
  • updateVolumeBar - 更新音量控制的滑块位置。
  • updateTimeLine - 更新播放进度。
  • updateDuration - 更新播放总时间。
  • control - 返回当前播放的视频控件的引用。
PlayerData 接口类 (\sources\JsSource\js\modern\playerdata.js):

该类封装了有关当前播放媒体的数据以及管理这些数据的方法。

var PlayerData = (function()
{
    return function()
    {
        this.getVideoInfo = function(video_src, callback, scope)
        {
            //...
        }

        this.getSeekOffset = function()
        {
            //...
        }

        this.setSeekOffset = function(offset)
        {
            //...
        }

        this.getSource = function()
        {
            //...
        }

        this.reset = function()
        {
            //...
        }

        this.parseInfo = function(info, path)
        {
            //...
        }

    	this.durationToReadableString = function(duration_sec)
        {
            //...
        }
        
        this.getVideoDuration = function()
        {
            //...
        }
    }
})();
  • getVideoInfo - 返回当前播放媒体的json格式信息。
  • getSeekOffset - 返回上次搜索操作的秒数。
  • setSeekOffset - 更新上次搜索操作的秒数。
  • getSource - 返回媒体源的相对路径。
  • reset - 重置数据。
  • parseInfo - 解析传入数据并返回json中指定节点的值。
  • durationToReadableString - 返回人类可读的时间字符串。
  • getVideoDuration - 返回媒体时长(秒)。
接口Player 类 (\sources\JsSource\js\player.js):

Player类是一个类工厂。它根据用户代理字符串和文件格式来决定下载和创建哪种播放器,当然,根据视频标签的功能来决定使用哪种播放器更为正确,相关信息可以像我下面获取持续时间一样从服务器请求。因此,该类在运行时决定并下载与媒体源和浏览器相关的适当脚本。

var Player = (function()
{
    //...

    return function(parentEl)
    {
        //...

        this.initSource = function(source)
        {
            //...
        }
    }

})();

该类有一个公共构造函数,带有一个作为HTML元素的传入参数,用于渲染播放器,以及一个决定使用哪个播放器的方法。

播放器加载器

var loadPlayer = function(type, cb, scope)
{
    var scriptLoader = new ScriptLoader();
    scriptLoader.load(document.location.protocol + '//' + document.location.host + '/js/' + type + '/playerview.js')
                .load(document.location.protocol + '//' + document.location.host + '/js/' + type + '/playerdata.js')
                .load(document.location.protocol + '//' + document.location.host + '/js/' + type + '/playercntr.js')
                .then(cb, scope);
}

播放器工厂是

if (getFileExtension(source).toLowerCase() === 'mp4')
{
    loadPlayer('modern', function()
    {
        var player = new PlayerCntr(_parentEl);
        player.initSource(source);
    }, this);
}
else
{
    var parser = new UAParser(navigator.userAgent);

    if (isModern(parser))
    {
        loadPlayer('modern', function()
        {
            var player = new PlayerCntr(_parentEl);
            player.initSource(source);
        }, this);
    }
    else
    {
        loadPlayer('fallback', function()
        {
            var player = new PlayerCntr(_parentEl);
            player.initSource(source);
        }, this);
    }
}

为了解析用户代理字符串,我使用了ua-parser项目中的解析器。它做得相当准确。

自定义播放器封装了所有围绕HTML原生视频控制的方法。由于要提供像播放器播放正常文件而不是无休止流的用户体验,它需要有关媒体源的信息(无休止,因为播放器只知道以秒为单位的持续时间,而不知道最终大小,这对于像原生控件那样在字节范围内搜索是必需的)。因此,客户端首先要做的是请求有关将要播放文件的信息。PlayerCntrinitSource方法向服务器发出请求以获取有关播放文件的信息。目前,只使用了持续时间、视频编解码器、格式,但在生产版本中,更多信息可能会有用,例如帧率、分辨率、音频/视频编解码器(流在容器内如何编码)。因此,根据持续时间,播放器构建时间轴条,以便用户可以使用时间戳而不是字节范围在文件中搜索。所以,一旦播放器渲染并调用initSource方法,视频标签的源将被更新,URL中将添加时间偏移(以秒为单位),最初为0,但如果用户移动时间轴上的滑块,视频标签的源将根据媒体的总时间按比例更新。因此,对服务器的调用将如下所示:

https://:8081/movies/movie7.mov?offset=0 - 从开头开始
https://:8081/movies/movie7.mov?offset=2167.990451497396 - 从时间轴上的特定位置开始

相应地,在服务器端,当对已播放资源发出新请求时,当前的转码和流式传输将被停止,并开始一个新的带时间偏移的转码和流式传输(服务器端我将在后面讨论)。您可以在提供的源代码中找到实现的详细信息。

现在说说适用于旧浏览器的播放器。

基本上,两种播放器的接口相似,主要区别在于,为了提供媒体转码块的平滑播放,我使用了两个视频标签。一个用于当前播放的块,另一个作为缓冲区。一旦当前播放的块到达末尾,当前播放的播放器和缓冲区将相互切换(当前播放器成为缓冲区,反之亦然),缓冲区将向服务器发送请求,以获取特定时间帧的新视频块。每个时间帧为2分钟。时间帧是硬编码的,与视频内容无关,但更好的方法可能是分析视频,以便在场景变化时选择每个块的结束时间,这将使块的切换更平滑,但会增加块的转码时间。所以现在对服务器的调用将如下所示:

https://:8081/movies/movie7.mov?offset=0&duration=120 - 从开头
https://:8081/movies/movie7.mov?offset=2760&duration=120 - 从时间轴上的特定位置

基本上,播放器的HTML标记看起来像

<div class="video-player">
  //...
<video width="100%" height="100%" class="visible" src="/movies/movie7.mov?offset=120&duration=240"></video> //current played player
<video width="100%" height="100%" class="hidden" src="/movies/movie7.mov?offset=240&duration=360"></video>  //buffer
<div class="video-controls">
    //...
  </div>
</div>

//after switching players

<div class="video-player">
  //...
<video width="100%" height="100%" class="hidden" src="/movies/movie7.mov?offset=360&duration=480"></video> //buffer
<video width="100%" height="100%" class="visible" src="/movies/movie7.mov?offset=240&duration=360"></video> //current played player
<div class="video-controls">
    //...
  </div>
</div>

 

PlayerCntr 类接口(\sources\JsSource\js\outdated\playercntr.js):

接口没有改变,差异隐藏在私有方法和事件处理程序中。视频标签的切换发生在onMediaEnd事件上,当前播放的视频标签变为隐藏,缓冲区变为可见。接下来,一旦当前播放的媒体达到当前播放时间与块总时间的10%比例,它就开始下载缓冲区的数据。这个操作将重复,直到当前播放时间达到媒体的总时间。实际上,10%的比例不应该是常量,应该根据带宽来计算。同样,转码媒体的比特率也应该是可变的。所有这些值都会影响视频播放的流畅性。再说一次,这只是概念。

接口PlayerView 类 (\sources\JsSource\js\outdated\playerview.js):

接口中新增了两个方法

  • buffer - 返回缓冲区视频控件的引用。
  • switchPlayed - 切换当前播放的视频和缓冲区。
PlayerData 接口类 (\sources\JsSource\js\outdated\playerdata.js):

接口中新增了五个方法

  • calculateOffset - 根据用户在时间轴上选择的特定值,返回秒数偏移量。
  • getChankDuration - 根据已播放块的总时间,返回下一块的持续时间(秒)。
  • readyToUpdateBuffer - 根据当前播放时间检查是否是时候更新缓冲区。
  • getTimeChanksPlayed - 返回已播放块的总时间。
  • setTimeChanksPlayed - 设置已播放块的总时间。

服务器端

服务器端的实现分为多个模块。每个模块负责特定类型的流,并在服务器启动时运行时加载。因此,可以不加载某个模块,这样某些流就会被禁用。目前,服务器实现了三种类型的流:流式传输mp4文件、转码并流式传输webmmatroskamp4(在每个视频关键帧处启动新片段选项,即流式传输到现代浏览器)。第三种流式传输类型是转码块的流式传输。

服务器本身还实现了两个处理程序,一个用于列出工作目录,另一个用于处理客户端获取文件信息的请求,该文件将被播放。

每个处理程序都必须实现IHandler接口

public interface IHandler : IDisposable
{
    string Name { get; }
    void SetEnvironment(IHandlerEnvironment env);

    bool IsSupported(IHttpContext context);
    void Process(IHttpContext context);
}
  •     Name - 返回处理程序的唯一名称。
  •     SetEnvironment(IHandlerEnvironment env) - 设置服务器的运行环境。
  •     IsSupported(IHttpContext context) - 返回服务器是否支持当前http请求的状态。
  •     Process(IHttpContext context) - 处理请求。

IHandlerEnvironment接口仅封装服务器的工作目录。

public interface IHandlerEnvironment : IDisposable
{
    string Directory { get; }
}

最重要的接口是IHttpContext。该接口的对象封装了所有传入请求的数据并传递给所有处理程序,它在收到新的http请求时创建。

public interface IHttpContext
{
    event EventHandler<handlereventargs> Closed;

    Uri URL { get; }

    int GetUniqueKeyPerRequest { get; }
    int GetUniqueKeyPerPath { get; }

    Stream InStream { get; }
    Stream OutStream { get; set; }

    NameValueCollection RequestHeaders { get; }
    CookieCollection RequestCookies { get; }

    ClientInfo BrowserCapabilities { get; }

    WebHeaderCollection ResponseHeaders { get; }
    CookieCollection ResponseCookies { get; }

    ISession Session { get; }

    Boundary Range { get; }
    int StatusCode { get; set; }
    void SetHandlerContext(object context);

    void OnProcessed();
    void OnLogging(EventLogEntryType logType, string message);
}

 

  •     Closed - 关闭传入请求的事件,订阅者必须释放与该请求相关的所有数据。
  •     URL - 返回传入请求的原始URL
  •     GetUniqueKeyPerRequest - 便利属性,根据会话ID和原始URL返回唯一的哈希值。
  •     GetUniqueKeyPerPath - 便利属性,根据会话ID和请求路径返回唯一的哈希值。
  •     InStream - 返回传入流。
  •     OutStream - 设置/返回传出流。
  •     RequestHeaders - 返回原始请求头。
  •     RequestCookies - 返回原始请求cookie。
  •     BrowserCapabilities - 根据传入的用户代理字符串返回客户端信息。
  •     ResponseHeaders - 返回响应头。
  •     ResponseCookies - 返回响应cookie。
  •     Session - 返回与请求关联的会话。
  •     Range - 在文件或分块流的情况下返回传出流的边界,在无限流的情况下返回整数最大值。
  •     StatusCode - 设置/返回响应的HTTP代码。
  •     SetHandlerContext(object context) - 设置处理程序上下文,可在Closed事件中获取。
  •     OnProcessed() - 处理程序在请求处理完毕后必须调用此方法。
  •     OnLogging(EventLogEntryType logType, string message) - 处理程序可以调用此方法打印调试信息。

该接口隐藏了随HTTP请求传入的原始HTTP上下文,仅提供处理请求的方法。服务器实现不值得仔细讨论。它基于异步(非阻塞)调用来接收传入的客户端请求以及对传入和传出流的读写。所有文件操作也都是异步的。

实现的详细信息在

服务器类 (\sources\HttpServer\httpServer\Server.cs)。

处理程序。

Media类型处理获取将要播放的媒体文件信息的请求。为了获取视频信息,它使用ffprobe(ffprobe是ffmpeg项目的一部分,它从多媒体流中收集信息并以人类和机器可读的方式打印出来)。

Media Class (\sources\HttpService\Handlers\Media.cs)。

这是获取信息的实现

private void StartFFProbe(IHttpContext context)
{
    ThreadPool.QueueUserWorkItem(delegate(object ctx)
    {
        IHttpContext httpContext = (IHttpContext)ctx;
        try
        {
            string urlpath = context.URL.LocalPath.Replace("/", @"\");
            NameValueCollection urlArgs = HttpUtility.ParseQueryString(context.URL.Query);
            string instance = HttpUtility.UrlDecode(Helpers.GetQueryParameter(urlArgs, "item"));

            string path = string.Format(@"{0}\{1}", _env.Directory, instance.Replace("/", @"\")).Replace(@"\\", @"\");

            ProcessStartInfo pr = new ProcessStartInfo(Path.Combine(new FileInfo(Assembly.GetEntryAssembly().Location).Directory.FullName, _ffprobe),
                string.Format(@"-i {0} -show_format -print_format json", path));
            pr.UseShellExecute = false;
            pr.RedirectStandardOutput = true;

            Process proc = new Process();
            proc.StartInfo = pr;
            proc.Start();

            string output = "{ \"seeking_type\": \"" + Helpers.GetSeekingType((new FileInfo(path)).Extension) + "\", \"media_info\": " + proc.StandardOutput.ReadToEnd() + " }";

            proc.WaitForExit();

            SetHeader(httpContext, 200, Helpers.GetMimeType(""));
            httpContext.OutStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(output));
            SetRange(httpContext);
        }
        catch (Exception e)
        {
            SetHeader(httpContext, 500, Helpers.GetMimeType(""));
            if (httpContext != null)
            {
                httpContext.OutStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(""));
            }

            context.OnLogging(EventLogEntryType.Error, string.Format("Media.StartFFProbe, Exception : {0}", e.Message));
        }
        finally
        {
            httpContext.OnProcessed();
        }
    }, context);
}

此过程的结果是JSON。除了视频信息之外,输出还包括客户端是否可以在流中搜索的信息。无限流和分块流有一种搜索类型,文件流则不同,并且可以在客户端进行搜索。

播放mp4文件的请求由Mp4StreamHandler处理。

Mp4StreamHandler 类 (\sources\HandlerExample1\Mp4StreamHandler.cs)。

这是mp4文件流式传输的实现

public void Process(IHttpContext httpContext)
{
    string urlpath = httpContext.URL.LocalPath.Replace("/", @"\");
    string ext = Path.GetExtension(urlpath).ToLower();

    try
    {
        string path = string.Format(@"{0}\{1}", _env.Directory, urlpath).Replace(@"\\", @"\");
        switch (ext)
        {
            case ".mp4":
                if (File.Exists(path))
                {
                    SetHeader(httpContext, 206, Helpers.GetMimeType(ext));
                    httpContext.OutStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize.Value, true);

                    SetRange(httpContext);

                    httpContext.OutStream.Seek(httpContext.Range.Left, SeekOrigin.Begin);

                    SetHeaderRange(httpContext);

                    httpContext.OnProcessed();
                }
                return;
            default:
                throw new NotSupportedException();
        }

    }
    catch (NotSupportedException e)
    {
        SetHeader(httpContext, 404, Helpers.GetMimeType(ext));
        httpContext.OutStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(""));

        httpContext.OnLogging(EventLogEntryType.Error, e.Message);
        httpContext.OnProcessed();
    }
    catch (Exception e) 
    {
        SetHeader(httpContext, 500, Helpers.GetMimeType(ext));
        httpContext.OutStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(""));

        httpContext.OnLogging(EventLogEntryType.Error, e.Message);
        httpContext.OnProcessed();
    }
}

这些处理程序的实现不保留对httpcontext的引用,并且所有管理传出流和httpcontext生命周期的责任将由调用该处理程序的服务器承担。因此,这些对象将在响应关闭时释放。
但在某些情况下,我们需要委托传出流和httpcontext的生命周期管理责任。在这种情况下,处理程序必须订阅关闭事件,并在事件的处理程序中设置状态,表明该处理程序将负责传出流和httpcontext。

更复杂的情况是实时转码并流传输到现代和过时播放器。我将开始描述我实现转码并流传输为webmmatroska格式的方法。

实时转码和流媒体传输的主要区别在于流没有字节长度,播放器只能使用以秒为单位的持续时间。这就是为什么在文件流的情况下,流中的搜索操作发生在服务器端,而不是客户端。通常的工作流程是,当有新的播放文件请求,且浏览器不支持该文件时,它会到达WbemStreamHandler(当然,如果是现代浏览器)。WbemStreamHandler如果尚未启动,将启动HTTP服务器,然后启动ffmpeg,并将其传出流指向已启动的服务器,因此HTTP服务器是IO服务器,仅用于将ffmpeg的流中继到客户端。如果客户端想在流中搜索,则当前流停止,并从新位置开始新的流。

WbemStreamHandler 类 (\sources\HandlerExample2\WbemStreamHandler.cs)。

这是启动ffmpeg的实现

该函数将被阻塞,直到http服务器启动并获得适当的服务器地址。

private void StartFFMpeg(IHttpContext context)
{
    ThreadPool.QueueUserWorkItem(delegate(object ctx)
    {
        _waitPort.WaitOne();
        try
        {
            IHttpContext httpContext = (IHttpContext)ctx;
            RemoveFFMpegProcess(httpContext);

            string urlpath = httpContext.URL.LocalPath.Replace("/", @"\");
            string path = string.Format(@"{0}\{1}", _env.Directory, urlpath).Replace(@"\\", @"\");

            NameValueCollection urlArgs = HttpUtility.ParseQueryString(httpContext.URL.Query);
            string offset = Helpers.GetQueryParameter(urlArgs, "offset");
            if (string.IsNullOrEmpty(offset))
            {
                offset = "00:00:00";
            }
            else
            {
                offset = Helpers.GetFormatedOffset(Convert.ToDouble(offset));
            }

            ProcessStartInfo pr = new ProcessStartInfo(Path.Combine(new FileInfo(Assembly.GetEntryAssembly().Location).Directory.FullName, _ffmpeg),
                string.Format(@"-ss {1} -i {0} {2} https://:{3}/{4}", path, offset, GetBrowserSupportedFFMpegFormat(context.BrowserCapabilities), _httpPort.ToString(), AddContext(httpContext)));
            //pr.UseShellExecute = true;
            //pr.RedirectStandardOutput = false;
            pr.UseShellExecute = false;
            pr.RedirectStandardOutput = true;

            Process proc = new Process();
            proc.StartInfo = pr;
            proc.Start();

            AddFFMpegProcess(httpContext, proc);

            //string output = proc.StandardOutput.ReadToEnd();

            httpContext.OnLogging(EventLogEntryType.SuccessAudit, "*************** WbemStreamHandler.StartFFMpeg ***************");
        }
        catch (Exception e)
        {
            context.OnLogging(EventLogEntryType.Error, string.Format("WbemStreamHandler.StartFFMpeg, Exception: {0}", e.Message));
        }

    }, context);
}

在这里我遇到了管理输出流带宽和转码性能的问题。转码是一个非常繁重的操作,通常ffmpeg会占用处理器操作时间的一半以上。某些格式的转码可能比播放需要更长的时间。在这种情况下,服务器必须动态地调整转码质量,以缩短转码时间(我仍在研究如何正确处理这个问题)。

为了管理输出流的带宽,服务器具有SpeedTester类,该类累积在特定时间内处理了多少字节的信息,并向服务器提供统计数据。统计数据累积在ClientStatistics类中。该类为每个请求和会话ID累积统计数据,所有客户端的总体统计数据保存在OverallStatistics类中。因此,服务器根据统计数据决定哪个流以及应该推迟多长时间。ClientStatistics类保存了转码数据的比特率以及输出流和转码操作这两个操作的中位比特率的信息。根据这两个比特率,我们可以计算比率和等待时间,即流可以等待多长时间。

下一步应该是根据播放体验和服务器负载来管理视频质量,以便在客户端获得流畅的视频播放。这一步需要服务器端更多的信息,也需要客户端的播放体验。

统计数据源代码:SpeedTester 类 (\sources\HttpService\httpServer\Utils\SpeedTester.cs), OverallStatistics 类 (\sources\HttpService\httpServer\Utils\OverallStatistics.cs), ClientStatistics 类 (\sources\HttpService\httpServer\Utils\ClientStatistics.cs), 以及三个用于累积和计算中位比特率的类:BinaryHeapMax 类 (\sources\HttpService\httpServer\Utils\BinaryHeapMax.cs), BinaryHeapMin 类 (\sources\HttpService\httpServer\Utils\BinaryHeapMin.cs) 和 Median 类 (\sources\HttpService\httpServer\Utils\Median.cs)。

现在我们开始讨论服务器端的分块转码和流媒体传输。我实现了两种不同版本的处理程序。一种是基于文件的,即ffmpeg将分块转码并存储在文件系统中,然后处理程序将其作为mp4文件流式传输,文件开头带有moov原子,以便立即开始播放,这在PseudoStreamHandler2中实现。另一种是基于内存的,即ffmpeg转码特定的分块并将其存储在FTP服务器上。因此,在这种情况下,ffmpeg的传出资源是FTP服务器,在PseudoStreamHandler中实现,处理程序将其每个转码后的分块保存在其运行时内存中。

基于文件的处理程序实现了两个功能,一个功能转码新的分块并将其存储在文件中,另一个功能处理已转码的分块。文件的引用保存在StreamCaсhe对象中。这是一个简单的FIFO缓存实现,每个文件可以保留两个实例。即,对于缓存中的特定源,最多可以有两个文件引用(当前播放的分块的引用和将要播放的缓冲区的引用),当第三个到来时,第一个将被释放并替换为第二个,然后新的成为第二个位置。

实现的详细信息在

PseudoStreamHandler2 类 (\sources\PseudoStreamHandler2\PseudoStreamHandler2.cs), StreamCache 类 (\sources\PseudoStreamHandler2\StreamCache.cs)。

基于内存缓存块的处理程序与上述处理程序非常相似,不同之处在于StreamCache保留了内存引用,并且处理程序实现了一个简单的FTP服务器。我只想指出,为了改进内存管理,StreamCache不会在每次有新块到来时都创建MemoryStream,它会重用当前未被使用且已播放的MemoryStream

实现的详细信息在

PseudoStreamHandler 类 (\sources\PseudoStreamHandler\PseudoStreamHandler.cs), StreamCache 类 (\sources\PseudoStreamHandler\StreamCache.cs), (ftp客户端连接处理的实现) FtpClient 类 (\sources\PseudoStreamHandler\FtpClient.cs), PassiveListener 类 (\sources\PseudoStreamHandler\PassiveListener.cs)。

围绕StreamCache和转码数据有很多问题。因为转码本身是沉重的操作,所以最好将所有转码后的块存储在某个存储中,以便能够直接流媒体而无需转码,并在缓存中没有该时间范围的引用时按需转码。此外,有必要锁定当前播放的缓冲区以防止其被重写,以防数据未完全流传输,并为缓存提供增长的可能性,而不是将其锁定在两个缓冲区上。但基本上,作为家庭网络解决方案,用于从媒体盒流传输到少数接收者,它是可以工作的。

结论

要使其正常工作,客户端和服务器端都需要付出巨大的努力。特别是由于转码本身将需要服务器端大量的资源,还需要客户端关于播放体验的反馈。即使它可以为少数客户端完成,在我看来,如果没有缓存转码数据并将其重新流传输到客户端,那么为大量客户端完成将非常困难。

正如我所说,这只是一个实验,我对反馈和新想法很感兴趣。也许这是一个过度的解决方案,或者它永远无法在实际环境中运行。

如何使用

在随附的源代码中,您将找到适用于Visual Studio 2008的解决方案:.\_build\MediaStreamer.sln。它创建一个http/https server的控制台应用程序。

编译前您必须安装ffmpeg,为此您需要从https://ffmpeg.net.cn/download.html#build-windows下载软件包。接下来,将存档解压到\workenv\ffmpeg\文件夹下。构建过程将自动将其复制到适当的位置。

为了方便起见,在构建解决方案之前,您可以将视频内容复制到 .\data 文件夹下,它将被复制到服务器的工作目录。

当解决方案成功构建后,您会发现一个名为 `._debug` 或 `._release` 的文件夹,具体取决于您选择的配置。在该文件夹中,只需运行 `start_http_server.bat` 文件即可启动 HTTP 服务器。服务器成功启动后,您会在该文件夹中找到 `PLAYER.lnk` 文件,它将帮助您启动默认的互联网浏览器。

 

 

© . All rights reserved.