Supongamos que tenemos varios monitores. Y queríamos usar estos monitores como guirnalda. Por ejemplo, haz que parpadeen al mismo tiempo. O tal vez cambie sincrónicamente el color de acuerdo con algún tipo de algoritmo inteligente. Y qué pasa si lo haces en un navegador, entonces puedes conectar teléfonos inteligentes y tabletas a esto. Todo eso está a la mano.

Y, como estamos usando un navegador, también puede agregar diseño de sonido. Después de todo, si es lo suficientemente preciso como para sincronizar los dispositivos a tiempo, puede reproducir sonidos en cada uno como si un sistema multicanal sonara.
Lo que se puede encontrar al sincronizar Web Audio y relojes de juego dentro de una aplicación javascript; cuántas "horas" diferentes hay en javasctipt (¡tres!) y por qué se necesitan todas, así como una aplicación preparada para node.js debajo del gato.
Mira el reloj
Para cualquier guirnalda en línea condicional, se requiere una sincronización precisa del reloj. Después de todo, puede ignorar cualquier retraso de red (incluso intermitente). Es suficiente proporcionar a los comandos de control una marca de tiempo y generar estos comandos un poco "hacia el futuro". En los clientes, se almacenarán en el búfer y luego se ejecutarán sincrónicamente y a tiempo.
O incluso puede ir más allá: tome el viejo algoritmo aleatorio determinista y use una semilla común (emitida por el servidor una vez, cuando esté conectada) en todos los dispositivos. Si usa dicha semilla
junto con el tiempo exacto, puede determinar completamente el comportamiento del algoritmo en todos los dispositivos. Imagínese: de hecho, no necesita una red o un servidor para cambiar el estado de forma única y sincrónica. La semilla ya contiene la totalidad (condicionalmente infinita) "grabación de video" de acciones por adelantado. Lo principal es la hora exacta.

Cada método tiene sus límites de aplicabilidad. Con la entrada instantánea del usuario, por supuesto, no hay nada que hacer, queda transmitirlo "tal cual". Pero todo lo que se puede calcular es calcular. En mi implementación, utilizo los tres enfoques, dependiendo de la situación.
Subjetivo "al mismo tiempo"
Idealmente, todo debería sonar "al mismo tiempo": no se necesita más de ± 10 ms de discrepancia para el peor par entre los dispositivos combinados. No puede contar con esa precisión desde la hora del sistema, y los métodos estándar para sincronizar la hora utilizando el protocolo NTP no están disponibles en el navegador. Por lo tanto, manejaremos nuestro servidor de sincronización. El principio es simple: el casco "pings" y aceptar "pongs" con la marca de tiempo del servidor. Si hace esto muchas veces seguidas, puede nivelar estadísticamente el error y obtener el tiempo de retraso promedio.
Código: calcular la hora del servidor en el cliente Los sockets web y las soluciones basadas en él son los más adecuados porque no requieren tiempo para crear una conexión TCP y puede "comunicarse" con ellos en ambas direcciones. No UDP o ICMP, por supuesto, pero incomparablemente más rápido que una conexión en frío normal que utiliza la API HTTP. Por lo tanto, socket.io. Todo es muy fácil allí:
Código: implementación socket.io
Sería bueno, en lugar de calcular el promedio, calcular la mediana; esto mejorará la precisión con una conexión inestable. La elección de los métodos de filtrado depende del lector. Deliberadamente simplifico el código aquí a favor de los esquemas. Mi solución completa se puede encontrar en el repositorio. performance.now ()
Permítame recordarle que el objeto de
performance
es una API que proporciona acceso a un temporizador de alta resolución. Compara:
Date.now()
devuelve el número de milisegundos desde el 1 de enero de 1970, y lo hace en forma entera . Es decir, el error solo del redondeo es 0.5 ms en promedio. Por ejemplo, en una operación de resta ab
puede "perder" sin éxito hasta 2 ms. Además, histórica y conceptualmente, el medidor de tiempo en sí mismo no garantiza una alta precisión y está afilado para trabajar con una escala de tiempo más grande.performance.now()
devuelve el número de milisegundos desde que se abrió la página web .
Esta es una API relativamente reciente, "afilada" específicamente para la medición precisa de intervalos de tiempo. Devuelve un valor de punto flotante , teóricamente dando un nivel de precisión cercano a las capacidades del sistema operativo en sí.
Creo que esta información es conocida por casi todos los desarrolladores de JavaScript. Pero no todos saben eso ...
Espectro
Debido al sensacional ataque de sincronización de Specter en 2018, todo llega al punto de que el temporizador de alta resolución se endurecerá artificialmente si no hay otra solución al problema de vulnerabilidad. Firefox, comenzando con la versión 60, redondea el valor de este temporizador a un milisegundo, y Edge, aún peor.
Esto es lo que dice
MDN :
La marca de tiempo no es en realidad de alta resolución. Para mitigar las amenazas de seguridad como Spectre, los navegadores actualmente redondean los resultados en diversos grados. (Firefox comenzó a redondearse a 1 milisegundo en Firefox 60). Algunos navegadores también pueden aleatorizar ligeramente la marca de tiempo. La precisión puede mejorar nuevamente en futuras versiones; Los desarrolladores de navegadores aún están investigando estos ataques de tiempo y la mejor manera de mitigarlos.
Ejecutemos la prueba y echemos un vistazo a los gráficos. Este es el resultado de la prueba en un intervalo de 10 ms:
Código de prueba: medición del tiempo en un ciclo function measureTimesLoop(length) { const d = new Array(length); const p = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); } return { d, p } }
Date.now()
performance.now()
Borde

