javascript - attribute - Transmisión de audio HTML5: ¿medir con precisión la latencia?
html title attribute (3)
Estoy creando una aplicación web multiplataforma donde el audio se genera sobre la marcha en el servidor y se transmite en vivo a un cliente de navegador, probablemente a través del elemento de audio HTML5. En el navegador, tendré animaciones controladas por Javascript que deben sincronizarse con precisión con el audio reproducido. "Preciso" significa que el audio y la animación deben estar dentro de un segundo el uno del otro, y con suerte dentro de 250 ms (piense en la sincronización de labios). Por varias razones, no puedo hacer el audio y la animación en el servidor y transmitir en vivo el video resultante.
Idealmente, habría poca o ninguna latencia entre la generación de audio en el servidor y la reproducción de audio en el navegador, pero entiendo que la latencia será difícil de controlar y probablemente en el rango de 3-7 segundos (navegador, entorno- , dependiente de la red y la fase de la luna). Sin embargo, puedo manejar eso si puedo medir con precisión la latencia real sobre la marcha para que mi Javascript del navegador sepa cuándo presentar el marco animado adecuado.
Entonces, necesito medir con precisión la latencia entre mi entrega de audio al servidor de transmisión (Icecast?) Y el audio que sale de los altavoces en la computadora que aloja el altavoz. Algunas posibilidades de cielo azul:
-
Agregue metadatos al flujo de audio y analícelo desde el audio en reproducción (entiendo que esto no es posible usando el elemento de audio estándar)
-
Agregue breves períodos de silencio puro al audio y luego detecte en el navegador (¿pueden los elementos de audio producir las muestras de audio reales?)
-
Consulte al servidor y al navegador sobre las diferentes profundidades del búfer
-
Decodifique el audio transmitido en Javascript y luego tome los metadatos
¿Alguna idea de cómo podría hacer esto?
Lo que intentaría es crear primero una marca de tiempo con performance.now , procesar los datos y grabarlos en un blob con la nueva API de grabadora web.
La grabadora web le pedirá acceso al usuario a su tarjeta de audio, esto puede ser un problema para su aplicación, pero parece obligatorio obtener la latencia real.
Tan pronto como se haga esto, hay muchas maneras de medir la latencia real entre la generación y el renderizado real. Básicamente, un evento de sonido.
Para mayor referencia y ejemplo:
https://github.com/mdn/web-dictaphone/
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder_API/Using_the_MediaRecorder_API
No hay forma de medir la latencia directamente, pero cualquier AudioElement genera eventos como ''reproducir'' si solo se reproduce (se dispara con bastante frecuencia), o ''se detiene'' si se detiene la transmisión, o ''espera'' si se están cargando datos. Entonces, lo que puede hacer es manipular su video en función de estos eventos.
Por lo tanto, juegue mientras está parado o esperando se dispara, luego continúe reproduciendo video si se vuelve a disparar.
Pero le aconsejo que verifique otros eventos que puedan afectar su flujo (el error, por ejemplo, sería importante para usted).
https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement
Utilice el evento
timeupdate
del elemento
<audio>
, que se dispara de tres a cuatro veces por segundo, para realizar animaciones precisas durante la transmisión de medios al verificar
.currentTime
del elemento
<audio>
.
Donde se pueden iniciar o detener animaciones o transiciones hasta varias veces por segundo.
Si está disponible en el navegador, puede usar
fetch()
para solicitar un recurso de audio, en
.then()
return
response.body.getReader()
que devuelve un
ReadableStream
del recurso;
cree un nuevo objeto
MediaSource
, establezca
<audio>
o
new Audio()
.src
en
objectURL
de
MediaSource
;
agregue los fragmentos de la primera secuencia en
.read()
encadenados
.then()
a
sourceBuffer
de
MediaSource
con
.mode
establecido en
"sequence"
;
agregue el resto de fragmentos a
sourceBuffer
en los eventos de
sourceBuffer
updateend
.
Si
fetch()
response.body.getReader()
no está disponible en el navegador, aún puede utilizar
timeupdate
o evento de
progress
del elemento
<audio>
para verificar
.currentTime
, iniciar o detener animaciones o transiciones en el segundo requerido de reproducción de medios de transmisión.
Use el evento
canplay
del elemento
<audio>
para reproducir medios cuando la transmisión haya acumulado buffers adecuados en
MediaSource
para continuar con la reproducción.
Puede usar un objeto con propiedades establecidas en números correspondientes a
.currentTime
de
<audio>
donde debe producirse la animación, y valores establecidos en la propiedad
css
del elemento que debe animarse para realizar animaciones precisas.
En
javascript
continuación, las animaciones ocurren cada veinte segundos, comenzando en
0
y cada sesenta segundos hasta que finaliza la reproducción de medios.
<!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>