Dan*_*com 21 javascript streaming html5 html5-audio
我正在构建一个跨平台的Web应用程序,其中音频在服务器上即时生成,并且可能通过HTML5音频元素直播到浏览器客户端.在浏览器上,我将使用Javascript驱动的动画,这些动画必须与播放的音频精确同步."精确"意味着音频和动画必须在彼此之间,并且希望在250ms内(想想唇形同步).由于各种原因,我无法在服务器上进行音频和动画,并对生成的视频进行实时流式传输.
理想情况下,服务器上的音频生成和浏览器上的音频播放之间几乎没有或没有延迟,但我的理解是延迟将难以控制,并且可能在3-7秒范围内(浏览器,环境 - ,网络和月相依赖).但是,我可以处理这个问题,如果我可以在运行中精确测量实际延迟,以便我的浏览器Javascript知道何时呈现正确的动画帧.
那么,我需要精确测量我将音频传输到流媒体服务器(Icecast?)和来自扬声器主机上扬声器的音频之间的延迟.一些蓝天的可能性:
将元数据添加到音频流,并从播放音频中解析它(我知道使用标准音频元素是不可能的)
为音频添加短暂的纯静音时段,然后在浏览器上检测它们(音频元素可以产生实际的音频样本吗?)
查询服务器和浏览器的各种缓冲区深度
在Javascript中解码流式音频,然后获取元数据
有关如何做到这一点的任何想法?
利用timeupdate
的事件<audio>
元件,其被烧制每秒三至四次,通过检查流的媒体期间执行精确动画.currentTime
的<audio>
元素.动画或过渡可以每秒开始或停止多次.
如果在浏览器中可用,您可以使用fetch()
请求音频资源,.then()
返回时response.body.getReader()
返回ReadableStream
资源; 创建一个新MediaSource
对象,并设置<audio>
或new Audio()
.src
到objectURL
的MediaSource
; 附加在第一流块.read()
链式.then()
到sourceBuffer
的MediaSource
与.mode
被设置为"sequence"
; sourceBuffer
在sourceBuffer
updateend
活动中附加剩余的块.
如果fetch()
response.body.getReader()
在浏览器中不可用,您仍然可以使用timeupdate
或progress
事件<audio>
元素来检查.currentTime
,启动或停止动画或转换所需的第二个流媒体播放.
当流已累积足够的缓冲区以继续播放时,使用元素canplay
事件<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>
Run Code Online (Sandbox Code Playgroud)
plnkr http://plnkr.co/edit/fIm1Qp?p=preview
您无法直接测量延迟,但任何 AudioElement 都会生成诸如“正在播放”(如果刚刚播放)的事件(经常触发),或者“停滞”(如果停止流式传输),或者“等待”(如果正在加载数据)。因此,您可以做的是根据此事件操纵您的视频。
因此,在停止或等待被触发时播放,如果再次播放则继续播放视频。
但我建议您检查可能影响您的流程的其他事件(例如错误对您来说很重要)。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement