Skip to content

Instantly share code, notes, and snippets.

@sAbakumoff
Forked from pinguinson/whatthefuck.js
Created March 28, 2016 03:31
Show Gist options
  • Save sAbakumoff/e92922a1494c01d26019 to your computer and use it in GitHub Desktop.
Save sAbakumoff/e92922a1494c01d26019 to your computer and use it in GitHub Desktop.
/***
Copyright (c) 2016, Alexander Choporov aka CoolCmd
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
***/
'use strict';
// Формат версии: https://developer.chrome.com/extensions/manifest/version
// В моем случае это UTC-дата выкладывания данной версии для скачивания.
// 1 - год >= 2016
// 2 - месяц 1..12
// 3 - день месяца 1..31
// 4 - указывается только для версий, выпущенных в один день. >= 1, по умолчанию 0.
const ВЕРСИЯ_ДОПОЛНЕНИЯ = '2016.3.24';
// Если в удаляемом диапазоне есть ключевой кадр, то будут удалены все последующие кадры до ключевого кадра, не входящего
// в удаляемый диапазон. Таким образом возможна ситуация, когда будет удалено всё просмотренное видео и часть непросмотренного,
// что приведет к остановке воспроизведения. Чтобы этого не произошло, НЕ_УДАЛЯТЬ_ВИДЕО должно превышать расстояние
// между ключевыми кадрами. В теории чем больше НЕ_УДАЛЯТЬ_ВИДЕО, тем выше расход памяти.
// TODO Рассчитывать в worker максимальное расстояние между ключевыми кадрами.
const ИНТЕРВАЛ_УДАЛЕНИЯ_ВИДЕО = 10; // Секунды.
const НЕ_УДАЛЯТЬ_ВИДЕО = 20; // Секунды. TODO Подобрать подходящее значение.
const ПЕРЕПОЛНЕНИЕ_БУФЕРА = 50; // Секунды. TODO Подобрать подходящее значение.
const ОСТАНАВЛИВАТЬ_ЕСЛИ_НЕ_ПРОСМОТРЕНО_МЕНЬШЕ = 0.2; // Секунды. TODO 0.1 недостаточно для скорости воспроизведения 2х.
const ИНТЕРВАЛ_ПОЛУЧЕНИЯ_СПИСКА_ВАРИАНТОВ_НАЧАЛО = 1000; // Миллисекунды.
const ИНТЕРВАЛ_ПОЛУЧЕНИЯ_СПИСКА_ВАРИАНТОВ_КОНЕЦ = 30000; // Миллисекунды.
const ИНТЕРВАЛ_ПОЛУЧЕНИЯ_СПИСКА_ВАРИАНТОВ_ШАГ = 1000; // Миллисекунды.
const ЗАГРУЖАТЬ_МЕТАДАННЫЕ_НЕ_ДОЛЬШЕ = 10000; // Миллисекунды.
const ЗАГРУЖАТЬ_СПИСОК_ВАРИАНТОВ_НЕ_ДОЛЬШЕ = 10000; // Миллисекунды.
const ЗАГРУЖАТЬ_СПИСОК_СЕГМЕНТОВ_НЕ_ДОЛЬШЕ = 20000; // Миллисекунды.
const СКРЫВАТЬ_УПРАВЛЕНИЕ_И_КУРСОР_ЧЕРЕЗ = 4000; // Миллисекунды.
const ИНТЕРВАЛ_ОБНОВЛЕНИЯ_СТАТИСТИКИ = 350; // Миллисекунды.
const ИНТЕРВАЛ_СБОРА_МЕТАДАННЫХ = 60000; // Миллисекунды.
const МИНИМАЛЬНАЯ_ГРОМКОСТЬ = 0.01;
const МАКСИМАЛЬНАЯ_ГРОМКОСТЬ = 1.00;
const ШАГ_ПОВЫШЕНИЯ_ГРОМКОСТИ_КЛАВОЙ = 0.04;
const ШАГ_ПОНИЖЕНИЯ_ГРОМКОСТИ_КЛАВОЙ = 0.02;
const ШАГ_ИЗМЕНЕНИЯ_ГРОМКОСТИ_МЫШЬЮ = 0.01;
const СЕГМЕНТ_ЖДЕТ_ЗАГРУЗКИ = 1;
const СЕГМЕНТ_ЗАГРУЖАЕТСЯ = 2;
const СЕГМЕНТ_ЗАГРУЖЕН = 3;
const СЕГМЕНТ_ПРЕОБРАЗОВАН = 4;
const СЕГМЕНТ_ДОБАВЛЯЕТСЯ = 5;
const СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ = 1;
const СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ = 2;
const СОСТОЯНИЕ_ЗАГРУЗКА = 3;
const СОСТОЯНИЕ_ВОСПРОИЗВЕДЕНИЕ = 4;
const СОСТОЯНИЕ_ПАУЗА = 5;
const ОБЕЩАНИЕ_ОТМЕНЕНО = null;
const КОД_ОТВЕТА = 'Сервер вернул код ';
const ЛЕВАЯ_КНОПКА = 0;
const СРЕДНЯЯ_КНОПКА = 1;
const ПРАВАЯ_КНОПКА = 2;
var г_лРаботаЗавершена = false;
var г_моОчередь = [];
// TODO Не нравится мне такая отмена. Если делать многоразовый XMLHttpRequest, то прикрутить к нему отмену.
var г_оОтменяемоеОбещаниеСписка = null;
function Проверить(пУсловие)
{
if (!пУсловие)
{
г_оОтладка.ПроверкаНеПройдена();
}
}
function ResolveRelativeUrl(sRelativeUrl, sAbsoluteBaseUrl)
{
return (new URL(sRelativeUrl, sAbsoluteBaseUrl)).href;
}
function ДобавитьСтиль(сСтиль)
// Возвращает вставленный <style>.
{
var элСтиль = document.createElement('style');
элСтиль.textContent = сСтиль;
return (document.head || document.documentElement).appendChild(элСтиль);
}
function ЗаменитьСтраницу(sHtml)
{
// Firefox 44 + Greasemonkey 3.6: document.open() не работает.
document.replaceChild(document.implementation.createDocumentType('html', '', ''), document.doctype);
var elHtml = document.createElement('html');
elHtml.setAttribute('lang', 'ru');
elHtml.setAttribute('dir', 'ltr');
document.replaceChild(elHtml, document.documentElement);
// Firefox 44: если innerHTML менять ПЕРЕД вызовом replaceChild (что в теории чуть быстрее), то при первом
// показе страницы будет видна работа transition. У Chrome 48 нет этой нежелательной... особенности.
elHtml.innerHTML = sHtml;
}
function ПеревестиСекундыВСтроку(чСекунды, лНужныСекунды)
// TODO Добавить i18n.
{
var ч = Math.floor(чСекунды / 60 % 60);
var с = Math.floor(чСекунды / 60 / 60) + (ч < 10 ? ' : 0' : ' : ') + ч;
if (лНужныСекунды)
{
ч = Math.floor(чСекунды % 60);
с += (ч < 10 ? ' : 0' : ' : ') + ч;
}
return с;
}
function ToHtmlText(сТекст)
// https://jsperf.com/htmlescape-replacevstextcontent/4
// TODO Кэшировать регулярные выражения если функция станет вызываться в цикле.
{
return ('' + сТекст).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function ToHtmlAttribute(сТекст)
{
return ToHtmlText(сТекст).replace(/"/g, '&quot;');
}
function ИзменитьSvgHref(узУзел, сЗначение)
{
узУзел.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', сЗначение);
}
function IsFiniteNumber(пЗначение)
// Отсеиваем Infinity, -Infinity, NaN.
{
return typeof пЗначение === 'number' && isFinite(пЗначение);
}
function ЭтоОбъект(пЗначение)
{
return typeof пЗначение === 'object' && пЗначение !== null;
}
function Сегмент(пДанные, чДлительность, лРазрыв, чНомер)
{
var чСостояние;
switch (arguments.length)
{
case 1:
Проверить(пДанные === СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ || пДанные === СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ);
чДлительность = 0;
лРазрыв = true;
чНомер = ++Сегмент._чНомер
чСостояние = СЕГМЕНТ_ЗАГРУЖЕН;
break;
case 3:
Проверить(typeof пДанные === 'string');
чНомер = ++Сегмент._чНомер
чСостояние = СЕГМЕНТ_ЖДЕТ_ЗАГРУЗКИ;
break;
case 4:
Проверить(typeof пДанные === 'object' || typeof пДанные === 'number');
чСостояние = СЕГМЕНТ_ПРЕОБРАЗОВАН;
break;
default:
Проверить(false);
}
this.пДанные = пДанные;
this.чДлительность = чДлительность;
this.лРазрыв = лРазрыв;
this.чНомер = чНомер;
this.чСостояние = чСостояние;
}
Сегмент._чНомер = 0;
г_моОчередь.Добавить = function(оСегмент, чИндекс)
{
// TODO Из-за ошибок очередь может переполниться и привести к падению оборзевателя, особенно 32-битного.
// Максимальный размер очереди зависит от ПЕРЕПОЛНЕНИЕ_БУФЕРА, но на данный момент для преобразованных сегментов
// длительность неизвестна, поэтому сделана более простая проверка, которая использует количество элементов в массиве.
// Формула весьма приблизительная и взята с запасом. Например, после понижения битрейта трансляции длительность
// сегментов может увеличиться вдвое, но в очереди может остаться значительное количество сегментов старой длительности +
// неизвестное количество элементов со сменой состояния трансляции. Также предполагается, что сервер не хранит
// сегменты трансляции дольше минуты.
const кМаксимальныйРазмерОчереди = ПЕРЕПОЛНЕНИЕ_БУФЕРА / (г_оСтатистика.ПолучитьTargetDuration() / 2) * 2;
if (this.length > кМаксимальныйРазмерОчереди)
{
г_оОтладка.ЗавершитьРаботу('Очередь переполнена');
}
Проверить(оСегмент instanceof Сегмент);
if (чИндекс === undefined)
{
this.push(оСегмент);
}
else
{
Проверить(typeof чИндекс === 'number' && чИндекс >= 0 && чИндекс <= this.length);
this.splice(чИндекс, 0, оСегмент);
}
return оСегмент;
};
г_моОчередь.Удалить = function(пЭлемент)
{
if (typeof пЭлемент === 'number')
{
Проверить(пЭлемент >= 0 && пЭлемент < this.length);
var чИндекс = пЭлемент;
}
else if ((чИндекс = this.indexOf(пЭлемент)) === -1)
{
Проверить(пЭлемент instanceof Сегмент);
return;
}
console.log('[Очередь] Удаляю сегмент %s Состояние=%s', this[чИндекс].чНомер, this[чИндекс].чСостояние);
if (this[чИндекс].чСостояние === СЕГМЕНТ_ЗАГРУЖАЕТСЯ && this[чИндекс].пДанные)
{
console.info('[Очередь] Отменяю загрузку сегмента %s', this[чИндекс].чНомер);
this[чИндекс].пДанные.Отменить();
}
this.splice(чИндекс, 1);
};
г_моОчередь.Очистить = function()
{
while (this.length !== 0)
{
// Удаление с конца быстрее.
this.Удалить(this.length - 1);
}
};
г_моОчередь.ПоказатьСостояние = function()
{
var сСостояние = '';
for (var оЭлемент of this)
{
сСостояние += `С${оЭлемент.чСостояние}№${оЭлемент.чНомер}`;
if (оЭлемент.пДанные === СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ)
{
сСостояние += 'Н';
}
else if (оЭлемент.пДанные === СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ)
{
сСостояние += 'К';
}
else if (оЭлемент.лРазрыв)
{
сСостояние += 'Р';
}
}
console.log(`[Очередь] ${сСостояние}`);
};
var г_оЖурнал = (function()
{
const КОЛИЧЕСТВО_ЗАПИСЕЙ_В_ЖУРНАЛЕ = 2000;
const МАКС_ДЛИНА_ЗАПИСИ = 3000;
const ВАЖНОСТЬ = [' ', '**', '!!', '@@'];
var _мЖурнал = new Array(КОЛИЧЕСТВО_ЗАПИСЕЙ_В_ЖУРНАЛЕ); // Кольцевой буфер.
var _чПоследняяЗапись = -1;
function ПеревестиАргументыВСтроку()
{
var чАргумент = 0;
var сРезультат = '';
if (arguments.length > 1 && typeof arguments[0] === 'string')
{
var мАргументы = arguments;
чАргумент = 1;
сРезультат = arguments[0].replace(/%\d*(?:\.(\d+))?[cdfos]/g, function(сПодстрока, сТочность)
{
if (чАргумент >= мАргументы.length)
{
return сПодстрока;
}
var пАргумент = мАргументы[чАргумент++];
switch (сПодстрока.charAt(сПодстрока.length - 1))
{
case 'c':
return '';
case 'd':
// A la Chrome 48.
if (typeof пАргумент !== 'number')
{
return 'NaN';
}
// Chrome 48 использует Math.floor(). Firefox 44 использует Math.trunc() и оставляет только младшие 32 бита.
return Math.floor(пАргумент);
case 'f':
// A la Chrome 48.
if (typeof пАргумент !== 'number')
{
return 'NaN';
}
if (сТочность !== undefined)
{
return пАргумент.toFixed(Math.min(сТочность, 100));
}
return пАргумент;
case 'o':
return ПеревестиОбъектВСтроку(пАргумент);
case 's':
return пАргумент;
default:
Проверить(false);
}
});
}
while (чАргумент < arguments.length)
{
сРезультат += (сРезультат === '' ? '' : ' ') + ПеревестиОбъектВСтроку(arguments[чАргумент++]);
}
return сРезультат;
}
function ПеревестиОбъектВСтроку(пОбъект)
// TODO symbol
{
if (typeof пОбъект === 'function')
{
return `[function ${пОбъект.name}]`;
}
if (ЭтоОбъект(пОбъект))
{
return JSON.stringify(пОбъект);
}
return пОбъект;
}
function ДобавитьВЖурнал(чВажность, сЗапись)
{
Проверить(typeof чВажность === 'number' && чВажность >= 0 && чВажность < ВАЖНОСТЬ.length && typeof сЗапись === 'string');
сЗапись = `${ВАЖНОСТЬ[чВажность]} ${(performance.now() / 1000).toFixed(3)} ${сЗапись}`;
if (сЗапись.length > МАКС_ДЛИНА_ЗАПИСИ)
{
сЗапись = `${сЗапись.substr(0, МАКС_ДЛИНА_ЗАПИСИ)}---8<---Отрезано ${сЗапись.length - МАКС_ДЛИНА_ЗАПИСИ}`;
}
if (++_чПоследняяЗапись === _мЖурнал.length)
{
_чПоследняяЗапись = 0;
}
_мЖурнал[_чПоследняяЗапись] = сЗапись;
}
function ПолучитьДанныеДляОтчета()
{
var чСледующаяЗапись = _чПоследняяЗапись + 1;
if (чСледующаяЗапись === _мЖурнал.length)
{
return _мЖурнал;
}
if (_мЖурнал[чСледующаяЗапись] === undefined)
{
return _мЖурнал.slice(0, чСледующаяЗапись);
}
return _мЖурнал.slice(чСледующаяЗапись).concat(_мЖурнал.slice(0, чСледующаяЗапись));
}
function ConsoleLog()
{
ДобавитьВЖурнал(0, ПеревестиАргументыВСтроку.apply(undefined, arguments));
}
function ConsoleInfo()
{
ДобавитьВЖурнал(1, ПеревестиАргументыВСтроку.apply(undefined, arguments));
}
function ConsoleWarn()
{
ДобавитьВЖурнал(2, ПеревестиАргументыВСтроку.apply(undefined, arguments));
}
function ConsoleError()
{
ДобавитьВЖурнал(3, ПеревестиАргументыВСтроку.apply(undefined, arguments));
}
console.log = ConsoleLog;
console.info = ConsoleInfo;
console.warn = ConsoleWarn;
console.error = ConsoleError;
return {
ПолучитьДанныеДляОтчета
};
})();
var г_оОтладка = (function()
{
var _сСписокВариантов = '';
var _мСпискиСегментов = [];
var _мТранспортныеПотоки = [];
var _мМедиаСегменты = [];
var _мбВидеоИнициализация = null;
var _мбАудиоИнициализация = null;
function ОчиститьСтек(сСтек)
// Чистка стека от лишних данных.
// Это сократит строку, упростит разбор, удалит персональные данные в Firefox (например, имя пользователя).
{
return сСтек
.replace(/blob:.+?(:\d+)/g, 'blob$1') // Адрес worker. TODO Исходный код worker находится в blob?
.replace(/chrome-extension:\/\/[a-z]+\//g, '') // Адрес главного файла.
}
function ПолучитьНазваниеВидюхи()
// Firefox 45: WEBGL_debug_renderer_info доступно только в Nightly и Developer Edition.
{
try
{
var oContext = document.createElement('canvas').getContext('webgl');
var oExtension = oContext.getExtension('WEBGL_debug_renderer_info');
return oContext.getParameter(oExtension.UNMASKED_VENDOR_WEBGL) + ' | ' + oContext.getParameter(oExtension.UNMASKED_RENDERER_WEBGL);
}
catch (и)
{
return '';
}
}
function СоздатьОтчет(сПричинаЗавершенияРаботы)
{
// В JSON не попадут свойства со значением undefined.
return {
ПричинаЗавершенияРаботы: ОчиститьСтек(сПричинаЗавершенияРаботы),
ВерсияДополнения: ВЕРСИЯ_ДОПОЛНЕНИЯ,
Оборзеватель: navigator.userAgent,
Видюха: ПолучитьНазваниеВидюхи(),
Адрес: location.href,
Время: (new Date()).toISOString(),
Настройки: г_оНастройки.ПолучитьДанныеДляОтчета(),
Статистика: г_оСтатистика.ПолучитьДанныеДляОтчета(),
СписокВариантов: _сСписокВариантов,
СпискиСегментов: _мСпискиСегментов,
Журнал: г_оЖурнал.ПолучитьДанныеДляОтчета()
};
}
function СоздатьСсылкуДляСкачиванияФайла(сИмя, сТип, оСодержимое)
{
var сАдрес = URL.createObjectURL(new Blob([оСодержимое], {type: сТип}));
сИмя = ToHtmlAttribute(сИмя);
return `<a href="${сАдрес}" download="${сИмя}">${сИмя}</a> `;
}
function ПоказатьПричинуЗавершенияРаботы(пДанные, буфТранспортныйПоток)
{
if (typeof пДанные === 'string')
{
var сКласс = 'gm-tw5-отладка-сообщение';
var сФорма = `<p>${ToHtmlText(пДанные)}</p>`;
}
else if (пДанные.ПричинаЗавершенияРаботы === 'ОСТАВИТЬ ОТЗЫВ')
{
var сКласс = 'gm-tw5-отладка-отзыв';
var сФорма =
`
<h3>Оставить отзыв о работе дополнения <q>Twitch&nbsp;5</q></h3>
<p>
Если вы увидели проблемы в трансляции и хотите написать об этом, то сначала запустите стандартный проигрыватель (кнопка Twitch в настройках в правом нижнем углу).
Если в нём всё так же плохо, то дополнение не виновато и отзыв оставлять не нужно.
Разработчик дополнения Twitch&nbsp;5 не имеет никакого отношения к владельцам Twitch.tv и не может повлять на работу их сайта и на качество трансляций.
</p>
<p>
Что вы хотите сообщить:
<textarea name="gm-tw5-отладка-отзыв" rows="6" minlength="4" maxlength="10000" spellcheck="true" required data-gm-tw5-отладка-фокус></textarea>
</p>
<p>
Вы можете оставить адрес электронной почты, чтобы разработчик, если нужно, мог с вами связаться.
Третьим лицам адрес передаётся, спам не рассылается.
<input name="gm-tw5-отладка-почта" type="email" maxlength="254" spellcheck="false">
</p>
<p>
Вместе с отзывом будет отправлена отладочная информация, которая поможет разобраться, что за хрень у вас происходит.
Отладочная информация не содержит ваших персональных данных.
<label for="gm-tw5-отладка-показатьотчет" class="gm-tw5-отладка-ссылкакнопка">Показать отладочную информацию.</label>
<input id="gm-tw5-отладка-показатьотчет" type="checkbox" hidden>
<textarea name="gm-tw5-отладка-отчет" rows="20" spellcheck="false" required readonly></textarea>
</p>
<p>
<input name="gm-tw5-отладка-типданных" type="hidden" value="отзыв">
<input type="submit" value="Отправить отзыв разработчику дополнения Twitch&nbsp;5">
</p>
`;
}
else
{
var сФайлы = '';
var сКласс = 'gm-tw5-отладка-ошибка';
var сФорма =
`
<h3>Во время работы дополнения <q>Twitch&nbsp;5</q> произошла ошибка</h3>
<p>
Дополнение пока находится в разработке, поэтому ошибки происходят чаще, чем хотелось бы.
</p>
<p>
Убедитесь, что у вас установлена последняя версия дополнения.
Также не забывайте обновлять браузер: в новых версиях нередко исправляются ошибки, связанные с воспроизведением видео и работой расширений.
</p>
<p>
Если у вас регулярно происходят ошибки, то возможно проблема связана с настройкой оборудования и программного обеспечения вашего компьютера.
Попробуйте запустить браузер с новым профилем (со стандартными настройками и без других расширений).
Также закройте все барахло, загруженное во время старта операционной системы и висящее в фоне.
</p>
<p>
Пожалуйста, отправьте отчет об ошибке разработчику дополнения, чтобы он смог эту ошибку исправить.
Отчет не содержит ваших персональных данных.
<label for="gm-tw5-отладка-показатьотчет" class="gm-tw5-отладка-ссылкакнопка">Показать содержимое отчета.</label>
<input id="gm-tw5-отладка-показатьотчет" type="checkbox" hidden>
<textarea name="gm-tw5-отладка-отчет" rows="20" spellcheck="false" required readonly></textarea>
${буфТранспортныйПоток ? 'Вместе с отчетом будет отправлено несколько секунд просмотренного видео.' : ''}
</p>
<p>
Кратко укажите причину, которая, по вашему мнению, могла вызвать эту ошибку.
Если у вас нет вариантов, то ничего не указывайте.
Например (щёлкните чтобы вставить):
<label class="gm-tw5-отладка-ссылкакнопка">просмотр одновременно нескольких трансляций (сколько?)</label>,
<label class="gm-tw5-отладка-ссылкакнопка">пробуждение компьютера после спячки</label>,
<label class="gm-tw5-отладка-ссылкакнопка">сильная загрузка процессора другими приложениями (игры, антивирусы)</label>,
<label class="gm-tw5-отладка-ссылкакнопка">сильная загрузка видеокарты другими приложениями (проигрыватели, игры)</label>,
<label class="gm-tw5-отладка-ссылкакнопка">нехватка оперативной памяти</label>,
<label class="gm-tw5-отладка-ссылкакнопка">перегрев процессора или видеокарты (в ноутбуке)</label>,
<label class="gm-tw5-отладка-ссылкакнопка">нападение на квартиру пришельцев-рептилоидов</label>,
<label class="gm-tw5-отладка-ссылкакнопка">проблемы с доступом в Интернет</label>,
<label class="gm-tw5-отладка-ссылкакнопка">смена видеорежима или других настроек операционной системы</label>.
<textarea name="gm-tw5-отладка-отзыв" rows="3" maxlength="10000" spellcheck="true"></textarea>
</p>
${сФайлы}
<p>
<input name="gm-tw5-отладка-типданных" type="hidden" value="ошибка">
<input type="submit" value="Отправить отчет об ошибке разработчику дополнения Twitch&nbsp;5" data-gm-tw5-отладка-фокус>
</p>
`;
}
window.stop();
var sTitle = document.title;
ЗаменитьСтраницу(`
<head>
<meta charset="utf-8">
<title>${ToHtmlText(sTitle)}</title>
<style>
html, body
{
height: 100%;
margin: 0;
padding: 0;
}
body
{
display: flex;
font: 15px/1.5 Arial, Helvetica Neue, Helvetica, sans-serif;
color: #fff;
text-align: center;
}
.gm-tw5-отладка-сообщение
{
background: #6441A5;
}
.gm-tw5-отладка-отзыв
{
background: linear-gradient(150deg, hsl(214, 70%, 26%) 20em, hsl(214, 60%, 68%)) fixed;
}
.gm-tw5-отладка-ошибка
{
background: hsl(0, 100%, 40%);
}
form
{
max-width: 50em;
margin: auto;
padding: 0 1em;
}
#gm-tw5-отладка-передотправкой
{
text-align: justify;
}
a
{
color: #ff5;
}
textarea, input, button
{
font: inherit;
font-weight: normal;
}
fieldset:not([hidden])
{
display: inline-block;
}
label > input[type="radio"]
{
vertical-align: -1px;
}
textarea, input[type="email"]
{
display: block;
box-sizing: border-box;
width: 100%;
}
#gm-tw5-отладка-показатьотчет:not(:checked) + textarea
{
display: none;
}
.gm-tw5-отладка-ссылкакнопка
{
border-bottom: .1em dashed;
cursor: pointer;
}
</style>
</head>
<body class="${сКласс}">
<form id="gm-tw5-отладка-передотправкой" method="post" action="http://coolcmd.tk/tw5/%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B5.php" enctype="multipart/form-data" accept-charset="UTF-8">
${сФорма}
</form>
<form id="gm-tw5-отладка-идетотправка">
<p>
Идет отправка данных\u2002<progress></progress>
</p>
<input type="submit" value="Прервать" data-gm-tw5-отладка-фокус>
</form>
<form id="gm-tw5-отладка-сбойотправки">
<p>
Не удалось отправить данные :(
</p>
<input type="submit" value="Попробовать еще раз" data-gm-tw5-отладка-фокус>
</form>
<form id="gm-tw5-отладка-послеотправки">
<p>
Данные было успешно отправлены. Спасибо!
</p>
<input type="submit" value="Продолжить просмотр трансляции" data-gm-tw5-отладка-фокус>
</form>
</body>
`);
ОтчетОбновлен();
ПоказатьФорму('gm-tw5-отладка-передотправкой');
document.body.addEventListener('click', ОбработатьЩелчок);
document.body.addEventListener('submit', ОбработатьОтправкуФормы);
г_оУправление.ОтключитьПолноэкранныйРежим();
function ОбработатьЩелчок(оСобытие)
{
if (оСобытие.target.classList.contains('gm-tw5-отладка-ссылкакнопка') && !оСобытие.target.hasAttribute('for'))
{
document.getElementsByName('gm-tw5-отладка-отзыв')[0].value += оСобытие.target.textContent + ' ';
}
}
var оЗапрос;
function ОбработатьОтправкуФормы(оСобытие)
{
оСобытие.preventDefault();
switch (оСобытие.target.id)
{
case 'gm-tw5-отладка-передотправкой':
document.getElementsByTagName('progress')[0].value = 0;
ПоказатьФорму('gm-tw5-отладка-идетотправка');
var узВидюха = оСобытие.target.querySelector('fieldset:not([hidden]) *:checked');
if (узВидюха)
{
пДанные.Видюха = узВидюха.getAttribute('value');
ОтчетОбновлен();
}
var оДанные = new FormData(оСобытие.target);
// BUG Firefox 44 + Greasemonkey 3.6: у буфТранспортныйПоток все свойства undefined.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1248865
if (буфТранспортныйПоток)
{
оДанные.append('gm-tw5-отладка-транспортныйпоток', new Blob([буфТранспортныйПоток], {type: 'video/mp2t'}));
}
оЗапрос = new XMLHttpRequest();
оЗапрос.upload.addEventListener('progress', function(оСобытие)
{
if (оСобытие.lengthComputable)
{
document.getElementsByTagName('progress')[0].value = оСобытие.loaded / оСобытие.total;
}
else
{
document.getElementsByTagName('progress')[0].removeAttribute('value');
}
});
оЗапрос.addEventListener('load', function()
{
ПоказатьФорму(this.status >= 200 && this.status <= 299 ? 'gm-tw5-отладка-послеотправки' : 'gm-tw5-отладка-сбойотправки');
});
оЗапрос.addEventListener('error', function()
{
ПоказатьФорму('gm-tw5-отладка-сбойотправки');
});
оЗапрос.addEventListener('abort', function()
{
ПоказатьФорму('gm-tw5-отладка-сбойотправки');
});
оЗапрос.open(оСобытие.target.getAttribute('method'), оСобытие.target.getAttribute('action'));
оЗапрос.send(оДанные);
break;
case 'gm-tw5-отладка-идетотправка':
оЗапрос.abort();
break;
case 'gm-tw5-отладка-сбойотправки':
document.querySelector('#gm-tw5-отладка-передотправкой *[type="submit"]').click();
break;
case 'gm-tw5-отладка-послеотправки':
location.reload();
break;
default:
Проверить(false);
}
}
function ОтчетОбновлен()
{
if (typeof пДанные !== 'string')
{
document.getElementsByName('gm-tw5-отладка-отчет')[0].value = JSON.stringify(пДанные);
var узВидюха = document.getElementsByTagName('fieldset')[0];
// Если ошибка произошла в worker, то причина в буфТранспортныйПоток и производитель видюхи не важен.
if (узВидюха && (пДанные.Видюха || буфТранспортныйПоток))
{
узВидюха.setAttribute('hidden', '');
}
}
}
function ПоказатьФорму(сИдентификаторФормы)
{
// Chrome 48: for...of не работает с DOM.
for (var ы = 0, узФорма; узФорма = document.forms[ы]; ++ы)
{
if (узФорма.id === сИдентификаторФормы)
{
узФорма.removeAttribute('hidden');
var узФокус = узФорма.querySelector('*[data-gm-tw5-отладка-фокус]');
if (узФокус)
{
узФокус.focus();
}
}
else
{
узФорма.setAttribute('hidden', '');
}
}
}
}
function СохранитьСписокВариантов(сСписокВариантов)
{
if (!г_лРаботаЗавершена)
{
_сСписокВариантов = сСписокВариантов;
}
}
function СохранитьСписокСегментов(сСписокСегментов)
{
if (!г_лРаботаЗавершена)
{
if (_мСпискиСегментов.length === 3)
{
_мСпискиСегментов.shift();
}
_мСпискиСегментов.push(сСписокСегментов);
}
}
function СохранитьТранспортныйПоток(буфТранспортныйПоток, чНомер)
{
}
function СохранитьСегментИнициализации(мбВидеоИнициализация, мбАудиоИнициализация)
{
}
function СохранитьМедиаСегмент(мбВидеоСегмент, мбАудиоСегмент, чНомер)
{
}
function ЗавершитьРаботу(сПричинаЗавершенияРаботы, лНеОтправлять, буфТранспортныйПоток)
{
if (!г_лРаботаЗавершена)
{
г_лРаботаЗавершена = true;
try
{
console.error('Завершаю работу');
г_моОчередь.ПоказатьСостояние();
г_оСайт.ЗавершитьСборМетаданныхТрансляции();
г_оСписок.Остановить(false);
г_оПреобразователь.Остановить();
г_оПроигрыватель.Остановить();
г_моОчередь.Очистить();
console.error('Работа завершена');
}
finally
{
ПоказатьПричинуЗавершенияРаботы(лНеОтправлять ? сПричинаЗавершенияРаботы : СоздатьОтчет(сПричинаЗавершенияРаботы), буфТранспортныйПоток);
}
}
}
function ПойманоИсключение(пИсключение)
{
ЗавершитьРаботу(ЭтоОбъект(пИсключение) ? пИсключение.stack : '' + пИсключение);
// На случай вложенного try.
throw undefined;
}
function ПроверкаНеПройдена()
{
ПойманоИсключение(new Error('Проверка не пройдена'));
}
function ПоказатьСообщениеИЗавершитьРаботу(сСообщение)
{
ЗавершитьРаботу(сСообщение, true);
throw undefined;
}
function ОставитьОтзыв()
{
ЗавершитьРаботу('ОСТАВИТЬ ОТЗЫВ');
}
return {
СохранитьСписокВариантов,
СохранитьСписокСегментов,
СохранитьТранспортныйПоток,
СохранитьСегментИнициализации,
СохранитьМедиаСегмент,
ЗавершитьРаботу,
ПойманоИсключение,
ПроверкаНеПройдена,
ПоказатьСообщениеИЗавершитьРаботу,
ОставитьОтзыв
};
})();
var г_оНастройки = (function()
{
const _оНачальныеНастройки =
{
чСлучайноеЧисло: Math.random(),
сПредыдущаяВерсия: ВЕРСИЯ_ДОПОЛНЕНИЯ,
чГромкость: 0.5, // МИНИМАЛЬНАЯ_ГРОМКОСТЬ..МАКСИМАЛЬНАЯ_ГРОМКОСТЬ
лПриглушить: false,
сНазваниеВарианта: 'CoolCmd',
кОдновременныхЗагрузок: 2,
чРазмерБуфера: 8.5, // Секунды. 8,5 секунд ≈ 3..6 сегментов.
// TODO Измерять чРазмерБуфера в сегментах?
// TODO чРазмерБуфера зависит от кОдновременныхЗагрузок.
// TODO Можно сделать 2 значения и постепенный рост с первого до второго.
чНачалоВоспроизведения: 3.0, // Секунды. 3,0 секунд ≈ 1..2 сегментов.
// TODO Измерять чНачалоВоспроизведения в сегментах?
// TODO чНачалоВоспроизведения всегда должен быть меньше чРазмерБуфера?
кЗаначка: 1, // Сегменты.
// TODO Заменить сегменты на время.
// TODO Ограничить заначку по времени.
чИнтервалОпроса: 100, // Проценты.
лОткрытьЧат: false,
оПоложениеОкнаЧата:
{
nX: 70,
nY: 100,
чШирина: 400,
чВысота: 600
}
};
const _мноОбязательныеНастройки = new Set(['чСлучайноеЧисло', 'сПредыдущаяВерсия']);
var _оНастройки;
function ПроверитьРезультатСохранения()
{
if (chrome.runtime.lastError)
{
г_оОтладка.ЗавершитьРаботу(`Не удалось сохранить настройки. ${chrome.runtime.lastError.message}`);
}
}
function ПроверитьРезультатУдаления()
{
if (chrome.runtime.lastError)
{
г_оОтладка.ЗавершитьРаботу(`Не удалось удалить настройки. ${chrome.runtime.lastError.message}`);
}
}
function Сохранить(сНазвание)
{
var оСохранить = _оНастройки;
if (сНазвание !== null)
{
Проверить(typeof _оНастройки[сНазвание] !== 'undefined');
оСохранить = Object.create(null);
оСохранить[сНазвание] = _оНастройки[сНазвание];
}
chrome.storage.local.set(оСохранить, ПроверитьРезультатСохранения);
console.log('[Настройки] Настройки сохранены', оСохранить);
}
function Восстановить()
{
return new Promise(function(фВыполнить, фОтказаться)
{
chrome.storage.local.get(null, function(оНастройки)
{
try
{
if (chrome.runtime.lastError)
{
фОтказаться(new Error(`Не удалось прочесть настройки. ${chrome.runtime.lastError.message}`));
}
else
{
console.log('[Настройки] Настройки восстановлены', оНастройки);
//
// Не переносим устаревшие настройки, добавляем новые.
//
_оНастройки = Object.create(null);
Проверить(ЭтоОбъект(_оНачальныеНастройки));
for (var сНазвание of Object.keys(_оНачальныеНастройки))
{
Проверить(_оНачальныеНастройки[сНазвание] !== undefined);
if (typeof оНастройки[сНазвание] === typeof _оНачальныеНастройки[сНазвание])
{
_оНастройки[сНазвание] = оНастройки[сНазвание];
}
else
{
_оНастройки[сНазвание] = _оНачальныеНастройки[сНазвание];
if (_мноОбязательныеНастройки.has(сНазвание))
{
Сохранить(сНазвание);
}
}
}
фВыполнить();
}
}
catch (пИсключение)
{
фОтказаться(пИсключение);
}
});
});
}
function Сбросить()
{
Проверить(_оНастройки);
console.log('[Настройки] Сбрасываю настройки');
chrome.storage.local.clear(ПроверитьРезультатУдаления);
for (var сНазвание of Object.keys(_оНачальныеНастройки))
{
if (!_мноОбязательныеНастройки.has(сНазвание))
{
_оНастройки[сНазвание] = _оНачальныеНастройки[сНазвание];
}
else
{
Сохранить(сНазвание);
}
}
}
function Изменить(сНазвание, пЗначение)
{
Проверить(typeof сНазвание === 'string' && сНазвание);
Проверить(пЗначение !== undefined);
Проверить(_оНастройки);
Проверить(typeof _оНастройки[сНазвание] === typeof пЗначение);
if (_оНастройки[сНазвание] !== пЗначение)
{
_оНастройки[сНазвание] = пЗначение;
Сохранить(сНазвание);
}
}
function Получить(сНазвание)
{
Проверить(typeof сНазвание === 'string' && сНазвание);
Проверить(_оНастройки);
Проверить(typeof _оНастройки[сНазвание] !== 'undefined');
return _оНастройки[сНазвание];
}
function ПолучитьДанныеДляОтчета()
{
return _оНастройки;
}
return {
Восстановить,
Сбросить,
Изменить,
Получить,
ПолучитьДанныеДляОтчета
};
})();
var г_оСтатистика = (function()
{
const ВЫДЕЛИТЬ_ОЖИДАНИЕ_ОТВЕТА = 1.0; // Секунды.
const ВЫДЕЛИТЬ_ПРЕОБРАЗОВАНО = 2; // Количество преобразованных сегментов.
const ВЫДЕЛИТЬ_НЕ_ПРОСМОТРЕНО_МИН = 1.5; // TODO Подобрать подходящее значение.
const ВЫДЕЛИТЬ_НЕ_ПРОСМОТРЕНО_МАКС = 15; // TODO Подобрать подходящее значение.
const ВЫДЕЛИТЬ_ПРОПУЩЕННЫЕ_КАДРЫ = 100; // Количество пропущенных кадров.
const ВЫДЕЛИТЬ_ЧАСТОТУ_КАДРОВ_АБС = 110; // TODO Подобрать подходящее значение.
const ВЫДЕЛИТЬ_ЧАСТОТУ_КАДРОВ_ОТН = 0.85; // TODO Подобрать подходящее значение.
const ДЛИТЕЛЬНОСТЬ_АНАЛИЗА = 60; // Секунды.
var _узОкно = null;
var _чТаймер = 0;
var _сСервер = '';
var _сПараметрыВидео = '';
var _сПараметрыСжатияВидео = '';
var _сПараметрыЗвука = '';
var _nTargetDuration = 0;
var _кСегментовВСписке = 0;
var _оТолщинаСегмента = null;
var _оТолщинаКанала = null;
var _оОжиданиеОтвета = null;
var _кбВсегоСкачано = 0;
var _кНеЗагруженныхСегментов = 0;
var _кНеполныхСегментов = 0;
var _кПереполненийБуфера = 0;
var _кПропущенныхКадров;
function Анализ(сИдентификатор, чРазмерИстории, чТочность)
{
Проверить(чРазмерИстории > 0 && чТочность >= 0);
this._узПоказать = document.getElementById(сИдентификатор);
this._мчИстория = new Array(чРазмерИстории); // Кольцевой буфер.
this._чТочность = чТочность;
this._Очистить();
}
Анализ.prototype._Очистить = function()
{
this._кЗаполнено = 0;
this._чИндекс = -1;
this._узПоказать.innerHTML = `
<td class="gm-tw5-статистика-анализ-последнее"><td>\u00A0\u00A0[</td>
<td><td>\u2009<\u2009</td>
<td><td>\u2009<\u2009</td>
<td><td>]<td class="gm-tw5-статистика-подробно">\u00A0\u00A0</td>
${'<td class="gm-tw5-статистика-анализ-история gm-tw5-статистика-подробно">'.repeat(this._мчИстория.length)}`;
};
Анализ.prototype._ВСтроку = function(чЧисло)
{
return чЧисло.toFixed(this._чТочность);
};
Анализ.prototype.Очистить = function()
{
if (this._кЗаполнено !== 0)
{
this._Очистить();
}
};
Анализ.prototype.ПолучитьПоследнееЧисло = function(чЗаглушка)
{
return this._кЗаполнено === 0 ? чЗаглушка : this._мчИстория[this._чИндекс];
};
Анализ.prototype.ДобавитьЧисло = function(чЧисло, лВыделить, фВыделить)
{
if (isNaN(чЧисло))
{
return;
}
if (this._кЗаполнено !== 0)
{
this._узПоказать.children[9 + this._чИндекс].classList.remove('gm-tw5-статистика-анализ-индекс');
}
if (this._кЗаполнено !== this._мчИстория.length)
{
++this._кЗаполнено;
}
if (++this._чИндекс === this._мчИстория.length)
{
this._чИндекс = 0;
}
this._мчИстория[this._чИндекс] = чЧисло;
var чМинимальное = Infinity;
var чМаксимальное = -Infinity;
var чСреднее = 0;
for (var ы = 0; ы < this._кЗаполнено; ++ы)
{
чМинимальное = Math.min(чМинимальное, this._мчИстория[ы]);
чМаксимальное = Math.max(чМаксимальное, this._мчИстория[ы]);
чСреднее += this._мчИстория[ы];
}
чСреднее /= this._кЗаполнено;
var мВыделить = фВыделить(чМинимальное, чСреднее, чМаксимальное);
Проверить(мВыделить.length === 3 && typeof мВыделить[0] === 'boolean' && typeof мВыделить[1] === 'boolean' && typeof мВыделить[2] === 'boolean');
var сЧисло = this._ВСтроку(чЧисло);
ОбновитьЗначение(this._узПоказать.children[0], сЧисло, лВыделить, false);
ОбновитьЗначение(this._узПоказать.children[2], this._ВСтроку(чМинимальное), мВыделить[0], false);
ОбновитьЗначение(this._узПоказать.children[4], this._ВСтроку(чСреднее), мВыделить[1], false);
ОбновитьЗначение(this._узПоказать.children[6], this._ВСтроку(чМаксимальное), мВыделить[2], false);
var уз = this._узПоказать.children[9 + this._чИндекс];
ОбновитьЗначение(уз, сЧисло, лВыделить, false);
уз.classList.add('gm-tw5-статистика-анализ-индекс');
};
function ОбновитьЗначение(пУзел, пЗначение, лВыделить, лОживить)
{
var узУзел = typeof пУзел === 'string' ? document.getElementById(пУзел) : пУзел;
var оКлассы = узУзел.classList;
оКлассы.toggle('gm-tw5-статистика-выделить', лВыделить);
if (лОживить)
{
if (оКлассы.contains('gm-tw5-статистика-оживить-1'))
{
оКлассы.remove('gm-tw5-статистика-оживить-1');
оКлассы.add('gm-tw5-статистика-оживить-2');
}
else
{
оКлассы.remove('gm-tw5-статистика-оживить-2');
оКлассы.add('gm-tw5-статистика-оживить-1');
}
}
узУзел.textContent = пЗначение;
}
function ПолучитьНазваниеПрофиляH264(nProfileIndication, nConstraintSetFlag)
// ITU-T H.264:2014 A.2 Profiles
{
switch (nProfileIndication)
{
case 66: return (nConstraintSetFlag & 0x40) === 0 ? 'Baseline' : 'Constrained Baseline'; // constraint_set1_flag
case 77: return 'Main';
case 88: return 'Extended';
case 100:
switch (nConstraintSetFlag & 0x0C)
{
case 0x08: return 'Progressive High'; // constraint_set4_flag
case 0x0C: return 'Constrained High'; // constraint_set4_flag | constraint_set5_flag
}
return 'High';
case 110: return (nConstraintSetFlag & 0x10) === 0 ? 'High 10' : 'High 10 Intra'; // constraint_set3_flag
case 122: return (nConstraintSetFlag & 0x10) === 0 ? 'High 4:2:2' : 'High 4:2:2 Intra'; // constraint_set3_flag
case 244: return (nConstraintSetFlag & 0x10) === 0 ? 'High 4:4:4 Predictive' : 'High 4:4:4 Intra'; // constraint_set3_flag
case 44: return 'CAVLC 4:4:4 Intra';
}
return nProfileIndication + 'C' + nConstraintSetFlag;
}
function ОбновитьСтатистику()
{
if (г_лРаботаЗавершена)
{
return;
}
document.getElementById('gm-tw5-статистика-длительностьпросмотра').textContent = ПеревестиСекундыВСтроку(performance.now() / 1000, true);
_кПропущенныхКадров = г_оПроигрыватель.ПолучитьКоличествоПропущенныхКадров();
ОбновитьЗначение('gm-tw5-статистика-пропущено', _кПропущенныхКадров, _кПропущенныхКадров >= ВЫДЕЛИТЬ_ПРОПУЩЕННЫЕ_КАДРЫ, false);
var кЖдетЗагрузки = 0, чЖдетЗагрузки = 0, чЗагружается = 0, кПреобразовано = 0;
for (var оСегмент of г_моОчередь)
{
switch (оСегмент.чСостояние)
{
case СЕГМЕНТ_ЖДЕТ_ЗАГРУЗКИ:
++кЖдетЗагрузки;
чЖдетЗагрузки += оСегмент.чДлительность;
break;
case СЕГМЕНТ_ЗАГРУЖАЕТСЯ:
чЗагружается += оСегмент.чДлительность;
break;
case СЕГМЕНТ_ЗАГРУЖЕН:
break;
case СЕГМЕНТ_ПРЕОБРАЗОВАН:
case СЕГМЕНТ_ДОБАВЛЯЕТСЯ:
++кПреобразовано;
break;
default:
Проверить(false);
}
}
var оВБуфере = г_оПроигрыватель.ПолучитьДлительностьВидеоВБуфере();
var уз = document.getElementById('gm-tw5-статистика-очередь');
ОбновитьЗначение(уз, чЖдетЗагрузки.toFixed(1), кЖдетЗагрузки > _кСегментовВСписке, false);
уз = уз.nextElementSibling;
уз.textContent = ' + ' + чЗагружается.toFixed(1);
уз = уз.nextElementSibling;
ОбновитьЗначение(уз, кПреобразовано, кПреобразовано >= ВЫДЕЛИТЬ_ПРЕОБРАЗОВАНО, false);
уз = уз.nextElementSibling;
ОбновитьЗначение(уз,оВБуфере.чНеПросмотрено.toFixed(1),
оВБуфере.чНеПросмотрено < ВЫДЕЛИТЬ_НЕ_ПРОСМОТРЕНО_МИН || оВБуфере.чНеПросмотрено >= г_оНастройки.Получить('чРазмерБуфера') + ВЫДЕЛИТЬ_НЕ_ПРОСМОТРЕНО_МАКС,
false);
уз = уз.nextElementSibling;
уз.textContent = ' + ' + оВБуфере.чПросмотрено.toFixed(1);
}
function ОкноПоказано()
{
return _узОкно !== null;
}
function СкрытьОкно()
{
if (ОкноПоказано())
{
clearInterval(_чТаймер);
_узОкно.remove();
_узОкно = null;
_оТолщинаСегмента = null;
_оТолщинаКанала = null;
_оОжиданиеОтвета = null;
}
}
function ПоказатьОкно()
{
if (ОкноПоказано())
{
return;
}
// Не вешать title на td, чтобы он не расползался на всю ширину окна.
document.getElementById('gm-tw5-заголовок').insertAdjacentHTML('beforebegin',
`
<div id="gm-tw5-статистика" draggable="true">
<style>
#gm-tw5-статистика
{
position: absolute;
top: 40px;
left: 0;
font-size: 12px;
font-weight: normal;
line-height: 1.3;
padding: .5em;
color: #FFF;
text-shadow: 0 0 10px #000;
background: rgba(0, 0, 0, .5);
box-sizing: border-box;
max-width: 100%;
overflow: auto;
cursor: auto;
}
#gm-tw5-статистика:hover,
#gm-tw5-статистика.тащить
{
background: rgba(0, 0, 0, 1);
}
#gm-tw5-статистика.тащить
{
outline: 1px solid hsl(195, 80%, 90%);
}
#gm-tw5-статистика:not(:hover):not(.тащить) .gm-tw5-статистика-подробно
{
display: none;
}
#gm-tw5-статистика > table
{
border-spacing: 0;
}
#gm-tw5-статистика th
{
padding: 0 .4em 0 0;
}
#gm-tw5-статистика td
{
padding: 0;
}
#gm-tw5-статистика th
{
text-align: left;
color: hsl(195, 80%, 90%);
}
.gm-tw5-статистика-выделить
{
font-weight: bold;
color: #FF6;
}
.gm-tw5-статистика-оживить-1, .gm-tw5-статистика-оживить-2
{
display: inline-block;
animation-duration: .3s;
animation-timing-function: cubic-bezier(0.9, 0.03, 0.69, 0.22) /* step-end */;
}
.gm-tw5-статистика-оживить-1
{
animation-name: gm-tw5-статистика-оживить-1;
}
.gm-tw5-статистика-оживить-2
{
animation-name: gm-tw5-статистика-оживить-2;
}
@keyframes gm-tw5-статистика-оживить-1
{
from {transform: scale(1.4, 1.4)}
80% {transform: scale(.85, .85)}
to {transform: none}
}
@keyframes gm-tw5-статистика-оживить-2
/* Копия gm-tw5-статистика-оживить-1 */
{
from {transform: scale(1.4, 1.4)}
80% {transform: scale(.85, .85)}
to {transform: none}
}
#gm-tw5-статистика-анализ
{
border-spacing: 1px 0;
text-align: right;
}
.gm-tw5-статистика-анализ-последнее
{
/* Не изменять высоту строки по наведению мыши */
border: 1px solid transparent;
min-width: 2em;
}
.gm-tw5-статистика-анализ-история
{
border: 1px solid #777;
padding: 0 2px !important;
}
.gm-tw5-статистика-анализ-история:empty
{
display: none;
}
.gm-tw5-статистика-анализ-индекс
{
border: 1px solid red;
}
#gm-tw5-статистика-толщинасегмента > td
{
border-bottom: 0;
}
#gm-tw5-статистика-толщинаканала > td
{
border-bottom: 0;
border-top: 0;
}
#gm-tw5-статистика-ожиданиеответа > td
{
border-top: 0;
}
</style>
<table>
<tr><th>Видео:<td>
<span title="Ширина и высота исходного видео"></span>
<span id="gm-tw5-статистика-видео" title="Средняя частота кадров исходного видео ± Максимальное отклонение от средней частоты кадров. Рассчитывается для каждого сегмента."></span>
<tr class="gm-tw5-статистика-подробно"><th>Сжатие:<td><span id="gm-tw5-статистика-сжатиевидео" title="Параметры сжатия видео: стандарт сжатия, профиль, уровень, максимальное количество опорных кадров, чересстрочное видео, частота кадров"></span>
<tr class="gm-tw5-статистика-подробно"><th>Звук:<td><span id="gm-tw5-статистика-звук" title="Параметры звука: стандарт сжатия, частота дискретизации, количество каналов, битрейт. Битрейт рассчитывается для каждого сегмента."></span>
<tr class="gm-tw5-статистика-подробно"><th>Сервер:<td><span id="gm-tw5-статистика-сервер" title="Сервер, с которого загружается видео"></span>
<tr><th>Очередь:<td>
<span id="gm-tw5-статистика-очередь" title="Длительность видео, которое ожидает загрузки из сети (секунды)"></span>
<span title="Длительность видео, которое в данный момент загружается из сети (секунды)"></span>
\u2002\u2002
<span title="Количество сегментов, которые были преобразованы из TS в MP4 и теперь ожидают добавления в проигрыватель"></span>
\u2002\u2002
<span title="Длительность непросмотренного видео в проигрывателе (секунды)"></span>
<span title="Длительность просмотренного видео в проигрывателе (секунды)"></span>
</table>
<table>
<tr><th>Список сегментов, с:<td><span id="gm-tw5-статистика-список" title="&lt; Теоретическая максимальная длительность сегментов\n\nКоличество сегментов в списке\u2002×\u2002Средняя длительность сегментов в списке\u2002=\u2002Длительность списка"></span>\u2002<span id="gm-tw5-статистика-добавлено" title="+ Количество новых сегментов в списке"></span>
<tr><th>Битрейт сегмента, Мбит/с:<td rowspan="3">
<table id="gm-tw5-статистика-анализ">
<tr id="gm-tw5-статистика-толщинасегмента" title="Сумма битрейтов видео, звука и служебной информации в загруженном сегменте. Битрейт звука можно посмотреть в параметрах звука выше.\n\nРасшифровка: последнее значение [минимальное за последнюю минуту < среднее за последнюю минуту < максимальное за последнюю минуту] [таблица со значениями за последнюю минуту, красным цветом выделено последнее добавленное].">
<tr id="gm-tw5-статистика-толщинаканала" title="Скорость загрузки сегментов из сети. Зависит много от чего, и в том числе от времени ожидания ответа от сервера (смотрите параметр ниже). Чтобы просматривать трансляцию без остановок, средняя скорость загрузки должна быть не ниже среднего битрейта загружаемых сегментов (смотрите параметр выше).\n\nВАЖНО! Если в настройках проигрывателя Twitch&nbsp;5 включена одновременная загрузка нескольких сегментов (по умолчанию включена), то НЕ обращайте на этот параметр внимания, потому что фактическая скорость загрузки будет ВЫШЕ показанной здесь.\n\nРасшифровка: последнее значение [минимальное за последнюю минуту < среднее за последнюю минуту < максимальное за последнюю минуту] [таблица со значениями за последнюю минуту, красным цветом выделено последнее добавленное].">
<tr id="gm-tw5-статистика-ожиданиеответа" title="Время, прошедшее от посылки запроса серверу на получение сегмента и до прихода ответа сервера с началом запрошенного сегмента. Зависит от расстояния до сервера и его загруженности.\n\nРасшифровка: последнее значение [минимальное за последнюю минуту < среднее за последнюю минуту < максимальное за последнюю минуту] [таблица со значениями за последнюю минуту, красным цветом выделено последнее добавленное].">
</table>
<tr><th>Скорость загрузки, Мбит/с:
<tr><th>Ожидание ответа, с:
<tr><th>Преобразовано за, мс:<td><span id="gm-tw5-статистика-преобразованоза" title="Время, затраченное на преобразование предпоследнего сегмента из TS в MP4. Показывает не то что вы думаете, если все ядра процессора прилично загружены."></span>
<tr><th>Время трансляции:<td><span id="gm-tw5-статистика-длительностьтрансляции" title="Примерно столько времени непрерывно захватывается видео для этой трансляции. Значение не может превышать 26,5 часов."></span>
<tr><th>Время просмотра:<td><span id="gm-tw5-статистика-длительностьпросмотра" title="Прошло времени с начала загрузки этой страницы"></span>
<tr><th>Всего скачано, МБайт:<td><span id="gm-tw5-статистика-скачано" title="Общий размер данных, которые дополнение Twitch&nbsp;5 загрузило из сети"></span>
<tr><th>Пропущено кадров:<td><span id="gm-tw5-статистика-пропущено" title="Количество кадров, которое браузер не смог отобразить. Ничего страшного, если это число растет пока вы мучаете браузер: например, переключаете вкладки или меняете размер окна. Другие, более неприятные причины: нестабильная частота кадров в загруженном видео, поблемы в браузере или операционной системе."></span>
<tr><th>Не загружено сегментов:<td><span id="gm-tw5-статистика-незагружено" title="Сервер отказался отдавать сегмент. Возможные причины:\n\n• Кратковременные проблемы с сетью. Если они продлятся несколько секунд, то трансляция будет считаться завершенной.\n\n• Скорость загрузки (смотрите параметр выше) настолько низкая, что сегмент был удален с сервера до того, как вы успели его загрузить. Если длительность видео, которое ожидает загрузки из сети (смотрите параметр выше), превышает 30 секунд, то, скорее всего, это ваш случай. Потыкайте настройки проигрывателя Twitch&nbsp;5 или уменьшите качество трансляции. Менять качество трансляции можно далеко не на всех каналах.\n\n• Проблемы на сервере (высокая нагрузка, ошибки и т.д.). Бывает редко."></span>
<tr><th>Неполных сегментов:<td><span id="gm-tw5-статистика-неполных" title="Часть сегмента была утеряна. Пока обнаруживается только потеря звука (кратковременная остановка воспроизведения), хотя чаще всего теряется видео (кратковременное замирание картинки). Это проблема сервера или ведущего трансляции, а не вашего подключения к Интернету. Если виноват сервер, то есть небольшая вероятность, что после перезагрузка страницы вы подключитесь к менее проблемному серверу."></span>
<tr><th>Переполнений буфера:<td><span id="gm-tw5-статистика-переполнено" title="В буфере проигрывателя накопилось непросмотренного видео (смотрите параметр выше в очереди) больше ${ПЕРЕПОЛНЕНИЕ_БУФЕРА} секунд. Часть этого видео была удалена. Ничего страшного, хотя как знать, вдруг из-за этого вы пропустили что-то интересненькое? Возможные причины переполнение буфера: долгое тыкание настроек проигрывателя, плохое качество связи, большое количество неполных сегментов (смотрите параметр выше)."></span>
</table>
</div>
`
);
Проверить(_узОкно = document.getElementById('gm-tw5-статистика'));
//
// Вывод редко изменяемых показаний.
//
document.getElementById('gm-tw5-статистика-видео').previousElementSibling.textContent = _сПараметрыВидео;
document.getElementById('gm-tw5-статистика-сжатиевидео').textContent = _сПараметрыСжатияВидео;
document.getElementById('gm-tw5-статистика-сервер').textContent = _сСервер;
ОбновитьЗначение('gm-tw5-статистика-незагружено', _кНеЗагруженныхСегментов, _кНеЗагруженныхСегментов !== 0, false);
ОбновитьЗначение('gm-tw5-статистика-неполных', _кНеполныхСегментов, _кНеполныхСегментов !== 0, false);
ОбновитьЗначение('gm-tw5-статистика-переполнено', _кПереполненийБуфера, _кПереполненийБуфера !== 0, false);
_чТаймер = setInterval(ОбновитьСтатистикуV8, ИНТЕРВАЛ_ОБНОВЛЕНИЯ_СТАТИСТИКИ);
ОбновитьСтатистику();
}
function ОбновитьСтатистикуV8()
{
try
{
ОбновитьСтатистику();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОчиститьИсторию()
{
if (_оТолщинаСегмента !== null)
{
_оТолщинаСегмента.Очистить();
_оТолщинаКанала.Очистить();
_оОжиданиеОтвета.Очистить();
}
}
function ПолучитьTargetDuration()
{
return _nTargetDuration;
}
function ПолучитьДанныеДляОтчета()
{
return {
ОкноПоказано: ОкноПоказано(),
Сервер: _сСервер,
ПараметрыВидео: _сПараметрыВидео + ' ' + _сПараметрыСжатияВидео,
ПараметрыЗвука: _сПараметрыЗвука,
ВсегоСкачано: _кбВсегоСкачано,
НеЗагруженныхСегментов: _кНеЗагруженныхСегментов,
НеполныхСегментов: _кНеполныхСегментов,
ПереполненийБуфера: _кПереполненийБуфера,
ПропущенныхКадров: _кПропущенныхКадров
};
}
function СкачаноНечто(кбСкачано)
{
if (isFinite(кбСкачано))
{
_кбВсегоСкачано += кбСкачано;
if (ОкноПоказано())
{
document.getElementById('gm-tw5-статистика-скачано').textContent = (_кбВсегоСкачано / 1024 / 1024).toFixed();
}
}
}
function ВыбранВариантТрансляции(моСписокВариантов, чВыбранныйВариант)
{
Проверить(моСписокВариантов);
_сСервер = /\/+([^\/]+)/.exec(моСписокВариантов[чВыбранныйВариант].сАбсолютныйАдресСпискаСегментов)[1];
if (ОкноПоказано())
{
document.getElementById('gm-tw5-статистика-сервер').textContent = _сСервер;
}
}
function РазобранСписокСегментов(nTargetDuration, кСегментовВСписке, чДлительностьСписка)
{
_nTargetDuration = nTargetDuration;
_кСегментовВСписке = кСегментовВСписке;
if (ОкноПоказано())
{
document.getElementById('gm-tw5-статистика-список').textContent =
`<${nTargetDuration}\u2002${кСегментовВСписке} × ${(чДлительностьСписка / кСегментовВСписке).toFixed(1)} = ${чДлительностьСписка.toFixed(1)}`;
}
}
function ДобавленыСегментыВОчередь(кСегментовДобавлено)
{
if (ОкноПоказано())
{
ОбновитьЗначение('gm-tw5-статистика-добавлено', '+' + кСегментовДобавлено,
г_оНастройки.Получить('чИнтервалОпроса') === 100 && (кСегментовДобавлено < 1 || кСегментовДобавлено > 2), true);
}
}
function ЗагруженСегмент(чРазмерСегмента, чДлительностьСегмента, чДлительностьЗагрузки, чОжиданиеОтвета)
{
if (!isNaN(чДлительностьЗагрузки) && ОкноПоказано())
{
if (_оТолщинаСегмента === null)
{
Проверить(_nTargetDuration !== 0);
var чРазмерИстории = Math.ceil(ДЛИТЕЛЬНОСТЬ_АНАЛИЗА / _nTargetDuration);
_оТолщинаСегмента = new Анализ('gm-tw5-статистика-толщинасегмента', чРазмерИстории, 1);
_оТолщинаКанала = new Анализ('gm-tw5-статистика-толщинаканала', чРазмерИстории, 1);
_оОжиданиеОтвета = new Анализ('gm-tw5-статистика-ожиданиеответа', чРазмерИстории, 1);
}
var чПоследняяТолщинаСегмента = чРазмерСегмента * 8 / 1000000 / чДлительностьСегмента;
var чСредняяТолщинаСегмента;
_оТолщинаСегмента.ДобавитьЧисло(чПоследняяТолщинаСегмента, false, function(чМинимальное, чСреднее, чМаксимальное)
{
чСредняяТолщинаСегмента = чСреднее;
return [false, false, false];
});
var чПоследняяТолщинаКанала = чРазмерСегмента * 8 / 1000 / чДлительностьЗагрузки;
_оТолщинаКанала.ДобавитьЧисло(чПоследняяТолщинаКанала, чПоследняяТолщинаКанала < чПоследняяТолщинаСегмента, function(чМинимальное, чСреднее, чМаксимальное)
{
return [чМинимальное < чСредняяТолщинаСегмента, чСреднее < чСредняяТолщинаСегмента, чМаксимальное < чСредняяТолщинаСегмента];
});
чОжиданиеОтвета /= 1000;
_оОжиданиеОтвета.ДобавитьЧисло(чОжиданиеОтвета, чОжиданиеОтвета >= ВЫДЕЛИТЬ_ОЖИДАНИЕ_ОТВЕТА, function(чМинимальное, чСреднее, чМаксимальное)
{
return [чМинимальное >= ВЫДЕЛИТЬ_ОЖИДАНИЕ_ОТВЕТА, чСреднее >= ВЫДЕЛИТЬ_ОЖИДАНИЕ_ОТВЕТА, чМаксимальное >= ВЫДЕЛИТЬ_ОЖИДАНИЕ_ОТВЕТА];
});
}
}
function НеЗагруженыСегменты(кСегментов)
{
Проверить(кСегментов > 0);
_кНеЗагруженныхСегментов += кСегментов;
if (ОкноПоказано())
{
ОбновитьЗначение('gm-tw5-статистика-незагружено', _кНеЗагруженныхСегментов, true, true);
}
}
function ПреобразованСегмент(оСегмент)
{
var оДанные = оСегмент.пДанные;
if (оДанные.кНеполныхСегментов !== 0)
{
Проверить(оДанные.кНеполныхСегментов > 0);
_кНеполныхСегментов += оДанные.кНеполныхСегментов;
if (ОкноПоказано())
{
ОбновитьЗначение('gm-tw5-статистика-неполных', _кНеполныхСегментов, true, true);
}
}
if (оСегмент.лРазрыв)
{
_сПараметрыВидео = `${оДанные.чШиринаКартинки}x${оДанные.чВысотаКартинки}`;
_сПараметрыСжатияВидео = `H.264\u2002${ПолучитьНазваниеПрофиляH264(оДанные.nProfileIndication, оДанные.nConstraintSetFlag)}`
+ `\u2002L${(оДанные.nLevelIndication / 10).toFixed(1)}\u2002RF${оДанные.nMaxNumberReferenceFrames}`;
if (оДанные.лЧересстрочное)
{
_сПараметрыСжатияВидео += '\u2002чересстрочное';
}
if (оДанные.чЧастотаКадров !== 0)
{
_сПараметрыСжатияВидео += `\u2002${оДанные.чЧастотаКадров < 0 ? '≈' : ''}${Math.abs(оДанные.чЧастотаКадров).toFixed(2)} к/с`;
}
_сПараметрыЗвука = ['AAC Main', 'AAC LC', 'AAC SSR', 'AAC LTP'][оДанные.nAudioObjectType - 1]
+ `\u2002${оДанные.чЧастотаДискретизации} Гц`
+ `\u2002${оДанные.чКоличествоКаналов} канал.`;
if (ОкноПоказано())
{
document.getElementById('gm-tw5-статистика-видео').previousElementSibling.textContent = _сПараметрыВидео;
document.getElementById('gm-tw5-статистика-сжатиевидео').textContent = _сПараметрыСжатияВидео;
}
}
if (!isNaN(оДанные.чСредняяЧастотаКадров))
{
var чОтносительноеОтклонение = оДанные.чМинЧастотаКадров / оДанные.чСредняяЧастотаКадров;
var чАбсолютноеОтклонение = 1000 / оДанные.чМинЧастотаКадров - 1000 / оДанные.чСредняяЧастотаКадров;
var лВыделитьЧастотуКадров = false;
if (чОтносительноеОтклонение < ВЫДЕЛИТЬ_ЧАСТОТУ_КАДРОВ_ОТН || чАбсолютноеОтклонение >= ВЫДЕЛИТЬ_ЧАСТОТУ_КАДРОВ_АБС)
{
лВыделитьЧастотуКадров = true;
console.warn('[Проигрыватель] Превышено отклонение длительности кадра в сегменте %s СредняяДлительностьКадра=%.2fмс ОтносительноеОтклонение=%.2f АбсолютноеОтклонение=%.2fмс',
оСегмент.чНомер, 1000 / оДанные.чСредняяЧастотаКадров, чОтносительноеОтклонение, чАбсолютноеОтклонение);
}
}
if (ОкноПоказано())
{
if (!isNaN(оДанные.чСредняяЧастотаКадров))
{
ОбновитьЗначение(
document.getElementById('gm-tw5-статистика-видео'),
`@ ${оДанные.чСредняяЧастотаКадров.toFixed(2)}
−${(оДанные.чСредняяЧастотаКадров - оДанные.чМинЧастотаКадров).toFixed(2)}
+${(оДанные.чМаксЧастотаКадров - оДанные.чСредняяЧастотаКадров).toFixed(2)}`,
лВыделитьЧастотуКадров,
false
);
}
document.getElementById('gm-tw5-статистика-звук').textContent = `${_сПараметрыЗвука}\u2002${оДанные.чТолщинаЗвука.toFixed()} кбит/с`;
document.getElementById('gm-tw5-статистика-преобразованоза').textContent = оДанные.чПреобразованЗа.toFixed();
}
}
function ПереполненБуферПроигрывателя()
{
++_кПереполненийБуфера;
if (ОкноПоказано())
{
ОбновитьЗначение('gm-tw5-статистика-переполнено', _кПереполненийБуфера, true, true);
}
}
function ИзмениласьДлительностьТрансляции(чДлительностьТрансляции)
{
document.getElementById('gm-tw5-статистика-длительностьтрансляции').textContent = ПеревестиСекундыВСтроку(чДлительностьТрансляции, false);
}
return {
ОкноПоказано,
СкрытьОкно,
ПоказатьОкно,
ОчиститьИсторию,
ПолучитьTargetDuration,
ПолучитьДанныеДляОтчета,
СкачаноНечто,
ВыбранВариантТрансляции,
РазобранСписокСегментов,
ДобавленыСегментыВОчередь,
ЗагруженСегмент,
НеЗагруженыСегменты,
ПреобразованСегмент,
ПереполненБуферПроигрывателя,
ИзмениласьДлительностьТрансляции
};
})();
var м_Чат = (function()
{
const ИНТЕРВАЛ_ОПРОСА_ОКНА = 500;
var _чОкноЧата = 0;
function ОбновитьПанель()
// Проще не удалять iframe, а изменять его src, но тогда в чате кнопки Вперед и Назад будут перезагружать чат.
{
var elFrame = document.getElementById('gm-tw5-чат');
if (г_оНастройки.Получить('лОткрытьЧат'))
{
if (!elFrame)
{
console.log('[Чат] Открываю панель чата');
elFrame = document.createElement('iframe');
elFrame.setAttribute('id', 'gm-tw5-чат');
elFrame.setAttribute('src', г_оСайт.ПолучитьАдресПанелиЧата());
document.getElementById('gm-tw5-проигрывательичат').appendChild(elFrame);
}
}
else
{
if (elFrame)
{
console.log('[Чат] Закрываю панель чата');
// HACK Chrome 48: чтобы сборщик мусора вернул занятую iframe память, нужно изменить адрес и дать время
// Chrome это изменение обработать. На моей тачке достаточно 50мс, увеличено на всякий случай.
elFrame.setAttribute('hidden', '');
elFrame.removeAttribute('id');
elFrame.removeAttribute('src');
setTimeout(
function()
{
elFrame.remove();
},
500
);
}
}
}
function НеСратьВКонсоль()
{
chrome.runtime.lastError;
}
function ОткрытьОкно()
{
г_оНастройки.Изменить('лОткрытьЧат', false);
ОбновитьПанель();
// TODO Из-за асинхронности, если второй вызов ОткрытьОкно() произойдет до обновления _чОкноЧата, то откроются два окна.
// На практике это вряд ли произойдет, потому что нажать на кнопку 2 раза за 30 мс довольно сложно...
chrome.windows.update(_чОкноЧата, {focused: true}, function()
{
try
{
// Если окно найдено, то не открывать новое.
if (!chrome.runtime.lastError)
{
return;
}
var оПоложениеОкна = г_оНастройки.Получить('оПоложениеОкнаЧата');
console.log('[Чат] Открываю окно чата', оПоложениеОкна, window.screen);
// Chrome не дает окну полностью выйти за пределы экрана, изменяя начальный размер и местоположение окна.
chrome.windows.create(
{
url: г_оСайт.ПолучитьАдресОкнаЧата(),
focused: true,
incognito: false,
type: 'popup',
left: оПоложениеОкна.nX,
top: оПоложениеОкна.nY,
width: оПоложениеОкна.чШирина,
height: оПоложениеОкна.чВысота
},
function(оОкно)
{
if (chrome.runtime.lastError)
{
console.warn(`Не удалось открыть окно ${г_оСайт.ПолучитьАдресОкнаЧата()}. ${chrome.runtime.lastError.message}`);
return;
}
_чОкноЧата = оОкно.id;
var чТаймер = setInterval(function()
{
// Не обращаемся к _чОкноЧата, это замыкание работает автономно.
chrome.windows.get(оОкно.id, null, function(оОкно)
{
try
{
if (г_лРаботаЗавершена || chrome.runtime.lastError)
{
console.log('[Чат] Окно чата закрыто');
clearInterval(чТаймер);
return;
}
var оПоложениеОкна = г_оНастройки.Получить('оПоложениеОкнаЧата');
if (оПоложениеОкна.nX !== оОкно.left
|| оПоложениеОкна.nY !== оОкно.top
|| оПоложениеОкна.чШирина !== оОкно.width
|| оПоложениеОкна.чВысота !== оОкно.height)
{
console.log('[Чат] Положение окна чата изменилось', оОкно, window.screen);
г_оНастройки.Изменить('оПоложениеОкнаЧата',
{
nX: оОкно.left,
nY: оОкно.top,
чШирина: оОкно.width,
чВысота: оОкно.height
});
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
});
},
ИНТЕРВАЛ_ОПРОСА_ОКНА);
}
);
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
});
}
function ЗакрытьОкно()
{
console.log('[Чат] Закрываю окно чата');
chrome.windows.remove(_чОкноЧата, НеСратьВКонсоль);
_чОкноЧата = 0;
}
function ПереключитьПанель()
{
г_оНастройки.Изменить('лОткрытьЧат', !г_оНастройки.Получить('лОткрытьЧат'));
ЗакрытьОкно();
ОбновитьПанель();
}
function Восстановить()
{
ЗакрытьОкно();
ОбновитьПанель();
}
function Запустить()
{
}
return {
Запустить,
Восстановить,
ПереключитьПанель,
ОткрытьОкно,
ЗакрытьОкно
};
})();
var м_Новости = (function()
{
const _мсНовости =
[
];
// Если с момента выхода ВЕРСИЯ_ДОПОЛНЕНИЯ прошло более СРОК_ГОДНОСТИ_ДОПОЛНЕНИЯ дней, то считаем, что
// автообновление дополнения не работает, например изменился адрес хостинга дополнения. На случай, если
// пользователь установит дополнение в последний день указанного диапазона, прибавляем еще несколько дней,
// чтобы дать время оборзевателю обновить дополнение.
const СРОК_ГОДНОСТИ_ДОПОЛНЕНИЯ = 15 + 3;
function ОбработатьЩелчок(оСобытие)
{
try
{
if (оСобытие.button === ЛЕВАЯ_КНОПКА)
{
if (!document.getElementById('gm-tw5-корпус').classList.toggle('gm-tw5-показатьновости'))
{
оСобытие.currentTarget.setAttribute('hidden', '');
оСобытие.currentTarget.removeEventListener('click', ОбработатьЩелчок);
г_оНастройки.Изменить('сПредыдущаяВерсия', ВЕРСИЯ_ДОПОЛНЕНИЯ);
// Откладываем удаление текста чтобы не портить анимацию закрытия.
}
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function СоздатьНовость(сЗаголовок, сСодержание)
{
Проверить(сЗаголовок && сСодержание);
return `<p class="gm-tw5-метка">${сЗаголовок}</p><p>${сСодержание}</p>`;
}
function ДобавитьНовости(сНовости)
{
var узКнопка = document.getElementById('gm-tw5-управление-новости');
var узТекст = document.getElementById('gm-tw5-новости-текст');
if (узКнопка.hasAttribute('hidden'))
{
узТекст.innerHTML = сНовости;
узКнопка.removeAttribute('hidden');
узКнопка.addEventListener('click', ОбработатьЩелчок);
}
else
{
узТекст.insertAdjacentHTML('beforeend', сНовости);
}
}
function ПеревестиВерсиюВМиллисекунды(сВерсия, чПрибавитьДни)
// Возвращает Date.getTime() даты выкладывания указанной версии для скачивания.
// Число можно использовать для сравнения версий.
{
var мчЧасти = /^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$/.exec(сВерсия);
мчЧасти[1] |= 0;
мчЧасти[2] |= 0;
мчЧасти[3] |= 0;
мчЧасти[4] |= 0;
Проверить(мчЧасти[1] >= 2016 && мчЧасти[1] <= 2050);
Проверить(мчЧасти[2] >= 1 && мчЧасти[2] <= 12);
Проверить(мчЧасти[3] >= 1 && мчЧасти[3] <= 31);
Проверить(мчЧасти[4] >= 0 && мчЧасти[4] <= 9);
return Date.UTC(мчЧасти[1], мчЧасти[2] - 1, мчЧасти[3] + чПрибавитьДни, 0, 0, 0, мчЧасти[4]);
}
function Запустить()
{
var чТекущаяВерсия = ПеревестиВерсиюВМиллисекунды(ВЕРСИЯ_ДОПОЛНЕНИЯ, 0);
//
// Ругаться если дополнение долго не обновлялось.
//
if (Date.now() > ПеревестиВерсиюВМиллисекунды(ВЕРСИЯ_ДОПОЛНЕНИЯ, СРОК_ГОДНОСТИ_ДОПОЛНЕНИЯ))
{
ДобавитьНовости(СоздатьНовость(
'У вас установлена слишком старая версия дополнения <q>Twitch&nbsp;5</q>',
`Это дополнение было выпущено ${ToHtmlText((new Date(чТекущаяВерсия)).toLocaleDateString())}.
Скорее всего в браузере не работает автообновление или дата на компьютере установлена неправильно.`
));
}
var сПредыдущаяВерсия = г_оНастройки.Получить('сПредыдущаяВерсия');
if (сПредыдущаяВерсия === ВЕРСИЯ_ДОПОЛНЕНИЯ)
{
return;
}
var сНовости = '';
var чПредыдущаяВерсия = ПеревестиВерсиюВМиллисекунды(сПредыдущаяВерсия, 0);
for (var ы = 0; ы < _мсНовости.length; ы += 2)
{
var чВерсия = ПеревестиВерсиюВМиллисекунды(_мсНовости[ы], 0);
if (чВерсия > чПредыдущаяВерсия && чВерсия <= чТекущаяВерсия)
{
сНовости += СоздатьНовость(ToHtmlText((new Date(чВерсия)).toLocaleDateString()), _мсНовости[ы + 1]);
}
}
if (сНовости === '')
{
г_оНастройки.Изменить('сПредыдущаяВерсия', ВЕРСИЯ_ДОПОЛНЕНИЯ);
}
else
{
ДобавитьНовости(сНовости);
}
}
return {
Запустить
};
})();
var г_оУправление = (function()
// TODO Как-то показать пользователю, что часть видео была пропущена?
{
const ИНТЕРВАЛ_ИЗМЕНЕНИЯ_ДИАПАЗОНА = 150; // Миллисекунды.
const ЗАДЕРЖКА_ИЗМЕНЕНИЯ_ДИАПАЗОНА = 3; // Интервалы.
var _чСостояние;
var _узКорпус;
var _чТаймерАвтоскрытия = 0;
var _оРазмерБуфера, _оНачальныйРазмерБуфера, _оИнтервалОпроса;
var _сНазваниеКанала;
var _узТащить = null;
var _чТащитьМышьX, _чТащитьМышьY;
var _чТащитьОкноX, _чТащитьОкноY;
var _sFullscreenElement = 'fullscreenElement';
var _sRequestFullscreen = 'requestFullscreen';
var _sExitFullscreen = 'exitFullscreen';
var _sFullscreenchange = 'fullscreenchange';
function ЗапуститьПолноэкранныйРежим()
{
if (!document.exitFullscreen)
{
_sFullscreenElement = 'webkitFullscreenElement';
_sRequestFullscreen = 'webkitRequestFullscreen';
_sExitFullscreen = 'webkitExitFullscreen';
_sFullscreenchange = 'webkitfullscreenchange';
Проверить(document[_sExitFullscreen]);
}
document.addEventListener(_sFullscreenchange, ОбновитьПолноэкранныйРежим);
}
function ОбновитьПолноэкранныйРежим()
{
try
{
var лАктивен = !!document[_sFullscreenElement];
console.log('[Управление] Полноэкранный режим: %s', лАктивен);
ИзменитьИзображениеКнопки('gm-tw5-управление-полноэкранный', `#gm-tw5-svg-полноэкранный-${лАктивен}`);
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ПереключитьПолноэкранныйРежим()
{
if (document[_sFullscreenElement])
{
console.log('[Управление] Выход из полноэкранного режима');
document[_sExitFullscreen]();
}
else
{
console.log('[Управление] Вход в полноэкранный режим');
_узКорпус[_sRequestFullscreen]();
}
}
function ОтключитьПолноэкранныйРежим()
{
if (document[_sFullscreenElement])
{
ПереключитьПолноэкранныйРежим();
}
}
function ОбработатьНачалоПеретаскивания(оСобытие)
{
try
{
_узТащить = оСобытие.target;
// Если выделить текст нажав Ctrl+A, то тащиться будет что попало.
if (_узТащить.getAttribute && _узТащить.getAttribute('draggable') === 'true')
{
console.log(`[Управление] Начинаю тащить %s`, _узТащить.id);
_чТащитьМышьX = оСобытие.clientX;
_чТащитьМышьY = оСобытие.clientY;
var оСтиль = window.getComputedStyle(_узТащить);
_чТащитьОкноX = parseInt(оСтиль.left, 10);
_чТащитьОкноY = parseInt(оСтиль.top, 10);
// Firefox 44: без этой строки перетаскивание не начнется.
оСобытие.dataTransfer.setData('text/prs.empty', '');
// Не рисовать перетаскиваемый элемент (document.head невидимый).
оСобытие.dataTransfer.setDragImage(document.head, 0, 0);
// Firefox 44 убирает :hover. Этот стиль заменит :hover.
_узТащить.classList.add('тащить');
}
else
{
_узТащить = null;
оСобытие.preventDefault();
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьПеретаскивание(оСобытие)
{
try
{
// Firefox 44: тащить могут кнопку из панели инструментов.
if (_узТащить)
{
_узТащить.style.left = `${оСобытие.clientX - _чТащитьМышьX + _чТащитьОкноX}px`;
_узТащить.style.top = `${оСобытие.clientY - _чТащитьМышьY + _чТащитьОкноY}px`;
// Не менять мышиный курсор на перечеркнутый круг.
оСобытие.preventDefault();
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьОкончаниеПеретаскивания(оСобытие)
{
// Firefox 44: тащить могут кнопку из панели инструментов.
if (_узТащить)
{
_узТащить.classList.remove('тащить');
_узТащить = null;
}
}
function Диапазон(чМинимум, чМаксимум, чШаг, чТочность, сНазваниеНастройки, сКудаВставить)
{
Проверить(чМинимум <= чМаксимум && чТочность >= 0);
this._чМинимум = чМинимум;
this._чМаксимум = чМаксимум;
this._чШаг = чШаг;
this._чТочность = чТочность;
this._сНазваниеНастройки = сНазваниеНастройки;
this._чТаймер = 0;
this._лУвеличить = false;
this._чНомерИнтервала = 0;
this._узУзел = document.getElementById(сКудаВставить);
this._узУзел.innerHTML = `<button type="button" class="gm-tw5-настройки-минус">−</button>
<input type="text" readonly class="gm-tw5-настройки-диапазон">
<button type="button" class="gm-tw5-настройки-плюс">+</button>`;
this._узУзел.addEventListener('mousedown', this);
this.Обновить();
}
Диапазон.prototype._Показать = function()
{
this._узУзел.children[1].value = this._чЗначение.toFixed(this._чТочность);
};
Диапазон.prototype.handleEvent = function(оСобытие)
{
try
{
switch (оСобытие.type)
{
case 'mousedown':
if (оСобытие.button === ЛЕВАЯ_КНОПКА && this._чТаймер === 0)
{
switch (оСобытие.target.className)
{
case 'gm-tw5-настройки-минус': this._лУвеличить = false; break;
case 'gm-tw5-настройки-плюс': this._лУвеличить = true; break;
default: return;
}
document.addEventListener('mouseup', this);
// Для очистки совести, если blur не сработает.
document.addEventListener('visibilitychange', this);
window.addEventListener('blur', this);
this._чТаймер = setInterval(this._ОбработатьТаймер.bind(this), ИНТЕРВАЛ_ИЗМЕНЕНИЯ_ДИАПАЗОНА);
this._чНомерИнтервала = 0;
this._ОбработатьТаймер();
}
break;
case 'visibilitychange':
if (!document.hidden)
{
break;
}
case 'blur':
case 'mouseup':
// Реагируем на отпускание любой кнопки.
if (this._чТаймер !== 0)
{
document.removeEventListener('mouseup', this);
document.removeEventListener('visibilitychange', this);
window.removeEventListener('blur', this);
clearInterval(this._чТаймер);
this._чТаймер = 0;
}
break;
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
};
Диапазон.prototype._ОбработатьТаймер = function()
{
try
{
++this._чНомерИнтервала;
if (this._чНомерИнтервала === 1 || this._чНомерИнтервала > ЗАДЕРЖКА_ИЗМЕНЕНИЯ_ДИАПАЗОНА)
{
if (this._лУвеличить)
{
this.Изменить(Math.min(this._чЗначение + this._чШаг, this._чМаксимум));
}
else
{
this.Изменить(Math.max(this._чЗначение - this._чШаг, this._чМинимум));
}
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
};
Диапазон.prototype.Получить = function()
{
return this._чЗначение;
};
Диапазон.prototype.Изменить = function(чНовоеЗначение)
{
Проверить(typeof чНовоеЗначение === 'number');
if (this._чЗначение !== чНовоеЗначение)
{
this._чЗначение = чНовоеЗначение;
г_оНастройки.Изменить(this._сНазваниеНастройки, чНовоеЗначение);
this._Показать();
}
};
Диапазон.prototype.Обновить = function()
{
this._чЗначение = г_оНастройки.Получить(this._сНазваниеНастройки);
Проверить(typeof this._чЗначение === 'number');
this._Показать();
};
function ИзменитьИзображениеКнопки(сИдентификаторКнопки, сИдентификаторИзображения)
{
ИзменитьSvgHref(document.getElementById(сИдентификаторКнопки).firstElementChild.firstElementChild, сИдентификаторИзображения);
}
function ЗапуститьАвтоскрытие()
{
_узКорпус.addEventListener('mousemove', ОтключитьАвтоскрытие);
ОтключитьАвтоскрытие();
}
function ВключитьАвтоскрытие()
{
try
{
if (_чТаймерАвтоскрытия !== 0)
{
clearTimeout(_чТаймерАвтоскрытия);
_чТаймерАвтоскрытия = 0;
_узКорпус.classList.add('gm-tw5-автоскрытие');
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОтключитьАвтоскрытие()
{
try
{
// Автоскрытие включено?
if (_чТаймерАвтоскрытия === 0)
{
_узКорпус.classList.remove('gm-tw5-автоскрытие');
}
else
{
clearTimeout(_чТаймерАвтоскрытия);
}
_чТаймерАвтоскрытия = setTimeout(ВключитьАвтоскрытие, СКРЫВАТЬ_УПРАВЛЕНИЕ_И_КУРСОР_ЧЕРЕЗ);
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ПереключитьПаузу()
{
if (_чСостояние !== СОСТОЯНИЕ_ПАУЗА)
{
console.info('[Управление] Ставлю на паузу');
// Не очищать буфер проигрывателя чтобы можно было рассматривать поставленный на паузу кадр.
г_оПроигрыватель.ОстановитьВоспроизведение(СОСТОЯНИЕ_ПАУЗА);
// СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ не нужен.
г_оСписок.Остановить(false);
// Удалить застрявшие в worker сегменты.
г_оПреобразователь.Остановить();
// Очистить очередь и отменить загрузку сегментов.
г_моОчередь.Очистить();
}
else
{
console.info('[Управление] Снимаю с паузы');
// Изменить состояние на СОСТОЯНИЕ_ЗАГРУЗКА и очистить буфер проигрывателя.
г_оПроигрыватель.ПерезагрузитьПроигрыватель();
г_оСписок.Запустить();
}
}
function ИзменитьВариантТрансляции()
{
// Изменить состояние на СОСТОЯНИЕ_ЗАГРУЗКА и очистить буфер проигрывателя.
г_оПроигрыватель.ПерезагрузитьПроигрыватель();
// СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ не нужен.
г_оСписок.Остановить(true);
// Удалить застрявшие в worker сегменты.
г_оПреобразователь.Остановить();
// Очистить очередь и отменить загрузку сегментов.
г_моОчередь.Очистить();
г_оСписок.Запустить();
}
function ПереключитьОкноСтатистики()
{
if (г_оСтатистика.ОкноПоказано())
{
г_оСтатистика.СкрытьОкно();
}
else
{
г_оСтатистика.ПоказатьОкно();
}
}
function ПереключитьОкноНастроек()
{
_узКорпус.classList.toggle('gm-tw5-показатьнастройки');
}
function ОбработатьИзменениеГромкости(оСобытие)
{
try
{
ИзменитьНастройкиЗвука(false, parseFloat(оСобытие.target.value));
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ИзменитьНастройкиЗвука(лПриглушить, чГромкость)
{
г_оНастройки.Изменить('лПриглушить', лПриглушить);
if (чГромкость !== undefined)
{
г_оНастройки.Изменить('чГромкость', чГромкость);
}
г_оПроигрыватель.ПрименитьНастройкиЗвука();
ОбновитьНастройкиЗвука();
ОтключитьАвтоскрытие();
}
function ОбновитьНастройкиЗвука()
{
document.getElementById('gm-tw5-управление-громкость').value = г_оНастройки.Получить('чГромкость');
ИзменитьИзображениеКнопки('gm-tw5-управление-приглушить', `#gm-tw5-svg-приглушить-${г_оНастройки.Получить('лПриглушить')}`);
}
function СброситьНастройки()
// TODO Менять качество трансляции.
{
г_оНастройки.Сбросить();
г_оСтатистика.ОчиститьИсторию();
г_оПроигрыватель.ПрименитьНастройкиЗвука();
ОбновитьНастройкиЗвука();
ОбновитьОкноНастроек();
м_Чат.Восстановить();
}
function ОбработатьЩелчок(оСобытие)
{
try
{
var сПозывной = оСобытие.target.id
|| оСобытие.target.name
|| оСобытие.target.parentNode.id
|| оСобытие.target.parentNode.name
|| оСобытие.target.parentNode.parentNode.id
|| оСобытие.target.parentNode.parentNode.name;
// Скрыть настройки, если щелкнули не кнопку, открывающую настройки, и не узел, входящий в состав настроек.
if (сПозывной !== 'gm-tw5-управление-настройки' && !document.getElementById('gm-tw5-настройки').contains(оСобытие.target))
{
_узКорпус.classList.remove('gm-tw5-показатьнастройки');
}
if (оСобытие.button !== ЛЕВАЯ_КНОПКА)
{
return;
}
switch (сПозывной)
{
case 'gm-tw5-управление-пауза':
ПереключитьПаузу();
break;
case 'gm-tw5-управление-приглушить':
ИзменитьНастройкиЗвука(!г_оНастройки.Получить('лПриглушить'));
break;
case 'gm-tw5-управление-апоговорить':
м_Чат.ПереключитьПанель();
break;
case 'gm-tw5-управление-настройки':
ПереключитьОкноНастроек();
break;
case 'gm-tw5-управление-полноэкранный':
ПереключитьПолноэкранныйРежим();
break;
case 'gm-tw5-настройки-загрузка':
Проверить(оСобытие.target.checked);
г_оНастройки.Изменить('кОдновременныхЗагрузок', parseInt(оСобытие.target.getAttribute('value'), 10));
г_оСтатистика.ОчиститьИсторию();
break;
case 'gm-tw5-настройки-заначка':
Проверить(оСобытие.target.checked);
г_оНастройки.Изменить('кЗаначка', parseInt(оСобытие.target.getAttribute('value'), 10));
г_оСтатистика.ОчиститьИсторию();
break;
case 'gm-tw5-настройки-статистика':
ПереключитьОкноСтатистики();
break;
case 'gm-tw5-настройки-отзыв':
г_оОтладка.ОставитьОтзыв();
break;
case 'gm-tw5-настройки-сброс':
СброситьНастройки();
break;
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьДвойнойЩелчок(оСобытие)
{
try
{
if (оСобытие.button === ЛЕВАЯ_КНОПКА)
{
// На всякий случай.
оСобытие.preventDefault();
ПереключитьПолноэкранныйРежим();
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьОтпусканиеМыши(оСобытие)
{
try
{
if (оСобытие.button === СРЕДНЯЯ_КНОПКА)
{
оСобытие.preventDefault();
м_Чат.ОткрытьОкно();
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьНажатиеКлавы(оСобытие)
{
try
{
if (г_лРаботаЗавершена)
{
return;
}
if (оСобытие.shiftKey || оСобытие.ctrlKey || оСобытие.altKey || оСобытие.metaKey)
{
return;
}
switch (оСобытие.keyCode)
{
case 13: // ENTER
if (!оСобытие.repeat)
{
ПереключитьПолноэкранныйРежим();
}
break;
case 61: // = Firefox
case 187: // = Chrome
if (!оСобытие.repeat)
{
ПереключитьОкноНастроек();
}
break;
case 173: // - Firefox
case 189: // - Chrome
if (!оСобытие.repeat)
{
ПереключитьОкноСтатистики();
}
break;
case 45: // INS
if (!оСобытие.repeat)
{
м_Чат.ПереключитьПанель();
}
break;
case 46: // DEL
if (!оСобытие.repeat)
{
ПереключитьПаузу();
ОтключитьАвтоскрытие();
}
break;
case 38: // UP
ИзменитьНастройкиЗвука(false, Math.min(г_оНастройки.Получить('чГромкость') + ШАГ_ПОВЫШЕНИЯ_ГРОМКОСТИ_КЛАВОЙ, МАКСИМАЛЬНАЯ_ГРОМКОСТЬ));
break;
case 40: // DOWN
ИзменитьНастройкиЗвука(false, Math.max(г_оНастройки.Получить('чГромкость') - ШАГ_ПОНИЖЕНИЯ_ГРОМКОСТИ_КЛАВОЙ, МИНИМАЛЬНАЯ_ГРОМКОСТЬ));
break;
case 33: // PAGE UP
if (!оСобытие.repeat)
{
ИзменитьНастройкиЗвука(false);
}
break;
case 34: // PAGE DOWN
if (!оСобытие.repeat)
{
ИзменитьНастройкиЗвука(true);
}
break;
default:
return;
}
оСобытие.stopPropagation();
оСобытие.preventDefault();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбновитьОкноНастроек()
{
var уз;
if (уз = document.querySelector(`input[name="gm-tw5-настройки-загрузка"][value="${г_оНастройки.Получить('кОдновременныхЗагрузок')}"]`))
{
уз.checked = true;
}
if (уз = document.querySelector(`input[name="gm-tw5-настройки-заначка"][value="${г_оНастройки.Получить('кЗаначка')}"]`))
{
уз.checked = true;
}
if (!_оРазмерБуфера)
{
_оРазмерБуфера = new Диапазон(1, 30, 0.5, 1, 'чРазмерБуфера', 'gm-tw5-настройки-размербуфера');
_оНачальныйРазмерБуфера = new Диапазон(1, 30, 0.5, 1, 'чНачалоВоспроизведения', 'gm-tw5-настройки-началовоспроизведения');
_оИнтервалОпроса = new Диапазон(50, 250, 10, 0, 'чИнтервалОпроса', 'gm-tw5-настройки-интервалопроса');
}
else
{
_оРазмерБуфера.Обновить();
_оНачальныйРазмерБуфера.Обновить();
_оИнтервалОпроса.Обновить();
}
}
function ОбработатьИзменениеВарианта(оСобытие)
{
try
{
console.info('[Управление] Выбран вариант %s', оСобытие.target.value)
Проверить(оСобытие.target.selectedIndex !== -1 && оСобытие.target.value);
г_оНастройки.Изменить('сНазваниеВарианта', оСобытие.target.value);
ИзменитьВариантТрансляции();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ВыбранВариантТрансляции(моСписокВариантов, чВыбранныйВариант)
{
var узВариант = document.getElementById('gm-tw5-настройки-вариант');
узВариант.length = 0;
if (моСписокВариантов)
{
for (var ы = 0; ы < моСписокВариантов.length; ++ы)
{
узВариант.add(new Option(моСписокВариантов[ы].сНазвание, моСписокВариантов[ы].сНазвание, ы === чВыбранныйВариант, ы === чВыбранныйВариант));
}
}
узВариант.disabled = узВариант.length < 2;
}
function ОтменитьДействие(оСобытие)
{
оСобытие.preventDefault();
}
function Запустить()
{
Проверить(_чСостояние === undefined);
// Chrome поддерживает var() только с версии 49.
const ОТСТУП_ОКНА = '8px';
const ПОЛЕ_НОВОСТЕЙ = '.8em';
const ВЫСОТА_ПАНЕЛИ = '40px';
const ОТСТУП_ЭЛЕМЕНТА_ПАНЕЛИ = '15px';
ЗаменитьСтраницу(`
<head>
<meta charset="utf-8">
<title>Прямой эфир - Twitch 5</title>
<style>
@namespace xlink 'http://www.w3.org/1999/xlink';
html, body, video
{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: #000;
}
body > svg
{
display: none;
}
#gm-tw5-проигрывательичат
{
display: flex; /* Flexbox при необходимости сожмет flex item до min-width */
height: 100%;
}
#gm-tw5-чат
{
border: 0;
width: 340px;
min-width: 250px;
}
#gm-tw5-корпус
{
flex: 1;
width: 100%; /* Для полноэкранного режима */
height: 100%;
min-width: 385px; /* Место для элементов панелей (включая обычно скрытую кнопку новостей) и окна настроек */
min-height: 290px; /* Место для обеих панелей и окна настроек */
overflow: hidden;
position: relative;
white-space: nowrap;
font: bold 16px Arial, Helvetica Neue, Helvetica, sans-serif;
line-height: 1.16;
text-shadow: 0 1px 1px #333;
color: #FFF;
fill: currentColor;
}
#gm-tw5-корпус input,
#gm-tw5-корпус button,
#gm-tw5-корпус select
{
font: inherit;
font-weight: normal;
}
#gm-tw5-крутилка
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border: 2px solid hsl(35, 100%, 90%);
border-radius: 8%;
padding: 10px;
width: 66px;
height: 66px;
background: hsl(35, 100%, 50%);
box-shadow: inset 0 0 1.8em hsla(0, 0%, 100%, .4);
pointer-events: none;
}
#gm-tw5-заголовок,
#gm-tw5-управление
{
position: absolute;
left: 0;
right: 0;
height: ${ВЫСОТА_ПАНЕЛИ};
display: flex;
flex-direction: row; /* TODO row для ltr, row-reverse для rtl */
align-items: center;
padding-left: ${ОТСТУП_ЭЛЕМЕНТА_ПАНЕЛИ};
transition: opacity .3s;
background: linear-gradient(to right, hsla(210, 30%, 37%, .7) 10%, hsla(210, 30%, 60%, .6) 40%, hsla(210, 30%, 60%, .6) 60%, hsla(210, 30%, 37%, .7) 90%);
cursor: auto;
}
#gm-tw5-заголовок
{
top: 0;
}
#gm-tw5-управление
{
bottom: 0;
}
.gm-tw5-элементпанели
{
margin-right: ${ОТСТУП_ЭЛЕМЕНТА_ПАНЕЛИ};
}
.gm-tw5-элементпанели > a
{
color: inherit;
text-decoration: none;
}
.gm-tw5-элементпанели > a:hover
{
color: hsl(35, 100%, 60%);
text-decoration: underline;
}
.gm-tw5-метка
{
padding-right: .5em;
color: hsl(195, 80%, 90%);
}
#gm-tw5-заголовок-названиетрансляции,
#gm-tw5-заголовок-названиеигры
{
overflow: hidden;
text-overflow: ellipsis;
}
#gm-tw5-заголовок-названиетрансляции
{
flex-grow: 1;
min-width: 4em;
}
#gm-tw5-заголовок-названиеигры
{
min-width: 6em;
}
.gm-tw5-управление-кнопка.gm-tw5-выделить
{
color: hsl(35, 100%, 60%);
fill: currentColor; /* Для Firefox и Chrome 48- */
}
.gm-tw5-управление-кнопка
{
padding: 0;
border: 0;
color: inherit;
background: transparent;
}
.gm-tw5-управление-кнопка,
.gm-tw5-управление-кнопка > svg
{
width: 24px;
height: 24px;
}
.gm-tw5-управление-кнопка > svg
{
transition: transform .3s;
}
/* Размер .gm-tw5-управление-кнопка должен оставаться постоянным, чтобы можно было нажимать на края кнопки */
.gm-tw5-управление-кнопка:active > svg
{
transform: scale(.9, .9);
transition-duration: .1s;
}
#gm-tw5-управление-громкость
{
width: 150px;
min-width: 50px;
color: inherit;
}
.gm-tw5-управление-кнопка:focus,
#gm-tw5-управление-громкость:focus
{
outline: 0;
}
.gm-tw5-управление-кнопка:hover,
#gm-tw5-управление-громкость:hover
{
-webkit-filter: drop-shadow(0 0 1px currentColor);
filter: drop-shadow(0 0 1px currentColor);
}
.gm-tw5-управление-позиция
{
flex-grow: 1;
}
.gm-tw5-автоскрытие:not([data-gm-tw5-состояние="${СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ}"])
{
cursor: none;
}
.gm-tw5-автоскрытие:not([data-gm-tw5-состояние="${СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ}"]) > #gm-tw5-заголовок:not(:hover),
.gm-tw5-автоскрытие:not([data-gm-tw5-состояние="${СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ}"]):not(.gm-tw5-показатьнастройки):not(.gm-tw5-показатьновости) > #gm-tw5-управление:not(:hover)
{
opacity: 0;
}
#gm-tw5-новости,
#gm-tw5-настройки
{
position: absolute;
right: ${ОТСТУП_ОКНА};
bottom: calc(${ВЫСОТА_ПАНЕЛИ} + ${ОТСТУП_ОКНА});
background: linear-gradient(45deg, hsla(210, 30%, 37%, .85) 50%, hsla(210, 30%, 50%, .85) 90%);
box-shadow: 0 0 .35em .15em hsla(210, 10%, 30%, .7);
font-size: 12px;
cursor: auto;
visibility: hidden;
transform: scaleY(0);
transition: transform .15s ease-out, visibility 0s ease .15s;
}
.gm-tw5-показатьновости #gm-tw5-новости,
.gm-tw5-показатьнастройки #gm-tw5-настройки
{
visibility: visible;
transform: scaleY(1);
transition: transform .15s ease-out;
}
/* Ширина <= min(#gm-tw5-новости:max-width, #gm-tw5-новости-текст:max-width). С высотой такой фокус не проходит. */
#gm-tw5-новости
{
padding: ${ПОЛЕ_НОВОСТЕЙ};
box-sizing: border-box;
max-width: calc(100% - ${ОТСТУП_ОКНА} * 2);
}
#gm-tw5-новости-текст
{
min-width: 12em; /* Хотя бы часть окна должна находиться над кнопкой */
max-width: 40em;
max-height: calc(100vh - ${ВЫСОТА_ПАНЕЛИ} * 2 - ${ОТСТУП_ОКНА} * 2 - ${ПОЛЕ_НОВОСТЕЙ} * 2);
overflow: auto;
white-space: normal;
}
#gm-tw5-новости-текст > p
{
margin: 0;
}
#gm-tw5-новости-текст > p + p
{
margin-top: 1em;
}
#gm-tw5-новости-текст > p + .gm-tw5-метка
{
border-width: 1px 0 0;
border-style: solid;
border-image: linear-gradient(to right, transparent, hsla(195, 80%, 90%, .8), transparent) 1;
padding-top: 1em;
}
#gm-tw5-настройки
{
padding: .5em;
border-collapse: collapse;
-webkit-user-select: none;
}
#gm-tw5-настройки input[type="radio"]
{
margin-left: 0;
vertical-align: -2px;
}
#gm-tw5-настройки label
{
margin-right: .5em;
}
#gm-tw5-настройки table
{
width: 100%;
}
#gm-tw5-настройки th
{
text-align: left;
}
#gm-tw5-настройки td
{
text-align: right;
}
#gm-tw5-настройки form
{
margin-top: .6em;
border-width: 1px 0 0;
border-style: solid;
border-image: linear-gradient(to right, transparent, hsl(195, 80%, 90%), transparent) 1;
padding-top: .8em;
}
#gm-tw5-настройки-вариант
{
height: 1.65em;
min-width: 8em;
}
.gm-tw5-настройки-диапазон
{
width: 3em;
text-align: right;
}
.gm-tw5-настройки-минус,
.gm-tw5-настройки-плюс
{
width: 2em;
}
/* На паузу ставят в том числе чтобы рассмотреть что-то на экране. Крутилка будет мешать. */
#gm-tw5-корпус[data-gm-tw5-состояние="${СОСТОЯНИЕ_ПАУЗА}"] #gm-tw5-крутилка,
#gm-tw5-корпус[data-gm-tw5-состояние="${СОСТОЯНИЕ_ВОСПРОИЗВЕДЕНИЕ}"] #gm-tw5-крутилка,
#gm-tw5-корпус:not([data-gm-tw5-состояние="${СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ}"]):not([data-gm-tw5-состояние="${СОСТОЯНИЕ_ЗАГРУЗКА}"]) use[xlink|href="#gm-tw5-svg-началотрансляции"],
#gm-tw5-корпус:not([data-gm-tw5-состояние="${СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ}"]) use[xlink|href="#gm-tw5-svg-завершениетрансляции"],
#gm-tw5-корпус[data-gm-tw5-состояние="${СОСТОЯНИЕ_ПАУЗА}"] use[xlink|href="#gm-tw5-svg-паузаfalse"],
#gm-tw5-корпус:not([data-gm-tw5-состояние="${СОСТОЯНИЕ_ПАУЗА}"]) use[xlink|href="#gm-tw5-svg-паузаtrue"]
{
display: none;
}
</style>
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<!--
Author: Freepik http://www.freepik.com
From: http://www.flaticon.com
License: CC BY 3.0
-->
<symbol id="gm-tw5-svg-началотрансляции" viewBox="0 0 25.734 25.734">
<path d="M21.229,2.483V1.399h0.945V0H3.56v1.399h0.945v1.083h0.174c0,3.274,3.577,7.68,6.106,10.384c-2.529,2.704-6.106,7.108-6.106,10.385H4.504v1.081H3.56v1.402h18.614v-1.402h-0.943v-1.081h-0.176c0-3.276-3.576-7.681-6.105-10.385c2.529-2.704,6.105-7.11,6.105-10.384h0.174V2.483zM13.647,12.868c1.873,1.938,6.465,7.023,6.465,10.385h-1.619c-0.044-0.218-0.139-0.429-0.332-0.597l-4.561-3.76c-0.36-0.297-0.877-0.303-1.246-0.015c-1.431,1.119-4.78,3.774-4.78,3.774c-0.194,0.168-0.289,0.379-0.333,0.597H5.623c0-3.361,4.591-8.447,6.465-10.385c-1.874-1.936-6.465-7.024-6.465-10.384h14.488C20.112,5.844,15.52,10.932,13.647,12.868zM8.997,5.276c-0.369,0.431-0.369,1.13,0,1.562l3.136,3.424c0.188,0.206,0.455,0.323,0.734,0.323s0.545-0.117,0.734-0.323l3.137-3.425c0.368-0.432,0.368-1.131,0-1.562L8.997,5.276L8.997,5.276z"/>
</symbol>
<!--
Author: Freepik http://www.freepik.com
From: http://www.flaticon.com
License: CC BY 3.0
-->
<symbol id="gm-tw5-svg-завершениетрансляции" viewBox="0 0 450 450">
<path d="M87.945,75.913c-1.877-7.41-9.408-11.893-16.818-10.018c-7.413,1.879-11.897,9.409-10.019,16.821l90.439,356.838c1.589,6.271,7.224,10.446,13.409,10.446c1.127,0,2.271-0.139,3.413-0.428c7.409-1.877,11.895-9.409,10.017-16.819L87.945,75.913zM388.98,176.419c-14.739-54.423-29.492-108.842-44.234-163.265c-1.598-5.891-4.399-12.21-14.929-12.842C246.929-5.691,192.503,76.854,109.614,70.85c-6.541-0.806-10.745,2.6-9.148,8.491c14.743,54.422,29.372,108.877,44.233,163.266c2.385,8.729,8.388,12.035,14.931,12.842c82.887,6.004,137.315-76.541,220.205-70.537C386.375,185.716,390.577,182.311,388.98,176.419zM323.934,20.857c4.066,15.015,8.138,30.029,12.204,45.044c-17.436,0.574-32.825,4.092-49.132,10.203c-4.065-15.015-8.137-30.03-12.202-45.044C291.109,24.951,306.498,21.43,323.934,20.857zM153.13,189.197c-4.627-17.059-9.246-34.122-13.868-51.182c18.328-0.531,34.591-4.503,51.602-11.227c-4.07-15.015-8.138-30.03-12.204-45.045c16.629-7.433,32.314-16.332,48.022-25.523c4.066,15.014,8.138,30.029,12.205,45.044c-15.709,9.19-31.395,18.092-48.023,25.524c4.623,17.06,9.244,34.122,13.866,51.182C187.718,184.693,171.457,188.665,153.13,189.197zM217.114,223.674c-4.129-15.234-8.256-30.47-12.384-45.706c16.513-7.377,32.087-16.201,47.683-25.327c4.128,15.236,8.256,30.471,12.383,45.707C249.202,207.475,233.626,216.297,217.114,223.674zM252.75,152.445c-4.619-17.061-9.242-34.122-13.863-51.183c15.706-9.17,31.403-17.945,48.119-25.157c4.624,17.06,9.246,34.121,13.867,51.181C284.16,134.5,268.458,143.274,252.75,152.445zM313.545,172.876c-4.129-15.234-8.256-30.47-12.385-45.706c16.211-6.045,31.521-9.521,48.843-10.086c4.129,15.236,8.257,30.471,12.386,45.705C345.066,163.355,329.754,166.83,313.545,172.876z"/>
</symbol>
<!--
Author: Freepik http://www.freepik.com
From: http://www.flaticon.com
License: CC BY 3.0
-->
<symbol id="gm-tw5-svg-паузаfalse" viewBox="0 0 1024 1024">
<path d="M901.347 960c23.75 0 43.181-19.423 43.181-43.181v-809.639c0-23.75-19.431-43.181-43.181-43.181h-259.087c-23.75 0-43.181 19.431-43.181 43.181v809.639c0 23.758 19.431 43.181 43.181 43.181h259.087zM381.737 960c23.75 0 43.181-19.423 43.181-43.181v-809.639c0-23.75-19.431-43.181-43.181-43.181h-259.087c-23.75 0-43.181 19.431-43.181 43.181v809.639c0 23.758 19.431 43.181 43.181 43.181h259.087z"/>
</symbol>
<!--
Author: Freepik http://www.freepik.com
From: http://www.flaticon.com
License: CC BY 3.0
-->
<symbol id="gm-tw5-svg-паузаtrue" viewBox="0 0 1024 1024">
<path d="M897.327 472.873l-725.933-418.864c-29.812-17.14-67.079 4.472-67.079 38.757v838.476c0 34.285 37.266 55.897 67.079 38.757l725.933-418.864c29.812-17.894 29.812-61.114 0-78.259z"/>
</symbol>
<!--
Author: Keyamoon http://keyamoon.com
License: GPL or CC BY 4.0
-->
<symbol id="gm-tw5-svg-приглушить-false" viewBox="0 0 896 1024">
<path d="M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"/>
<path d="M719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z"/>
</symbol>
<!--
Author: Keyamoon http://keyamoon.com
License: GPL or CC BY 4.0
-->
<symbol id="gm-tw5-svg-приглушить-true" viewBox="0 0 896 1024">
<path d="M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"/>
<path d="M960 619.148v84.852h-84.852l-107.148-107.148-107.148 107.148h-84.852v-84.852l107.148-107.148-107.148-107.148v-84.852h84.852l107.148 107.148 107.148-107.148h84.852v84.852l-107.148 107.148 107.148 107.148z"/>
</symbol>
<!--
Author: Google https://www.google.com/design
License: CC BY 4.0
-->
<symbol id="gm-tw5-svg-новости" viewBox="0 0 24 24">
<path d="M18.615 16.393l2.222 2.222v1.085h-17.675v-1.085l2.222-2.222v-5.478q0-2.584 1.318-4.496t3.644-2.481v-0.775q0-0.672 0.465-1.163t1.189-0.491 1.189 0.491 0.465 1.163v0.775q2.326 0.569 3.644 2.481t1.318 4.496v5.478zM12 23.008q-0.93 0-1.576-0.646t-0.646-1.525h4.445q0 0.879-0.672 1.525t-1.55 0.646z"/>
</symbol>
<!--
Author: Yannick http://yanlu.de
From: http://www.flaticon.com
License: CC BY 3.0
-->
<symbol id="gm-tw5-svg-апоговорить" viewBox="0 0 1024 1024">
<path d="M512 76.389c-277.671 0-502.767 176.902-502.767 395.032 0 103.282 50.492 197.444 133.162 267.832-3.16 68.591-17.382 160.814-67.659 208.361 96.029 0 194.356-63.205 252.677-108.741 57.172 17.812 119.444 27.581 184.587 27.581 277.671 0 502.767-176.902 502.767-395.032s-225.096-395.032-502.767-395.032zM296.528 579.156c-39.719 0-71.824-32.105-71.824-71.824s32.105-71.824 71.824-71.824 71.824 32.105 71.824 71.824-32.105 71.824-71.824 71.824zM512 579.156c-39.719 0-71.824-32.105-71.824-71.824s32.105-71.824 71.824-71.824 71.824 32.105 71.824 71.824-32.105 71.824-71.824 71.824zM727.472 579.156c-39.719 0-71.824-32.105-71.824-71.824s32.105-71.824 71.824-71.824 71.824 32.105 71.824 71.824-32.105 71.824-71.824 71.824z"/>
</symbol>
<!--
Author: Google https://www.google.com/design
License: CC BY 4.0
-->
<symbol id="gm-tw5-svg-настройки" viewBox="0 0 1024 1024">
<path d="M512 662q62 0 106-44t44-106-44-106-106-44-106 44-44 106 44 106 106 44zM830 554l90 70q14 10 4 28l-86 148q-8 14-26 8l-106-42q-42 30-72 42l-16 112q-4 18-20 18h-172q-16 0-20-18l-16-112q-38-16-72-42l-106 42q-18 6-26-8l-86-148q-10-18 4-28l90-70q-2-14-2-42t2-42l-90-70q-14-10-4-28l86-148q8-14 26-8l106 42q42-30 72-42l16-112q4-18 20-18h172q16 0 20 18l16 112q38 16 72 42l106-42q18-6 26 8l86 148q10 18-4 28l-90 70q2 14 2 42t-2 42z"/>
</symbol>
<!--
Author: P.J. Onori http://somerandomdude.com/
License: CC BY-SA 3.0
-->
<symbol id="gm-tw5-svg-полноэкранный-false" viewBox="0 0 1024 1024">
<path d="M813.896 739.096l-121.298-121.298-74.799 74.799 121.298 121.298-121.298 121.298h317.395v-317.395zM406.202 88.805h-317.395v317.395l121.298-121.298 120.161 120.054 74.799-74.799-120.161-120.054zM406.202 692.597l-74.799-74.799-121.298 121.298-121.298-121.298v317.395h317.395l-121.298-121.298zM935.195 88.805h-317.395l121.298 121.298-120.162 120.054 74.799 74.799 120.161-120.054 121.298 121.298z"/>
</symbol>
<!--
Author: P.J. Onori http://somerandomdude.com/
License: CC BY-SA 3.0
-->
<symbol id="gm-tw5-svg-полноэкранный-true" viewBox="0 0 1024 1024">
<path d="M746.422 823.635l125.211 125.211 77.212-77.212-125.211-125.211 125.211-125.211h-327.634v327.634zM75.154 402.789h327.634v-327.634l-125.211 125.211-123.927-124.037-77.212 77.212 123.927 124.037zM75.154 871.634l77.212 77.212 125.211-125.211 125.211 125.211v-327.634h-327.634l125.211 125.211zM621.211 402.789h327.634l-125.211-125.211 124.146-124.037-77.212-77.212-124.146 124.037-125.211-125.211z"/>
</symbol>
</svg>
<div id="gm-tw5-проигрывательичат">
<div id="gm-tw5-корпус">
<!-- UNDONE Firefox 45: иногда глючит перемотка -->
<video preload="metadata" poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEUAAAD///+l2Z/dAAAACklEQVR42mNkAAAABAACjOzX5wAAAABJRU5ErkJggg=="></video>
<div id="gm-tw5-заголовок">
<div id="gm-tw5-заголовок-названиетрансляции" class="gm-tw5-элементпанели">
<a></a>
</div>
<div id="gm-tw5-заголовок-названиеигры" class="gm-tw5-элементпанели">
<span class="gm-tw5-метка">Игра:</span><a></a>
</div>
<div id="gm-tw5-заголовок-количествозрителей" class="gm-tw5-элементпанели" title="Количество зрителей, смотрящих сейчас эту трансляцию, включая вас и, возможно, автора трансляции. Это число обновляется с довольно большой задержкой.">
<span class="gm-tw5-метка">Зрителей:</span><span></span>
</div>
</div>
<svg id="gm-tw5-крутилка" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="#gm-tw5-svg-началотрансляции"/>
<use xlink:href="#gm-tw5-svg-завершениетрансляции"/>
</svg>
<div id="gm-tw5-управление">
<button id="gm-tw5-управление-пауза" class="gm-tw5-управление-кнопка gm-tw5-элементпанели" type="button" title="Остановить воспроизведение.\nКнопка DEL на клавиатуре.">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="#gm-tw5-svg-паузаfalse"/>
<use xlink:href="#gm-tw5-svg-паузаtrue"/>
</svg>
</button>
<button id="gm-tw5-управление-приглушить" class="gm-tw5-управление-кнопка gm-tw5-элементпанели" type="button" title="Отключить звук.\nКнопки PGUP и PGDN на клавиатуре.">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href=""/>
</svg>
</button>
<input id="gm-tw5-управление-громкость" class="gm-tw5-элементпанели" type="range" min="${МИНИМАЛЬНАЯ_ГРОМКОСТЬ}" max="${МАКСИМАЛЬНАЯ_ГРОМКОСТЬ}" step="${ШАГ_ИЗМЕНЕНИЯ_ГРОМКОСТИ_МЫШЬЮ}" title="Изменить громкость.\nКнопки СТРЕЛКА ВВЕРХ и СТРЕЛКА ВНИЗ на клавиатуре.">
<div class="gm-tw5-управление-позиция gm-tw5-элементпанели">
<span id="gm-tw5-управление-позиция" title="Продолжительность этой трансляции"></span>
</div>
<button hidden id="gm-tw5-управление-новости" class="gm-tw5-управление-кнопка gm-tw5-