estadísticasVersión del navegador: 44.17763.771.0
Date.now ()
intervalo promedio: 1.0538336052202284 ms
desviación del intervalo promedio, RMS: 0.7547819181245603 ms
mediana del intervalo: 1 ms
performance.now ()
intervalo promedio: 1.567100970873786 ms
desviación del intervalo promedio, RMS: 0.6748006785171455 ms
mediana del intervalo: 1.5015000000003056 ms
Firefox

estadísticasVersión del navegador: 71.0
Date.now ()
intervalo promedio: 1.0168350168350169 ms
desviación del intervalo promedio, RMS: 0.21645930182417966 ms
mediana del intervalo: 1 ms
performance.now ()
intervalo promedio: 1.0134453781512605 ms
desviación del intervalo promedio, RMS: 0.1734108492762375 ms
mediana del intervalo: 1 ms
Cromo

estadísticasVersión del navegador: 79.0.3945.88
Date.now ()
intervalo promedio: 1.02442996742671 ms
desviación del intervalo promedio, RMS: 0.49858684744444 ms
mediana del intervalo: 1 ms
performance.now ()
intervalo promedio: 0.005555847229948915 ms
desviación del intervalo promedio, RMS: 0.027497846727194235 ms
mediana del intervalo: 0.0050000089686363935 ms
Ok, Chrome, haz zoom a 1 ms.

Por lo tanto, Chrome todavía está aguantando, y su implementación de
performance.now()
aún no se ha estrangulado y el paso es hermoso 0.005 ms. ¡Bajo Edge, el temporizador
performance.now()
es más duro que
Date.now()
! En Firefox, ambos temporizadores tienen la misma precisión de milisegundos.
En esta etapa, ya se pueden sacar algunas conclusiones. Pero hay otro temporizador en javascript (sin el cual no podemos prescindir).
WebAudio API Timer
Esta es una bestia ligeramente diferente. Se utiliza para colas de audio retrasadas. El hecho es que los eventos de audio (reproducción de notas, gestión de efectos) no pueden depender de herramientas de JavaScript asíncronas estándar:
setInterval
y
setTimeout
, debido a su error demasiado grande. Y esto no es solo el error
de los valores del temporizador (con los que tratamos anteriormente), sino que es el error con el que la máquina de eventos ejecuta eventos. Y ya es algo alrededor de 5-25 ms, incluso en condiciones de invernadero.
Gráficos para el caso asíncrono debajo del spoilerEl resultado de la prueba en un intervalo de 100 ms:
Código de prueba: medición del tiempo en un ciclo asincrónico function pause(duration) { return new Promise((resolve) => { setInterval(() => { resolve(); }, duration); }); } async function measureTimesInAsyncLoop(length) { const d = new Array(length); const p = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); await pause(1); } return { d, p } }
Date.now()
performance.now()
Borde

