aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Taylor <user234683@users.noreply.github.com>2021-08-23 19:50:57 -0700
committerJesús <heckyel@hyperbola.info>2021-08-29 20:52:57 -0500
commita7da23c6da411442dc4ef6af91447820d0b0e517 (patch)
tree811186bcf0bfbc7a175af6efbaaf955413a5f522
parentc9a75042d24ed969e0cf5ae0d7b76ccb3c41a93b (diff)
downloadyt-local-a7da23c6da411442dc4ef6af91447820d0b0e517.tar.lz
yt-local-a7da23c6da411442dc4ef6af91447820d0b0e517.tar.xz
yt-local-a7da23c6da411442dc4ef6af91447820d0b0e517.zip
Add video quality selector
Signed-off-by: Jesús <heckyel@hyperbola.info>
-rw-r--r--youtube/static/js/av-merge.js113
-rw-r--r--youtube/templates/watch.html48
-rw-r--r--youtube/watch.py39
3 files changed, 134 insertions, 66 deletions
diff --git a/youtube/static/js/av-merge.js b/youtube/static/js/av-merge.js
index 389884a..bbc78b5 100644
--- a/youtube/static/js/av-merge.js
+++ b/youtube/static/js/av-merge.js
@@ -21,48 +21,67 @@
// TODO: Call abort to cancel in-progress appends?
-var video_source = data['pair_sources'][data['pair_idx']][0];
-var audio_source = data['pair_sources'][data['pair_idx']][1];
-
-var audioStream = null;
-var videoStream = null;
-var seeking = false;
-
-var video = document.querySelector('video');
-var mediaSource = null;
-
-setup();
+var avMerge;
+function avInitialize(...args){
+ avMerge = new AVMerge(...args);
+}
-function setup() {
+function AVMerge(video, srcPair, startTime){
+ this.videoSource = srcPair[0];
+ this.audioSource = srcPair[1];
+ this.videoStream = null;
+ this.audioStream = null;
+ this.seeking = false;
+ this.startTime = startTime;
+ this.video = video;
+ this.mediaSource = null;
+ this.setup();
+}
+AVMerge.prototype.setup = function() {
if ('MediaSource' in window
- && MediaSource.isTypeSupported(audio_source['mime_codec'])
- && MediaSource.isTypeSupported(video_source['mime_codec'])) {
- mediaSource = new MediaSource();
- video.src = URL.createObjectURL(mediaSource);
- mediaSource.addEventListener('sourceopen', sourceOpen);
+ && MediaSource.isTypeSupported(this.audioSource['mime_codec'])
+ && MediaSource.isTypeSupported(this.videoSource['mime_codec'])) {
+ this.mediaSource = new MediaSource();
+ this.video.src = URL.createObjectURL(this.mediaSource);
+ this.mediaSource.onsourceopen = this.sourceOpen.bind(this);
} else {
reportError('Unsupported MIME type or codec: ',
- audio_source['mime_codec'],
- video_source['mime_codec']);
+ this.audioSource['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(_) {
- videoStream = new Stream(mediaSource, video_source);
- audioStream = new Stream(mediaSource, audio_source);
+ this.videoStream.setup();
+ this.audioStream.setup();
- videoStream.setup();
- audioStream.setup();
-
- video.addEventListener('timeupdate', checkBothBuffers);
- video.addEventListener('seeking', debounce(seek, 500));
- //video.addEventListener('seeked', function() {console.log('seeked')});
+ this.video.ontimeupdate = this.checkBothBuffers.bind(this);
+ this.video.onseeking = debounce(this.seek.bind(this), 500);
+ //this.video.onseeked = function() {console.log('seeked')};
+}
+AVMerge.prototype.checkBothBuffers = function() {
+ 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(mediaSource, source) {
+function Stream(avMerge, source, startTime) {
+ this.avMerge = avMerge;
+ this.video = avMerge.video;
this.url = source['url'];
this.mimeCodec = source['mime_codec']
this.streamType = source['acodec'] ? 'audio' : 'video';
@@ -70,11 +89,12 @@ function Stream(mediaSource, source) {
this.initRange = source['init_range'];
this.indexRange = source['index_range'];
- this.mediaSource = mediaSource;
+ this.startTime = startTime;
+ this.mediaSource = avMerge.mediaSource;
this.sidx = null;
this.appendRetries = 0;
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.addEventListener('error', (e) => {
this.reportError('sourceBuffer error', e);
@@ -124,7 +144,7 @@ Stream.prototype.setupSegments = async function(sidxBox){
this.reportDebug('sidx', this.sidx);
this.reportDebug('appending first segment');
- this.fetchSegmentIfNeeded(0);
+ this.fetchSegmentIfNeeded(this.getSegmentIdx(this.startTime));
}
Stream.prototype.appendSegment = function(segmentIdx, chunk) {
// 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
// not to delete current one
- var currentSegment = this.getSegmentIdx(video.currentTime);
+ var currentSegment = this.getSegmentIdx(this.video.currentTime);
this.reportDebug('QuotaExceededError. Deleting segments.');
var numDeleted = 0;
var i = 0;
@@ -191,15 +211,15 @@ Stream.prototype.shouldFetchNextSegment = function(nextSegment) {
return false;
}
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);
}
Stream.prototype.checkBuffer = async function() {
this.reportDebug('check Buffer');
- if (seeking) {
+ if (this.avMerge.seeking) {
return;
}
- var nextSegment = this.getSegmentIdx(video.currentTime) + 1;
+ var nextSegment = this.getSegmentIdx(this.video.currentTime) + 1;
if (this.shouldFetchNextSegment(nextSegment)) {
this.fetchSegmentIfNeeded(nextSegment);
@@ -242,7 +262,7 @@ Stream.prototype.fetchSegmentIfNeeded = function(segmentIdx) {
);
}
Stream.prototype.handleSeek = async function() {
- var segmentIdx = this.getSegmentIdx(video.currentTime);
+ var segmentIdx = this.getSegmentIdx(this.video.currentTime);
this.fetchSegmentIfNeeded(segmentIdx);
}
Stream.prototype.reportDebug = function(...args) {
@@ -255,23 +275,6 @@ Stream.prototype.reportError = function(...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
function fetchRange(url, start, end, cb) {
diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html
index 9d7a36c..2c85e16 100644
--- a/youtube/templates/watch.html
+++ b/youtube/templates/watch.html
@@ -55,8 +55,17 @@
</video>
</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 %}
{% if time_start != 0 %}
@@ -93,8 +102,41 @@
<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 (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>
diff --git a/youtube/watch.py b/youtube/watch.py
index 3ca39ad..b879c37 100644
--- a/youtube/watch.py
+++ b/youtube/watch.py
@@ -46,6 +46,7 @@ def get_video_sources(info):
if fmt['acodec'] and fmt['vcodec']:
source = {
'type': 'video/' + fmt['ext'],
+ 'quality_string': short_video_quality_string(fmt),
}
source.update(fmt)
uni_sources.append(source)
@@ -59,6 +60,7 @@ def get_video_sources(info):
source = {
'type': 'audio/' + fmt['ext'],
'bitrate': fmt['audio_bitrate'],
+ 'quality_string': audio_quality_string(fmt),
}
source.update(fmt)
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')):
source = {
'type': 'video/' + fmt['ext'],
+ 'quality_string': short_video_quality_string(fmt),
}
source.update(fmt)
source['mime_codec'] = (source['type'] + '; codecs="'
@@ -415,15 +418,24 @@ def video_quality_string(format):
return '?'
-def audio_quality_string(format):
- if format['acodec']:
- result = str(format['audio_bitrate'] or '?') + 'k'
- if format['audio_sample_rate']:
- result += ' ' + str(format['audio_sample_rate']) + ' Hz'
+def short_video_quality_string(fmt):
+ result = str(fmt['quality'] or '?') + 'p'
+ if fmt['fps']:
+ result += ' ' + str(fmt['fps']) + 'fps'
+ return result
+
+
+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 format['vcodec']:
+ elif fmt['vcodec']:
return 'video only'
-
return '?'
@@ -551,13 +563,23 @@ def get_watch_page(video_id=None):
})
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',
uni_idx, 'height',
default=360)
video_width = yt_data_extract.deep_get(source_info, 'uni_sources',
uni_idx, 'width',
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
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],
**source_info,
+ using_pair_sources = using_pair_sources,
)