
If a fetchRange network request finished after the quality was changed, there would be a "InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable" because appendSegment was trying to append to the sourceBuffer that was unusable after the video src was changed to a new mediaSource. Adds a close method to the AVMerge class to properly clean and close everything so these sorts of errors won't happen. Signed-off-by: Jesús <heckyel@hyperbola.info>
468 lines
22 KiB
HTML
468 lines
22 KiB
HTML
{% set page_title = title %}
|
|
{% extends "base.html" %}
|
|
{% import "common_elements.html" as common_elements %}
|
|
{% import "comments.html" as comments with context %}
|
|
{% block style %}
|
|
<link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
|
|
<link href="/youtube.com/static/watch.css" rel="stylesheet"/>
|
|
{% if settings.use_video_player == 2 %}
|
|
<!-- plyr -->
|
|
<link href="/youtube.com/static/modules/plyr/plyr.css" rel="stylesheet"/>
|
|
<!--/ plyr -->
|
|
<style>
|
|
/* Prevent this div from blocking right-click menu for video
|
|
e.g. Firefox playback speed options */
|
|
.plyr__poster {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
{% endif %}
|
|
{% endblock style %}
|
|
|
|
{% block main %}
|
|
{% if playability_error %}
|
|
<div class="playability-error">
|
|
<span>{{ 'Error: ' + playability_error }}
|
|
{% if invidious_reload_button %}
|
|
<a href="{{ video_url }}&use_invidious=0"><br>
|
|
Reload without invidious (for usage of new identity button).</a>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
{% elif (uni_sources.__len__() == 0 or live) and hls_formats.__len__() != 0 %}
|
|
<div class="live-url-choices">
|
|
<span>Copy a url into your video player:</span>
|
|
<ol>
|
|
{% for fmt in hls_formats %}
|
|
<li class="url-choice"><div class="url-choice-label">{{ fmt['video_quality'] }}: </div><input class="url-choice-copy" value="{{ fmt['url'] }}" readonly onclick="this.select();"></li>
|
|
{% endfor %}
|
|
</ol>
|
|
</div>
|
|
{% else %}
|
|
<figure class="sc-video">
|
|
<video id="js-video-player" playsinline controls>
|
|
{% if uni_sources %}
|
|
<source src="{{ uni_sources[uni_idx]['url'] }}" type="{{ uni_sources[uni_idx]['type'] }}" data-res="{{ uni_sources[uni_idx]['quality'] }}">
|
|
{% endif %}
|
|
|
|
{% for source in subtitle_sources %}
|
|
{% if source['on'] %}
|
|
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
|
|
{% else %}
|
|
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
|
|
{% endif %}
|
|
{% endfor %}
|
|
</video>
|
|
</figure>
|
|
|
|
<script src="/youtube.com/static/js/av-merge.js"></script>
|
|
{% if using_pair_sources %}
|
|
<!-- Initialize av-merge -->
|
|
<script>
|
|
var srcPair = data['pair_sources'][data['pair_idx']];
|
|
var video = document.getElementById('js-video-player');
|
|
var videoSource = srcPair[0];
|
|
// Do it dynamically rather than as the default in jinja
|
|
// in case javascript is disabled
|
|
avInitialize(video, srcPair, 0);
|
|
</script>
|
|
{% endif %}
|
|
|
|
{% if time_start != 0 %}
|
|
<script>
|
|
document.getElementById('js-video-player').currentTime = {{ time_start|tojson }};
|
|
</script>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<div class="sc-info">
|
|
<div class="video-info">
|
|
<h1 class="v-title">{{ title }}</h1>
|
|
|
|
<ul class="labels">
|
|
{%- if unlisted -%}
|
|
<li class="is-unlisted">Unlisted</li>
|
|
{%- endif -%}
|
|
{%- if age_restricted -%}
|
|
<li class="age-restricted">Age-restricted</li>
|
|
{%- endif -%}
|
|
{%- if limited_state -%}
|
|
<li>Limited state</li>
|
|
{%- endif -%}
|
|
{%- if live -%}
|
|
<li>Live</li>
|
|
{%- endif -%}
|
|
</ul>
|
|
|
|
<address class="v-uploaded">Uploaded by <a href="{{ uploader_channel_url }}">{{ uploader }}</a></address>
|
|
<span class="v-views">{{ view_count }} views</span>
|
|
<time class="v-published" datetime="{{ time_published_utc }}">Published on {{ time_published }}</time>
|
|
<span class="v-likes-dislikes">{{ like_count }} likes {{ dislike_count }} dislikes</span>
|
|
|
|
<div class="external-player-controls">
|
|
<input class="speed" id="speed-control" type="text" title="Video speed">
|
|
<script src="/youtube.com/static/js/speedyplay.js"></script>
|
|
<select id="quality-select">
|
|
{% for src in uni_sources %}
|
|
<option value='{"type": "uni", "index": {{ loop.index0 }}}' {{ 'selected' if loop.index0 == uni_idx and not using_pair_sources else '' }} >{{ src['quality_string'] }} (integrated)</option>
|
|
{% endfor %}
|
|
{% for src_pair in pair_sources %}
|
|
<option value='{"type": "pair", "index": {{ loop.index0}}}' {{ 'selected' if loop.index0 == pair_idx and using_pair_sources else '' }} >{{ src_pair[0]['quality_string'] }}, {{ src_pair[1]['quality_string'] }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<script>
|
|
document.getElementById('quality-select').addEventListener(
|
|
'change', function(e) {
|
|
var video = document.getElementById('js-video-player');
|
|
var selection = JSON.parse(this.value);
|
|
var currentVideoTime = video.currentTime;
|
|
var videoPaused = video.paused;
|
|
var videoSpeed = video.playbackRate;
|
|
var videoSource;
|
|
if (avMerge)
|
|
avMerge.close();
|
|
if (selection.type == 'uni'){
|
|
videoSource = data['uni_sources'][selection.index];
|
|
video.src = videoSource.url;
|
|
} else {
|
|
let srcPair = data['pair_sources'][selection.index];
|
|
videoSource = srcPair[0];
|
|
avInitialize(video, srcPair, currentVideoTime);
|
|
}
|
|
setVideoDimensions(videoSource.height, videoSource.width);
|
|
video.currentTime = currentVideoTime;
|
|
if (!videoPaused){
|
|
video.play();
|
|
}
|
|
video.playbackRate = videoSpeed;
|
|
}
|
|
);
|
|
</script>
|
|
</div>
|
|
<input class="v-checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
|
|
|
|
<span class="v-direct-link"><a href="https://youtu.be/{{ video_id }}" rel="noopener noreferrer" target="_blank">Direct Link</a></span>
|
|
|
|
<details class="v-download">
|
|
<summary class="download-dropdown-label">Download</summary>
|
|
<ul class="download-dropdown-content">
|
|
{% for format in download_formats %}
|
|
<li class="download-format">
|
|
<a class="download-link" href="{{ format['url'] }}" download="{{ title }}.{{ format['ext'] }}">
|
|
{{ format['ext'] }} {{ format['video_quality'] }} {{ format['audio_quality'] }} {{ format['file_size'] }} {{ format['codecs'] }}
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
{% for download in other_downloads %}
|
|
<li class="download-format">
|
|
<a href="{{ download['url'] }}" download>
|
|
{{ download['ext'] }} {{ download['label'] }}
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
<span class="v-description">{{ common_elements.text_runs(description)|escape|urlize|timestamps|safe }}</span>
|
|
|
|
<div class="v-music-list">
|
|
{% if music_list.__len__() != 0 %}
|
|
<hr>
|
|
<table>
|
|
<caption>Music</caption>
|
|
<tr>
|
|
{% for attribute in music_attributes %}
|
|
<th>{{ attribute }}</th>
|
|
{% endfor %}
|
|
</tr>
|
|
{% for track in music_list %}
|
|
<tr>
|
|
{% for attribute in music_attributes %}
|
|
<td>{{ track.get(attribute.lower(), '') }}</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
{% endif %}
|
|
</div>
|
|
<details class="v-more-info">
|
|
<summary>More info</summary>
|
|
<div class="more-info-content">
|
|
<p>Tor exit node: {{ ip_address }}</p>
|
|
{% if invidious_used %}
|
|
<p>Used Invidious as fallback.</p>
|
|
{% endif %}
|
|
<p class="allowed-countries">Allowed countries: {{ allowed_countries|join(', ') }}</p>
|
|
{% if settings.use_sponsorblock_js %}
|
|
<ul class="more-actions">
|
|
<li><label><input type=checkbox id=skip_sponsors checked>skip sponsors</label> <span id=skip_n></span>
|
|
</ul>
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div class="side-videos">
|
|
|
|
<!-- playlist -->
|
|
{% if playlist %}
|
|
<div class="site-playlist">
|
|
<div class="playlist-header">
|
|
<a href="{{ playlist['url'] }}" title="{{ playlist['title'] }}"><h3>{{ playlist['title'] }}</h3></a>
|
|
<ul class="playlist-metadata">
|
|
<li><label for="playlist-autoplay-toggle">Autoplay: </label><input type="checkbox" id="playlist-autoplay-toggle"></li>
|
|
{% if playlist['current_index'] is none %}
|
|
<li>[Error!]/{{ playlist['video_count'] }}</li>
|
|
{% else %}
|
|
<li>{{ playlist['current_index']+1 }}/{{ playlist['video_count'] }}</li>
|
|
{% endif %}
|
|
<li><a href="{{ playlist['author_url'] }}" title="{{ playlist['author'] }}">{{ playlist['author'] }}</a></li>
|
|
</ul>
|
|
</div>
|
|
<nav class="playlist-videos">
|
|
{% for info in playlist['items'] %}
|
|
{# non-lazy load for 5 videos surrounding current video #}
|
|
{# for non-js browsers or old such that IntersectionObserver doesn't work #}
|
|
{# -10 is sentinel to not load anything if there's no current_index for some reason #}
|
|
{% if (playlist.get('current_index', -10) - loop.index0)|abs is lt(5) %}
|
|
{{ common_elements.item(info, include_badges=false, lazy_load=false) }}
|
|
{% else %}
|
|
{{ common_elements.item(info, include_badges=false, lazy_load=true) }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</nav>
|
|
{% if playlist['id'] is not none %}
|
|
<script>
|
|
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
|
|
// lazy load playlist images
|
|
// copied almost verbatim from
|
|
// https://css-tricks.com/tips-for-rolling-your-own-lazy-loading/
|
|
// IntersectionObserver isn't supported in pre-quantum
|
|
// firefox versions, but the alternative of making it
|
|
// manually is a performance drain, so oh well
|
|
let observer = new IntersectionObserver(lazyLoad, {
|
|
|
|
// where in relation to the edge of the viewport, we are observing
|
|
rootMargin: "100px",
|
|
// how much of the element needs to have intersected
|
|
// in order to fire our loading function
|
|
threshold: 1.0
|
|
});
|
|
|
|
function lazyLoad(elements) {
|
|
elements.forEach(item => {
|
|
if (item.intersectionRatio > 0) {
|
|
// set the src attribute to trigger a load
|
|
item.target.src = item.target.dataset.src;
|
|
// stop observing this element. Our work here is done!
|
|
observer.unobserve(item.target);
|
|
};
|
|
});
|
|
};
|
|
|
|
// Tell our observer to observe all img elements with a "lazy" class
|
|
let lazyImages = document.querySelectorAll('img.lazy');
|
|
lazyImages.forEach(img => {
|
|
observer.observe(img);
|
|
});
|
|
// @license-end
|
|
</script>
|
|
{% endif %}
|
|
</div>
|
|
{% elif settings.related_videos_mode != 0 %}
|
|
<div class="related-autoplay"><label for="related-autoplay-toggle">Autoplay: </label><input type="checkbox" id="related-autoplay-toggle"></div>
|
|
{% endif %}
|
|
|
|
{% if settings.related_videos_mode != 0 or playlist %}
|
|
<script>
|
|
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
|
|
let playability_error = {{ 'true' if playability_error else 'false' }};
|
|
{% if playlist and playlist['current_index'] is not none %}
|
|
{% set isPlaylist = true %}
|
|
{% endif %}
|
|
|
|
{% if isPlaylist %}
|
|
// from https://stackoverflow.com/a/6969486
|
|
function escapeRegExp(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
}
|
|
let playlist_id = {{ playlist['id']|tojson }};
|
|
playlist_id = escapeRegExp(playlist_id);
|
|
{% endif %}
|
|
|
|
// read cookies on whether to autoplay
|
|
// pain in the ass:
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
|
|
{% if isPlaylist %}
|
|
let cookieValue = document.cookie.replace(new RegExp(
|
|
'(?:(?:^|.*;\\s*)autoplay_' + playlist_id + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
|
|
{% else %}
|
|
let cookieValue = document.cookie.replace(new RegExp(
|
|
'(?:(?:^|.*;\\s*)autoplay\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
|
|
{% endif %}
|
|
|
|
let autoplayEnabled = 0;
|
|
if(cookieValue.length === 0){
|
|
autoplayEnabled = 0;
|
|
} else {
|
|
autoplayEnabled = Number(cookieValue);
|
|
}
|
|
|
|
// check the autoplay checkbox if autoplay is on
|
|
{% if isPlaylist %}
|
|
let PlaylistAutoplayCheck = document.getElementById('playlist-autoplay-toggle');
|
|
if(autoplayEnabled){
|
|
PlaylistAutoplayCheck.checked = true;
|
|
}
|
|
{% else %}
|
|
let RelatedAutoplayCheck = document.getElementById('related-autoplay-toggle');
|
|
if(autoplayEnabled){
|
|
RelatedAutoplayCheck.checked = true;
|
|
}
|
|
{% endif %}
|
|
|
|
// listen for checkbox to turn autoplay on and off
|
|
let cookie = 'autoplay'
|
|
{% if isPlaylist %}
|
|
cookie += '_' + playlist_id;
|
|
PlaylistAutoplayCheck.addEventListener( 'change', function() {
|
|
if(this.checked) {
|
|
autoplayEnabled = 1;
|
|
document.cookie = cookie + '=1; SameSite=Strict';
|
|
} else {
|
|
autoplayEnabled = 0;
|
|
document.cookie = cookie + '=0; SameSite=Strict';
|
|
}
|
|
});
|
|
{% else %}
|
|
RelatedAutoplayCheck.addEventListener( 'change', function() {
|
|
if(this.checked) {
|
|
autoplayEnabled = 1;
|
|
document.cookie = cookie + '=1; SameSite=Strict';
|
|
} else {
|
|
autoplayEnabled = 0;
|
|
document.cookie = cookie + '=0; SameSite=Strict';
|
|
}
|
|
});
|
|
{% endif %}
|
|
|
|
const vid = document.getElementById('js-video-player');
|
|
if(!playability_error){
|
|
// play the video if autoplay is on
|
|
if(autoplayEnabled){
|
|
vid.play();
|
|
}
|
|
}
|
|
|
|
// determine next video url
|
|
{% if isPlaylist %}
|
|
let currentIndex = {{ playlist['current_index']|tojson }};
|
|
{% if playlist['current_index']+1 == playlist['items']|length %}
|
|
let nextVideoUrl = null;
|
|
{% else %}
|
|
let nextVideoUrl = {{ (playlist['items'][playlist['current_index']+1]['url'])|tojson }};
|
|
{% endif %}
|
|
|
|
// scroll playlist to proper position
|
|
// item height + gap == 100
|
|
let pl = document.querySelector('.playlist-videos');
|
|
pl.scrollTop = 100*currentIndex;
|
|
{% else %}
|
|
{% if related|length == 0 %}
|
|
let nextVideoUrl = null;
|
|
{% else %}
|
|
let nextVideoUrl = {{ (related[0]['url'])|tojson }};
|
|
{% endif %}
|
|
{% endif %}
|
|
let nextVideoDelay = 1000;
|
|
|
|
// go to next video when video ends
|
|
// https://stackoverflow.com/a/2880950
|
|
if (nextVideoUrl) {
|
|
if(playability_error){
|
|
videoEnded();
|
|
} else {
|
|
vid.addEventListener('ended', videoEnded, false);
|
|
}
|
|
function nextVideo(){
|
|
if(autoplayEnabled){
|
|
window.location.href = nextVideoUrl;
|
|
}
|
|
}
|
|
function videoEnded(e) {
|
|
window.setTimeout(nextVideo, nextVideoDelay);
|
|
}
|
|
}
|
|
// @license-end
|
|
</script>
|
|
{% endif %}
|
|
<!-- /playlist -->
|
|
|
|
{% if subtitle_sources %}
|
|
<details id="transcript-details">
|
|
<summary>Transcript</summary>
|
|
<div id="transcript-div">
|
|
<select id="select-tt">
|
|
{% for source in subtitle_sources %}
|
|
<option>{{ source['label'] }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<label for="transcript-use-table">Table view</label>
|
|
<input id="transcript-use-table" type="checkbox">
|
|
<table id="transcript-table"></table>
|
|
</div>
|
|
</details>
|
|
{% endif %}
|
|
|
|
|
|
{% if settings.related_videos_mode != 0 %}
|
|
<details class="related-videos-outer" {{'open' if settings.related_videos_mode == 1 else ''}}>
|
|
<summary>Related Videos</summary>
|
|
<nav class="related-videos-inner">
|
|
{% for info in related %}
|
|
{{ common_elements.item(info, include_badges=false) }}
|
|
{% endfor %}
|
|
</nav>
|
|
</details>
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
<!-- comments -->
|
|
{% if settings.comments_mode != 0 %}
|
|
{% if comments_disabled %}
|
|
<div class="comments-area-outer comments-disabled">Comments disabled</div>
|
|
{% else %}
|
|
<details class="comments-area-outer" {{'open' if settings.comments_mode == 1 else ''}}>
|
|
<summary>{{ comment_count|commatize }} comment{{'s' if comment_count != 1 else ''}}</summary>
|
|
<div class="comments-area-inner comments-area">
|
|
{% if comments_info %}
|
|
{{ comments.video_comments(comments_info) }}
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
|
|
data = {{ js_data|tojson }};
|
|
// @license-end
|
|
</script>
|
|
<script src="/youtube.com/static/js/common.js"></script>
|
|
<script src="/youtube.com/static/js/transcript-table.js"></script>
|
|
{% if settings.use_video_player == 2 %}
|
|
<!-- plyr -->
|
|
<script src="/youtube.com/static/modules/plyr/plyr.min.js"
|
|
integrity="sha512-LxSGuB4I2iAln3VLWi8t3RYhEks4/2rtcCw6kqiBghbqBJHXg5ikpeRxEOm0luiIuKDiqwNI3rsCXI/d+MPPAA=="
|
|
crossorigin="anonymous"></script>
|
|
<script src="/youtube.com/static/js/plyr-start.js"></script>
|
|
<!-- /plyr -->
|
|
{% elif settings.use_video_player == 1 %}
|
|
<script src="/youtube.com/static/js/hotkeys.js"></script>
|
|
{% endif %}
|
|
{% if settings.use_comments_js %} <script src="/youtube.com/static/js/comments.js"></script> {% endif %}
|
|
{% if settings.use_sponsorblock_js %} <script src="/youtube.com/static/js/sponsorblock.js"></script> {% endif %}
|
|
{% endblock main %}
|