HTML5<音频>/<视频>以及使用 FFMPEG 进行实时转码

发布于 2024-09-17 19:37:49 字数 656 浏览 9 评论 0原文

因此,在我的网络服务器上,我想使用 FFMPEG 对媒体文件进行转码,以便与 HTML 标记一起使用。够简单吧?

当 HTTP 客户端请求转换后的文件时,转换需要实时进行。理想情况下,文件将在转码时流回 HTTP 客户端(而不是在最后转码后,因为在开始发送回任何数据之前可能需要一段时间)。

这没什么问题,只不过在当今的浏览器中,HTML5 音频或视频标记会在多个带有 Range 标头的 HTTP 请求中请求媒体文件。 有关详细信息,请参阅此问题

在上面链接的问题中,您可以看到 Safari 请求文件的奇怪块,包括结尾的几个字节。这会带来一个问题,即 Web 服务器必须等待转换完成,才能传递文件的最终字节以符合 Range 请求。

那么我的问题是,我的思路对吗?是否有更好的方法将转码内容传递到 标记,而无需等待整个转换完成?提前致谢!

So from my web server, I would like to use FFMPEG to transcode a media file for use with an HTML <audio> or <video> tag. Easy enough right?

The conversion would need to take place in real-time, when an HTTP client requested the converted file. Ideally the file would be streamed back to the HTTP client as it is being transcoded (and not afterwards at the end, since that would potentially take a while before any data starts being sent back).

This would be fine, except that in today's browsers, an HTML5 audio or video tag requests the media file in multiple HTTP requests with the Range header. See this question for details.

In that question linked above, you can see that Safari requests weird chunks of the file, including the ending few bytes. This poses a problem in that the web server WOULD have to wait for the conversion to finish, in order to deliver the final bytes of the file to conform to the Range request.

So my question is, is my train of thought right? Is there a better way to deliver transcoding content to an <audio> or <video> tag that wouldn't involve waiting for the entire conversion to finish? Thanks in advance!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

jJeQQOZ5 2024-09-24 19:37:49

我最近遇到了同样的问题,因为我想将我的库提供给浏览器。令人惊讶的是,通过 ffmpeg 发送流并即时交付的想法效果非常好。主要问题是支持查找...

接下来,您可以使用 Flask 在 Python 中找到代码片段来解决问题:

我们需要一个函数来流式传输内容:

@app.route('/media/<path:path>.ogv')
def media_content_ogv(path):
    d= os.path.abspath( os.path.join( config.media_folder, path ) )
    if not os.path.isfile( d ): abort(404)
    start= request.args.get("start") or 0
    def generate():
        cmdline= list()
        cmdline.append( config.ffmpeg )
        cmdline.append( "-i" )
        cmdline.append( d );
        cmdline.append( "-ss" )
        cmdline.append( str(start) );
        cmdline.extend( config.ffmpeg_args )
        print cmdline
        FNULL = open(os.devnull, 'w')
        proc= subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=FNULL )
        try:
            f= proc.stdout
            byte = f.read(512)
            while byte:
                yield byte
                byte = f.read(512)
        finally:
            proc.kill()

    return Response(response=generate(),status=200,mimetype='video/ogg',headers={'Access-Control-Allow-Origin': '*', "Content-Type":"video/ogg","Content-Disposition":"inline","Content-Transfer-Enconding":"binary"})

然后我们需要一个函数来返回持续时间:

@app.route('/media/<path:path>.js')
def media_content_js(path):
    d= os.path.abspath( os.path.join( config.media_folder, path ) )
    if not os.path.isfile( d ): abort(404)
    cmdline= list()
    cmdline.append( config.ffmpeg )
    cmdline.append( "-i" )
    cmdline.append( d );
    duration= -1
    FNULL = open(os.devnull, 'w')
    proc= subprocess.Popen( cmdline, stderr=subprocess.PIPE, stdout=FNULL )
    try:
        for line in iter(proc.stderr.readline,''):
            line= line.rstrip()
            #Duration: 00:00:45.13, start: 0.000000, bitrate: 302 kb/s
            m = re.search('Duration: (..):(..):(..)\...', line)
            if m is not None: duration= int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3)) + 1
    finally:
        proc.kill()

    return jsonify(duration=duration)

最后,我们破解它使用 videojs 转换为 HTML5:

<!DOCTYPE html>
<html>
<head>
    <link href="//vjs.zencdn.net/4.5/video-js.css" rel="stylesheet">
    <script src="//vjs.zencdn.net/4.5/video.js"></script>
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
</head>
<body>
    <video id="video" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264">
    </video>
    <script>
        var video= videojs('video');
        video.src("media/testavi.avi.ogv");

        // hack duration
        video.duration= function() { return video.theDuration; };
        video.start= 0;
        video.oldCurrentTime= video.currentTime;
        video.currentTime= function(time) 
        { 
            if( time == undefined )
            {
                return video.oldCurrentTime() + video.start;
            }
            console.log(time)
            video.start= time;
            video.oldCurrentTime(0);
            video.src("media/testavi.avi.ogv?start=" + time);
            video.play();
            return this;
        };

        $.getJSON( "media/testavi.avi.js", function( data ) 
        {
            video.theDuration= data.duration;
        });
    </script>
