Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@pinguinson
Created March 26, 2016 15:08
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pinguinson/aef1c8b293ae4ab1e3d6 to your computer and use it in GitHub Desktop.
Save pinguinson/aef1c8b293ae4ab1e3d6 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-элементпанели gm-tw5-выделить" type="button" title="Что новенького?">
<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-новости"/>
</svg>
</button>
<button id="gm-tw5-управление-апоговорить" class="gm-tw5-управление-кнопка gm-tw5-элементпанели" type="button" title="Показать чат.\nКнопка INS на клавиатуре.">
<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-апоговорить"/>
</svg>
</button>
<button id="gm-tw5-управление-настройки" class="gm-tw5-управление-кнопка gm-tw5-элементпанели" type="button" title="Показать настройки проигрывателя.\nКнопка = на клавиатуре.">
<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-настройки"/>
</svg>
</button>
<button id="gm-tw5-управление-полноэкранный" class="gm-tw5-управление-кнопка gm-tw5-элементпанели" type="button" title="Развернуть проигрыватель на весь экран.\nКнопка ENTER на клавиатуре.">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href=""/>
</svg>
</button>
</div>
<div id="gm-tw5-настройки">
<table>
<tr><th class="gm-tw5-метка">Качество трансляции:<td title="Чем ниже качество, тем ниже требования к производительности компьютера и скорости подключения к Интернету.\n\nЕсли у вас часто приостанавливается воспроизведение (в статистике средняя скорость загрузки постоянно меньше битрейта сегмента) или слишком много потерянных кадров, и кручение других настроек не помагает, то попробуйте уменьшить качество трансляции. Менять качество трансляции можно далеко не на всех каналах.\n\nSOURCE - исходное видео максимального качества. HIGH - качество ниже, исходное видео пережато на сервере Twitch. MOBILE - самое низкое качество, понять что происходит на экране практически невозможно. :)">
<select id="gm-tw5-настройки-вариант" disabled></select>
<tr><th class="gm-tw5-метка">Одновременных загрузок:<td title="Видео загружается сравнительно небольшими частями - сегментами. Одновременная загрузка нескольких сегментов может повысить общую скорость загрузки и избежать периодических приостановок воспроизведения. Но если скорость вашего подключения к Интернету не сильно выше битрейта сегмента (смотрите в статистике), то, возможно, уменьшение этой настройки даст лучшие результаты.">
<label><input name="gm-tw5-настройки-загрузка" type="radio" value="1">1</label>
<label><input name="gm-tw5-настройки-загрузка" type="radio" value="2">2</label>
<label><input name="gm-tw5-настройки-загрузка" type="radio" value="3">3</label>
<tr><th class="gm-tw5-метка">Размер буфера:<td id="gm-tw5-настройки-размербуфера" title="Загруженные сегменты сначала помещаются в буфер. Воспроизведение начнется как только продолжительность видео в буфере станет не меньше указанного значения. Это позволяет не прерывать воспроизведение из-за кратковременных затыков в сети или на сервере.\n\nЕсли у вас часто приостанавливается воспроизведение, то попробуйте увеличить это значение.\n\nУвеличение размера буфера увеличивает задержку трансляции. Задержка трансляции - это время, прошедшее с захвата видео на компьютере ведущего трансляции и до просмотра вами. Низкая задержка важна только тем, кто пользуется чатом.">
<tr><th class="gm-tw5-метка">Начать воспроизведение:<td id="gm-tw5-настройки-началовоспроизведения" title="Отличие от размера буфера: эта настройка влияет только на начало первого воспроизведения. Позволяет после загрузки страницы быстрее начать просмотр.\n\nВ каких случаях это полезно:\n\n• Вам хочется как можно скорее понять что происходит на экране и перейти к следующей трансляции если что-то не понравилось (например, позорно низкий битрейт).\n\n• У вас редко бывают проблемы со скоростью загрузки видео, а значит ждать заполнения буфера не обязательно.\n\nПока эта настройка не работает как нужно, если включена одновременная загрузка нескольких сегментов (смотрите выше).">
<tr><th class="gm-tw5-метка">Заначка:<td title="На некоторых каналах ненулевая заначка может понизить ожидание ответа от сервера (смотрите в статистике), а значит, повысить скорость загрузки.\n\nНебольшие недостатки заначки:\n• Увеличенная задержка трансляции (читайте о ней в описании размера буфера).\n• Немного &quot;странное&quot; завершение трансляции.">
<label><input name="gm-tw5-настройки-заначка" type="radio" value="0">0</label>
<label><input name="gm-tw5-настройки-заначка" type="radio" value="1">1</label>
<label><input name="gm-tw5-настройки-заначка" type="radio" value="2">2</label>
<tr><th class="gm-tw5-метка">Интервал опроса:<td id="gm-tw5-настройки-интервалопроса" title="Периодически необходимо скачивать список воспроизведения и проверять, не появились ли в нём новые сегменты для загрузки. Эта настройка влияет на частоту скачивания списка. Я не уверен, что из её кручения можно извлечь какую-то пользу.">
</table>
<form action="${г_оСайт.ПолучитьАдресУбогогоПроигрывателя()}">
<input type="hidden" name="Twitch5" value="0">
<button type="submit" title="Открыть страницу канала на сайте Twitch.tv чтобы:\n• Смотреть эту трансляцию стандартным проигрывателем (Flash)\n• Прочитать описание канала\n• Подписаться на понравившийся канал\n• Совершить другие действия, которые не доступны в проигрывателе Twitch 5">Twitch</button>
<button id="gm-tw5-настройки-статистика" type="button" title="Показать статистику. Не для средних умов. :)\nКнопка - на клавиатуре.">Статистика</button>
<button id="gm-tw5-настройки-отзыв" type="button" title="Нашли ошибку в дополнении Twitch&nbsp;5? Есть идеи как его улучшить? Отправьте свой отзыв разработчику.">Оставить отзыв</button>
<button id="gm-tw5-настройки-сброс" type="button" title="Присвоить всем настройкам проигрывателя стандартные значения">Сброс</button>
</form>
</div>
<div id="gm-tw5-новости">
<div id="gm-tw5-новости-текст"></div>
</div>
</div>
<!-- iframe -->
</div>
</body>
`);
Проверить(_узКорпус = document.getElementById('gm-tw5-корпус'));
ЗапуститьАвтоскрытие();
ЗапуститьПолноэкранныйРежим();
ОбновитьПолноэкранныйРежим();
ОбновитьНастройкиЗвука();
ОбновитьОкноНастроек();
м_Новости.Запустить();
м_Чат.Восстановить();
document.addEventListener('dragstart', ОбработатьНачалоПеретаскивания);
document.addEventListener('dragover', ОбработатьПеретаскивание);
document.addEventListener('dragend', ОбработатьОкончаниеПеретаскивания);
_узКорпус.addEventListener('click', ОбработатьЩелчок);
var уз = document.getElementsByTagName('video')[0];
уз.addEventListener('contextmenu', ОтменитьДействие);
уз.addEventListener('dblclick', ОбработатьДвойнойЩелчок);
document.getElementById('gm-tw5-управление-громкость').addEventListener('input', ОбработатьИзменениеГромкости);
document.getElementById('gm-tw5-настройки-вариант').addEventListener('change', ОбработатьИзменениеВарианта);
document.addEventListener('keydown', ОбработатьНажатиеКлавы);
ИзменитьСостояние(СОСТОЯНИЕ_ЗАГРУЗКА);
}
function ИзменитьСостояние(чНовоеСостояние)
{
console.log('[Управление] Состояние трансляции изменилось с %s на %s', _чСостояние, чНовоеСостояние);
if (_чСостояние === чНовоеСостояние)
{
Проверить(чНовоеСостояние !== undefined);
return;
}
_узКорпус.setAttribute('data-gm-tw5-состояние', чНовоеСостояние);
switch (чНовоеСостояние)
{
case СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ:
ПоказатьМетаданныеТрансляции(undefined, undefined, null, null, null, null, null);
г_оСайт.НачатьСборМетаданныхТрансляции();
break;
case СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ:
г_оСайт.ЗавершитьСборМетаданныхТрансляции();
ПоказатьМетаданныеТрансляции(undefined, undefined, 'Трансляция завершена', null, null, null, null);
break;
case СОСТОЯНИЕ_ЗАГРУЗКА:
if (_чСостояние === undefined || _чСостояние === СОСТОЯНИЕ_ПАУЗА)
{
ПоказатьМетаданныеТрансляции(undefined, undefined, null, null, null, null, null);
}
break;
case СОСТОЯНИЕ_ВОСПРОИЗВЕДЕНИЕ:
break;
case СОСТОЯНИЕ_ПАУЗА:
г_оСайт.ЗавершитьСборМетаданныхТрансляции();
ПоказатьМетаданныеТрансляции(undefined, undefined, undefined, undefined, undefined, null, undefined);
break;
default:
Проверить(false);
}
_чСостояние = чНовоеСостояние;
}
function ПолучитьСостояние()
{
Проверить(_чСостояние !== undefined);
return _чСостояние;
}
function ПоказатьМетаданныеТрансляции(сНазваниеКанала, сАдресКанала, сНазваниеТрансляции, сНазваниеИгры, сАдресИгры, чКоличествоЗрителей, чДлительностьТрансляции)
// undefined - не менять, null - скрыть.
{
if (сНазваниеКанала && !_сНазваниеКанала)
{
_сНазваниеКанала = сНазваниеКанала;
document.title = `${сНазваниеКанала} - ${document.title}`;
}
if (сНазваниеТрансляции !== undefined)
{
var уз = document.getElementById('gm-tw5-заголовок-названиетрансляции');
if (сНазваниеТрансляции === null)
{
уз.setAttribute('hidden', '');
}
else
{
уз.lastElementChild.setAttribute('href', сАдресКанала);
уз.lastElementChild.setAttribute('title', сНазваниеТрансляции);
уз.lastElementChild.textContent = сНазваниеТрансляции;
уз.removeAttribute('hidden');
}
}
if (сНазваниеИгры !== undefined)
{
уз = document.getElementById('gm-tw5-заголовок-названиеигры');
if (сНазваниеИгры === null)
{
уз.setAttribute('hidden', '');
}
else
{
уз.lastElementChild.setAttribute('href', сАдресИгры);
уз.lastElementChild.setAttribute('title', сНазваниеИгры);
уз.lastElementChild.textContent = сНазваниеИгры;
уз.removeAttribute('hidden');
}
}
if (чКоличествоЗрителей !== undefined)
{
уз = document.getElementById('gm-tw5-заголовок-количествозрителей');
if (чКоличествоЗрителей === null)
{
уз.setAttribute('hidden', '');
}
else
{
уз.lastElementChild.textContent = чКоличествоЗрителей;
уз.removeAttribute('hidden');
}
}
if (чДлительностьТрансляции !== undefined)
{
document.getElementById('gm-tw5-управление-позиция').textContent =
чДлительностьТрансляции === null ? '' : ПеревестиСекундыВСтроку(чДлительностьТрансляции / 1000, false);
}
}
return {
Запустить,
ИзменитьСостояние,
ПолучитьСостояние,
ОтключитьПолноэкранныйРежим,
ВыбранВариантТрансляции,
ПоказатьМетаданныеТрансляции
};
})();
var г_оПроигрыватель = (function()
{
var _узПроигрыватель;
var _oMediaSource, _oVideoSourceBuffer, _oAudioSourceBuffer;
var _сРазмерБуфера = 'чНачалоВоспроизведения';
function ПоказатьСостояниеПроигрывателя(сВажность, сЗапись)
{
Проверить(typeof сЗапись === 'string');
var оВидеоБуфер = _oMediaSource.sourceBuffers.length !== 0 && _oVideoSourceBuffer ? _oVideoSourceBuffer.buffered : null;
var оАудиоБуфер = _oMediaSource.sourceBuffers.length !== 0 && _oAudioSourceBuffer ? _oAudioSourceBuffer.buffered : null;
var оОбщийБуфер = _узПроигрыватель.buffered;
if (сВажность === 'log' && ((оВидеоБуфер && оВидеоБуфер.length > 1) || (оАудиоБуфер && оАудиоБуфер.length > 1) || оОбщийБуфер.length > 1))
{
сВажность = 'info';
}
if (_узПроигрыватель.error)
{
сВажность = 'warn';
}
arguments[1] = `${сЗапись.charAt(0) === '[' ? '' : '[Проигрыватель] '}${сЗапись} •••`
+ ' video=' + ПеревестиОбластиВСтроку(оВидеоБуфер)
+ ' audio=' + ПеревестиОбластиВСтроку(оАудиоБуфер)
+ ' buffered=' + ПеревестиОбластиВСтроку(оОбщийБуфер)
+ ' currentTime=' + _узПроигрыватель.currentTime
+ ' paused=' + _узПроигрыватель.paused
+ ' seeking=' + _узПроигрыватель.seeking
+ ' ended=' + _узПроигрыватель.ended
+ ' readyState=' + _узПроигрыватель.readyState
+ ' networkState=' + _узПроигрыватель.networkState
+ ' played=' + ПеревестиОбластиВСтроку(_узПроигрыватель.played)
+ ' seekable=' + ПеревестиОбластиВСтроку(_узПроигрыватель.seekable)
+ ' duration=' + _узПроигрыватель.duration
+ ' error=' + (_узПроигрыватель.error ? _узПроигрыватель.error.code : _узПроигрыватель.error)
+ ' MS.readyState=' + _oMediaSource.readyState
+ ' MS.buffers=' + _oMediaSource.sourceBuffers.length;
console[сВажность].apply(console, Array.prototype.slice.call(arguments, 1));
}
function ПеревестиОбластиВСтроку(оОбласти)
{
if (!оОбласти || оОбласти.length === 0)
{
return 'пусто';
}
var мсРезультат = [];
for (var ы = 0; ы < оОбласти.length; ++ы)
{
мсРезультат.push(оОбласти.start(ы) + '-' + оОбласти.end(ы));
}
return мсРезультат.join(' ');
}
function СледитьЗаСобытиямиПроигрывателя(оСобытие)
{
try
{
switch (оСобытие.type)
{
case 'abort':
case 'emptied':
case 'progress':
case 'durationchange':
case 'playing':
case 'waiting':
case 'seeking':
case 'seeked':
ПоказатьСостояниеПроигрывателя('log', `[HTMLMediaElement] ${оСобытие.type}`);
break;
case 'error':
ПоказатьСостояниеПроигрывателя('warn', `[HTMLMediaElement] ${оСобытие.type}`);
break;
case 'loadstart':
ПоказатьСостояниеПроигрывателя('log', `[HTMLMediaElement] ${оСобытие.type} src=${оСобытие.target.src} currentSrc=${оСобытие.target.currentSrc}`);
break;
case 'timeupdate':
console.log('[HTMLMediaElement] timeupdate readyState=%s НеПросмотрено=%.2fс currentTime=%s', оСобытие.target.readyState, ПолучитьДлительностьВидеоВБуфере().чНеПросмотрено, оСобытие.target.currentTime);
break;
default:
console.log(`[HTMLMediaElement] ${оСобытие.type}`);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ПолучитьДлительностьВидеоВБуфере()
// Возвращает
// {
// чПросмотрено: длительность просмотренного видео >= 0 с
// чНеПросмотрено: длительность не просмотренного видео >= 0 с
// }
// Ямы не учитываются.
{
var чПросмотрено = 0, чНеПросмотрено = 0;
var оБуфер = _узПроигрыватель.buffered;
if (оБуфер.length !== 0)
{
var чНачало = оБуфер.start(0), чКонец = оБуфер.end(оБуфер.length - 1);
var чТекущееВремя = Math.min(Math.max(_узПроигрыватель.currentTime, чНачало), чКонец);
чПросмотрено = чТекущееВремя - чНачало;
чНеПросмотрено = чКонец - чТекущееВремя;
}
return {чПросмотрено, чНеПросмотрено};
}
function ПоказатьДлительностьТрансляции()
{
// На самом деле это видео было создано раньше (секунды).
// Особая точность не требуется, константа добавлена для очистки совести.
const ПОДПРАВИТЬ_ДЛИТЕЛЬНОСТЬ_ТРАНСЛЯЦИИ = 10;
if (г_оСтатистика.ОкноПоказано())
{
var оБуфер = _узПроигрыватель.buffered;
if (оБуфер.length !== 0)
{
г_оСтатистика.ИзмениласьДлительностьТрансляции(оБуфер.end(оБуфер.length - 1) + ПОДПРАВИТЬ_ДЛИТЕЛЬНОСТЬ_ТРАНСЛЯЦИИ);
}
}
}
function ПолучитьКоличествоПропущенныхКадров()
{
Проверить(_узПроигрыватель);
return typeof _узПроигрыватель.getVideoPlaybackQuality === 'function'
? _узПроигрыватель.getVideoPlaybackQuality().droppedVideoFrames
: _узПроигрыватель.webkitDroppedFrameCount;
}
function ПрименитьНастройкиЗвука()
{
Проверить(_узПроигрыватель);
_узПроигрыватель.volume = г_оНастройки.Получить('чГромкость');
_узПроигрыватель.muted = г_оНастройки.Получить('лПриглушить');
}
function УвеличитьСкоростьВоспроизведения()
{
Проверить(_узПроигрыватель);
if (_узПроигрыватель.playbackRate !== 2.0)
{
_узПроигрыватель.playbackRate = 2.0;
}
}
function ВосстановитьСкоростьВоспроизведения()
{
Проверить(_узПроигрыватель);
if (_узПроигрыватель.playbackRate !== 1.0)
{
_узПроигрыватель.playbackRate = 1.0;
}
}
function НачатьВоспроизведение()
{
ПроверитьПозициюВоспроизведения(true);
ПоказатьСостояниеПроигрывателя('info', 'Начало воспроизведения');
Проверить(_узПроигрыватель.paused);
_узПроигрыватель.play();
г_оУправление.ИзменитьСостояние(СОСТОЯНИЕ_ВОСПРОИЗВЕДЕНИЕ);
}
function ОстановитьВоспроизведение(чНовоеСостояние)
{
_узПроигрыватель.pause();
if (чНовоеСостояние !== undefined)
{
г_оУправление.ИзменитьСостояние(чНовоеСостояние);
}
}
function НачатьВоспроизведениеЕслиНужно()
{
// progress может прийти после нажатия на паузу.
if (г_оУправление.ПолучитьСостояние() !== СОСТОЯНИЕ_ПАУЗА && _узПроигрыватель.paused)
{
if (_oMediaSource.readyState === 'ended')
{
// TODO Во время обработки durationchange, _узПроигрыватель.buffered не всегда учитывает
// результат последнего вызова appendBuffer()?
if (_узПроигрыватель.buffered.length !== 0)
{
НачатьВоспроизведение();
// Ждем вызова ОбработатьEnded().
}
}
else
{
var чНеПросмотрено = ПолучитьДлительностьВидеоВБуфере().чНеПросмотрено;
var чРазмерБуфера = г_оНастройки.Получить(_сРазмерБуфера);
if (чНеПросмотрено >= чРазмерБуфера)
{
console.info('[Проигрыватель] В буфере не просмотрено %.3fс >= %.3fс', чНеПросмотрено, чРазмерБуфера);
НачатьВоспроизведение();
}
}
}
}
function ПредотвратитьПереполнениеБуфера()
// В 32-битных оборзевателях размер буфера ≈ 150 MiB. При толщине видео 20 Мбит/с этого хватит ≈ на 1 минуту.
// Причины переполнения:
// - серьезный затык в сети
// - изменение настроек
// - очень большое количество разрывов
// - выход из спячки (BUG)
{
var чНеПросмотрено = ПолучитьДлительностьВидеоВБуфере().чНеПросмотрено;
if (чНеПросмотрено >= ПЕРЕПОЛНЕНИЕ_БУФЕРА)
{
var оБуфер = _узПроигрыватель.buffered;
// Немного добавить, чтобы непросмотренного точно хватило для начала воспроизведения.
var чПеремоткаНа = оБуфер.end(оБуфер.length - 1) - г_оНастройки.Получить('чРазмерБуфера') - 0.5;
ПоказатьСостояниеПроигрывателя('warn', 'Буфер проигрывателя переполнен ПЕРЕПОЛНЕНИЕ_БУФЕРА=%sс НеПросмотрено=%.3fс ПеремоткаНа=%sс', ПЕРЕПОЛНЕНИЕ_БУФЕРА, чНеПросмотрено, чПеремоткаНа);
// Временная остановка нужна, чтобы при возобновлении воспроизведения пройти необходимые проверки:
// перепрыгивание ям в ПроверитьПозициюВоспроизведения (Chrome)
// и зависание воспроизведения в НачатьВоспроизведение (Firefox).
if (!_узПроигрыватель.paused)
{
ОстановитьВоспроизведение();
}
// Изменяем позицию воспроизведения, чтобы проигрыватель и js начали удалять просмотренное видео из буфера.
// Во время удаления учитывается currentTime и не учитываются played и paused.
_узПроигрыватель.currentTime = чПеремоткаНа;
г_оСтатистика.ПереполненБуферПроигрывателя();
}
}
function БуферИсчерпан()
{
ПоказатьСостояниеПроигрывателя('info', 'Приостанавливаю воспроизведение до заполнения буфера');
_сРазмерБуфера = 'чРазмерБуфера';
ОстановитьВоспроизведение(СОСТОЯНИЕ_ЗАГРУЗКА);
}
function ПерезагрузитьПроигрыватель()
{
ПоказатьСостояниеПроигрывателя('info', 'Перезагрузка проигрывателя');
г_оУправление.ИзменитьСостояние(СОСТОЯНИЕ_ЗАГРУЗКА);
_узПроигрыватель.load();
// load() удалила все буферы из _oMediaSource.
_oVideoSourceBuffer = null;
_oAudioSourceBuffer = null;
// Ждем вызова ОбработатьSourceOpen().
}
function ПроверитьПозициюВоспроизведения(лНачалоВоспроизведения)
{
var оБуфер = _узПроигрыватель.buffered;
var чПоследняяОбласть = оБуфер.length - 1;
var чТекущееВремя = _узПроигрыватель.currentTime;
for (var чОбласть = 0; чОбласть <= чПоследняяОбласть; ++чОбласть)
{
// Chrome 47: currentTime может быть на 0.000001 секунду меньше присвоенной величины.
// Если это не учесть, то получим бесконечную перемотку.
if (оБуфер.start(чОбласть) - чТекущееВремя > 0.0005)
{
// Позиция воспроизведения находится вне загруженной области.
ПоказатьСостояниеПроигрывателя(чТекущееВремя === 0 ? 'info' : 'warn', 'Перемотка на начало воспроизведения');
_узПроигрыватель.currentTime = оБуфер.start(чОбласть) + 0.0005;
return;
}
// Chrome 48: Иногда currentTime вылезает за пределы последней области.
if (оБуфер.end(чОбласть) - чТекущееВремя > -0.0005 || чОбласть === чПоследняяОбласть)
{
// Chrome 46: Если позиция воспроизведения расположена слишком близко к концу области, то currentTime
// не изменится и будут посланы только два события: play и playing.
if (оБуфер.end(чОбласть) - чТекущееВремя < ОСТАНАВЛИВАТЬ_ЕСЛИ_НЕ_ПРОСМОТРЕНО_МЕНЬШЕ)
{
if (чОбласть !== чПоследняяОбласть)
{
ПоказатьСостояниеПроигрывателя('warn', 'Перепрыгиваю яму');
_узПроигрыватель.currentTime = оБуфер.start(чОбласть + 1) + 0.0005;
}
else if (_oMediaSource.readyState !== 'ended')
{
Проверить(!лНачалоВоспроизведения);
БуферИсчерпан();
}
}
return;
}
}
}
function ОбработатьTimeUpdate()
// Chrome 46: Если включено аппаратное декодирование и в буфере проигрывателя не просмотрено менее определенного порога,
// то добавление сегмента приведет к рывку видео и единовременному увеличению счетчика пропущенных кадров. Величина порога
// зависит от видео (непонятно от каких параметров) и равна 0,2..0,6 с. В Firefox 42 такой проблемы нет.
// https://code.google.com/p/chromium/issues/detail?id=535200
{
try
{
if (!_узПроигрыватель.paused && !_узПроигрыватель.seeking)
{
ПроверитьПозициюВоспроизведения(false);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьProgress()
// Firefox 42, Chrome 48: Может прийти в любой момент, например во время работы ДобавитьСегмент().
// Chrome 46: Приходит если в течении пары сотен мс не было вызова appendBuffer().
{
try
{
ПредотвратитьПереполнениеБуфера();
НачатьВоспроизведениеЕслиНужно();
ПоказатьДлительностьТрансляции();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьDurationChange()
{
try
{
if (IsFiniteNumber(_узПроигрыватель.duration))
{
НачатьВоспроизведениеЕслиНужно();
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьEnded()
{
try
{
// Проигрыватель уже вызвал pause().
ПерезагрузитьПроигрыватель();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьSourceOpen()
{
try
{
ПоказатьСостояниеПроигрывателя('log', 'sourceopen');
ДобавитьСледующийСегмент();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function УдалитьПросмотренноеВидео()
// BUG Изменить обработку _oMediaSource.readyState === 'closed'.
{
var чПросмотрено = ПолучитьДлительностьВидеоВБуфере().чПросмотрено;
if (чПросмотрено < ИНТЕРВАЛ_УДАЛЕНИЯ_ВИДЕО + НЕ_УДАЛЯТЬ_ВИДЕО)
{
return Promise.resolve();
}
return new Promise(function(фВыполнить, фОтказаться)
{
var чУдалитьДо = _узПроигрыватель.currentTime - НЕ_УДАЛЯТЬ_ВИДЕО;
ПоказатьСостояниеПроигрывателя('log', 'Удаляю просмотренное видео Просмотрено=%.3fс УдалитьДо=%.3fс', чПросмотрено, чУдалитьДо);
var чУдаленоЗа = performance.now();
var кНужноУдалить = 2;
_oVideoSourceBuffer.addEventListener('updateend', Удалено);
_oAudioSourceBuffer.addEventListener('updateend', Удалено);
_oVideoSourceBuffer.remove(0, чУдалитьДо);
_oAudioSourceBuffer.remove(0, чУдалитьДо);
function Удалено()
{
try
{
if (--кНужноУдалить === 0)
{
_oVideoSourceBuffer.removeEventListener('updateend', Удалено);
_oAudioSourceBuffer.removeEventListener('updateend', Удалено);
if (_oMediaSource.readyState === 'closed')
{
фОтказаться(new Error('Не удалось удалить просмотренное видео'));
}
else
{
чУдаленоЗа = performance.now() - чУдаленоЗа;
чПросмотрено = ПолучитьДлительностьВидеоВБуфере().чПросмотрено;
ПоказатьСостояниеПроигрывателя(чПросмотрено !== 0 ? 'log' : 'warn', 'Просмотренное видео удалено за %dмс Просмотрено=%.3fс', чУдаленоЗа, чПросмотрено);
фВыполнить();
}
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
});
}
function ДобавитьСегмент(оСегмент, мбВидеоСегмент, мбАудиоСегмент)
// BUG Изменить обработку _oMediaSource.readyState === 'closed'.
{
return new Promise(function(фВыполнить, фОтказаться)
{
ПоказатьСостояниеПроигрывателя('log', 'Добавляю сегмент %s Длительность=%.3fс', оСегмент.чНомер, оСегмент.чДлительность);
var чДобавленЗа = performance.now();
var кНужноДобавить = 2;
_oVideoSourceBuffer.addEventListener('updateend', Добавлено);
_oAudioSourceBuffer.addEventListener('updateend', Добавлено);
_oVideoSourceBuffer.appendBuffer(мбВидеоСегмент);
_oAudioSourceBuffer.appendBuffer(мбАудиоСегмент);
function Добавлено()
{
try
{
if (--кНужноДобавить === 0)
{
_oVideoSourceBuffer.removeEventListener('updateend', Добавлено);
_oAudioSourceBuffer.removeEventListener('updateend', Добавлено);
if (_oMediaSource.readyState !== 'open')
{
фОтказаться(new Error('Не удалось добавить сегмент в проигрыватель'));
}
else
{
чДобавленЗа = performance.now() - чДобавленЗа;
ПоказатьСостояниеПроигрывателя('log', 'Добавлен сегмент %s за %dмс', оСегмент.чНомер, чДобавленЗа);
фВыполнить();
}
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
});
}
function ДобавитьСледующийСегмент()
// TODO Не пихать после load() слишком много. Chrome не успевает посылать progress.
// TODO Ограничить количество преобразованных, но не добавленных сегментов.
{
Проверить(_узПроигрыватель);
var оСегмент = г_моОчередь[0];
if (!оСегмент || оСегмент.чСостояние !== СЕГМЕНТ_ПРЕОБРАЗОВАН)
{
return;
}
if (_oMediaSource.readyState !== 'open')
{
console.log('[Проигрыватель] MediaSource.readyState=%s', _oMediaSource.readyState);
return;
}
if (оСегмент.лРазрыв && _oMediaSource.sourceBuffers.length !== 0)
{
ПоказатьСостояниеПроигрывателя('info', 'Сегмент %s вызвал окончание потока', оСегмент.чНомер);
// Какой-то оборзеватель не проигрывал несколько последних секунд до вызова endOfStream().
_oMediaSource.endOfStream();
// Ждем вызова ОбработатьDurationChange() или ОбработатьProgress().
return;
}
if (typeof оСегмент.пДанные === 'number')
{
Проверить(оСегмент.лРазрыв && _узПроигрыватель.paused);
г_оУправление.ИзменитьСостояние(оСегмент.пДанные);
г_моОчередь.Удалить(0);
ДобавитьСледующийСегмент();
return;
}
if (_oMediaSource.sourceBuffers.length === 0)
{
Проверить(оСегмент.лРазрыв);
console.info('[Проигрыватель] Добавляю буферы %s и %s', оСегмент.пДанные.sVideoMimeType, оСегмент.пДанные.sAudioMimeType);
if (!MediaSource.isTypeSupported(оСегмент.пДанные.sVideoMimeType) || !MediaSource.isTypeSupported(оСегмент.пДанные.sAudioMimeType))
{
г_оОтладка.ПоказатьСообщениеИЗавершитьРаботу(`
Ваш браузер не смог показать эту трансляцию.
Если вы видите это сообщение на всех каналах Twitch.tv, значит ваш браузер не умеет воспроизводить H.264 или AAC.
В этом случае, пожалуйста, удалите из вашего браузера дополнение "Twitch 5", чтобы продолжить просмотр трансляций.
`);
}
_oVideoSourceBuffer = _oMediaSource.addSourceBuffer(оСегмент.пДанные.sVideoMimeType);
_oAudioSourceBuffer = _oMediaSource.addSourceBuffer(оСегмент.пДанные.sAudioMimeType);
}
оСегмент.чСостояние = СЕГМЕНТ_ДОБАВЛЯЕТСЯ;
var оОбещание = УдалитьПросмотренноеВидео();
if (оСегмент.лРазрыв)
{
оОбещание = оОбещание.then(function()
{
return ДобавитьСегмент(оСегмент, оСегмент.пДанные.мбВидеоИнициализация, оСегмент.пДанные.мбАудиоИнициализация);
});
}
оОбещание.then(function()
{
return ДобавитьСегмент(оСегмент, оСегмент.пДанные.мбВидеоСегмент, оСегмент.пДанные.мбАудиоСегмент);
})
.then(function()
{
г_моОчередь.Удалить(оСегмент);
ДобавитьСледующийСегмент();
})
.catch(function(пИсключение)
{
try
{
ПоказатьСостояниеПроигрывателя('warn', '' + пИсключение);
}
finally
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
});
}
function Запустить()
{
Проверить(!_узПроигрыватель);
if (typeof MediaSource !== 'function')
{
г_оОтладка.ПоказатьСообщениеИЗавершитьРаботу(`
Дополнение "Twitch 5" не может работать в вашем браузере (нет Media Source Extensions).
Пожалуйста, обновите ваш браузер или удалите из него дополнение "Twitch 5".
`);
}
_узПроигрыватель = document.getElementsByTagName('video')[0];
for (var сСобытие of ['loadstart', 'progress', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'play', 'pause', 'ratechange', 'resize', 'volumechange'])
{
_узПроигрыватель.addEventListener(сСобытие, СледитьЗаСобытиямиПроигрывателя);
}
_узПроигрыватель.addEventListener('timeupdate', ОбработатьTimeUpdate);
_узПроигрыватель.addEventListener('progress', ОбработатьProgress);
_узПроигрыватель.addEventListener('durationchange', ОбработатьDurationChange);
_узПроигрыватель.addEventListener('ended', ОбработатьEnded);
ПрименитьНастройкиЗвука();
_oMediaSource = new MediaSource();
_oMediaSource.addEventListener('sourceopen', ОбработатьSourceOpen);
_узПроигрыватель.setAttribute('src', URL.createObjectURL(_oMediaSource));
// BUG Chrome 49: иногда после смены src MediaSource.readyState остается closed.
_узПроигрыватель.load();
}
function Остановить()
{
if (_узПроигрыватель)
{
console.log('[Проигрыватель] Убиваю проигрыватель');
_узПроигрыватель.removeAttribute('src');
_узПроигрыватель.load();
}
}
return {
Запустить,
Остановить,
ПерезагрузитьПроигрыватель,
ПолучитьДлительностьВидеоВБуфере,
ПолучитьКоличествоПропущенныхКадров,
ПрименитьНастройкиЗвука,
УвеличитьСкоростьВоспроизведения,
ВосстановитьСкоростьВоспроизведения,
ОстановитьВоспроизведение,
ДобавитьСледующийСегмент
};
})();
var г_оСписок = (function()
{
// сНазвание
// nPeakBitrate
// сАбсолютныйАдресСпискаСегментов
var _моСписокВариантов = null;
var _чВыбранныйВариант;
// undefined - начальное состояние.
// null - трансляция завершена.
// object - идет трансляция.
var _оПоследнийСписок;
var _лРазрыв;
var _чИнтервалОпроса;
var _чТаймер = 0;
function РазобратьСписок(bMasterPlaylist, sPlaylistAbsoluteUrl, sPlaylist)
{
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16
const МАКС_ПОДДЕРЖИВАЕМАЯ_ВЕРСИЯ_HLS = 7;
// Иногда вместе с кодом 200 вместо списка прилетает HTML неожиданного содержания. Например
// 400 Bad Request (nginx) или сообщение об окончании денег на счете интернет-провайдера.
if (!sPlaylist.startsWith('#EXTM3U'))
{
throw `Вместо списка загружена какая-то фигня длиною ${sPlaylist.length}\n${sPlaylist}`;
}
if (bMasterPlaylist)
{
var mapRenditionGroups = new Map(); // Важен порядок элементов.
var oRenditionGroup;
var лНужноСортировать = false;
}
else
{
var моСегменты = [];
var оНовыйСегмент; // Используется для проверки порядка следования тегов.
var nMediaSequenceNumber = 0;
var nTargetDuration;
var лКонецСписка;
var чДлительностьСписка = 0; // Статистика.
}
var чВерсия = 1;
var asMatches;
var рвТегИлиАдрес = /^#(EXT[A-Z0-9\-]+)(?::(.+))?$|^[^#\r\n].*$/mg;
рвТегИлиАдрес.lastIndex = 7; // Пропускаем #EXTM3U.
while (asMatches = рвТегИлиАдрес.exec(sPlaylist))
{
var сАдрес = asMatches[0], сНазваниеТега = asMatches[1], сЗначениеТега = asMatches[2];
if (сАдрес.charAt(0) !== '#')
{
сАдрес = ResolveRelativeUrl(сАдрес, sPlaylistAbsoluteUrl);
if (bMasterPlaylist)
{
Проверить(oRenditionGroup);
oRenditionGroup.сАбсолютныйАдресСпискаСегментов = сАдрес;
oRenditionGroup = undefined;
}
else
{
Проверить(оНовыйСегмент);
Проверить(typeof оНовыйСегмент.чДлительность !== 'undefined');
Проверить(!лКонецСписка);
чДлительностьСписка += оНовыйСегмент.чДлительность;
оНовыйСегмент.лРазрыв = !!оНовыйСегмент.лРазрыв;
оНовыйСегмент.сАдрес = сАдрес;
моСегменты.push(оНовыйСегмент);
оНовыйСегмент = undefined;
}
continue;
}
switch (сНазваниеТега)
{
//
// Теги общие для обоих типов списка.
// #EXT-X-INDEPENDENT-SEGMENTS не используется.
//
case 'EXTM3U':
Проверить(false);
break;
case 'EXT-X-VERSION':
Проверить(чВерсия === 1);
чВерсия = ParseDecimalInteger(сЗначениеТега);
Проверить(чВерсия >= 2 && чВерсия <= МАКС_ПОДДЕРЖИВАЕМАЯ_ВЕРСИЯ_HLS);
break;
case 'EXT-X-START':
Проверить(false);
break;
//
// Теги списка вариантов.
//
case 'EXT-X-MEDIA':
Проверить(bMasterPlaylist);
// Не менять oRenditionGroup, которая используется для проверки порядка следования тегов.
var oNewRenditionGroup = Object.create(null);
var кОбязательныхАтрибутов = 3;
for (var мсАтрибут of РазобратьСписокАтрибутов(сЗначениеТега))
{
switch (мсАтрибут[0])
{
case 'TYPE':
Проверить(мсАтрибут[1] === 'VIDEO');
--кОбязательныхАтрибутов;
break;
case 'GROUP-ID':
var sGroupId = ParseQuotedString(мсАтрибут[1]);
Проверить(!mapRenditionGroups.has(sGroupId));
mapRenditionGroups.set(sGroupId, oNewRenditionGroup);
--кОбязательныхАтрибутов;
break;
case 'NAME':
oNewRenditionGroup.сНазвание = ParseQuotedString(мсАтрибут[1]);
--кОбязательныхАтрибутов;
break;
case 'URI': // Видео и звук должны находиться в одном файле.
case 'FORCED':
case 'INSTREAM-ID':
Проверить(false);
break;
}
}
Проверить(кОбязательныхАтрибутов === 0);
break;
case 'EXT-X-STREAM-INF':
Проверить(bMasterPlaylist);
Проверить(!oRenditionGroup); // Пропущен адрес?
var nPeakBitrate = undefined;
for (var мсАтрибут of РазобратьСписокАтрибутов(сЗначениеТега))
{
switch (мсАтрибут[0])
{
case 'BANDWIDTH':
nPeakBitrate = ParseDecimalInteger(мсАтрибут[1]);
// Иногда после завершения трансляции BANDWIDTH=0.
break;
case 'VIDEO':
Проверить(oRenditionGroup = mapRenditionGroups.get(ParseQuotedString(мсАтрибут[1])));
break;
case 'AUDIO':
case 'SUBTITLES':
case 'CLOSED-CAPTIONS':
Проверить(false);
break;
}
}
Проверить(nPeakBitrate !== undefined);
if (!oRenditionGroup) // #EXT-X-STREAM-INF без #EXT-X-MEDIA?
{
лНужноСортировать = true;
oRenditionGroup = Object.create(null);
oRenditionGroup.сНазвание = (nPeakBitrate / 1000000).toFixed(1) + ' Мбит/с';
mapRenditionGroups.set(mapRenditionGroups.size, oRenditionGroup);
}
oRenditionGroup.nPeakBitrate = nPeakBitrate;
break;
case 'EXT-X-I-FRAME-STREAM-INF':
case 'EXT-X-SESSION-DATA':
Проверить(bMasterPlaylist);
break;
//
// Теги списка сегментов.
//
case 'EXT-X-TARGETDURATION':
Проверить(!bMasterPlaylist);
Проверить(nTargetDuration === undefined);
nTargetDuration = ParseDecimalInteger(сЗначениеТега);
Проверить(nTargetDuration > 0 && nTargetDuration < 24 * 60 * 60);
break;
case 'EXT-X-MEDIA-SEQUENCE':
Проверить(!bMasterPlaylist);
Проверить(nMediaSequenceNumber === 0);
nMediaSequenceNumber = ParseDecimalInteger(сЗначениеТега);
break;
case 'EXT-X-ENDLIST':
Проверить(!bMasterPlaylist);
Проверить(лКонецСписка === undefined);
Проверить(сЗначениеТега === undefined);
лКонецСписка = true;
console.info('[Список] #EXT-X-ENDLIST после сегмента %s', nMediaSequenceNumber + моСегменты.length - 1);
break;
case 'EXT-X-DISCONTINUITY-SEQUENCE':
Проверить(!bMasterPlaylist);
break;
case 'EXT-X-PLAYLIST-TYPE':
case 'EXT-X-I-FRAMES-ONLY':
Проверить(false);
break;
//
// Теги для сегментов в списке сегментов.
//
case 'EXTINF':
Проверить(!bMasterPlaylist);
оНовыйСегмент = оНовыйСегмент || Object.create(null);
Проверить(typeof оНовыйСегмент.чДлительность === 'undefined');
оНовыйСегмент.чДлительность = ParseExtinfTag(сЗначениеТега, чВерсия);
Проверить(оНовыйСегмент.чДлительность !== 0);
if (оНовыйСегмент.чДлительность - nTargetDuration > 1.0)
{
console.warn('[Список] Длительность сегмента превышена на %sс', оНовыйСегмент.чДлительность - nTargetDuration);
}
break;
case 'EXT-X-DISCONTINUITY':
Проверить(!bMasterPlaylist);
оНовыйСегмент = оНовыйСегмент || Object.create(null);
Проверить(typeof оНовыйСегмент.лРазрыв === 'undefined');
Проверить(сЗначениеТега === undefined);
оНовыйСегмент.лРазрыв = true;
console.warn('[Список] #EXT-X-DISCONTINUITY у сегмента %s', nMediaSequenceNumber + моСегменты.length);
break;
case 'EXT-X-PROGRAM-DATE-TIME':
Проверить(!bMasterPlaylist);
break;
case 'EXT-X-BYTERANGE':
case 'EXT-X-KEY':
case 'EXT-X-MAP':
Проверить(false);
break;
}
}
if (bMasterPlaylist)
{
Проверить(!oRenditionGroup); // Пропущен адрес?
var моСписокВариантов = [];
for (oRenditionGroup of mapRenditionGroups.values())
{
Проверить(oRenditionGroup.сАбсолютныйАдресСпискаСегментов); // #EXT-X-MEDIA без #EXT-X-STREAM-INF?
моСписокВариантов.push(oRenditionGroup);
console.info('[Список] Добавлен вариант Название=%s PeakBitrate=%s', oRenditionGroup.сНазвание, oRenditionGroup.nPeakBitrate);
}
// Twitch: Атрибут BANDWIDTH варианта Source отражает текущее значение, которое может быть
// намного меньше максимального. В этом случае сортировка переместит вариант Source в
// неправильную позицию. Варианты уже отсортированы в порядке убывания BANDWIDTH.
if (лНужноСортировать)
{
моСписокВариантов.sort(function(а, б)
{
return б.nPeakBitrate - а.nPeakBitrate;
});
}
return моСписокВариантов;
}
Проверить(!оНовыйСегмент); // Пропущен адрес?
Проверить(nTargetDuration !== undefined); // Пропущен #EXT-X-TARGETDURATION?
г_оСтатистика.РазобранСписокСегментов(nTargetDuration, моСегменты.length, чДлительностьСписка);
console.log('[Список] Разобран список сегментов TargetDuration=%s MediaSequenceNumber=%s Сегменты=%o', nTargetDuration, nMediaSequenceNumber, моСегменты);
return {
моСегменты,
nMediaSequenceNumber,
nTargetDuration,
лКонецСписка: !!лКонецСписка
};
}
function РазобратьСписокАтрибутов(сИсходныйТекст)
{
Проверить(сИсходныйТекст);
var рвАтрибут = /([A-Z0-9\-]+)=("[^"]+"|[^",][^,]*)(?:,|$)/g;
var амАтрибуты = new Map();
while (рвАтрибут.lastIndex !== сИсходныйТекст.length)
{
var nLastIndex = рвАтрибут.lastIndex;
var asMatches = рвАтрибут.exec(сИсходныйТекст);
Проверить(asMatches && asMatches.index === nLastIndex);
Проверить(!амАтрибуты.has(asMatches[1]));
амАтрибуты.set(asMatches[1], asMatches[2]);
}
return амАтрибуты;
}
function ParseDecimalInteger(сИсходныйТекст)
{
Проверить(/^[0-9]{1,15}$/.test(сИсходныйТекст));
return parseInt(сИсходныйТекст, 10);
}
function ParseFloatingPoint(сИсходныйТекст)
{
// Нам нужны минимум 3 цифры после запятой.
Проверить(/^[0-9]{1,14}\.[0-9]{1,15}$/.test(сИсходныйТекст));
return parseFloat(сИсходныйТекст);
}
function ParseQuotedString(сИсходныйТекст)
{
Проверить(сИсходныйТекст && сИсходныйТекст.length > 2 && сИсходныйТекст.charAt(0) === '"' && сИсходныйТекст.charAt(сИсходныйТекст.length - 1) === '"');
return сИсходныйТекст.slice(1, -1);
}
function ParseDecimalResolution(сИсходныйТекст)
{
Проверить(сИсходныйТекст);
var чРазделитель = сИсходныйТекст.indexOf('x');
Проверить(чРазделитель !== -1);
return [ParseDecimalInteger(сИсходныйТекст.slice(0, чРазделитель)), ParseDecimalInteger(сИсходныйТекст.slice(чРазделитель + 1))];
}
function ParseExtinfTag(сИсходныйТекст, чВерсия)
{
Проверить(сИсходныйТекст);
var чЗапятая = сИсходныйТекст.indexOf(',');
if (чЗапятая !== -1)
{
сИсходныйТекст = сИсходныйТекст.slice(0, чЗапятая);
}
if (чВерсия < 3)
{
return ParseDecimalInteger(сИсходныйТекст);
}
try
{
return ParseFloatingPoint(сИсходныйТекст);
}
catch (и)
{
return ParseDecimalInteger(сИсходныйТекст);
}
}
function ДобавитьСегментыВОчередь(оНовыйСписок)
{
Проверить(оНовыйСписок.моСегменты.length !== 0);
if (!_оПоследнийСписок)
{
// После окончания трансляции список может быть доступен довольно долго.
if (оНовыйСписок.лКонецСписка)
{
throw 'Найден #EXT-X-ENDLIST';
}
// Выполнить перед добавлением сегментов.
var оДобавлено = г_моОчередь.Добавить(new Сегмент(СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ));
console.info('[Список] Добавлен сегмент %s Состояние=СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ', оДобавлено.чНомер);
var чРазмерБуфера = г_оНастройки.Получить('чРазмерБуфера');
for (чИндекс = Math.max(оНовыйСписок.моСегменты.length - г_оНастройки.Получить('кЗаначка') - 1, 1); --чИндекс !== 0;)
{
if ((чРазмерБуфера -= оНовыйСписок.моСегменты[чИндекс].чДлительность) <= 0)
{
break;
}
}
чПервыйНовыйСегмент = -1;
_лРазрыв = true;
}
else
{
var чИндекс = 0;
var чСдвиг = оНовыйСписок.nMediaSequenceNumber - _оПоследнийСписок.nMediaSequenceNumber;
var чПервыйНовыйСегмент = _оПоследнийСписок.моСегменты.length - чСдвиг;
if (чСдвиг < 0)
{
// HACK Twitch: По ошибке нам прислали старый список. Выкидываем его и ждем следующий.
console.warn('[Список] Новый MediaSequenceNumber на %s меньше последнего', -чСдвиг);
_чИнтервалОпроса = _оПоследнийСписок.nTargetDuration / 2 * 1000;
return;
}
Проверить(оНовыйСписок.nMediaSequenceNumber + оНовыйСписок.моСегменты.length >= _оПоследнийСписок.nMediaSequenceNumber + _оПоследнийСписок.моСегменты.length);
if (чПервыйНовыйСегмент < 0)
{
// Firefox 43: из-за нехватки памяти в системе, загрузка списка может задержаться на несколько десятков секунд.
console.warn('[Список] Пропущено сегментов: %s', -чПервыйНовыйСегмент);
г_оСтатистика.НеЗагруженыСегменты(-чПервыйНовыйСегмент);
_лРазрыв = true;
}
if (оНовыйСписок.nTargetDuration !== _оПоследнийСписок.nTargetDuration)
{
console.warn('[Список] TargetDuration изменился с %s на %s', _оПоследнийСписок.nTargetDuration, оНовыйСписок.nTargetDuration);
}
}
var кСегментовДобавлено = 0;
for (var оСегмент; оСегмент = оНовыйСписок.моСегменты[чИндекс]; ++чИндекс)
{
if (чИндекс < чПервыйНовыйСегмент)
{
// HACK Twitch: EXT-X-MEDIA-SEQUENCE на 1 меньше после начала трансляции.
if (_оПоследнийСписок.моСегменты[чСдвиг + чИндекс].сАдрес !== оСегмент.сАдрес
|| _оПоследнийСписок.моСегменты[чСдвиг + чИндекс].чДлительность !== оСегмент.чДлительность)
{
console.warn('[Список] У сегмента %s изменился адрес %s ==> %s или длительность %s ==> %s',
оНовыйСписок.nMediaSequenceNumber + чИндекс,
_оПоследнийСписок.моСегменты[чСдвиг + чИндекс].сАдрес, оСегмент.сАдрес,
_оПоследнийСписок.моСегменты[чСдвиг + чИндекс].чДлительность, оСегмент.чДлительность
);
_лРазрыв = true;
}
}
else
{
if (_лРазрыв)
{
оСегмент.лРазрыв = true;
_лРазрыв = false;
}
// Делаем копию свойств, потому что во время нахождения в очереди они будут изменены.
var оДобавлено = г_моОчередь.Добавить(new Сегмент(оСегмент.сАдрес, оСегмент.чДлительность, оСегмент.лРазрыв));
console.log('[Список] Добавлен сегмент %s MediaSequenceNumber=%s Длительность=%s Разрыв=%s',
оДобавлено.чНомер, оНовыйСписок.nMediaSequenceNumber + чИндекс, оДобавлено.чДлительность, оДобавлено.лРазрыв);
++кСегментовДобавлено;
}
}
_оПоследнийСписок = оНовыйСписок;
г_оСтатистика.ДобавленыСегментыВОчередь(кСегментовДобавлено);
if (оНовыйСписок.лКонецСписка)
{
throw 'Найден #EXT-X-ENDLIST';
}
_чИнтервалОпроса = оНовыйСписок.nTargetDuration * (кСегментовДобавлено === 0 ? 0.5 : г_оНастройки.Получить('чИнтервалОпроса') / 100) * 1000;
}
function ОбновитьСписки()
// BUG Twitch: завершить трансляцию, если долго (сколько?) не добавлялись новые сегменты.
{
_чТаймер = 0;
if (_моСписокВариантов !== null)
{
// Копия этой загрузки есть ниже по тексту.
console.log('[Список] Загружаю список сегментов');
var оОбещание = г_оОтменяемоеОбещаниеСписка = ЗагрузитьФайл(_моСписокВариантов[_чВыбранныйВариант].сАбсолютныйАдресСпискаСегментов,
false, ЗАГРУЖАТЬ_СПИСОК_СЕГМЕНТОВ_НЕ_ДОЛЬШЕ, true);
}
else
{
var сАбсолютныйАдресСпискаВариантов_;
var оОбещание = г_оСайт.ПолучитьАбсолютныйАдресСпискаВариантов()
.then(function(сАбсолютныйАдресСпискаВариантов)
{
console.log('[Список] Загружаю список вариантов');
сАбсолютныйАдресСпискаВариантов_ = сАбсолютныйАдресСпискаВариантов;
return г_оОтменяемоеОбещаниеСписка = ЗагрузитьФайл(сАбсолютныйАдресСпискаВариантов,
false, ЗАГРУЖАТЬ_СПИСОК_ВАРИАНТОВ_НЕ_ДОЛЬШЕ, true);
})
.then(function(мРезультат)
{
console.log('[Список] Загружен список вариантов за %dмс', мРезультат[1]);
г_оОтладка.СохранитьСписокВариантов(мРезультат[0]);
_моСписокВариантов = РазобратьСписок(true, сАбсолютныйАдресСпискаВариантов_, мРезультат[0]);
ВыбратьВариантТрансляции();
// Копия этой загрузки есть выше по тексту.
console.log('[Список] Загружаю список сегментов');
return г_оОтменяемоеОбещаниеСписка = ЗагрузитьФайл(_моСписокВариантов[_чВыбранныйВариант].сАбсолютныйАдресСпискаСегментов,
false, ЗАГРУЖАТЬ_СПИСОК_СЕГМЕНТОВ_НЕ_ДОЛЬШЕ, true);
});
}
оОбещание.then(function(мРезультат)
{
// Эта строка должна находиться после последнего вызова ЗагрузитьФайл().
г_оОтменяемоеОбещаниеСписка = null;
console.log('[Список] Загружен список сегментов за %dмс', мРезультат[1]);
г_оОтладка.СохранитьСписокСегментов(мРезультат[0]);
ДобавитьСегментыВОчередь(РазобратьСписок(false, _моСписокВариантов[_чВыбранныйВариант].сАбсолютныйАдресСпискаСегментов, мРезультат[0]));
ЗапланироватьСледующееОбновлениеСписка();
ЗагрузитьСледующийСегмент();
})
.catch(function(пПричина)
{
try
{
// Эта строка должна находиться после последнего вызова ЗагрузитьФайл().
г_оОтменяемоеОбещаниеСписка = null;
// Завершение трансляции?
if (typeof пПричина === 'string')
{
console.warn('[Список] Завершаю трансляцию. %s', пПричина);
ЗавершитьТрансляцию();
ОчиститьСписокВариантовТрансляции();
ЗапланироватьСледующееОбновлениеСписка();
ЗагрузитьСледующийСегмент();
}
// Выполнение ЗагрузитьФайл() отменено?
else if (пПричина === ОБЕЩАНИЕ_ОТМЕНЕНО)
{
console.info('[Список] Загрузка списка была отменена');
}
// Проверить() или незапланированное исключение.
else
{
г_оОтладка.ПойманоИсключение(пПричина);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
});
}
function ЗапланироватьСледующееОбновлениеСписка()
{
Проверить(_чТаймер === 0);
Проверить(_чИнтервалОпроса >= 500);
console.log('[Список] Загрузка списка начнется через %dмс', _чИнтервалОпроса);
_чТаймер = setTimeout(ОбновитьСпискиV8, _чИнтервалОпроса);
}
function ОбновитьСпискиV8()
{
try
{
ОбновитьСписки();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ВыбратьВариантТрансляции()
// Изменяет _чВыбранныйВариант.
{
if (_моСписокВариантов.length === 0)
{
throw 'Список вариантов пуст';
}
var сЖелаемыйВариант = г_оНастройки.Получить('сНазваниеВарианта');
// По умолчанию берем вариант с максимальным качеством.
_чВыбранныйВариант = 0;
for (var ы = 0; ы < _моСписокВариантов.length; ++ы)
{
if (_моСписокВариантов[ы].сНазвание === сЖелаемыйВариант)
{
_чВыбранныйВариант = ы;
break;
}
}
console.log('[Список] Выбран вариант трансляции %s ЖелаемыйВариант=%s', _моСписокВариантов[_чВыбранныйВариант].сНазвание, сЖелаемыйВариант);
г_оУправление.ВыбранВариантТрансляции(_моСписокВариантов, _чВыбранныйВариант);
г_оСтатистика.ВыбранВариантТрансляции(_моСписокВариантов, _чВыбранныйВариант);
}
function ОчиститьСписокВариантовТрансляции()
{
if (_моСписокВариантов !== null)
{
console.log('[Список] Очищаю список вариантов трансляции');
_моСписокВариантов = null;
_чВыбранныйВариант = undefined;
г_оУправление.ВыбранВариантТрансляции(_моСписокВариантов, _чВыбранныйВариант);
}
}
function ЗавершитьТрансляцию()
// Изменяет _чИнтервалОпроса.
{
if (_оПоследнийСписок !== null)
{
_оПоследнийСписок = null;
_чИнтервалОпроса = ИНТЕРВАЛ_ПОЛУЧЕНИЯ_СПИСКА_ВАРИАНТОВ_НАЧАЛО;
var оДобавлено = г_моОчередь.Добавить(new Сегмент(СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ));
console.info('[Список] Добавлен сегмент %s Состояние=СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ', оДобавлено.чНомер);
}
else if (_чИнтервалОпроса < ИНТЕРВАЛ_ПОЛУЧЕНИЯ_СПИСКА_ВАРИАНТОВ_КОНЕЦ)
{
_чИнтервалОпроса += ИНТЕРВАЛ_ПОЛУЧЕНИЯ_СПИСКА_ВАРИАНТОВ_ШАГ;
}
}
function Остановить(лВыбратьВариантТрансляции)
// Переводит г_оСписок в исходное состояние.
// Не добавляет в очередь СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ.
// лВыбратьВариантТрансляции = false - очищает список вариантов трансляции.
// лВыбратьВариантТрансляции = true - выбирает вариант в списке вариантов трансляции.
{
if (_чТаймер !== 0)
{
console.log('[Список] Отменяю таймер');
clearTimeout(_чТаймер);
_чТаймер = 0;
}
if (г_оОтменяемоеОбещаниеСписка !== null)
{
console.log('[Список] Отменяю обещание');
г_оОтменяемоеОбещаниеСписка.Отменить();
г_оОтменяемоеОбещаниеСписка = null;
}
_оПоследнийСписок = undefined;
_чИнтервалОпроса = undefined;
if (лВыбратьВариантТрансляции)
{
ВыбратьВариантТрансляции();
}
else
{
ОчиститьСписокВариантовТрансляции();
}
}
return {
Запустить: ОбновитьСписки,
Остановить
};
})();
function ЗагрузитьСледующийСегмент()
// Сегменты могут загружаться в произвольном порядке.
{
var кЗаначка = г_оНастройки.Получить('кЗаначка');
for (var кКонец = г_моОчередь.length - 1; кКонец >= 0; --кКонец)
{
if (кЗаначка === 0 || г_моОчередь[кКонец].чСостояние !== СЕГМЕНТ_ЖДЕТ_ЗАГРУЗКИ || typeof г_моОчередь[кКонец].пДанные === 'number')
{
break;
}
--кЗаначка;
}
var кОдновременныхЗагрузок = г_оНастройки.Получить('кОдновременныхЗагрузок');
for (var ы = 0; ы <= кКонец; ++ы)
{
var оСегмент = г_моОчередь[ы];
if (оСегмент.чСостояние === СЕГМЕНТ_ЖДЕТ_ЗАГРУЗКИ)
{
Проверить(typeof оСегмент.пДанные === 'string');
ЗагрузитьСегмент(оСегмент);
}
else if (оСегмент.чСостояние !== СЕГМЕНТ_ЗАГРУЖАЕТСЯ)
{
continue;
}
if (--кОдновременныхЗагрузок === 0)
{
break;
}
}
г_оПреобразователь.ПреобразоватьСледующийСегмент();
}
function ЗагрузитьСегмент(оСегмент)
{
var чЗагружатьНеДольше = 5000 // Ожидание ответа.
+ оСегмент.чДлительность * г_оНастройки.Получить('кОдновременныхЗагрузок') // Если толщина сегмента и толщина канала совпадают.
* 2 // Проблемы со связью.
* 1000; // Перевод в миллисекунды.
console.log('[Загрузка] Запрошен сегмент %s ЗагружатьНеДольше=%dмс', оСегмент.чНомер, чЗагружатьНеДольше);
оСегмент.чСостояние = СЕГМЕНТ_ЗАГРУЖАЕТСЯ;
(оСегмент.пДанные = ЗагрузитьФайл(оСегмент.пДанные, true, чЗагружатьНеДольше, г_оСтатистика.ОкноПоказано()))
.then(function(мРезультат)
{
Проверить(г_моОчередь.indexOf(оСегмент) !== -1 && оСегмент.чСостояние === СЕГМЕНТ_ЗАГРУЖАЕТСЯ && оСегмент.пДанные instanceof Promise);
console.log('[Загрузка] Получен сегмент %s Размер=%s ДлительностьЗагрузки=%dмс ОжиданиеОтвета=%dмс',
оСегмент.чНомер, мРезультат[0].byteLength, мРезультат[1], мРезультат[2]);
оСегмент.чСостояние = СЕГМЕНТ_ЗАГРУЖЕН;
оСегмент.пДанные = мРезультат[0];
г_оСтатистика.ЗагруженСегмент(мРезультат[0].byteLength, оСегмент.чДлительность, мРезультат[1], мРезультат[2]);
ЗагрузитьСледующийСегмент();
})
.catch(function(пПричина)
{
try
{
оСегмент.пДанные = null;
// Не удалось загрузить файл?
if (typeof пПричина === 'string')
{
Проверить(г_моОчередь.indexOf(оСегмент) !== -1 && оСегмент.чСостояние === СЕГМЕНТ_ЗАГРУЖАЕТСЯ);
console.warn('[Загрузка] Не удалось загрузить сегмент %s. %s', оСегмент.чНомер, пПричина);
г_оСтатистика.НеЗагруженыСегменты(1);
ОбработатьНеудачнуюЗагрузкуСегмента(оСегмент);
ЗагрузитьСледующийСегмент();
}
// Выполнение ЗагрузитьФайл() отменено?
else if (пПричина === ОБЕЩАНИЕ_ОТМЕНЕНО)
{
console.info('[Загрузка] Загрузка сегмента %s была отменена', оСегмент.чНомер);
}
// Проверить() или незапланированное исключение.
else
{
г_оОтладка.ПойманоИсключение(пПричина);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
});
}
function ОбработатьНеудачнуюЗагрузкуСегмента(оСегмент)
// TODO Выкинуть похожий цикл из ДобавитьСегментыВОчередь()?
{
г_моОчередь.ПоказатьСостояние();
var кСегментов = г_оНастройки.Получить('кЗаначка') + 1;
// TODO Использовать чНачалоВоспроизведения или чРазмерБуфера?
var чДлительность = г_оНастройки.Получить('чНачалоВоспроизведения');
for (var ы = г_моОчередь.length - 1; ы >= 0; --ы)
{
if (г_моОчередь[ы] === оСегмент || typeof г_моОчередь[ы].пДанные === 'number')
{
кСегментов = чДлительность = -1;
}
if ((г_моОчередь[ы].чСостояние === СЕГМЕНТ_ЖДЕТ_ЗАГРУЗКИ || г_моОчередь[ы].чСостояние === СЕГМЕНТ_ЗАГРУЖАЕТСЯ) && typeof г_моОчередь[ы].пДанные !== 'number')
{
if (кСегментов > 0)
{
--кСегментов;
}
else if (чДлительность > 0)
{
чДлительность -= г_моОчередь[ы].чДлительность;
}
else
{
г_моОчередь.Удалить(ы);
}
}
}
г_моОчередь.ПоказатьСостояние();
}
var г_оПреобразователь = (function()
{
var _сАдрес;
var _oWorker = null;
var _чПоследнийЗагруженный;
function СоздатьWorker()
{
Проверить(_oWorker === null);
Проверить(_сАдрес);
console.log('[Преобразование] Создаю worker');
_oWorker = new Worker(_сАдрес);
_oWorker.addEventListener('error', ОбработатьОшибкуПреобразования);
_oWorker.addEventListener('message', ОбработатьОкончаниеПреобразованияV8);
console.log('[Преобразование] Worker создан');
}
function УбитьWorker()
{
if (_oWorker !== null)
{
console.log('[Преобразование] Убиваю worker');
_oWorker.terminate();
_oWorker = null;
}
}
function ПреобразоватьСледующийСегмент()
// СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ:
// Создает worker пока сегменты закачиваются.
// Отсылается в worker чтобы не изменилась его позиция в очереди.
// СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ:
// Выталкивает из worker застрявшие там сегменты.
// TODO Когда все сегменты будут вытолкнуты, убить worker.
{
var кУдалить = 0, чУдалить;
// Нельзя преобразовывать сегменты в произвольном порядке.
for (var чСегмент = 0, оСегмент; оСегмент = г_моОчередь[чСегмент]; ++чСегмент)
{
if (оСегмент.чСостояние > СЕГМЕНТ_ЗАГРУЖЕН)
{
continue;
}
if (оСегмент.чСостояние < СЕГМЕНТ_ЗАГРУЖЕН)
{
break;
}
if (typeof оСегмент.пДанные !== 'number' && оСегмент.чНомер !== _чПоследнийЗагруженный + 1)
{
console.warn('[Преобразование] Cегменты между %s и %s не были загружены', _чПоследнийЗагруженный, оСегмент.чНомер);
оСегмент.лРазрыв = true;
}
_чПоследнийЗагруженный = оСегмент.чНомер;
if (оСегмент.пДанные === СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ && _oWorker === null)
{
СоздатьWorker();
console.log('[Преобразование] Пропускаю сегмент %s Состояние=СОСТОЯНИЕ_НАЧАЛО_ТРАНСЛЯЦИИ', оСегмент.чНомер);
оСегмент.чСостояние = СЕГМЕНТ_ПРЕОБРАЗОВАН;
continue;
}
if (оСегмент.пДанные === СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ && _oWorker === null)
{
console.log('[Преобразование] Пропускаю сегмент %s Состояние=СОСТОЯНИЕ_ЗАВЕРШЕНИЕ_ТРАНСЛЯЦИИ', оСегмент.чНомер);
оСегмент.чСостояние = СЕГМЕНТ_ПРЕОБРАЗОВАН;
continue;
}
console.log('[Преобразование] Отсылаю сегмент %s', оСегмент.чНомер);
_oWorker.postMessage(
[ВЕРСИЯ_ДОПОЛНЕНИЯ, оСегмент.пДанные, оСегмент.чДлительность, оСегмент.лРазрыв, оСегмент.чНомер],
typeof оСегмент.пДанные === 'number' ? undefined : [оСегмент.пДанные]
);
if (++кУдалить === 1)
{
чУдалить = чСегмент;
}
}
if (кУдалить !== 0)
{
г_моОчередь.splice(чУдалить, кУдалить);
}
г_оПроигрыватель.ДобавитьСледующийСегмент();
}
function ОбработатьОшибкуПреобразования(оСобытие)
{
г_оОтладка.ЗавершитьРаботу(`Произошла ошибка в worker в строке ${оСобытие.lineno}. ${оСобытие.message}`);
}
function ОбработатьОкончаниеПреобразованияV8(оСобытие)
{
try
{
ОбработатьОкончаниеПреобразования(оСобытие);
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ОбработатьОкончаниеПреобразования(оСобытие)
{
var мДанные = оСобытие.data;
Проверить(Array.isArray(мДанные));
if (мДанные.length === 2 || мДанные.length === 3)
{
Проверить(typeof мДанные[1] === 'string');
if (мДанные[0] === 'error')
{
г_оОтладка.ЗавершитьРаботу(мДанные[1], false, мДанные[2]);
}
else
{
Проверить(мДанные.length === 2);
Проверить(мДанные[0] === 'log' || мДанные[0] === 'info' || мДанные[0] === 'warn');
console[мДанные[0]](мДанные[1]);
}
return;
}
Проверить(мДанные.length === 4);
for (var чВставить = 0, оСегмент; оСегмент = г_моОчередь[чВставить]; ++чВставить)
{
if (оСегмент.чСостояние < СЕГМЕНТ_ПРЕОБРАЗОВАН)
{
break;
}
}
оСегмент = г_моОчередь.Добавить(new Сегмент(мДанные[0], мДанные[1], мДанные[2], мДанные[3]), чВставить);
if (typeof оСегмент.пДанные === 'number')
{
console.log('[Преобразование] Получен сегмент %s Индекс=%s Состояние=%s', оСегмент.чНомер, чВставить, оСегмент.пДанные);
}
else
{
console.log('[Преобразование] Получен сегмент %s Индекс=%s Длительность=%.3fс Разрыв=%s ПреобразованЗа=%d', оСегмент.чНомер, чВставить, оСегмент.чДлительность, оСегмент.лРазрыв, оСегмент.пДанные.чПреобразованЗа);
г_оСтатистика.ПреобразованСегмент(оСегмент);
}
г_оПроигрыватель.ДобавитьСледующийСегмент();
}
function Запустить()
// Chrome 48, Greasemonkey 3.6: если дополнение обновилось во время своей работы, то будет загружен
// обновленный worker.js, который, возможно, не совместим с запущенным старым content.js.
// TODO Добавить worker.js в этот файл в виде строки.
{
Проверить(!_сАдрес);
console.log('[Преобразование] Загружаю worker.js');
return ЗагрузитьФайл(chrome.extension.getURL('worker.js'), false, 30000 /* TODO 0 */, false)
.then(function(мРезультат)
{
_сАдрес = URL.createObjectURL(new Blob([мРезультат[0]], {type: 'application/javascript;charset=UTF-8'}));
console.log('[Преобразование] worker.js загружен');
});
}
return {
Запустить,
Остановить: УбитьWorker,
ПреобразоватьСледующийСегмент
};
})();
function ЗагрузитьФайл(сАдресФайла, лДвоичныеДанные, чЗагружатьНеДольше, лНужнаСтатистика, оЗаголовки)
{
Проверить(typeof сАдресФайла === 'string' && (сАдресФайла.startsWith('http://') || сАдресФайла.startsWith('https://') || сАдресФайла.startsWith('chrome-extension://')));
Проверить(чЗагружатьНеДольше === 0 || чЗагружатьНеДольше > 50);
var фОтменить;
var оОбещание = new Promise(function(фВыполнить, фОтказаться)
{
фОтменить = function()
{
if (оЗапрос)
{
ЗагрузитьФайл_ОтключитьТаймер(оЗапрос);
оЗапрос.removeEventListener('abort', ЗагрузитьФайл_OnAbort);
оЗапрос.abort();
}
фОтказаться(ОБЕЩАНИЕ_ОТМЕНЕНО);
};
var оЗапрос = new XMLHttpRequest();
оЗапрос.addEventListener('timeout', ЗагрузитьФайл_OnTimeout);
оЗапрос.addEventListener('error', ЗагрузитьФайл_OnError);
оЗапрос.addEventListener('abort', ЗагрузитьФайл_OnAbort);
оЗапрос.addEventListener('load', ЗагрузитьФайл_OnLoad);
if (лНужнаСтатистика)
{
оЗапрос.addEventListener('loadstart', ЗагрузитьФайл_OnLoadStart);
оЗапрос.addEventListener('progress', ЗагрузитьФайл_OnProgress);
}
оЗапрос.open('GET', сАдресФайла);
оЗапрос.responseType = лДвоичныеДанные ? 'arraybuffer' : 'text';
оЗапрос.timeout = чЗагружатьНеДольше;
оЗапрос.фВыполнить = фВыполнить;
оЗапрос.фОтказаться = фОтказаться;
if (оЗаголовки)
{
for (var сЗаголовок of Object.keys(оЗаголовки))
{
оЗапрос.setRequestHeader(сЗаголовок, оЗаголовки[сЗаголовок]);
}
}
оЗапрос.send();
// BUG Chrome 48: иногда запрос не завершается, не посылая никаких событий.
// BUG Chrome 48: иногда таймер срабатывает через секунду, что намного раньше положенного срока.
var чНачало = performance.now();
оЗапрос.чТаймер = чЗагружатьНеДольше === 0 ? 0 : setTimeout(
function()
{
г_оОтладка.ЗавершитьРаботу(`Загрузка не завершилась в течении ${performance.now() - чНачало}мс ЗагружатьНеДольше=${чЗагружатьНеДольше} НужнаСтатистика=${лНужнаСтатистика} ВремяОтправкиЗапроса=${оЗапрос.чВремяОтправкиЗапроса} ВремяПолученияОтвета=${оЗапрос.чВремяПолученияОтвета} ВремяОкончанияЗагрузки=${оЗапрос.чВремяОкончанияЗагрузки} readyState=${оЗапрос.readyState} status=${оЗапрос.status} Адрес=${сАдресФайла}`);
},
// Добавить немного чтобы исключить ложные срабатывания.
чЗагружатьНеДольше + 5000
);
});
оОбещание.Отменить = фОтменить;
return оОбещание;
}
function ЗагрузитьФайл_ОтключитьТаймер(оЗапрос)
{
if (оЗапрос.чТаймер)
{
clearTimeout(оЗапрос.чТаймер);
оЗапрос.чТаймер = 0;
}
}
function ЗагрузитьФайл_OnTimeout()
{
try
{
ЗагрузитьФайл_ОтключитьТаймер(this);
this.фОтказаться(`Превышено время загрузки ${this.timeout.toFixed()}мс`);
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ЗагрузитьФайл_OnError()
{
try
{
ЗагрузитьФайл_ОтключитьТаймер(this);
this.фОтказаться('Произошла неизвестная ошибка во время загрузки файла');
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ЗагрузитьФайл_OnAbort()
// Chrome 49 сам вызывает abort() во время выгрузки страницы, засыпания компа и еще по каким-то неведомым мне причинам.
{
try
{
console.warn('[ЗагрузитьФайл] onabort');
ЗагрузитьФайл_ОтключитьТаймер(this);
this.фОтказаться('Оборзеватель отменил загрузку файла');
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ЗагрузитьФайл_OnLoadStart()
// TODO Событие может задержаться, например на одноядерном процессоре? В этом случае время вызова send() даст более точный результат?
{
try
{
this.чВремяОтправкиЗапроса = performance.now();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ЗагрузитьФайл_OnProgress()
// За время приема одного сегмента onreadystatechange вызывается несколько сотен раз. onprogress вызывается примерно каждые 50 мс.
// TODO Firefox 42: первое onprogress прилетает через 50 мс после начала обработки ответа.
{
try
{
this.чВремяОкончанияЗагрузки = performance.now();
if (typeof this.чВремяПолученияОтвета === 'undefined')
{
this.чВремяПолученияОтвета = this.чВремяОкончанияЗагрузки;
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ЗагрузитьФайл_OnLoad()
{
try
{
ЗагрузитьФайл_ОтключитьТаймер(this);
if (this.response && this.status >= 200 && this.status <= 299)
{
if (this.status !== 200)
{
console.warn('[ЗагрузитьФайл] Сервер вернул код %s %s', this.status, this.statusText);
}
// Нам нужен размер сжатых данных. progress дает размер разжатых данных.
var сРазмер = this.getResponseHeader('Content-Length');
// Content-Length отсутствует если используется Transfer-Encoding: chunked или файл загружен с диска.
if (сРазмер)
{
// TODO HTTP2 жмет заголовки, поэтому их размер меньше.
г_оСтатистика.СкачаноНечто(this.statusText.length + this.getAllResponseHeaders().length + (+сРазмер) + 17);
}
this.фВыполнить(
[
this.response,
this.чВремяОкончанияЗагрузки - this.чВремяОтправкиЗапроса,
this.чВремяПолученияОтвета - this.чВремяОтправкиЗапроса
]);
}
else
{
console.warn('[ЗагрузитьФайл] Сервер вернул код %s %s\n%s', this.status, this.statusText, this.responseType !== 'text' ? '' : this.response);
this.фОтказаться(КОД_ОТВЕТА + this.status);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
var г_оСайт = (function()
{
const ВОЗМОЖНО_ПРЯМАЯ_ТРАНСЛЯЦИЯ = 1;
const ПРЯМАЯ_ТРАНСЛЯЦИЯ_FLASH = 2;
const ПРЯМАЯ_ТРАНСЛЯЦИЯ_HLS = 3;
const ПРОФИЛЬ_КАНАЛА = 4;
const ЧАТ_КАНАЛА = 5;
const НАШ_ПРОИГРЫВАТЕЛЬ = 6;
var _сКодКанала;
var _сАбсолютныйАдресСпискаВариантов;
var _чПротухнетПосле = 0;
var _чТаймерСбораМетаданных = 0;
var _оОтменяемоеОбещаниеМетаданных = null;
function ОбработатьЩелчок(оСобытие)
{
try
{
var узЩелчок = оСобытие.target;
do
{
if (узЩелчок.tagName === 'A')
{
// Ссылка на трансляцию...
if (узЩелчок.getAttribute('href')
// в шапке профиля
&& (узЩелчок.getAttribute('class') === 'channel-link'
// в каталоге
|| узЩелчок.hasAttribute('data-channel-link')
// в команде под видео
|| узЩелчок.getAttribute('id') === 'live_channel_name'
// в команде в списке слева
|| (узЩелчок.parentNode.classList.contains('member') && узЩелчок.parentNode.parentNode.getAttribute('id') === 'team_member_list')
// в шапке на главной
|| узЩелчок.getAttribute('data-tt_content') === 'carousel'
// переход с подставного канала на настоящий
|| узЩелчок.getAttribute('data-tt_content') === 'host_channel'
// свой канал слева
|| узЩелчок.getAttribute('data-tt_content') === 'self_channel'
// в списке подписок справа
|| узЩелчок.getAttribute('data-tt_content') === 'sidebar_follow'
// в related channels слева
|| узЩелчок.getAttribute('data-tt_content') === 'related_channels'
// в promoted channels слева
|| узЩелчок.getAttribute('data-tt_content') === 'promoted_channels'
// в recommended channels слева
|| узЩелчок.getAttribute('data-tt_content') === 'recommended_channels'))
{
// Отключить AJAX.
оСобытие.stopPropagation();
// TODO Менять адрес по событию mousedown?
var оРазобранныйАдрес = РазобратьАдрес(узЩелчок);
// Адрес еще не изменен?
if (оРазобранныйАдрес.чСтраница === ВОЗМОЖНО_ПРЯМАЯ_ТРАНСЛЯЦИЯ)
{
var сАдрес = ПолучитьАдресНашегоПроигрывателя(оРазобранныйАдрес);
console.info('[Сайт] Меняю адрес ссылки %s ==> %s', узЩелчок.href, сАдрес);
узЩелчок.setAttribute('href', сАдрес);
}
//
// Отмечать текущую и просмотренные трансляции.
//
// Переход по ссылке?
if (оСобытие.button === ЛЕВАЯ_КНОПКА || оСобытие.button === СРЕДНЯЯ_КНОПКА)
{
// Chrome 48: for...of не работает с DOM.
var сузПросмотр = document.getElementsByClassName('gm-tw5-просмотр');
if (сузПросмотр.length !== 0)
{
сузПросмотр[0].classList.remove('gm-tw5-просмотр');
}
while (узЩелчок = узЩелчок.parentElement)
{
if (узЩелчок.classList.contains('item'))
{
узЩелчок.classList.add('gm-tw5-просмотрено', 'gm-tw5-просмотр');
break;
}
}
}
}
break;
}
}
while (узЩелчок = узЩелчок.parentElement);
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ПолучитьАбсолютныйАдресСпискаВариантов()
{
Проверить(_сКодКанала);
var чСейчас = performance.now();
if (чСейчас < _чПротухнетПосле)
{
console.log('[Сайт] Адрес списка вариантов протухнет через %dмс', _чПротухнетПосле - чСейчас);
return Promise.resolve(_сАбсолютныйАдресСпискаВариантов);
}
console.log('[Сайт] Загружаю access token');
return (г_оОтменяемоеОбещаниеСписка = ЗагрузитьФайл(
`https://api.twitch.tv/api/channels/${_сКодКанала}/access_token`,
false, ЗАГРУЖАТЬ_СПИСОК_ВАРИАНТОВ_НЕ_ДОЛЬШЕ, true
))
.then(
function(мРезультат)
{
console.log('[Сайт] Access token загружен за %dмс', мРезультат[1], мРезультат[0]);
var оРезультат = JSON.parse(мРезультат[0]);
Проверить(typeof оРезультат.token === 'string' && оРезультат.token !== '' && typeof оРезультат.sig === 'string' && оРезультат.sig !== '');
var оДоступ = JSON.parse(оРезультат.token);
Проверить(оДоступ.channel === _сКодКанала);
Проверить(IsFiniteNumber(оДоступ.expires));
// В 2016.01.01 адрес протухает через 20 минут.
_чПротухнетПосле = оДоступ.expires - Date.now() / 1000;
console[Math.abs(_чПротухнетПосле - 20 * 60) > 3 * 60 ? 'warn' : 'info']('[Сайт] Адрес списка вариантов протухнет через %dс', _чПротухнетПосле);
// BUG На компе пользователя время (часовой пояс) может быть установлено неправильно.
// TODO Взять заголовок Date из ответа сервера?
_чПротухнетПосле = performance.now() + 3 * 60 * 1000;
_сАбсолютныйАдресСпискаВариантов = `http://usher.justin.tv/api/channel/hls/${_сКодКанала}.m3u8?token=${encodeURIComponent(оРезультат.token)}&sig=${encodeURIComponent(оРезультат.sig)}&allow_source=true`;
return _сАбсолютныйАдресСпискаВариантов;
},
function(пПричина)
{
// Канала не существует || запрещенное название канала?
if (пПричина === (КОД_ОТВЕТА + 404) || пПричина === (КОД_ОТВЕТА + 422))
{
г_оОтладка.ПоказатьСообщениеИЗавершитьРаботу('Похоже, что вы набрали неправильный адрес. Канала с указанным названием не существует.');
}
else
{
throw пПричина;
}
}
);
}
function ЗагрузитьМетаданныеТрансляции()
{
console.log('[Сайт] Загружаю метаданные трансляции');
Проверить(_сКодКанала);
Проверить(ЗАГРУЖАТЬ_МЕТАДАННЫЕ_НЕ_ДОЛЬШЕ < ИНТЕРВАЛ_СБОРА_МЕТАДАННЫХ);
Проверить(_оОтменяемоеОбещаниеМетаданных === null);
return (_оОтменяемоеОбещаниеМетаданных = ЗагрузитьФайл(
`https://api.twitch.tv/kraken/streams/${_сКодКанала}`,
false, ЗАГРУЖАТЬ_МЕТАДАННЫЕ_НЕ_ДОЛЬШЕ, true,
{'Accept': 'application/vnd.twitchtv.v3+json'}
))
.then(function(мРезультат)
{
_оОтменяемоеОбещаниеМетаданных = null;
console.log('[Сайт] Загружены метаданные трансляции за %dмс', мРезультат[1], мРезультат[0]);
var оРезультат = JSON.parse(мРезультат[0]);
// Идет доступная для всех трансляция?
if (оРезультат.stream)
{
var чНачалоТрансляции = Date.parse(оРезультат.stream.created_at); // ISO 8601
Проверить(
оРезультат.stream.channel.name === _сКодКанала
&& IsFiniteNumber(оРезультат.stream.viewers)
&& !isNaN(чНачалоТрансляции)
);
г_оУправление.ПоказатьМетаданныеТрансляции(
оРезультат.stream.channel.display_name,
ПолучитьАдресПрофиляКанала(),
оРезультат.stream.channel.status || оРезультат.stream.channel.display_name || 'Без названия',
оРезультат.stream.game || 'Не указана',
ПолучитьАдресКаталогаИгры(оРезультат.stream.game),
оРезультат.stream.viewers,
Date.now() - чНачалоТрансляции
);
}
})
.catch(function(пПричина)
{
try
{
_оОтменяемоеОбещаниеМетаданных = null;
// Не удалось загрузить файл?
if (typeof пПричина === 'string')
{
console.warn('[Сайт] Не удалось загрузить метаданные трансляции. %s', пПричина);
}
// Выполнение ЗагрузитьФайл() отменено?
else if (пПричина === ОБЕЩАНИЕ_ОТМЕНЕНО)
{
console.info('[Сайт] Загрузка метаданных трансляции была отменена');
}
// Проверить() или незапланированное исключение.
else
{
г_оОтладка.ПойманоИсключение(пПричина);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
});
}
function НачатьСборМетаданныхТрансляции()
{
if (_чТаймерСбораМетаданных === 0)
{
console.log('[Сайт] Начинаю сбор метаданных');
// BUG Заменить на setTimeout() чтобы затыки не мешали.
_чТаймерСбораМетаданных = setInterval(ЗагрузитьМетаданныеТрансляцииV8, ИНТЕРВАЛ_СБОРА_МЕТАДАННЫХ);
ЗагрузитьМетаданныеТрансляции();
}
}
function ЗагрузитьМетаданныеТрансляцииV8()
{
try
{
ЗагрузитьМетаданныеТрансляции();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
function ЗавершитьСборМетаданныхТрансляции()
{
console.log('[Сайт] Завершаю сбор метаданных');
if (_чТаймерСбораМетаданных !== 0)
{
clearInterval(_чТаймерСбораМетаданных);
_чТаймерСбораМетаданных = 0;
}
if (_оОтменяемоеОбещаниеМетаданных !== null)
{
_оОтменяемоеОбещаниеМетаданных.Отменить();
_оОтменяемоеОбещаниеМетаданных = null;
}
}
function РазобратьАдрес(оАдрес)
// У Twitch долбанутая система адресов. Например /directory - это список игр, /derectory - канал.
// У страниц прямой трансляции, архивной трансляции и профиля одного канала исходный код совпадает.
// Чтобы быстро выделить страницу с прямой трансляцией, нужно разбирать путь в адресе.
// Прямая трансляция: /канал
// Прямая трансляция HLS: /канал/hls
// Архивная трансляция: /канал/разная/фигня
// Профиль канала: /канал/profile
// Чат канала: /канал/chat
{
var чСтраница, сКодКанала;
if (оАдрес.protocol === 'chrome-extension:')
{
// TODO Использовать URLSearchParams.
мсЧасти = оАдрес.search.match(/(?:\?|&)channel=([^&]+)(?:&|$)/);
чСтраница = НАШ_ПРОИГРЫВАТЕЛЬ;
сКодКанала = мсЧасти ? мсЧасти[1] : 'job';
}
else
{
var мсЧасти = оАдрес.pathname.match(/^\/([^/]+)(\/[^/]+)?\/?$/);
if (мсЧасти)
{
// Вместо кода канала пользователь мог вставить название канала.
сКодКанала = мсЧасти[1].toLowerCase();
switch (мсЧасти[2])
{
case undefined: чСтраница = оАдрес.search.indexOf('Twitch5=0') === -1 ? ВОЗМОЖНО_ПРЯМАЯ_ТРАНСЛЯЦИЯ : ПРЯМАЯ_ТРАНСЛЯЦИЯ_FLASH; break;
case '/hls': чСтраница = ПРЯМАЯ_ТРАНСЛЯЦИЯ_HLS; break;
case '/profile': чСтраница = ПРОФИЛЬ_КАНАЛА; break;
case '/chat': чСтраница = ЧАТ_КАНАЛА; break;
}
}
}
return {чСтраница, сКодКанала};
}
function ПолучитьАдресНашегоПроигрывателя(оРазобранныйАдрес)
{
Проверить(оРазобранныйАдрес.сКодКанала);
return `${chrome.extension.getURL('player.html')}?channel=${оРазобранныйАдрес.сКодКанала}`;
}
function ПолучитьАдресУбогогоПроигрывателя()
// TODO Часть адреса search забита в HTML окна настроек и РазобратьАдрес().
{
Проверить(_сКодКанала);
return `https://www.twitch.tv/${_сКодКанала}`;
}
function ПолучитьАдресПрофиляКанала()
{
Проверить(_сКодКанала);
return `https://www.twitch.tv/${_сКодКанала}/profile`;
}
function ПолучитьАдресПанелиЧата()
{
Проверить(_сКодКанала);
return `https://www.twitch.tv/${_сКодКанала}/chat`;
}
function ПолучитьАдресОкнаЧата()
{
Проверить(_сКодКанала);
return `https://www.twitch.tv/${_сКодКанала}/chat?popout=Twitch5`;
}
function ПолучитьАдресКаталогаИгры(сНазваниеИгры)
{
return 'https://www.twitch.tv/directory' + (сНазваниеИгры ? '/game/' + encodeURIComponent(сНазваниеИгры) : '');
}
function ПеренаправитьНаНашПроигрыватель(оРазобранныйАдрес)
{
var сАдрес = ПолучитьАдресНашегоПроигрывателя(оРазобранныйАдрес);
console.info('[Сайт] Перенаправление %s ==> %s', location.href, сАдрес);
location.replace(сАдрес);
}
function НастроитьПеренаправление(оРазобранныйАдрес)
// Три способа перенаправления:
// 1. Очень быстрый. Меняет адрес ссылки в ОбработатьЩелчок().
// 2. Быстрый. Ждет вставку в head элемента, который встречается только на странице трансляции.
// Используется при переходе со стороннего сайта или закладки.
// 3. Медленный. Ждет вставку в body элемента, который встречается только на странице трансляции.
// Используется при переходе по AJAX, когда первый способ не сработал, например в результатах поиска.
{
// Firefox 44 + Greasemonkey 3.6: небольшая часть документа уже разобрана.
if (оРазобранныйАдрес.чСтраница === ВОЗМОЖНО_ПРЯМАЯ_ТРАНСЛЯЦИЯ
&& document.head
&& document.querySelector('link[href^="android-app://"]'))
{
ПеренаправитьНаНашПроигрыватель(оРазобранныйАдрес);
return;
}
// Ждем вставку элементов, которые встречается только на странице трансляции.
(new MutationObserver(function(моЗаписи, оНаблюдатель)
{
try
{
// Быстрый способ.
if (оРазобранныйАдрес.чСтраница === ВОЗМОЖНО_ПРЯМАЯ_ТРАНСЛЯЦИЯ)
{
for (var оЗапись of моЗаписи)
{
// Chrome 48: for...of не работает с DOM.
for (var ы = 0, узДобавлено; узДобавлено = оЗапись.addedNodes[ы]; ++ы)
{
// Чтобы перенаправление сработало быстро, искомый элемент должен находится в самом начале head,
// желательно до stylesheet и синхронных script.
if (узДобавлено.nodeName === 'LINK'
&& узДобавлено.hasAttribute('href')
&& узДобавлено.getAttribute('href').startsWith('android-app://'))
{
оНаблюдатель.disconnect();
ПеренаправитьНаНашПроигрыватель(оРазобранныйАдрес);
return;
}
// Один из script за каким-то хером вставляет и тут же удаляет body. Это происходит после
// вставки искомого элемента и поэтому нам не мешает.
// После разбора head отключаем разбор записей, чтобы зря не гонять сотни циклов.
if (узДобавлено.nodeName === 'BODY')
{
оРазобранныйАдрес.чСтраница = 0;
return;
}
}
}
}
// Медленный способ. Искомый элемент не встречается в записях. Добавляется в массиве с моЗаписи.length ≈ 70.
if (моЗаписи.length > 50
&& document.getElementById('player')
&& (оРазобранныйАдрес = РазобратьАдрес(location)).чСтраница === ВОЗМОЖНО_ПРЯМАЯ_ТРАНСЛЯЦИЯ)
{
оНаблюдатель.disconnect();
ПеренаправитьНаНашПроигрыватель(оРазобранныйАдрес);
}
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
}))
.observe(document.documentElement, {childList: true, subtree: true});
}
function Запустить()
{
var оРазобранныйАдрес = РазобратьАдрес(location);
if (оРазобранныйАдрес.чСтраница === НАШ_ПРОИГРЫВАТЕЛЬ)
{
_сКодКанала = оРазобранныйАдрес.сКодКанала;
ЗапуститьПроигрыватель();
return;
}
if (оРазобранныйАдрес.чСтраница === ПРЯМАЯ_ТРАНСЛЯЦИЯ_HLS)
{
ПеренаправитьНаНашПроигрыватель(оРазобранныйАдрес);
return;
}
НастроитьПеренаправление(оРазобранныйАдрес);
document.addEventListener('click', ОбработатьЩелчок, true);
ДобавитьСтиль(`
.gm-tw5-просмотрено .title
{
color: #8C8C8C !important;
}
.gm-tw5-просмотр .title
{
color: hsl(18, 100%, 50%) !important;
}
`);
}
return {
Запустить,
ПолучитьАбсолютныйАдресСпискаВариантов,
ПолучитьАдресУбогогоПроигрывателя,
ПолучитьАдресПанелиЧата,
ПолучитьАдресОкнаЧата,
НачатьСборМетаданныхТрансляции,
ЗавершитьСборМетаданныхТрансляции
};
})();
//
// Запускалка.
//
function ЗапуститьПроигрыватель()
{
// Меняет document.readyState с loading на interactive или complete.
window.stop();
window.addEventListener('beforeunload', ОбработатьBeforeUnload);
Promise.all([г_оНастройки.Восстановить(), г_оПреобразователь.Запустить()])
.then(function()
{
г_оУправление.Запустить();
г_оПроигрыватель.Запустить();
г_оСписок.Запустить();
})
.catch(г_оОтладка.ПойманоИсключение);
}
function ОбработатьBeforeUnload(оСобытие)
{
try
{
console.log('beforeunload');
м_Чат.ЗакрытьОкно();
// Это строка должна быть последней в try.
г_лРаботаЗавершена = true;
}
catch (пИсключение)
{
// Предлагать отправку отчета только один раз.
if (!г_лРаботаЗавершена)
{
оСобытие.returnValue = 'Во время работы дополнения Twitch 5 произошла ошибка!';
г_оОтладка.ПойманоИсключение(пИсключение);
}
}
}
try
{
г_оСайт.Запустить();
}
catch (пИсключение)
{
г_оОтладка.ПойманоИсключение(пИсключение);
}
@sseletskyy
Copy link

I ne len' zhe bylo pisat'?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment