diff options
Diffstat (limited to 'youtube')
-rw-r--r-- | youtube/static/js/av-merge.js | 113 | ||||
-rw-r--r-- | youtube/templates/watch.html | 48 | ||||
-rw-r--r-- | youtube/watch.py | 39 |
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, ) |