[英]HTML5 audio streaming: precisely measure latency?
我正在構建一個跨平台的Web應用程序,其中音頻在服務器上即時生成,並且可能通過HTML5音頻元素直播到瀏覽器客戶端。 在瀏覽器上,我將使用Javascript驅動的動畫,必須與播放的音頻精確同步。 “精確”意味着音頻和動畫必須在彼此之間,並且希望在250ms內(想想唇形同步)。 由於各種原因,我無法在服務器上進行音頻和動畫,並對生成的視頻進行實時流式傳輸。
理想情況下,服務器上的音頻生成和瀏覽器上的音頻播放之間幾乎沒有或沒有延遲,但我的理解是延遲將難以控制,並且可能在3-7秒范圍內(瀏覽器,環境 - ,網絡和月相依賴)。 但是,我可以處理這個問題,如果我可以在運行中精確測量實際延遲,以便我的瀏覽器Javascript知道何時呈現正確的動畫幀。
那么,我需要精確測量我將音頻傳輸到流媒體服務器(Icecast?)和來自揚聲器主機上揚聲器的音頻之間的延遲。 一些藍天的可能性:
將元數據添加到音頻流,並從播放音頻中解析它(我知道使用標准音頻元素是不可能的)
為音頻添加短暫的純靜音時段,然后在瀏覽器上檢測它們(音頻元素可以產生實際的音頻樣本嗎?)
查詢服務器和瀏覽器的各種緩沖區深度
在Javascript中解碼流式音頻,然后獲取元數據
有關如何做到這一點的任何想法?
利用<audio>
元素的timeupdate
事件,每秒觸發三到四次,通過檢查<audio>
元素的.currentTime
,在媒體流傳輸過程中執行精確的動畫。 動畫或過渡可以每秒開始或停止多次。
如果在瀏覽器中可用,則可以使用fetch()
來請求音頻資源,在.then()
返回response.body.getReader()
,它返回資源的ReadableStream
; 創建一個新的MediaSource
對象,將<audio>
或new Audio()
.src
objectURL
為MediaSource
objectURL
; 追加第一流塊在.read()
鏈.then()
到sourceBuffer
的MediaSource
與.mode
設置為"sequence"
; 在sourceBuffer
updateend
事件sourceBuffer
剩余的塊附加到sourceBuffer
。
如果fetch()
response.body.getReader()
在瀏覽器中不可用,您仍然可以使用timeupdate
或<audio>
元素的progress
事件來檢查.currentTime
,在所需的第二個流媒體播放時啟動或停止動畫或轉換。
當流在MediaSource
上累積了足夠的緩沖區以繼續播放時,使用canplay
事件的<audio>
元素播放媒體。
您可以使用一個對象,其屬性設置為與<audio>
.currentTime
相對應的.currentTime
,其中應該出現動畫,並將值設置為元素的css
屬性,該元素應該被動畫化以執行精確的動畫。
在下面的javascript
中,動畫每20秒發生一次,從0
開始,每60秒一次,直到媒體播放結束。
<!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>
您無法直接測量延遲,但任何AudioElement都會生成“播放”等事件,如果它剛播放(經常觸發),或者如果停止流式傳輸則“停頓”,或者如果數據正在加載則“等待”。 所以你能做的就是根據這些事件操縱你的視頻。
因此,在停頓或等待被解雇時播放,然后如果再次發射則繼續播放視頻。
但我建議您檢查可能影響您的流量的其他事件(例如,錯誤對您很重要)。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement
我會嘗試首先創建一個帶有performance.now的時間戳,處理數據,並使用新的web記錄器api將其記錄在blob中。
網絡錄音機將要求用戶訪問他的聲卡,這可能是您的應用程序的問題,但它似乎是必須獲得真正的延遲。
一旦完成,有很多方法可以測量生成和實際渲染之間的實際延遲。 基本上是一個聲音事件。
有關進一步參考和示例:
https://github.com/mdn/web-dictaphone/
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder_API/Using_the_MediaRecorder_API
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.