【问题标题】:HTML5 audio streaming: precisely measure latency?HTML5 音频流:精确测量延迟?
【发布时间】:2016-08-04 13:13:41
【问题描述】:

我正在构建一个跨平台网络应用程序,其中音频在服务器上即时生成,并可能通过 HTML5 音频元素实时流式传输到浏览器客户端。在浏览器上,我将拥有 Javascript 驱动的动画,这些动画必须与播放的音频精确同步。 “精确”意味着音频和动画必须在一秒钟之内,并希望在 250 毫秒内(想想口型同步)。由于各种原因,我无法在服务器上制作音频和动画并直播生成的视频。

理想情况下,服务器上的音频生成和浏览器上的音频播放之间几乎没有延迟,但我的理解是延迟将难以控制,可能在 3-7 秒范围内(浏览器- 、环境、网络和月相相关)。不过,如果我可以实时精确测量实际延迟,以便我的浏览器 Javascript 知道何时呈现正确的动画帧,我可以处理这个问题。

因此,我需要精确测量将音频传送到流媒体服务器(Icecast?)与从托管扬声器的计算机上的扬声器发出的音频之间的延迟。一些蓝天的可能性:

  • 将元数据添加到音频流中,并从正在播放的音频中解析它(我知道使用标准音频元素是不可能的)

  • 为音频添加短暂的纯静默时间,然后在浏览器上检测它们(音频元素能否产生实际的音频样本?)

  • 向服务器和浏览器查询各种缓冲区深度

  • 用 Javascript 解码流式音频,然后抓取元数据

关于我如何做到这一点的任何想法?

【问题讨论】:

  • 我认为没有办法衡量差异,所以我通常使用WebAudioAPI在浏览器中分析流客户端。使用该技术,您可以制作与在浏览器中播放的 Icecast 流密切相关的动画(例如均衡器)。

标签: javascript html streaming html5-audio


【解决方案1】:

利用<audio> 元素的timeupdate 事件(每秒触发三到四次)通过检查.currentTime<audio> 元素在媒体流期间执行精确的动画。每秒最多可以启动或停止几次动画或过渡。

如果在浏览器可用,您可以使用fetch() 请求音频资源,在.then() 返回response.body.getReader(),它返回资源的ReadableStream;创建一个新的MediaSource 对象,将MediaSource<audio>new Audio() .src 设置为objectURL;将.read()处的第一个流块附加到MediaSource.then()链接sourceBuffer.mode设置为"sequence";在 sourceBuffer updateend events 处将剩余的块附加到 sourceBuffer

如果fetch() response.body.getReader() 在浏览器中不可用,您仍然可以使用<audio> 元素的timeupdateprogress 事件来检查.currentTime,在流式传输所需的第二秒开始或停止动画或过渡媒体播放。

当流在MediaSource 累积足够的缓冲区以继续播放时,使用<audio> 元素的canplay 事件播放媒体。

您可以使用属性设置为对应于应发生动画的<audio>.currentTime 的数字的对象,并将值设置为应设置动画以执行精确动画的元素的css 属性。

在下面的javascript,动画每二十秒出现一次,从0开始,每六十秒出现一次,直到媒体播放结束。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">    
<head>
  <meta charset="utf-8" />
  <title></title>
  <style>
    body {
      width: 90vw;
      height: 90vh;
      background: #000;
      transition: background 1s;
    }

    span {
      font-family: Georgia;
      font-size: 36px;
      opacity: 0;
    }
  </style>
</head>