estadísticasVersión del navegador: 44.17763.771.0
Date.now ()
intervalo promedio: 25.595959595959595 ms
desviación del intervalo promedio, RMS: 10.12639235162126 ms
mediana del intervalo: 28 ms
performance.now ()
intervalo promedio: 25.862596938775525 ms
desviación del intervalo promedio, RMS: 10.123711255512573 ms
Intervalo medio: 27.027099999999336 ms
Firefox

estadísticasVersión del navegador: 71.0
Date.now ()
intervalo promedio: 1.6914893617021276 ms
desviación del intervalo promedio, RMS: 0.6018870280772611 ms
mediana del intervalo: 2 ms
performance.now ()
intervalo promedio: 1.7865168539325842 ms
desviación del intervalo promedio, RMS: 0.6442818510935484 ms
mediana del intervalo: 2 ms
Cromo

estadísticasVersión del navegador: 79.0.3945.88
Date.now ()
intervalo promedio: 4.7878787878787888, ms
desviación del intervalo promedio, RMS: 0.7557553886872682 ms
mediana del intervalo: 5 ms
performance.now ()
intervalo promedio: 4.783989898979516 ms
desviación del intervalo promedio, RMS: 0.6483716900974945 ms
intervalo medio: 4.750000000058208 ms
Quizás alguien recordará las primeras aplicaciones experimentales de audio HTML. Antes de que WebAudio de pleno derecho llegara a los navegadores, todos parecían un poco borrachos, descuidados. Solo porque usaron
setTimeout
como secuenciador.
La API moderna de WebAudio, por el contrario, ofrece una resolución garantizada de hasta 0,02 ms (especulación basada en la frecuencia de muestreo de 44100Hz). Esto se debe al hecho de que se utiliza un mecanismo diferente para la reproducción de sonido diferido que
setTimeout
:
source.start(when);
De hecho, cualquier reproducción de una muestra de audio está "retrasada". Solo para perderlo "no se pospone", debe posponerlo "hasta ahora".
source.start(audioCtx.currentTime);
Acerca de la música generada por software en tiempo realSi toca una melodía sintetizada por programa a partir de notas, entonces estas notas deben agregarse a la cola de reproducción con un poco de anticipación. Luego, a pesar de todas las restricciones e irregularidades no fundamentales de los temporizadores, la melodía se reproducirá perfectamente.
En otras palabras, la melodía sintetizada en tiempo real no debe ser "inventada" en tiempo real, sino un poco por adelantado.
Un temporizador para gobernarlos a todos
Dado que
audioCtx.currentTime
tan estable y preciso, ¿tal vez deberíamos usarlo como la fuente principal de tiempo relativo? Ejecutemos la prueba nuevamente.
Código de prueba: medición de tiempo síncrono en un ciclo function measureTimesInLoop(length) { const d = new Array(length); const p = new Array(length); const a = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); a[i] = audioCtx.currentTime * 1000; } return { d, p, a } }
Date.now()
performance.now()
audioCtx.currentTime
Borde

estadísticasVersión del navegador: 44.17763.771.0
Date.now ()
intervalo promedio: 1.037037037037037 ms
desviación del intervalo promedio, RMS: 0.6166609846299806 ms
mediana del intervalo: 1 ms
performance.now ()
intervalo promedio: 1.5447103117505993 ms
desviación del intervalo promedio, RMS: 0.4390514285320851 ms
mediana del intervalo: 1.5015000000000782 ms
audioCtx.currentTime
intervalo promedio: 2.955751134714949 ms
desviación del intervalo promedio, RMS: 0.6193645611529503 ms
Intervalo medio: 2.902507781982422 ms
Firefox

estadísticasVersión del navegador: 71.0
Date.now ()
intervalo promedio: 1.005128205128205 ms
desviación del intervalo promedio, RMS: 0.12392867665225249 ms
mediana del intervalo: 1 ms
performance.now ()
intervalo promedio: 1.00513698630137 ms
desviación del intervalo promedio, RMS: 0.07148844433269844 ms
mediana del intervalo: 1 ms
audioCtx.currentTime
Firefox no actualiza el valor del temporizador de audio en el bucle de sincronización
Cromo

estadísticasVersión del navegador: 79.0.3945.88
Date.now ()
intervalo promedio: 1.0207612456747406 ms
desviación del intervalo promedio, RMS: 0.49870223457982504 ms
mediana del intervalo: 1 ms
performance.now ()
intervalo promedio: 0.005414502034674972 ms
desviación del intervalo promedio, RMS: 0.027441293974958335 ms
intervalo medio: 0.004999999873689376 ms
audioCtx.currentTime
intervalo promedio: 3.0877599266656963 ms
desviación del intervalo promedio, RMS: 1.1445555956407658 ms
mediana del intervalo: 2.9024943310650997 ms
Gráficos para el caso asíncrono debajo del spoilerCódigo de prueba: medición del tiempo en un ciclo asincrónicoEl resultado de la prueba en un intervalo de 100 ms:
function pause(duration) { return new Promise((resolve) => { setInterval(() => { resolve(); }, duration); }); } async function measureTimesInAsyncLoop(length) { const d = new Array(length); const p = new Array(length); const a = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); await pause(1); } return { d, p } }
Date.now()
performance.now()
audioCtx.currentTime
Borde

estadísticasVersión del navegador: 44.17763.771.0
Date.now ():
intervalo promedio: 24.505050505050505 ms
desviación del intervalo promedio: 11.513166584195204 ms
mediana del intervalo: 26 ms
performance.now ():
intervalo promedio: 24.50935757575754 ms
desviación del intervalo promedio: 11.679091435527388 ms
mediana del intervalo: 25.525499999999738 ms
audioCtx.currentTime:
intervalo promedio: 24.76005164944396 ms
desviación del intervalo promedio: 11.311571546205316 ms
mediana del intervalo: 26.121139526367187 ms
Firefox

estadísticasVersión del navegador: 71.0
Date.now ():
intervalo promedio: 1.6875 ms
desviación del intervalo promedio: 0.6663410663216448 ms
mediana del intervalo: 2 ms
performance.now ():
intervalo promedio: 1.7234042553191489 ms
desviación del intervalo promedio: 0.6588877688171075 ms
mediana del intervalo: 2 ms
audioCtx.currentTime:
intervalo promedio: 10.158730158730123 ms
desviación del intervalo promedio: 1.4512471655330046 ms
intervalo medio: 8.707482993195299 ms
Cromo

estadísticasVersión del navegador: 79.0.3945.88
Date.now ():
intervalo promedio: 4.585858585858586 ms
desviación del intervalo promedio: 0.9102125516015199 ms
mediana del intervalo: 5 ms
performance.now ():
intervalo promedio: 4.592424242424955 ms
desviación del intervalo promedio: 0.719936993603155 ms
Intervalo medio: 4.605000001902226 ms
audioCtx.currentTime:
intervalo promedio: 10.12648022171832 ms
desviación del intervalo promedio: 1.4508887886499262 ms
mediana del intervalo: 8.707482993197118 ms
Bueno, no funcionará. "Fuera", este temporizador es el más inexacto. Firefox no actualiza el valor del temporizador dentro del bucle. Pero en general: la resolución es de 3 ms y peor y notable jitter. Quizás el valor de
audioCtx.currentTime
refleja la posición en el búfer en anillo del controlador de la tarjeta de audio. En otras palabras, muestra el tiempo mínimo que aún es posible retrasar de forma segura la reproducción.
Y que hacer Después de todo, necesitamos un temporizador preciso para sincronizar con el servidor y lanzar eventos de JavaScript en la pantalla, ¡y un temporizador de audio para eventos de sonido!
Resulta que necesita sincronizar todos los temporizadores entre sí:
- Cliente
audioCtx.currentTime
con el performance.now()
cliente.now performance.now()
en el cliente. - Y cliente
performance.now()
con performance.now()
del lado del servidor.
Sincronizado, sincronizado

En general, esto es bastante divertido si lo piensa: puede tener dos buenas fuentes de tiempo A y B, cada una de las cuales es muy gruesa y ruidosa en la salida (A '= A + err
A ; B' = B + err
B ) para que pueda incluso ser inutilizable por sí solo. Pero la diferencia d entre las fuentes originales no ruidosas puede restaurarse con mucha precisión.
Dado que la distancia de tiempo real entre los relojes ideales es constante, tomando mediciones n veces, reduciremos el error de medición err n veces, respectivamente. A menos, por supuesto, que el reloj funcione a la misma velocidad.
Si no sincronizado
La mala noticia es que no, no van a la misma velocidad. Y no estoy hablando de la divergencia de horas en el servidor y en el cliente, esto es comprensible y esperado. Lo que es más inesperado:
audioCtx.currentTime
divergiendo gradualmente de
performance.now()
. Está dentro del cliente. Es posible que no nos demos cuenta, pero a veces, bajo carga, el sistema de audio puede no tragar una pequeña pieza de datos y (al contrario de la naturaleza del búfer en anillo) el tiempo de audio cambiará en relación con el tiempo del sistema. Esto no ocurre tan raramente, simplemente no afecta a muchas personas: pero si, por ejemplo, lanzas dos videos de YouTube al mismo tiempo simultáneamente en diferentes computadoras, no es un hecho que dejen de reproducirse al mismo tiempo. Y el punto, por supuesto, no está en la publicidad.
Por lo tanto, para un funcionamiento estable y sincrónico. Necesitamos revisar
periódicamente todos los relojes entre sí, utilizando la hora del servidor, como referencia. Y luego aparece el equilibrio en cuántas mediciones usar para promediar: cuanto más, más preciso, pero mayor es la posibilidad de que un salto brusco en
audioCtx.currentTime
caiga en la ventana de tiempo en la que filtramos los valores. Luego, si, por ejemplo, usamos la ventana de minutos, todos los minutos tendremos el tiempo transcurrido. La elección de los filtros es amplia:
exponencial ,
mediana ,
filtro de Kalman , etc. Pero esta compensación es en cualquier caso.
Ventana de tiempo
En el caso de sincronizar
audioCtx.currentTime
con
performance.now()
, en un bucle asíncrono, para no interferir con la interfaz de usuario, podemos tomar una medida, digamos, 100 ms.
Suponga que el error de medición err = errA + errB = 1 + 3 = 4 ms
En consecuencia, en 1 segundo podemos reducirlo a 0,4 ms, y en 10 segundos a 0,04 ms. Una mejora adicional del resultado no tiene sentido, y una buena ventana para el filtrado será: 1 - 10 segundos.
En el caso de la sincronización de red, los retrasos y los errores ya son mucho más significativos, pero no hay un salto repentino en el tiempo, como es el caso de
audioCtx.currentTime
. Y puede permitirse acumular estadísticas realmente excelentes. Después de todo, err para ping puede ser de hasta 500 ms. Y las medidas en sí no podemos hacer tan a menudo.
En este punto, propongo parar. Si alguien estaba interesado, estaré encantado de decirle cómo "dibujar el resto de la lechuza". Pero como parte de la historia sobre los temporizadores, creo que mi historia ha terminado.
Y quiero compartir lo que obtuve. De todos modos, el año nuevo.
Que paso
Descargo de responsabilidad: Técnicamente, este es un sitio de relaciones públicas en Habré, pero este es un proyecto de mascotas de código abierto completamente sin fines de lucro en el que prometo nunca: poner anuncios o ganar dinero de otra manera. Por el contrario, he recaudado más casos de mi dinero ahora para sobrevivir a un posible efecto de habra. Por lo tanto, por favor, buena gente, no me rompan y no me alcancen. Todo esto es puramente divertido.
Feliz año nuevo, Habr!
Puede girar las perillas y controlar la visualización, la música y los efectos de audio. Si tiene una tarjeta de video normal, vaya a la configuración y configure el número de partículas al 100%.
Requiere WebAudio y WebGL.
UPD: no funciona en Safari con macOS Mojave. Desafortunadamente, no hay forma de averiguar rápidamente qué está sucediendo, debido a la ausencia de este Safari en sí. iOS parece estar funcionando.
UPD2: Si
snowtime.fun y
web.snowtime.fun no responden, pruebe el nuevo subdominio
habr .snowtime.fun . Trasladó el servidor a otro centro de datos, y la antigua IP se almacenó en caché en DNS,
expire=1w
. :(
Repositorio:
bitbucketAl escribir este artículo, se utilizaron ilustraciones de
macrovector / Freepik .