</body>

可以在 https://github.com/derolf/transcoder 找到一个工作示例。

德罗

I recently run into the same issue since I want to serve my library to browsers. Surprisingly, the idea to send the stream through ffmpeg and deliver on the fly works quite well. The primary problem was to support seeking...

Following, you find code sniplets in Python using Flask to solve the problem:

We need a function to stream the content:

@app.route('/media/<path:path>.ogv')
def media_content_ogv(path):
    d= os.path.abspath( os.path.join( config.media_folder, path ) )
    if not os.path.isfile( d ): abort(404)
    start= request.args.get("start") or 0
    def generate():
        cmdline= list()
        cmdline.append( config.ffmpeg )
        cmdline.append( "-i" )
        cmdline.append( d );
        cmdline.append( "-ss" )
        cmdline.append( str(start) );
        cmdline.extend( config.ffmpeg_args )
        print cmdline
        FNULL = open(os.devnull, 'w')
        proc= subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=FNULL )
        try:
            f= proc.stdout
            byte = f.read(512)
            while byte:
                yield byte
                byte = f.read(512)
        finally:
            proc.kill()

    return Response(response=generate(),status=200,mimetype='video/ogg',headers={'Access-Control-Allow-Origin': '*', "Content-Type":"video/ogg","Content-Disposition":"inline","Content-Transfer-Enconding":"binary"})

Then we need a function to return the duration:

@app.route('/media/<path:path>.js')
def media_content_js(path):
    d= os.path.abspath( os.path.join( config.media_folder, path ) )
    if not os.path.isfile( d ): abort(404)
    cmdline= list()
    cmdline.append( config.ffmpeg )
    cmdline.append( "-i" )
    cmdline.append( d );
    duration= -1
    FNULL = open(os.devnull, 'w')
    proc= subprocess.Popen( cmdline, stderr=subprocess.PIPE, stdout=FNULL )
    try:
        for line in iter(proc.stderr.readline,''):
            line= line.rstrip()
            #Duration: 00:00:45.13, start: 0.000000, bitrate: 302 kb/s
            m = re.search('Duration: (..):(..):(..)\...', line)
            if m is not None: duration= int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3)) + 1
    finally:
        proc.kill()

    return jsonify(duration=duration)

And finally, we hack that into HTML5 using videojs:

<!DOCTYPE html>
<html>
<head>
    <link href="//vjs.zencdn.net/4.5/video-js.css" rel="stylesheet">
    <script src="//vjs.zencdn.net/4.5/video.js"></script>
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
</head>
<body>
    <video id="video" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264">
    </video>
    <script>
        var video= videojs('video');
        video.src("media/testavi.avi.ogv");

        // hack duration
        video.duration= function() { return video.theDuration; };
        video.start= 0;
        video.oldCurrentTime= video.currentTime;
        video.currentTime= function(time) 
        { 
            if( time == undefined )
            {
                return video.oldCurrentTime() + video.start;
            }
            console.log(time)
            video.start= time;
            video.oldCurrentTime(0);
            video.src("media/testavi.avi.ogv?start=" + time);
            video.play();
            return this;
        };

        $.getJSON( "media/testavi.avi.js", function( data ) 
        {
            video.theDuration= data.duration;
        });
    </script>
</body>

A working example can be found at https://github.com/derolf/transcoder .

dero

淡淡绿茶香 2024-09-24 19:37:49

感谢卡米洛的回复。我仔细研究了有关 Range 请求的 HTTP 规范,发现:

The header SHOULD indicate the total length of the full entity-body, unless
this length is unknown or difficult to determine. The asterisk "*" character
means that the instance-length is unknown at the time when the response was
generated.

所以这实际上只是测试浏览器在回复 Content-Range: bytes 0-1/* 时如何反应的问题,例如。我会让你知道发生了什么。

Thanks for the reply Camilo. I took a closer look at the HTTP spec regarding the Range request and found:

The header SHOULD indicate the total length of the full entity-body, unless
this length is unknown or difficult to determine. The asterisk "*" character
means that the instance-length is unknown at the time when the response was
generated.

So it's really just a matter of testing how the browsers react when replying with a Content-Range: bytes 0-1/*, for example. I'll let you know what happens.

韵柒 2024-09-24 19:37:49

我知道这是一个旧线程,但如果有人发现它并需要帮助,我无论如何都会发布它。

“user3612643”的答案是正确的,解决了寻找问题。然而,这引入了一个新问题。当前时间不再正确。要解决此问题,我们必须复制原始的 currentTime 函数。

现在,每次 video.js 调用 currentTime (不带参数)时,它都会调用 oldCurrentTime,这是原始的 currentTime 函数。其余的与“user3612643”的答案相同(谢谢!)。这适用于最新的 video.js (7.7.6)

    video = videojs("video");
    video.src({
      src: 'http://localhost:4000/api/video/sdf',
      type: 'video/webm'
    });


     // hack duration
     video.duration= function() {return video.theDuration; };
     video.start= 0;

     // The original code for "currentTime"
     video.oldCurrentTime = function currentTime(seconds) {
      if (typeof seconds !== 'undefined') {
        if (seconds < 0) {
          seconds = 0;
        }

        this.techCall_('setCurrentTime', seconds);
        return;
      }
      this.cache_.currentTime = this.techGet_('currentTime') || 0;
      return this.cache_.currentTime;
    }

      // Our modified currentTime
     video.currentTime= function(time) 
     { 
         if( time == undefined )
         {
             return video.oldCurrentTime() + video.start;
         }
         video.start= time;
         video.oldCurrentTime(0);
         video.src({
           src: "http://localhost:4000/api/video/sdf?start=" + time,
           type: 'video/webm'
          });
         video.play();
         return this;
     };

     // Get the dureation of the movie
     $.getJSON( "http://localhost:4000/api/video/sdf/getDuration", function( data ) 
     {
         video.theDuration= data.duration;
     });

I know this is an old thread but I post it anyway if someone finds this and need help.

'user3612643' answer is correct, that fixes the seek problem. However that introduces a new problem. The current time is no longer correct. To fix this we have to copy the original currentTime function.

Now everytime video.js calls currentTime (with no parameters) it will call oldCurrentTime which is the original currentTime function. The rest is the same as 'user3612643's answer (Thanks!). This works with the newest video.js (7.7.6)

    video = videojs("video");
    video.src({
      src: 'http://localhost:4000/api/video/sdf',
      type: 'video/webm'
    });


     // hack duration
     video.duration= function() {return video.theDuration; };
     video.start= 0;

     // The original code for "currentTime"
     video.oldCurrentTime = function currentTime(seconds) {
      if (typeof seconds !== 'undefined') {
        if (seconds < 0) {
          seconds = 0;
        }

        this.techCall_('setCurrentTime', seconds);
        return;
      }
      this.cache_.currentTime = this.techGet_('currentTime') || 0;
      return this.cache_.currentTime;
    }

      // Our modified currentTime
     video.currentTime= function(time) 
     { 
         if( time == undefined )
         {
             return video.oldCurrentTime() + video.start;
         }
         video.start= time;
         video.oldCurrentTime(0);
         video.src({
           src: "http://localhost:4000/api/video/sdf?start=" + time,
           type: 'video/webm'
          });
         video.play();
         return this;
     };

     // Get the dureation of the movie
     $.getJSON( "http://localhost:4000/api/video/sdf/getDuration", function( data ) 
     {
         video.theDuration= data.duration;
     });
哆啦不做梦 2024-09-24 19:37:49

AFAIK 你可以在 ffmpeg 中编码到标准输出。因此,您可以将 HTTP 服务器配置为:

  • 在收到 GET 时开始编码以进行缓存。
  • 将请求的字节范围流传输到客户端。
  • 填充缓冲区并将其用于后续范围。

我一无所知,但我认为你可以在不知道最终流长度的情况下逃脱。

顺便说一句,我认为这很容易受到 DoS 攻击。

AFAIK you can encode to stdout in ffmpeg. So you could configure your HTTP server to:

  • start encoding to cache when GET recieved.
  • stream requested range of bytes to client.
  • filling the buffer and using it for subsequent ranges.

I'm clueless but I think you can get away without knowing the final stream's lenght.

On a side note, I think this is prone to DoS.

屌丝范 2024-09-24 19:37:49

这应该可以通过 VLC 实现,我可以通过将 VLC 设置为托管大型 avi 文件来使其工作并将其转码为 OGG,然后我的 html5 引用了该流:

<source src="http://localhost:8081/stream.ogg">

它能够在 vlc 中转码,并在我的 chrome 浏览器和 Android 手机上渲染得很好,但我最终采用了 不同的解决方案,而不是创建我自己的网络应用程序来托管我的媒体收藏并为请求的文件创建流 - 我看了看,但找不到已经存在的免费流,它以我需要/喜欢的方式做到了这一点。

This should be doable via VLC, I was able to get it to work by setting VLC to host a large avi file and transcode it to OGG, then my html5 referenced the stream:

<source src="http://localhost:8081/stream.ogg">

It was able to transcode in vlc, and render just fine in my chrome browser and on my android phone, but I ended up taking a different solution rather than going through the work of creating my own webapp to host my media collection and create streams for requested files - I looked and couldn't find a free one already out there that did it in a way I needed/liked.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文