Add video quality selector
Signed-off-by: Jesús <heckyel@hyperbola.info>
This commit is contained in:
parent
c9a75042d2
commit
a7da23c6da
@ -21,48 +21,67 @@
|
|||||||
// TODO: Call abort to cancel in-progress appends?
|
// TODO: Call abort to cancel in-progress appends?
|
||||||
|
|
||||||
|
|
||||||
var video_source = data['pair_sources'][data['pair_idx']][0];
|
var avMerge;
|
||||||
var audio_source = data['pair_sources'][data['pair_idx']][1];
|
|
||||||
|
|
||||||
var audioStream = null;
|
function avInitialize(...args){
|
||||||
var videoStream = null;
|
avMerge = new AVMerge(...args);
|
||||||
var seeking = false;
|
}
|
||||||
|
|
||||||
var video = document.querySelector('video');
|
function AVMerge(video, srcPair, startTime){
|
||||||
var mediaSource = null;
|
this.videoSource = srcPair[0];
|
||||||
|
this.audioSource = srcPair[1];
|
||||||
setup();
|
this.videoStream = null;
|
||||||
|
this.audioStream = null;
|
||||||
|
this.seeking = false;
|
||||||
function setup() {
|
this.startTime = startTime;
|
||||||
|
this.video = video;
|
||||||
|
this.mediaSource = null;
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
AVMerge.prototype.setup = function() {
|
||||||
if ('MediaSource' in window
|
if ('MediaSource' in window
|
||||||
&& MediaSource.isTypeSupported(audio_source['mime_codec'])
|
&& MediaSource.isTypeSupported(this.audioSource['mime_codec'])
|
||||||
&& MediaSource.isTypeSupported(video_source['mime_codec'])) {
|
&& MediaSource.isTypeSupported(this.videoSource['mime_codec'])) {
|
||||||
mediaSource = new MediaSource();
|
this.mediaSource = new MediaSource();
|
||||||
video.src = URL.createObjectURL(mediaSource);
|
this.video.src = URL.createObjectURL(this.mediaSource);
|
||||||
mediaSource.addEventListener('sourceopen', sourceOpen);
|
this.mediaSource.onsourceopen = this.sourceOpen.bind(this);
|
||||||
} else {
|
} else {
|
||||||
reportError('Unsupported MIME type or codec: ',
|
reportError('Unsupported MIME type or codec: ',
|
||||||
audio_source['mime_codec'],
|
this.audioSource['mime_codec'],
|
||||||
video_source['mime_codec']);
|
this.videoSource['mime_codec']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AVMerge.prototype.sourceOpen = function(_) {
|
||||||
|
this.videoStream = new Stream(this, this.videoSource, this.startTime);
|
||||||
|
this.audioStream = new Stream(this, this.audioSource, this.startTime);
|
||||||
|
|
||||||
function sourceOpen(_) {
|
this.videoStream.setup();
|
||||||
videoStream = new Stream(mediaSource, video_source);
|
this.audioStream.setup();
|
||||||
audioStream = new Stream(mediaSource, audio_source);
|
|
||||||
|
|
||||||
videoStream.setup();
|
this.video.ontimeupdate = this.checkBothBuffers.bind(this);
|
||||||
audioStream.setup();
|
this.video.onseeking = debounce(this.seek.bind(this), 500);
|
||||||
|
//this.video.onseeked = function() {console.log('seeked')};
|
||||||
video.addEventListener('timeupdate', checkBothBuffers);
|
}
|
||||||
video.addEventListener('seeking', debounce(seek, 500));
|
AVMerge.prototype.checkBothBuffers = function() {
|
||||||
//video.addEventListener('seeked', function() {console.log('seeked')});
|
this.audioStream.checkBuffer();
|
||||||
|
this.videoStream.checkBuffer();
|
||||||
|
}
|
||||||
|
AVMerge.prototype.seek = function(e) {
|
||||||
|
if (this.mediaSource.readyState === 'open') {
|
||||||
|
this.seeking = true;
|
||||||
|
this.audioStream.handleSeek();
|
||||||
|
this.videoStream.handleSeek();
|
||||||
|
this.seeking = false;
|
||||||
|
} else {
|
||||||
|
this.reportWarning('seek but not open? readyState:',
|
||||||
|
this.mediaSource.readyState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Stream(avMerge, source, startTime) {
|
||||||
function Stream(mediaSource, source) {
|
this.avMerge = avMerge;
|
||||||
|
this.video = avMerge.video;
|
||||||
this.url = source['url'];
|
this.url = source['url'];
|
||||||
this.mimeCodec = source['mime_codec']
|
this.mimeCodec = source['mime_codec']
|
||||||
this.streamType = source['acodec'] ? 'audio' : 'video';
|
this.streamType = source['acodec'] ? 'audio' : 'video';
|
||||||
@ -70,11 +89,12 @@ function Stream(mediaSource, source) {
|
|||||||
this.initRange = source['init_range'];
|
this.initRange = source['init_range'];
|
||||||
this.indexRange = source['index_range'];
|
this.indexRange = source['index_range'];
|
||||||
|
|
||||||
this.mediaSource = mediaSource;
|
this.startTime = startTime;
|
||||||
|
this.mediaSource = avMerge.mediaSource;
|
||||||
this.sidx = null;
|
this.sidx = null;
|
||||||
this.appendRetries = 0;
|
this.appendRetries = 0;
|
||||||
this.appendQueue = []; // list of [segmentIdx, data]
|
this.appendQueue = []; // list of [segmentIdx, data]
|
||||||
this.sourceBuffer = mediaSource.addSourceBuffer(this.mimeCodec);
|
this.sourceBuffer = this.mediaSource.addSourceBuffer(this.mimeCodec);
|
||||||
this.sourceBuffer.mode = 'segments';
|
this.sourceBuffer.mode = 'segments';
|
||||||
this.sourceBuffer.addEventListener('error', (e) => {
|
this.sourceBuffer.addEventListener('error', (e) => {
|
||||||
this.reportError('sourceBuffer error', e);
|
this.reportError('sourceBuffer error', e);
|
||||||
@ -124,7 +144,7 @@ Stream.prototype.setupSegments = async function(sidxBox){
|
|||||||
this.reportDebug('sidx', this.sidx);
|
this.reportDebug('sidx', this.sidx);
|
||||||
|
|
||||||
this.reportDebug('appending first segment');
|
this.reportDebug('appending first segment');
|
||||||
this.fetchSegmentIfNeeded(0);
|
this.fetchSegmentIfNeeded(this.getSegmentIdx(this.startTime));
|
||||||
}
|
}
|
||||||
Stream.prototype.appendSegment = function(segmentIdx, chunk) {
|
Stream.prototype.appendSegment = function(segmentIdx, chunk) {
|
||||||
// cannot append right now, schedule for updateend
|
// cannot append right now, schedule for updateend
|
||||||
@ -147,7 +167,7 @@ Stream.prototype.appendSegment = function(segmentIdx, chunk) {
|
|||||||
}
|
}
|
||||||
// Delete 3 segments (arbitrary) from beginning of buffer, making sure
|
// Delete 3 segments (arbitrary) from beginning of buffer, making sure
|
||||||
// not to delete current one
|
// not to delete current one
|
||||||
var currentSegment = this.getSegmentIdx(video.currentTime);
|
var currentSegment = this.getSegmentIdx(this.video.currentTime);
|
||||||
this.reportDebug('QuotaExceededError. Deleting segments.');
|
this.reportDebug('QuotaExceededError. Deleting segments.');
|
||||||
var numDeleted = 0;
|
var numDeleted = 0;
|
||||||
var i = 0;
|
var i = 0;
|
||||||
@ -191,15 +211,15 @@ Stream.prototype.shouldFetchNextSegment = function(nextSegment) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var entry = this.sidx.entries[nextSegment - 1];
|
var entry = this.sidx.entries[nextSegment - 1];
|
||||||
var currentTick = video.currentTime * this.sidx.timeScale;
|
var currentTick = this.video.currentTime * this.sidx.timeScale;
|
||||||
return currentTick > (entry.tickStart + entry.subSegmentDuration*0.15);
|
return currentTick > (entry.tickStart + entry.subSegmentDuration*0.15);
|
||||||
}
|
}
|
||||||
Stream.prototype.checkBuffer = async function() {
|
Stream.prototype.checkBuffer = async function() {
|
||||||
this.reportDebug('check Buffer');
|
this.reportDebug('check Buffer');
|
||||||
if (seeking) {
|
if (this.avMerge.seeking) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var nextSegment = this.getSegmentIdx(video.currentTime) + 1;
|
var nextSegment = this.getSegmentIdx(this.video.currentTime) + 1;
|
||||||
|
|
||||||
if (this.shouldFetchNextSegment(nextSegment)) {
|
if (this.shouldFetchNextSegment(nextSegment)) {
|
||||||
this.fetchSegmentIfNeeded(nextSegment);
|
this.fetchSegmentIfNeeded(nextSegment);
|
||||||
@ -242,7 +262,7 @@ Stream.prototype.fetchSegmentIfNeeded = function(segmentIdx) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Stream.prototype.handleSeek = async function() {
|
Stream.prototype.handleSeek = async function() {
|
||||||
var segmentIdx = this.getSegmentIdx(video.currentTime);
|
var segmentIdx = this.getSegmentIdx(this.video.currentTime);
|
||||||
this.fetchSegmentIfNeeded(segmentIdx);
|
this.fetchSegmentIfNeeded(segmentIdx);
|
||||||
}
|
}
|
||||||
Stream.prototype.reportDebug = function(...args) {
|
Stream.prototype.reportDebug = function(...args) {
|
||||||
@ -255,23 +275,6 @@ Stream.prototype.reportError = function(...args) {
|
|||||||
reportError(String(this.streamType) + ':', ...args);
|
reportError(String(this.streamType) + ':', ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkBothBuffers() {
|
|
||||||
audioStream.checkBuffer();
|
|
||||||
videoStream.checkBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function seek(e) {
|
|
||||||
if (mediaSource.readyState === 'open') {
|
|
||||||
seeking = true;
|
|
||||||
audioStream.handleSeek();
|
|
||||||
videoStream.handleSeek();
|
|
||||||
seeking = false;
|
|
||||||
} else {
|
|
||||||
this.reportWarning('seek but not open? readyState:',
|
|
||||||
mediaSource.readyState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
function fetchRange(url, start, end, cb) {
|
function fetchRange(url, start, end, cb) {
|
||||||
|
@ -55,8 +55,17 @@
|
|||||||
</video>
|
</video>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
{% if pair_sources and (not uni_sources or pair_sources[pair_idx][0]['quality'] != uni_sources[uni_idx]['quality']) %}
|
<script src="/youtube.com/static/js/av-merge.js"></script>
|
||||||
<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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if time_start != 0 %}
|
{% if time_start != 0 %}
|
||||||
@ -93,8 +102,41 @@
|
|||||||
<div class="external-player-controls">
|
<div class="external-player-controls">
|
||||||
<input class="speed" id="speed-control" type="text" title="Video speed">
|
<input class="speed" id="speed-control" type="text" title="Video speed">
|
||||||
<script src="/youtube.com/static/js/speedyplay.js"></script>
|
<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 (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>
|
</div>
|
||||||
|
|
||||||
<input class="v-checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
|
<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>
|
<span class="v-direct-link"><a href="https://youtu.be/{{ video_id }}" rel="noopener noreferrer" target="_blank">Direct Link</a></span>
|
||||||
|
@ -46,6 +46,7 @@ def get_video_sources(info):
|
|||||||
if fmt['acodec'] and fmt['vcodec']:
|
if fmt['acodec'] and fmt['vcodec']:
|
||||||
source = {
|
source = {
|
||||||
'type': 'video/' + fmt['ext'],
|
'type': 'video/' + fmt['ext'],
|
||||||
|
'quality_string': short_video_quality_string(fmt),
|
||||||
}
|
}
|
||||||
source.update(fmt)
|
source.update(fmt)
|
||||||
uni_sources.append(source)
|
uni_sources.append(source)
|
||||||
@ -59,6 +60,7 @@ def get_video_sources(info):
|
|||||||
source = {
|
source = {
|
||||||
'type': 'audio/' + fmt['ext'],
|
'type': 'audio/' + fmt['ext'],
|
||||||
'bitrate': fmt['audio_bitrate'],
|
'bitrate': fmt['audio_bitrate'],
|
||||||
|
'quality_string': audio_quality_string(fmt),
|
||||||
}
|
}
|
||||||
source.update(fmt)
|
source.update(fmt)
|
||||||
source['mime_codec'] = (source['type'] + '; codecs="'
|
source['mime_codec'] = (source['type'] + '; codecs="'
|
||||||
@ -68,6 +70,7 @@ def get_video_sources(info):
|
|||||||
elif all(fmt[attr] for attr in ('vcodec', 'quality', 'width')):
|
elif all(fmt[attr] for attr in ('vcodec', 'quality', 'width')):
|
||||||
source = {
|
source = {
|
||||||
'type': 'video/' + fmt['ext'],
|
'type': 'video/' + fmt['ext'],
|
||||||
|
'quality_string': short_video_quality_string(fmt),
|
||||||
}
|
}
|
||||||
source.update(fmt)
|
source.update(fmt)
|
||||||
source['mime_codec'] = (source['type'] + '; codecs="'
|
source['mime_codec'] = (source['type'] + '; codecs="'
|
||||||
@ -415,15 +418,24 @@ def video_quality_string(format):
|
|||||||
return '?'
|
return '?'
|
||||||
|
|
||||||
|
|
||||||
def audio_quality_string(format):
|
def short_video_quality_string(fmt):
|
||||||
if format['acodec']:
|
result = str(fmt['quality'] or '?') + 'p'
|
||||||
result = str(format['audio_bitrate'] or '?') + 'k'
|
if fmt['fps']:
|
||||||
if format['audio_sample_rate']:
|
result += ' ' + str(fmt['fps']) + 'fps'
|
||||||
result += ' ' + str(format['audio_sample_rate']) + ' Hz'
|
return result
|
||||||
return result
|
|
||||||
elif format['vcodec']:
|
|
||||||
return 'video only'
|
|
||||||
|
|
||||||
|
|
||||||
|
def audio_quality_string(fmt):
|
||||||
|
if fmt['acodec']:
|
||||||
|
if fmt['audio_bitrate']:
|
||||||
|
result = '%d' % fmt['audio_bitrate'] + 'k'
|
||||||
|
else:
|
||||||
|
result = '?k'
|
||||||
|
if fmt['audio_sample_rate']:
|
||||||
|
result += ' ' + '%.3G' % (fmt['audio_sample_rate']/1000) + 'kHz'
|
||||||
|
return result
|
||||||
|
elif fmt['vcodec']:
|
||||||
|
return 'video only'
|
||||||
return '?'
|
return '?'
|
||||||
|
|
||||||
|
|
||||||
@ -551,13 +563,23 @@ def get_watch_page(video_id=None):
|
|||||||
})
|
})
|
||||||
|
|
||||||
source_info = get_video_sources(info)
|
source_info = get_video_sources(info)
|
||||||
uni_idx = source_info['uni_idx']
|
uni_sources = source_info['uni_sources']
|
||||||
|
pair_sources = source_info['pair_sources']
|
||||||
|
uni_idx, pair_idx = source_info['uni_idx'], source_info['pair_idx']
|
||||||
video_height = yt_data_extract.deep_get(source_info, 'uni_sources',
|
video_height = yt_data_extract.deep_get(source_info, 'uni_sources',
|
||||||
uni_idx, 'height',
|
uni_idx, 'height',
|
||||||
default=360)
|
default=360)
|
||||||
video_width = yt_data_extract.deep_get(source_info, 'uni_sources',
|
video_width = yt_data_extract.deep_get(source_info, 'uni_sources',
|
||||||
uni_idx, 'width',
|
uni_idx, 'width',
|
||||||
default=640)
|
default=640)
|
||||||
|
|
||||||
|
pair_quality = yt_data_extract.deep_get(pair_sources, pair_idx, 0,
|
||||||
|
'quality')
|
||||||
|
uni_quality = yt_data_extract.deep_get(uni_sources, uni_idx, 'quality')
|
||||||
|
using_pair_sources = (
|
||||||
|
pair_sources and (not uni_sources or pair_quality != uni_quality)
|
||||||
|
)
|
||||||
|
|
||||||
# 1 second per pixel, or the actual video width
|
# 1 second per pixel, or the actual video width
|
||||||
theater_video_target_width = max(640, info['duration'] or 0, video_width)
|
theater_video_target_width = max(640, info['duration'] or 0, video_width)
|
||||||
|
|
||||||
@ -642,6 +664,7 @@ def get_watch_page(video_id=None):
|
|||||||
},
|
},
|
||||||
font_family=youtube.font_choices[settings.font],
|
font_family=youtube.font_choices[settings.font],
|
||||||
**source_info,
|
**source_info,
|
||||||
|
using_pair_sources = using_pair_sources,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user