<body>
  <audio controls></audio>
  <br>
  <span></span>
  <script type="text/javascript">
    window.onload = function() {
      var url = "/path/to/audio";
      // given 240 seconds total duration of audio 
      // 240/12 = 20
      // properties correspond to `<audio>` `.currentTime`,
      // values correspond to color to set at element
      var colors = {
        0: "red",
        20: "blue",
        40: "green",
        60: "yellow",
        80: "orange",
        100: "purple",
        120: "violet",
        140: "brown",
        160: "tan",
        180: "gold",
        200: "sienna",
        220: "skyblue"
      };
      var body = document.querySelector("body");
      var mediaSource = new MediaSource;
      var audio = document.querySelector("audio");
      var span = document.querySelector("span");
      var color = window.getComputedStyle(body)
                  .getPropertyValue("background-color");
      //console.log(mediaSource.readyState); // closed
      var mimecodec = "audio/mpeg";

      audio.oncanplay = function() {
        this.play();
      }

      audio.ontimeupdate = function() {         
        // 240/12 = 20
        var curr = Math.round(this.currentTime);

        if (colors.hasOwnProperty(curr)) {
          // set `color` to `colors[curr]`
          color = colors[curr]
        }
        // animate `<span>` every 60 seconds
        if (curr % 60 === 0 && span.innerHTML === "") {
          var t = curr / 60;
          span.innerHTML = t + " minute" + (t === 1 ? "" : "s") 
                           + " of " + Math.round(this.duration) / 60 
                          + " minutes of audio";
          span.animate([{
              opacity: 0
            }, {
              opacity: 1
            }, {
              opacity: 0
            }], {
              duration: 2500,
              iterations: 1
            })
            .onfinish = function() {
              span.innerHTML = ""
            }
        }
        // change `background-color` of `body` every 20 seconds
        body.style.backgroundColor = color;
        console.log("current time:", curr
                   , "current background color:", color
                  , "duration:", this.duration);
      }
      // set `<audio>` `.src` to `mediaSource`
      audio.src = URL.createObjectURL(mediaSource);
      mediaSource.addEventListener("sourceopen", sourceOpen);

      function sourceOpen(event) {
        // if the media type is supported by `mediaSource`
        // fetch resource, begin stream read, 
        // append stream to `sourceBuffer`
        if (MediaSource.isTypeSupported(mimecodec)) {
          var sourceBuffer = mediaSource.addSourceBuffer(mimecodec);
          // set `sourceBuffer` `.mode` to `"sequence"`
          sourceBuffer.mode = "sequence";

          fetch(url)
          // return `ReadableStream` of `response`
          .then(response => response.body.getReader())
          .then(reader => {

            var processStream = (data) => {
              if (data.done) {
                  return;
              }
              // append chunk of stream to `sourceBuffer`
              sourceBuffer.appendBuffer(data.value);
            }
            // at `sourceBuffer` `updateend` call `reader.read()`,
            // to read next chunk of stream, append chunk to 
            // `sourceBuffer`
            sourceBuffer.addEventListener("updateend", function() {
              reader.read().then(processStream);
            });
            // start processing stream
            reader.read().then(processStream);
            // do stuff `reader` is closed, 
            // read of stream is complete
            return reader.closed.then(() => {
              // signal end of stream to `mediaSource`
              mediaSource.endOfStream();
              return  mediaSource.readyState;
            })
          })
          // do stuff when `reader.closed`, `mediaSource` stream ended
          .then(msg => console.log(msg))
        } 
        // if `mimecodec` is not supported by `MediaSource`  
        else {
          alert(mimecodec + " not supported");
        }
      };
    }
  </script>
</body>
</html>

plnkrhttp://plnkr.co/edit/fIm1Qp?p=preview

【讨论】:

【解决方案2】:

您无法直接测量延迟时间,但任何 AudioElement 都会生成事件,例如,如果它刚刚播放(经常触发),则会生成“正在播放”,如果停止流式传输,则会生成“停止”,或者如果正在加载数据,则会生成“等待”。所以你可以做的是根据这些事件来操作你的视频。

所以在停止或等待被触发时播放,如果再次触发则继续播放视频。

但我建议您检查可能影响您的流程的其他事件(例如错误对您很重要)。

https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement

【讨论】:

    【解决方案3】:

    我会尝试首先使用performance.now 创建一个时间戳,处理数据,然后使用新的网络记录器 api 将其记录在一个 blob 中。

    网络记录器将要求用户访问他的声卡,这对您的应用来说可能是个问题,但似乎必须获得真正的延迟。

    一旦完成,就有很多方法可以测量生成和实际渲染之间的实际延迟。基本上,一个声音事件。

    更多参考和示例:

    Recorder demo

    https://github.com/mdn/web-dictaphone/

    https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder_API/Using_the_MediaRecorder_API

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-09-02
      • 1970-01-01
      • 2012-09-16
      • 2013-06-12
      • 1970-01-01
      • 1970-01-01
      • 2013-12-09
      • 1970-01-01
      相关资源
      最近更新 更多