Проблемы с отлавливанием создания элементов, делаю расширение для ютуба
Я пытаюсь создать для себя расширение для ютуба. Его суть заключается в следующем:
- При первой загрузке видео мы устанавливаем скорость 1.5 для видео. (Я смотрю 99% видео именно в этой скорости).
- При первой загрузке видео, а так же при измении src (то есть.. если включили другой ролик) мы ставим ролик на паузу, пока не прогрузится достаточное кол-во контента (минимум 15 секунд).
- Снимаем ролик с паузы, если паузу поставил скрипт из-за недостаточного кол-ва загруженного контента, а так же мы находимся на данной странице (то есть.. чтобы видео не начало проигрывать, если мы открыли несколько вкладок).
- Добавляем показатель временной шкалы с учетом скорости.
- Добавляем
input[type="range"]возле временной шкалы, для быстрого изменения скорости. - Добавляем показатель значения скорости, а так же прошлого элемента управления (input).
В чем собственно проблема:
- Иногда при загрузке страницы, не добавляется временная шкала, а так же input. (Чаще это происходит, если вкладка не была активна.)
- Иногда при загрузке страницы добавляется временная шкала, а так же input, НО, они не реагируют не на что.
- Иногда не ставится пауза.
- Иногда не снимается пауза, когда достаточное кол-во контента загружено.
- Иногда снимается пауза, когда достаточное кол-во контента загружено, но сразу ставится опять на паузу и дальше не снимается пауза. (Возможно это происходит, когда пауза снимается, контента загружено ровно на 15 секунд и опять срабыватывает авто пауза и при этом же срабатывает догрузка контента, но это как-то остается незамеченным.)
Вероятнее всего я что-то упустил в описании.
Прикладываю код:
main.js (Оставил некоторые комментарии)
const skipNodeName = ['#text', '#comment'];
let playbackSpeed = 1.5;
let speedValue;
let slider;
let video;
let timeDurationWithSpeed;
let autoPause = 0;
const observer = new MutationObserver((mutations) =>
{
mutations.forEach(mutation =>
{
if (mutation.type == 'childList')
{
for (let node of mutation.addedNodes)
{
if (!(node instanceof HTMLElement))
{
continue;
}
if (node.classList.contains('html5-main-video'))
{
// Если созданный элемент имеет класс html5-main-video, значит это ролик на ютубе
video = node;
if (!document.hidden)
{
autoPause = true;
}
// Ставим ролик на паузу
video.pause();
// Слушаем события
video.addEventListener('timeupdate', onChangeTime); // следим за изменением времени
video.addEventListener('ratechange', onChangeSpeed); // следим за изменением скорости
video.addEventListener('play', onChangeStatusPlaying); // следим за состоянием проигрывания
video.addEventListener('progress', onProgress); // следим за состоянием буфферизации
// Устанавливаем стандартную скорость для ролика
video.playbackRate = playbackSpeed;
}
else if (node.classList.contains('ytp-time-duration'))
{
// Добавляем после временной шкалы ютуба следующие элементы:
// Слайдер, им мы изменяем скорость
slider = document.createElement('input');
slider.type = 'range';
slider.min = 0.25;
slider.max = 2.5;
slider.step = 0.05;
slider.value = playbackSpeed;
slider.className = 'sliderSpeed';
node.after(slider);
// Показатель текущей значения слайдера/скорости
speedValue = document.createElement('span');
speedValue.innerText = `x${playbackSpeed}`;
speedValue.className = 'sliderSpeed__value';
node.after(speedValue);
// Показываем скорости прошло времени/сколько длится видео со скоростью
timeDurationWithSpeed = document.createElement('span');
timeDurationWithSpeed.innerText = `(${secondToTime(video.currentTime / playbackSpeed)} / ${secondToTime(video.duration / playbackSpeed)})`;
timeDurationWithSpeed.className = 'timeDurationWithSpeed';
node.after(timeDurationWithSpeed);
// Слушаем прокрутку колесиком мышки над слайдером, дабы более точно выставлять значение
slider.addEventListener('wheel', (e) =>
{
const value = parseFloat(slider.value);
const step = parseFloat(slider.step);
const sign = Math.sign(e.deltaY);
slider.value = value - sign * step;
slider.dispatchEvent(new Event('input', { bubbles: true }));
e.preventDefault();
});
// Следим за изменением значения у слайдера и устанавливаем его value как скорость видео
slider.addEventListener('input', (e) =>
{
video.playbackRate = parseFloat(e.target.value);
});
// Добавляем стили
insertStyle();
}
}
}
else if (mutation.type == 'attributes')
{
if ((mutation.target instanceof HTMLElement) && mutation.target.classList.contains('html5-main-video'))
{
// Если изменился атрибут src у тега video, тогда ставим видео на паузу
// let urlParams = new URLSearchParams(window.location.search.substr(1));
// if (urlParams.has('list') || urlParams.has('index'))
// {
// autoPause = true;
// }
if (!document.hidden)
{
autoPause = true;
}
video.pause();
}
}
});
});
// Запускаем обсервер
observer.observe(document, { subtree: true, childList: true, attributeFilter: ['src']});
// В функциях onProgress и onChangeStatusPlaying, мы проверяем, на сколько прогружено видео
// Если прогруженный кусочек меньше 15 секунд, то функция isNotEnoughBuffered вернет true
// Если же прогружено больше 15 секунд - мы снимаем с паузы, при условии, что до этого скрипт ставил на паузу
function onProgress()
{
if (isNotEnoughBuffered())
{
if (!video.paused)
{
autoPause = true;
video.pause();
}
}
else if (autoPause)
{
autoPause = false;
if (!document.hidden)
{
video.play();
}
}
}
function onChangeStatusPlaying()
{
if (isNotEnoughBuffered())
{
if (!video.paused)
{
autoPause = true;
video.pause();
}
}
}
// При изменении скорости видео мы меняем значение у value слайдера (если изменение не через него)
// А так же меняем значение временной шкалы со скоростью
function onChangeSpeed()
{
playbackSpeed = this.playbackRate;
if (!speedValue || !slider || !timeDurationWithSpeed)
{
return;
}
if (slider.value != playbackSpeed)
{
slider.value = playbackSpeed;
}
speedValue.innerText = `x${playbackSpeed.toFixed(2)}`;
timeDurationWithSpeed.innerText = `(${secondToTime(video.currentTime / playbackSpeed)} / ${secondToTime(video.duration / playbackSpeed)})`
}
// При изменении текущего времени видео меняем значение временной шкалы со скоростью
function onChangeTime()
{
if (isNotEnoughBuffered())
{
video.pause();
}
if (!timeDurationWithSpeed)
{
return;
}
timeDurationWithSpeed.innerText = `(${secondToTime(video.currentTime / playbackSpeed)} / ${secondToTime(video.duration / playbackSpeed)})`
}
/**
* Приводим секунды к формату чч:мм:сс
* @param {number} second Кол-во секунд
* @returns {string} чч:мм:сс
*/
function secondToTime(second)
{
const hour = second / 3600 ^ 0;
const min = (second - hour * 3600) / 60 ^ 0;
const sec = second - hour * 3600 - min * 60 ^ 0;
return (hour ? (hour < 10 ? '0' + hour : hour) +':' : '') +(min < 10 ? '0' +min : min) +':' +(sec < 10 ? '0' +sec : sec);
}
/**
* Возвращает текущий buffered индекс
* @returns {number}
*/
function getThisBufferedIndex()
{
let buffered = video.buffered;
let currentTime = video.currentTime;
let index = buffered.length - 1;
for (index; index >= 0; index--)
{
if (buffered.start(index) <= currentTime && buffered.end(index) >= currentTime)
{
return index;
}
}
return 0;
}
/**
* Достаточно ли прогружено видео
* @returns {boolean}
*/
function isNotEnoughBuffered()
{
let bufferedEnd;
let bufferedIndex = getThisBufferedIndex();
try
{
bufferedEnd = video.buffered.end(bufferedIndex);
}
catch(e)
{
return 1;
}
let timeToEnd = bufferedEnd - video.currentTime;
return bufferedEnd < video.duration && timeToEnd < 15;
}
function insertStyle()
{
let style = document.createElement('style');
style.innerText =
`
.sliderSpeed
{
margin-left: 5px;
-webkit-appearance: none;
outline: none;
background: transparent;
}
.sliderSpeed::-webkit-slider-runnable-track
{
width: 100%;
height: 10px;
cursor: pointer;
background: #181818;
border-radius: 5px;
border: 1px solid #DC4242;
}
.sliderSpeed::-webkit-slider-thumb
{
-webkit-appearance: none;
height: 15px;
width: 15px;
cursor: pointer;
border-radius: 50px;
background: #DC4242;
border: 1px solid #181818;
margin-top: -3px;
}
.sliderSpeed__value
{
margin-left: 10px;
color: #DC4242;
}
.timeDurationWithSpeed
{
margin-left: 5px;
}
`;
document.head.appendChild(style);
}
manifest.json
{
"manifest_version": 2,
"name": "YouTube Helper",
"description": "Desc",
"version": "1.0",
"browser_action":
{
"default_icon": "icon.png"
},
"content_scripts":
[
{
"matches":["*://*/*"],
"run_at": "document_start",
"js":
[
"main.js"
]
}
],
"permissions":
[
"activeTab",
"tabs"
]
}