HTML5音频流:精确测量延迟?

时间:2016-08-04 13:13:41

标签: javascript html5 streaming html5-audio

我正在构建一个跨平台的Web应用程序,其中音频在服务器上即时生成,并且可能通过HTML5音频元素直播到浏览器客户端。在浏览器上,我将使用Javascript驱动的动画,这些动画必须与播放的音频精确同步。 "精确"意味着音频和动画必须在彼此之间,并且希望在250ms内(想想唇形同步)。出于各种原因,我无法在服务器上播放音频和动画,并直接生成视频。

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

因此,我需要精确测量我将音频传输到流媒体服务器(Icecast?)之间的延迟,以及来自扬声器主机上扬声器的音频。一些蓝天的可能性:

  • 向音频流添加元数据,并从播放音频中解析(我明白这不可能使用标准音频元素)

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

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

  • 在Javascript中解码流式音频,然后获取元数据

有关如何做到这一点的想法吗?

3 个答案:

答案 0 :(得分:7)

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

如果在浏览器中可用,您可以.then()使用response.body.getReader()请求音频资源,ReadableStream返回MediaSource,返回<audio>资源;创建一个新的new Audio()对象,将.srcobjectURL MediaSource设置为.read()的{​​{1}};将.then() sourceBuffer MediaSource .mode追加到"sequence" sourceBuffer并将sourceBuffer设置为updateend的第一个流块;在fetch() response.body.getReader()次事件中将剩余的块附加到timeupdate

如果浏览器无法使用progress <audio>,您仍然可以使用.currentTimecanplay<audio>元素事件来检查MediaSource ,在所需的第二个流媒体播放时启动或停止动画或转换。

当流在.currentTime累积了足够的缓冲区以继续播放时,使用<audio>元素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> 应该出现动画,并将值设置为$dt = Carbon::today(); // Use today to display only date 元素的属性,应该动画以执行精确动画。

return false; 以下,动画会每隔20秒发生一次,从<textarea name="editor1" id="editor1" rows="10" cols="80">--</textarea> 开始,每隔60秒发生一次,直到媒体播放结束。

CKEDITOR.replace( 'editor1' ); //Replace text area with CKEditor

CKEDITOR.instances.editor1.on( 'save', function( evt ) {    
        alert(evt.editor.getData());
        return false; //Prevents Page Refresh
        });

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

答案 1 :(得分:1)

您无法直接测量延迟,但任何AudioElement都会生成诸如“播放”之类的事件。如果它只是玩(经常被解雇),或者“停滞不前”#39;如果停止流式传输,或“等待”#39;如果正在加载数据。所以你能做的就是根据这些事件操纵你的视频。

所以在停顿或等待被解雇时播放,然后如果再次开枪就继续播放视频。

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

reference manual

答案 2 :(得分:0)

我会尝试首先使用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