简体   繁体   English

HTML5<audio> /<video> 并使用 FFMPEG 实时转码

[英]HTML5 <audio>/<video> and live transcoding with FFMPEG

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.因此,从我的 Web 服务器,我想使用 FFMPEG 对媒体文件进行转码,以便与 HTML <audio><video>标签一起使用。 Easy enough right?够简单了吧?

The conversion would need to take place in real-time, when an HTTP client requested the converted file.当 HTTP 客户端请求转换后的文件时,转换需要实时进行。 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).理想情况下,文件将在被转码时流回 HTTP 客户端(而不是在最后,因为这可能需要一段时间才能开始发送任何数据)。

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.这很好,除了在今天的浏览器中,HTML5 音频或视频标签在多个带有Range标头的 HTTP 请求中请求媒体文件。 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.在上面链接的那个问题中,您可以看到 Safari 请求文件的奇怪块,包括结尾的几个字节。 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.这带来了一个问题,因为 Web 服务器必须等待转换完成,才能传递文件的最终字节以符合Range请求。

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?是否有更好的方法将转码内容传送到<audio><video>标签,而无需等待整个转换完成? Thanks in advance!提前致谢!

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.令人惊讶的是,通过 ffmpeg 发送流并即时交付的想法非常有效。 The primary problem was to support seeking...主要问题是支持寻求...

Following, you find code sniplets in Python using Flask to solve the problem:下面,你使用Flask在Python中找到代码片段来解决问题:

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:最后,我们使用 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>

A working example can be found at https://github.com/derolf/transcoder .可以在https://github.com/derolf/transcoder找到一个工作示例。

dero德罗

Thanks for the reply Camilo .谢卡米洛的回复。 I took a closer look at the HTTP spec regarding the Range request and found:我仔细查看了有关 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.

So it's really just a matter of testing how the browsers react when replying with a Content-Range: bytes 0-1/* , for example.因此,这实际上只是测试浏览器在回复Content-Range: bytes 0-1/*时如何反应的问题,例如。 I'll let you know what happens.我会让你知道会发生什么。

AFAIK you can encode to stdout in ffmpeg. AFAIK 您可以在 ffmpeg 中编码为标准输出。 So you could configure your HTTP server to:因此,您可以将 HTTP 服务器配置为:

  • start encoding to cache when GET recieved.收到 GET 时开始编码以缓存。
  • 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.附带说明一下,我认为这很容易发生 DoS。

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. 'user3612643' 答案是正确的,解决了搜索问题。 However that introduces a new problem.然而,这引入了一个新问题。 The current time is no longer correct.当前时间不再正确。 To fix this we have to copy the original currentTime function.为了解决这个问题,我们必须复制原始的currentTime函数。

Now everytime video.js calls currentTime (with no parameters) it will call oldCurrentTime which is the original currentTime function.现在,每次 video.js 调用currentTime (不带参数)时,它都会调用oldCurrentTime ,这是原始的currentTime函数。 The rest is the same as 'user3612643's answer (Thanks!).其余的与'user3612643's answer相同(谢谢!)。 This works with the newest video.js (7.7.6)这适用于最新的 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;
     });

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

<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.它能够在 vlc 中转码,并在我的 chrome 浏览器和我的 android 手机上呈现得很好,但我最终采用了不同的解决方案,而不是通过创建我自己的 web 应用程序来托管我的媒体收藏并创建流请求的文件 - 我看了看,找不到一个已经在那里以我需要/喜欢的方式完成的免费文件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM