Misalkan kita memiliki beberapa monitor. Dan kami ingin menggunakan monitor ini sebagai karangan bunga. Misalnya, buat mereka berkedip pada saat bersamaan. Atau mungkin secara serentak mengubah warnanya menurut semacam algoritma pintar. Dan bagaimana jika Anda melakukannya di browser - maka Anda dapat menghubungkan ponsel cerdas dan tablet ke ini. Semua itu ada di tangan.

Dan, karena kami menggunakan browser, Anda juga dapat menambahkan desain suara. Lagi pula, jika cukup akurat untuk menyinkronkan perangkat dalam waktu, maka Anda dapat memutar suara pada masing-masing seolah-olah satu sistem multichannel terdengar.
Apa yang bisa ditemui ketika menyinkronkan Audio Web dan jam gameplay di dalam aplikasi javascript; berapa banyak "jam" yang berbeda ada di javasctipt (tiga!) dan mengapa semuanya diperlukan, serta aplikasi siap pakai untuk node.js di bawah kucing.
Periksa jamnya
Untuk garland online bersyarat, sinkronisasi jam yang akurat diperlukan. Setelah semua, maka Anda dapat mengabaikan penundaan jaringan (bahkan terputus-putus). Cukup memberi cap kontrol waktu dan menghasilkan perintah-perintah ini sedikit "ke masa depan". Pada klien, mereka akan disangga dan kemudian dieksekusi secara sinkron dan tepat waktu.
Atau Anda bahkan dapat melangkah lebih jauh - ambil algoritma acak deterministik lama yang baik dan gunakan satu seed yang umum (dikeluarkan oleh server satu kali, saat terhubung) pada semua perangkat. Jika Anda menggunakan seed tersebut
bersama dengan waktu yang tepat, Anda dapat sepenuhnya menentukan perilaku algoritma pada semua perangkat. Bayangkan saja: pada kenyataannya, Anda tidak perlu jaringan atau server untuk secara unik dan sinkron mengubah keadaan. Seed sudah berisi seluruh "perekaman video" tindakan sebelumnya. Yang utama adalah waktu yang tepat.

Setiap metode memiliki batas penerapannya. Dengan input pengguna instan, tentu saja, tidak ada yang bisa dilakukan, tetap mengirimkannya "apa adanya". Tapi semua yang bisa dihitung harus dihitung. Dalam implementasi saya, saya menggunakan ketiga pendekatan, tergantung pada situasinya.
Subyektif "pada saat yang sama"
Idealnya, semuanya harus berbunyi "pada saat yang sama" - tidak lebih dari ± 10 ms perbedaan diperlukan untuk pasangan terburuk di antara perangkat gabungan. Anda tidak dapat mengandalkan keakuratan seperti itu dari waktu sistem, dan metode standar untuk menyinkronkan waktu menggunakan protokol NTP tidak tersedia di browser. Karena itu, kami akan mengarahkan server sinkronisasi kami. Prinsipnya sederhana: helm "ping" dan terima "pong" dengan cap waktu server. Jika Anda melakukan ini berkali-kali berturut-turut, Anda dapat secara statistik meratakan kesalahan dan mendapatkan waktu tunda rata-rata.
Kode: menghitung waktu server pada klien Soket web dan solusi berdasarkannya paling cocok karena mereka tidak memerlukan waktu untuk membuat koneksi TCP, dan Anda dapat "berkomunikasi" dengan mereka dari dua arah. Bukan UDP atau ICMP, tentu saja, tetapi jauh lebih cepat daripada koneksi dingin biasa menggunakan HTTP API. Karena itu, socket.io. Semuanya sangat mudah di sana:
Kode: implementasi socket.io
Akan lebih baik, daripada menghitung rata-rata, untuk menghitung median - ini akan meningkatkan akurasi dengan koneksi yang tidak stabil. Pilihan metode penyaringan terserah pembaca. Saya sengaja menyederhanakan kode di sini demi skema. Solusi lengkap saya dapat ditemukan di repositori. performance.now ()
Biarkan saya mengingatkan Anda bahwa objek
performance
adalah API yang menyediakan akses ke timer resolusi tinggi. Bandingkan:
Date.now()
mengembalikan jumlah milidetik sejak 1 Januari 1970, dan melakukannya dalam bentuk bilangan bulat . Artinya, kesalahan hanya dari pembulatan rata-rata adalah 0,5 ms. Misalnya, pada satu operasi pengurangan ab
Anda tidak berhasil “kehilangan” hingga 2 ms. Selain itu, secara historis dan konseptual, meteran waktu itu sendiri tidak menjamin akurasi tinggi dan dipertajam untuk bekerja dengan skala waktu yang lebih besar.performance.now()
mengembalikan jumlah milidetik sejak halaman web dibuka .
Ini adalah API yang relatif baru, "diasah" khusus untuk pengukuran interval waktu yang akurat. Mengembalikan nilai floating-point , secara teoritis memberikan tingkat akurasi yang dekat dengan kemampuan OS itu sendiri.
Saya pikir informasi ini diketahui hampir semua pengembang javascript. Tetapi tidak semua orang tahu bahwa ...
Momok
Karena serangan timing sensasional Specter pada tahun 2018, semuanya berjalan ke titik bahwa timer resolusi tinggi akan secara kasar dibuat jika tidak ada solusi lain untuk masalah kerentanan. Firefox, mulai dari versi 60, membulatkan nilai pengatur waktu ini menjadi milidetik, dan Edge, bahkan lebih buruk.
Inilah yang dikatakan
MDN :
Cap waktu sebenarnya bukan resolusi tinggi. Untuk mengurangi ancaman keamanan seperti Spectre, browser saat ini membulatkan hasilnya ke berbagai tingkat. (Firefox mulai membulatkan ke 1 milidetik di Firefox 60.) Beberapa browser mungkin juga sedikit mengacak cap waktu. Presisi dapat meningkat lagi di rilis mendatang; pengembang peramban masih menyelidiki serangan waktu ini dan cara terbaik untuk menguranginya.
Mari kita jalankan tes dan lihat grafiknya. Ini adalah hasil pengujian pada interval 10 ms:
Kode uji: pengukuran waktu dalam satu siklus 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()
Edge

statistikVersi Browser: 44.17763.771.0
Date.now ()
Interval rata-rata: 1.0538336052202284 ms
penyimpangan dari interval rata-rata, RMS: 0.7547819181245603 ms
median interval: 1 ms
performance.now ()
interval rata-rata: 1.567100970873786 ms
penyimpangan dari interval rata-rata, RMS: 0.6748006785171455 ms
interval median: 1,5015000000003056 ms
Firefox

statistikVersi Browser: 71.0
Date.now ()
interval rata-rata: 1.0168350168350169 ms
penyimpangan dari interval rata-rata, RMS: 0,21645930182417966 ms
median interval: 1 ms
performance.now ()
Interval rata-rata: 1.0134453781512605 ms
penyimpangan dari interval rata-rata, RMS: 0.1734108492762375 ms
median interval: 1 ms
Chrome

statistikVersi Browser: 79.0.3945.88
Date.now ()
interval rata-rata: 1.02442996742671 ms
penyimpangan dari interval rata-rata, RMS: 0.49858684744444 ms
median interval: 1 ms
performance.now ()
interval rata-rata: 0,005555847229948915 ms
penyimpangan dari interval rata-rata, RMS: 0,027497846727194235 ms
interval median: 0,0050000089686363935 ms
Oke, Chrome, perbesar hingga 1 msec.

Jadi, Chrome masih bertahan, dan penerapannya
performance.now()
belum dicekik dan langkahnya indah 0,005 ms. Di bawah Edge, timer
performance.now()
lebih kasar daripada
Date.now()
! Di Firefox, kedua penghitung waktu memiliki akurasi milidetik yang sama.
Pada tahap ini, beberapa kesimpulan sudah bisa ditarik. Tetapi ada timer lain di javascript (yang tanpanya kita tidak bisa melakukannya tanpa).
Timer WebAudio API
Ini adalah binatang yang sedikit berbeda. Ini digunakan untuk antrian audio yang tertunda. Faktanya adalah bahwa peristiwa audio (memainkan catatan, mengelola efek) tidak dapat mengandalkan alat javascript asinkron standar:
setInterval
dan
setTimeout
- karena kesalahannya yang terlalu besar. Dan ini bukan hanya kesalahan
nilai-nilai timer (dengan yang kita bahas sebelumnya), tetapi ini adalah kesalahan yang mesin acara mengeksekusi peristiwa. Dan itu sudah sekitar 5-25 ms, bahkan dalam kondisi rumah kaca.
Grafik untuk kasus asinkron di bawah spoilerHasil tes selama interval 100 ms:
Kode uji: pengukuran waktu dalam siklus asinkron 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()
Edge

statistikVersi Browser: 44.17763.771.0
Date.now ()
Interval rata-rata: 25.59595959595959595 ms
penyimpangan dari interval rata-rata, RMS: 10.12639235162126 ms
median interval: 28 ms
performance.now ()
Interval rata-rata: 25.862596938775525 ms
penyimpangan dari interval rata-rata, RMS: 10.123711255512573 ms
interval median: 27.027099999999336 ms
Firefox

statistikVersi Browser: 71.0
Date.now ()
interval rata-rata: 1.6914893617021276 ms
penyimpangan dari interval rata-rata, RMS: 0.6018870280772611 ms
median interval: 2 ms
performance.now ()
interval rata-rata: 1.7865168539325842 ms
penyimpangan dari interval rata-rata, RMS: 0.6442818510935484 ms
median interval: 2 ms
Chrome

statistikVersi Browser: 79.0.3945.88
Date.now ()
interval rata-rata: 4.787878787878787888, ms
penyimpangan dari interval rata-rata, RMS: 0.7557553886872682 ms
median interval: 5 ms
performance.now ()
interval rata-rata: 4.783989898979516 ms
penyimpangan dari interval rata-rata, RMS: 0,6483716900974945 ms
interval median: 4.750000000058208 ms
Mungkin seseorang akan mengingat aplikasi audio HTML eksperimental pertama. Sebelum WebAudio lengkap datang ke browser - mereka semua terdengar seperti sedikit mabuk, ceroboh. Hanya karena mereka menggunakan
setTimeout
sebagai sequencer.
WebAudio API modern, sebaliknya, memberikan resolusi dijamin hingga 0,02 ms (spekulasi berdasarkan frekuensi sampling 44100Hz). Ini disebabkan oleh fakta bahwa mekanisme yang berbeda digunakan untuk pemutaran suara yang tertunda daripada
setTimeout
:
source.start(when);
Faktanya, setiap reproduksi sampel audio "tertunda". Hanya untuk menghilangkannya "tidak ditunda", Anda harus menundanya "sampai sekarang".
source.start(audioCtx.currentTime);
Tentang musik yang dihasilkan perangkat lunak waktu-nyataJika Anda memainkan melodi yang disintesis-program dari catatan, maka catatan ini perlu ditambahkan sedikit di antrean pemutaran terlebih dahulu. Kemudian, terlepas dari semua pembatasan dan penyimpangan non-fundamental dari timer, melodi akan bermain dengan sangat lancar.
Dengan kata lain, melodi yang disintesis secara waktu nyata tidak boleh "diciptakan" secara waktu nyata, tetapi sedikit di muka.
Satu pengatur waktu untuk mengatur semuanya
Karena
audioCtx.currentTime
sangat stabil dan akurat, mungkin kita harus menggunakannya sebagai sumber utama waktu relatif? Ayo jalankan tes lagi.
Kode uji: mengukur pengukuran waktu sinkron dalam satu siklus 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
Edge

statistikVersi Browser: 44.17763.771.0
Date.now ()
Interval rata-rata: 1.037037037037037 ms
penyimpangan dari interval rata-rata, RMS: 0,6166609846299806 ms
median interval: 1 ms
performance.now ()
interval rata-rata: 1.5447103117505993 ms
penyimpangan dari interval rata-rata, RMS: 0.4390514285320851 ms
interval median: 1,5015000000000782 ms
audioCtx.currentTime
interval rata-rata: 2.955751134714949 ms
penyimpangan dari interval rata-rata, RMS: 0,6193645611529503 ms
interval median: 2.902507781982422 ms
Firefox

statistikVersi Browser: 71.0
Date.now ()
interval rata-rata: 1,005128205128205 ms
penyimpangan dari interval rata-rata, RMS: 0.12392867665225249 ms
median interval: 1 ms
performance.now ()
interval rata-rata: 1,00513698630137 ms
penyimpangan dari interval rata-rata, RMS: 0,07148844433269844 ms
median interval: 1 ms
audioCtx.currentTime
Firefox tidak memperbarui nilai timer audio dalam loop sinkronisasi
Chrome

statistikVersi Browser: 79.0.3945.88
Date.now ()
interval rata-rata: 1.0207612456747406 ms
penyimpangan dari interval rata-rata, RMS: 0.49870223457982504 ms
median interval: 1 ms
performance.now ()
interval rata-rata: 0,005414502034674972 ms
penyimpangan dari interval rata-rata, RMS: 0,027441293974958335 ms
interval median: 0,004999999873689376 ms
audioCtx.currentTime
interval rata-rata: 3.0877599266656963 ms
penyimpangan dari interval rata-rata, RMS: 1.1445555956407658 ms
interval median: 2.9024943310650997 ms
Grafik untuk kasus asinkron di bawah spoilerKode uji: pengukuran waktu dalam siklus asinkronHasil tes selama interval 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
Edge

statistikVersi Browser: 44.17763.771.0
Date.now ():
interval rata-rata: 24.505050505050505 ms
deviasi dari interval rata-rata: 11.513166584195204 ms
median interval: 26 ms
performance.now ():
interval rata-rata: 24.50935757575754 ms
deviasi dari interval rata-rata: 11.679091435527388 ms
interval median: 25.525499999999738 ms
audioCtx.currentTime:
Interval rata-rata: 24.76005164944396 ms
deviasi dari interval rata-rata: 11.311571546205316 ms
interval median: 26.121139526367187 ms
Firefox

statistikVersi Browser: 71.0
Date.now ():
interval rata-rata: 1,6875 ms
deviasi dari interval rata-rata: 0.6663410663216448 ms
median interval: 2 ms
performance.now ():
interval rata-rata: 1.7234042553191489 ms
deviasi dari interval rata-rata: 0.6588877688171075 ms
median interval: 2 ms
audioCtx.currentTime:
interval rata-rata: 10.158730158730123 ms
deviasi dari interval rata-rata: 1.4512471655330046 ms
interval median: 8.707482993195299 ms
Chrome

statistikVersi Browser: 79.0.3945.88
Date.now ():
Interval rata-rata: 4,585858585858586 ms
deviasi dari interval rata-rata: 0,9102125516015199 ms
median interval: 5 ms
performance.now ():
interval rata-rata: 4,592424242424955 ms
deviasi dari interval rata-rata: 0.719936993603155 ms
interval median: 4.605000001902226 ms
audioCtx.currentTime:
interval rata-rata: 10.12648022171832 ms
deviasi dari interval rata-rata: 1,4508887886499262 ms
interval median: 8.707482993197118 ms
Yah, itu tidak akan berhasil. "Di luar", timer ini adalah yang paling tidak akurat. Firefox tidak memperbarui nilai timer di dalam loop. Tetapi secara umum: resolusi adalah 3 ms dan jitter lebih buruk dan terlihat. Mungkin nilai
audioCtx.currentTime
mencerminkan posisi di buffer cincin driver kartu audio. Dengan kata lain, ini menunjukkan waktu minimum yang memungkinkan untuk menunda pemutaran dengan aman.
Dan apa yang harus dilakukan? Bagaimanapun, kita membutuhkan penghitung waktu yang akurat untuk menyinkronkan dengan server dan meluncurkan acara javascript di layar, dan penghitung audio untuk acara suara!
Ternyata Anda harus menyinkronkan semua penghitung waktu satu sama lain:
- Klien
audioCtx.currentTime
dengan klien performance.now()
pada klien. - Dan client
performance.now()
dengan performance.now()
sisi server.
Disinkronkan, disinkronkan

Secara umum, ini cukup lucu jika Anda memikirkannya: Anda dapat memiliki dua sumber waktu A dan B, yang masing-masing sangat kasar dan berisik pada keluaran (A '= A + err
A ; B' = B + err
B ) sehingga dapat bahkan tidak dapat digunakan sendiri. Tetapi perbedaan antara sumber asli yang tidak berisik dapat dipulihkan dengan sangat akurat.
Karena jarak waktu sebenarnya antara jam ideal adalah konstan, mengambil pengukuran n kali, kita akan mengurangi kesalahan pengukuran sesekali kali. Kecuali, tentu saja, jam berjalan pada kecepatan yang sama.
Ya tidak disinkronkan
Berita buruknya adalah tidak, mereka tidak pergi dengan kecepatan yang sama. Dan saya tidak berbicara tentang perbedaan jam di server dan pada klien - ini bisa dimengerti dan diharapkan. Apa yang lebih tak terduga:
audioCtx.currentTime
secara bertahap menyimpang dari
performance.now()
. Itu ada di dalam klien. Kita mungkin tidak melihat, tetapi kadang-kadang, di bawah beban, sistem audio mungkin tidak menelan sepotong kecil data dan (bertentangan dengan sifat buffer cincin) waktu audio akan bergeser relatif terhadap waktu sistem. Ini jarang terjadi, itu tidak menjadi perhatian banyak orang: tetapi jika, misalnya, Anda meluncurkan dua video YouTube secara bersamaan di komputer yang berbeda, itu bukan fakta bahwa mereka akan berhenti bermain pada saat yang sama. Dan intinya, tentu saja, bukan dalam iklan.
Jadi, untuk operasi yang stabil dan sinkron. Kita perlu memeriksa kembali semua jam
secara teratur , menggunakan waktu server - sebagai referensi. Dan kemudian trade-off muncul dalam berapa banyak pengukuran yang digunakan untuk rata-rata: semakin banyak - semakin akurat, tetapi semakin besar kemungkinan lompatan tajam pada
audioCtx.currentTime
jatuh ke jendela waktu di mana kita memfilter nilai. Kemudian, jika kita, misalnya, menggunakan jendela menit, maka semua menit kita akan memiliki waktu berlalu. Pilihan filter luas:
eksponensial ,
median ,
filter Kalman , dll. Tetapi pertukaran ini dalam hal apapun.
Jendela waktu
Dalam kasus sinkronisasi
audioCtx.currentTime
dengan
performance.now()
, dalam loop asinkron, agar tidak mengganggu UI, kita dapat mengambil satu pengukuran, katakanlah, 100 ms.
Asumsikan bahwa kesalahan pengukuran err = errA + errB = 1 + 3 = 4 ms
Dengan demikian, dalam 1 detik kita dapat menguranginya menjadi 0,4 ms, dan dalam 10 detik menjadi 0,04 ms. Peningkatan hasil lebih lanjut tidak masuk akal, dan jendela yang bagus untuk penyaringan adalah: 1 - 10 detik.
Dalam hal sinkronisasi jaringan, keterlambatan dan kesalahan sudah jauh lebih signifikan, tetapi tidak ada lompatan tajam dalam waktu, seperti dalam kasus
audioCtx.currentTime
. Dan Anda dapat membiarkan diri Anda mengumpulkan statistik yang sangat bagus. Toh, err untuk ping bisa sampai 500 ms. Dan pengukuran itu sendiri bisa kita lakukan tidak begitu sering.
Pada titik ini, saya mengusulkan untuk berhenti. Jika ada yang tertarik, dengan senang hati saya akan memberi tahu Anda cara "menggambar burung hantu yang tersisa." Tetapi sebagai bagian dari cerita tentang pengatur waktu, saya pikir kisah saya sudah berakhir.
Dan saya ingin membagikan apa yang saya dapatkan. Semua sama, tahun baru.
Apa yang terjadi
Penafian: Secara teknis, ini adalah situs PR di Habré, tetapi ini adalah sepenuhnya proyek nir-laba opensource yang saya berjanji tidak akan pernah: untuk memasang iklan, atau untuk menghasilkan uang dengan cara lain. Sebaliknya, saya telah mengumpulkan lebih banyak contoh dari uang saya sekarang untuk bertahan dari kemungkinan perilaku. Karena itu, tolong, orang-orang baik, jangan hancurkan aku dan jangan hubungi aku. Ini semua murni menyenangkan.
Selamat Tahun Baru, Habr!
Anda dapat memutar kenop dan mengontrol visualisasi, musik dan efek audio. Jika Anda memiliki kartu video normal, buka pengaturan dan atur jumlah partikel menjadi 100%.
Membutuhkan WebAudio dan WebGL.
UPD: Tidak berfungsi di Safari di bawah macOS Mojave. Sayangnya, tidak ada cara untuk dengan cepat mengetahui apa yang terjadi, karena tidak adanya Safari itu sendiri. iOS tampaknya berfungsi.
UPD2: Jika
snowtime.fun dan
web.snowtime.fun tidak merespons, coba subdomain
habr .snowtime.fun baru. Dia memindahkan server ke pusat data lain, dan IP lama di-cache dalam DNS,
expire=1w
. :(
Repositori:
bitbucketSaat menulis artikel ini, ilustrasi
macrovector / Freepik digunakan.