/*! * skaz_speed.js — тест скорости серверов by Skaz (движок + окно для Лампы). * Встроен в tv.js (readfile) — кнопка «Тест скорости» в by Skaz; и страница speed.skaz.tv (stream/speed/index.php). * * Как устроен (идея — speed.tv.team, но наши адреса и быстрее): * 1) пинг всех серверов (по 4 параллельно): 1 прогрев + 4 замера, медиана и джиттер; * сервер не ответил на прогрев дважды по 4 с → «недоступен», дальше не мучаем; * 2) скорость — только у 4 лучших по пингу (+ текущий сервер): 1 с разгон + 4 с замер; * жёсткий таймер на каждый сервер — тест не виснет, даже если сервер отдаёт по байту; * fetch-стрим читает и выбрасывает данные (память не копится), XHR — мелкими кусками; * 3) итог словами (SD/HD/FHD/4K, стабильность) и выбор сервера через Lampa.Select. * ES5 — для старых ТВ. */ (function () { 'use strict'; if (window.SkazSpeed2) return; // Без дублей по хосту: «VIP FR 2» (srv 8) = тот же fr2.skaz.tv, что и «VIP» (srv 0) — не меряем дважды var SERVERS = [ { id: 0, name: 'VIP', host: 'fr2.skaz.tv' }, { id: 2, name: 'VIP RU', host: 'ru.byskaz.ru' }, { id: 16, name: 'VIP RU 2', host: 'ru6.byskaz.ru' }, { id: 17, name: 'VIP RU 3', host: 'ru3.byskaz.ru' }, { id: 26, name: 'VIP RU 4', host: 'ru4.byskaz.ru' }, { id: 20, name: 'VIP RU 7', host: 'ru7.byskaz.ru' }, { id: 21, name: 'VIP RU 8', host: 'ru8.byskaz.ru' }, { id: 23, name: 'VIP Амур', host: 'ru5.byskaz.ru' }, { id: 4, name: 'VIP Кассиопея', host: 'fr.skaz.tv' }, { id: 22, name: 'VIP Сириус', host: 'hk.skaz.tv' }, { id: 10, name: 'VIP NL', host: 'fr3.skaz.tv' }, { id: 25, name: 'VIP NL 2', host: 'ro.skaz.tv' }, { id: 13, name: 'VIP DE', host: 'gr.skaz.tv' }, { id: 14, name: 'VIP DE 2', host: 'de.skaz.tv' }, { id: 3, name: 'VIP PL', host: 'pl.skaz.tv' }, { id: 19, name: 'VIP GB', host: 'gb.skaz.tv' }, { id: 24, name: 'VIP UA', host: 'ukr.skaz.tv' } ]; var CFG = { pingParallel: 4, pingCount: 4, // замеров после прогрева pingTimeout: 2500, warmTimeout: 4000, // первый запрос (TLS) — дольше, плюс 1 повтор: далёкие/медленные маршруты не отбраковываем зря // мс на один пинг (жёсткий таймер, xhr.timeout на ТВ бывает не работает) dlTop: 4, // сколько лучших по пингу качать dlStreams: 3, dlGrace: 1000, // разгон, не считаем dlTime: 4000, // замер dlNoData: 3000, // ни байта за 3 с → сервер «не отдаёт» dlHardExtra: 1500 // запас сверх grace+time — потом рубим в любом случае }; // Порог битрейта (Мбит/с) с запасом ~1.5x, сверху вниз var QUALITY = [ { mbps: 30, name: '4K' }, { mbps: 12, name: 'Full HD' }, { mbps: 6, name: 'HD' }, { mbps: 3, name: 'SD' } ]; function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); } function base(s) { return 'https://' + s.host + ':8080/backend/'; } function rnd() { return Math.random().toString(36).slice(2); } function median(a) { if (!a.length) return -1; var b = a.slice().sort(function (x, y) { return x - y; }), m = Math.floor(b.length / 2); return b.length % 2 ? b[m] : (b[m - 1] + b[m]) / 2; } function jitter(a) { if (a.length < 2) return 0; var s = 0; for (var i = 1; i < a.length; i++) s += Math.abs(a[i] - a[i - 1]); return s / (a.length - 1); } function clamp(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } function esc(t) { return String(t).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function later(fn) { setTimeout(fn, 0); } var S = { running: false, aborted: false, _xhrs: [], // ---------- пинг ---------- _pingOnce: function (url, cb, timeout) { var fired = false, t0 = now(), full = url + '?cors=true&r=' + rnd(); var xhr = new XMLHttpRequest(); var finish = function (ms) { if (fired) return; fired = true; clearTimeout(timer); if (ms < 0) later(function () { try { xhr.abort(); } catch (e) {} }); later(function () { cb(ms); }); }; timeout = timeout || CFG.pingTimeout; var timer = setTimeout(function () { finish(-1); }, timeout); xhr.onload = function () { if (xhr.status < 200 || xhr.status >= 400) return finish(-1); var ms = now() - t0; try { var p = performance.getEntriesByName(full); if (p && p.length) { var e = p[p.length - 1], d = e.responseStart - e.requestStart; if (!(d > 0)) d = e.duration; if (d > 0 && d < ms) ms = d; } } catch (ex) {} finish(Math.max(1, Math.round(ms))); }; xhr.onerror = xhr.onabort = xhr.ontimeout = function () { finish(-1); }; try { xhr.open('GET', full, true); xhr.timeout = timeout + 500; xhr.send(); } catch (e) { finish(-1); } }, _pingServer: function (srv, cb) { var self = this, url = base(srv) + 'empty.php', samples = [], left = CFG.pingCount; // прогрев (TLS-рукопожатие) — не считаем; даём 4 с и одну повторную попытку, потом «недоступен» var warm = function (tries) { self._pingOnce(url, function (ms) { if (self.aborted) return; if (ms < 0) return tries > 1 ? warm(tries - 1) : cb({ ok: false }); measure(); }, CFG.warmTimeout); }; var measure = function () { var next = function () { if (self.aborted) return; if (left-- <= 0) { if (samples.length < 2) return cb({ ok: false }); return cb({ ok: true, ping: Math.round(median(samples)), jitter: Math.round(jitter(samples)) }); } self._pingOnce(url, function (v) { if (v >= 0) samples.push(v); next(); }); }; next(); }; warm(2); }, // ---------- скорость ---------- _download: function (srv, onTick, cb) { var self = this, url = base(srv) + 'garbage.php'; var useFetch = !!(window.fetch && window.AbortController && window.ReadableStream); var bytes = 0, mark = 0, t0 = now(), tMeasure = 0, done = false, ctrls = [], xhrs = [], errs = 0; var finish = function (mbps) { if (done) return; done = true; clearInterval(ticker); clearTimeout(hard); // сначала отдаём результат, потом обрываем запросы — abort() на ТВ может подтормаживать later(function () { cb(mbps); }); setTimeout(function () { for (var i = 0; i < ctrls.length; i++) { try { ctrls[i].abort(); } catch (e) {} } for (var j = 0; j < xhrs.length; j++) { try { xhrs[j].abort(); } catch (e) {} } ctrls = []; xhrs = []; }, 50); }; var result = function () { var sec = tMeasure ? (now() - tMeasure) / 1000 : 0; return sec > 0.3 ? Math.round((bytes - mark) * 8 / sec / 1e4) / 100 : 0; }; var retry = function (start) { if (done || self.aborted) return; if (++errs > 12) return; // сервер сыплет ошибками — не долбим, дождёмся таймера setTimeout(start, 300); }; var streamFetch = function () { if (done || self.aborted) return; var c = new AbortController(); ctrls.push(c); fetch(url + '?cors=true&ckSize=25&r=' + rnd(), { cache: 'no-store', credentials: 'omit', signal: c.signal }) .then(function (r) { if (!r.ok || !r.body) throw new Error('http'); var reader = r.body.getReader(); var pump = function () { if (done || self.aborted) { try { reader.cancel(); } catch (e) {} return; } return reader.read().then(function (res) { if (res.done) return streamFetch(); bytes += res.value.byteLength; return pump(); }); }; return pump(); }) .catch(function () { retry(streamFetch); }); }; var streamXhr = function () { if (done || self.aborted) return; var xhr = new XMLHttpRequest(), prev = 0; xhrs.push(xhr); xhr.onprogress = function (e) { if (done || self.aborted) return; if (e.loaded > prev) { bytes += e.loaded - prev; prev = e.loaded; } }; xhr.onload = function () { if (!done) streamXhr(); }; xhr.onerror = xhr.ontimeout = function () { retry(streamXhr); }; try { xhr.open('GET', url + '?cors=true&ckSize=5&r=' + rnd(), true); try { xhr.responseType = 'arraybuffer'; } catch (e) {} xhr.send(); } catch (e) { retry(streamXhr); } }; for (var i = 0; i < CFG.dlStreams; i++) (useFetch ? streamFetch : streamXhr)(); var ticker = setInterval(function () { if (self.aborted) return finish(0); var el = now() - t0; if (!tMeasure) { if (el >= CFG.dlGrace) { tMeasure = now(); mark = bytes; } return; } if (bytes === 0 && el >= CFG.dlNoData) return finish(0); if (onTick) onTick(result()); if (now() - tMeasure >= CFG.dlTime) finish(result()); }, 250); var hard = setTimeout(function () { finish(result()); }, CFG.dlGrace + CFG.dlTime + CFG.dlHardExtra); }, // Кого качать: лучшие по пингу; если среди них в основном RU — добавляем лучший зарубежный // (пинг до RU почти всегда меньше, а зарубежный может оказаться быстрее по скорости); + текущий сервер. _isRu: function (r) { return /\.byskaz\.ru$/.test(r.srv.host); }, _pickForSpeed: function (results, cur) { var self = this; var alive = results.filter(function (r) { return r.ok; }) .sort(function (a, b) { return (a.ping + a.jitter) - (b.ping + b.jitter); }); var pick = alive.slice(0, CFG.dlTop); var ru = pick.filter(function (r) { return self._isRu(r); }).length; if (ru * 2 > pick.length) { var foreign = alive.filter(function (r) { return !self._isRu(r) && pick.indexOf(r) < 0; })[0]; if (foreign) pick.push(foreign); } if (cur !== undefined) alive.forEach(function (r) { if (String(r.id) === String(cur) && pick.indexOf(r) < 0) pick.push(r); }); return pick; }, // ---------- оценка ---------- _quality: function (mbps) { for (var i = 0; i < QUALITY.length; i++) if (mbps >= QUALITY[i].mbps) return QUALITY[i].name; return ''; }, _score: function (r) { if (!r.ok || !(r.dl > 0)) return -1; return 0.5 * clamp(r.dl / 40) + 0.35 * clamp(1 - r.ping / 200) + 0.15 * clamp(1 - r.jitter / 60); }, _verdict: function (r) { var q = this._quality(r.dl); var v = q ? 'потянет ' + q : 'мало скорости'; if (r.jitter > 40) v += ', нестабильно'; return v; }, // ---------- UI ---------- _row: function (r) { var ping = r.ok ? r.ping + ' мс' + (r.jitter > 40 ? ' ±' + r.jitter : '') : (r.ok === false ? 'нет ответа' : '…'); var dl = !r.ok ? '' : r.dl === undefined ? (r.testing ? (r.live ? r.live + ' Мбит/с…' : 'замер…') : '') : r.dl > 0 ? r.dl + ' Мбит/с' : 'не отдаёт'; return '