From 3e8b137f676acefb1f7f98576820715d38b7efcb Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Wed, 17 Mar 2021 01:28:25 +0000 Subject: [PATCH 1/4] Fix and enhance captions subsystem --- .../ft-video-player/ft-video-player.js | 26 ++++++- src/renderer/views/Watch/Watch.js | 75 +++++++++++-------- src/renderer/views/Watch/Watch.vue | 3 +- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js index cc57f376..46716955 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -51,7 +51,7 @@ export default Vue.extend({ type: Array, default: null }, - captionList: { + captionHybridList: { type: Array, default: () => { return [] } }, @@ -196,6 +196,7 @@ export default Vue.extend({ this.player = videojs(videoPlayer, { html5: { + preloadTextTracks: false, vhs: { limitRenditionByPlayerDimensions: false, smoothQualityChange: false, @@ -246,6 +247,9 @@ export default Vue.extend({ this.player.on('ready', function () { v.$emit('ready') v.checkAspectRatio() + if (v.captionHybridList.length !== 0) { + v.transformAndInsertCaptions() + } }) this.player.on('ended', function () { @@ -716,6 +720,26 @@ export default Vue.extend({ this.determineDefaultQualityDash() }, + transformAndInsertCaptions: async function() { + let captionList + if (this.captionHybridList[0] instanceof Promise) { + captionList = await Promise.all(this.captionHybridList) + this.$emit('store-caption-list', captionList) + } else { + captionList = this.captionHybridList + } + + for (const caption of captionList) { + this.player.addRemoteTextTrack({ + kind: 'subtitles', + src: caption.baseUrl || caption.url, + srclang: caption.languageCode, + label: caption.label || caption.name.simpleText, + type: caption.type + }, true) + } + }, + toggleFullWindow: function() { if (!this.player.isFullscreen_) { if (this.player.isFullWindow) { diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 00745b4d..979ffa97 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -69,7 +69,7 @@ export default Vue.extend({ activeSourceList: [], videoSourceList: [], audioSourceList: [], - captionSourceList: [], + captionHybridList: [], // [] -> Promise[] -> string[] (URIs) recommendedVideos: [], downloadLinks: [], watchingPlaylist: false, @@ -153,7 +153,7 @@ export default Vue.extend({ this.firstLoad = true this.activeFormat = this.defaultVideoFormat this.videoStoryboardSrc = '' - this.captionSourceList = [] + this.captionHybridList = [] this.downloadLinks = [] this.checkIfPlaylist() @@ -290,17 +290,6 @@ export default Vue.extend({ this.isLiveContent = result.player_response.videoDetails.isLiveContent this.isUpcoming = result.player_response.videoDetails.isUpcoming ? result.player_response.videoDetails.isUpcoming : false - if (!this.isLive && !this.isUpcoming) { - const captionTracks = - result.player_response.captions && - result.player_response.captions.playerCaptionsTracklistRenderer - .captionTracks - - if (typeof captionTracks !== 'undefined') { - await this.createCaptionUrls(captionTracks) - } - } - if (this.videoDislikeCount === null && !this.hideVideoLikesAndDislikes) { this.videoDislikeCount = 0 } @@ -387,16 +376,22 @@ export default Vue.extend({ return object }) - let captionLinks = result.player_response.captions - if (typeof captionLinks !== 'undefined') { - captionLinks = captionLinks.playerCaptionsTracklistRenderer.captionTracks.map((caption) => { + + const captionTracks = + result.player_response.captions && + result.player_response.captions.playerCaptionsTracklistRenderer + .captionTracks + + if (typeof captionTracks !== 'undefined') { + this.captionHybridList = this.createCaptionPromiseList(captionTracks) + + const captionLinks = captionTracks.map((caption) => { const label = `${caption.name.simpleText} (${caption.languageCode}) - text/vtt` - const object = { + + return { url: caption.baseUrl, label: label } - - return object }) this.downloadLinks = this.downloadLinks.concat(captionLinks) @@ -525,7 +520,7 @@ export default Vue.extend({ this.videoDescriptionHtml = result.descriptionHtml this.recommendedVideos = result.recommendedVideos this.isLive = result.liveNow - this.captionSourceList = result.captions.map(caption => { + this.captionHybridList = result.captions.map(caption => { caption.url = this.invidiousInstance + caption.url caption.type = '' caption.dataSource = 'invidious' @@ -1047,29 +1042,43 @@ export default Vue.extend({ }) }, - createCaptionUrls: function (captionTracks) { - this.captionSourceList = captionTracks.map(caption => { + createCaptionPromiseList: function (captionTracks) { + return captionTracks.map(caption => new Promise((resolve, reject) => { caption.type = 'text/vtt' caption.charset = 'charset=utf-8' caption.dataSource = 'local' + caption.baseUrl += '&fmt=vtt' $.get(caption.baseUrl, response => { - xml2vtt - .Parse(new XMLSerializer().serializeToString(response)) - .then(vtt => { - caption.baseUrl = `data:${caption.type};${caption.charset},${vtt}` - }) - .catch(err => - console.log(`Error while converting XML to VTT : ${err}`) - ) + // The character '#' needs to be percent-encoded in a (data) URI + // because it signals an identifier, which means anything after it + // is automatically removed when the URI is used as a source + let vtt = response.replace(/#/g, '%23') + + // A lot of videos have messed up caption positions that need to be removed + // This can be either because this format isn't really used by YouTube + // or because it's expected for the player to be able to somehow + // wrap the captions so that they won't step outside its boundaries + // + // Auto-generated captions are also all aligned to the start + // so those instances must also be removed + // In addition, all aligns seem to be fixed to "start" when they do pop up in normal captions + // If it's prominent enough that people start to notice, it can be removed then + if (caption.kind === 'asr') { + vtt = vtt.replace(/ align:start| position:\d{1,3}%/g, '') + } else { + vtt = vtt.replace(/ position:\d{1,3}%/g, '') + } + + caption.baseUrl = `data:${caption.type};${caption.charset},${vtt}` + resolve(caption) }).fail((xhr, textStatus, error) => { console.log(xhr) console.log(textStatus) console.log(error) + reject(error) }) - - return caption - }) + })) }, getWatchedProgress: function () { diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue index cb428351..8aa78b32 100644 --- a/src/renderer/views/Watch/Watch.vue +++ b/src/renderer/views/Watch/Watch.vue @@ -18,7 +18,7 @@ ref="videoPlayer" :dash-src="dashSrc" :source-list="activeSourceList" - :caption-list="captionSourceList" + :caption-hybrid-list="captionHybridList" :storyboard-src="videoStoryboardSrc" :format="activeFormat" :thumbnail="thumbnail" @@ -27,6 +27,7 @@ @ready="checkIfWatched" @ended="handleVideoEnded" @error="handleVideoError" + @store-caption-list="captionHybridList = $event" />
Date: Wed, 17 Mar 2021 01:30:35 +0000 Subject: [PATCH 2/4] Remove unnecessary packages and snippets of code --- package-lock.json | 5 ----- package.json | 1 - .../components/ft-video-player/ft-video-player.js | 10 ---------- .../components/ft-video-player/ft-video-player.vue | 9 --------- src/renderer/views/Watch/Watch.js | 1 - 5 files changed, 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 67ce048c..5800ad72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18399,11 +18399,6 @@ } } }, - "yt-xml2vtt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/yt-xml2vtt/-/yt-xml2vtt-1.2.0.tgz", - "integrity": "sha512-4ZzqHIUfdPFa0Xb+8M3vsbokXooOhQuFuXa8bw4CJ5V0xWjRA/CPlZ3u0VTYoce4sUmMgoOVN7Xcj8NpUNujXA==" - }, "ytdl-core": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.5.0.tgz", diff --git a/package.json b/package.json index 807f023c..4f146d1f 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "yt-comment-scraper": "^3.0.2", "yt-dash-manifest-generator": "1.1.0", "yt-trending-scraper": "^1.1.1", - "yt-xml2vtt": "^1.2.0", "ytdl-core": "^4.5.0", "ytpl": "^2.0.5", "ytsr": "^3.3.1" diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js index 46716955..c50ea7b1 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -145,16 +145,6 @@ export default Vue.extend({ return this.$store.getters.getAutoplayVideos } }, - watch: { - sourceList: function () { - this.determineFormatType() - }, - captionList: function () { - this.player.caption({ - data: this.captionList - }) - } - }, mounted: function () { this.id = this._uid diff --git a/src/renderer/components/ft-video-player/ft-video-player.vue b/src/renderer/components/ft-video-player/ft-video-player.vue index 06674ac6..60c63a88 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.vue +++ b/src/renderer/components/ft-video-player/ft-video-player.vue @@ -18,15 +18,6 @@ :label="source.qualityLabel" :selected="source.qualityLabel === selectedDefaultQuality" > -
diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 979ffa97..b7fd1bb0 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -1,6 +1,5 @@ import Vue from 'vue' import { mapActions } from 'vuex' -import xml2vtt from 'yt-xml2vtt' import $ from 'jquery' import fs from 'fs' import ytDashGen from 'yt-dash-manifest-generator' From becf86e9459c76235632f3be60a1b10b0c798a97 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Fri, 19 Mar 2021 02:36:45 +0000 Subject: [PATCH 3/4] Provide translated caption for user's locale --- src/renderer/views/Watch/Watch.js | 51 +++++++++++++++++++++++++++++-- static/locales/en-US.yaml | 1 + 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index b7fd1bb0..cbe1e48f 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -382,6 +382,16 @@ export default Vue.extend({ .captionTracks if (typeof captionTracks !== 'undefined') { + const standardLocale = localStorage.getItem('locale').replace('_', '-') + const noLocaleCaption = !captionTracks.some(track => + track.languageCode === standardLocale && track.kind !== 'asr' + ) + + if (!standardLocale.startsWith('en') && noLocaleCaption) { + const baseUrl = result.player_response.captions.playerCaptionsRenderer.baseUrl + this.tryAddingAutoGeneratedLocaleCaption(captionTracks, standardLocale, baseUrl) + } + this.captionHybridList = this.createCaptionPromiseList(captionTracks) const captionLinks = captionTracks.map((caption) => { @@ -1041,14 +1051,51 @@ export default Vue.extend({ }) }, + tryAddingAutoGeneratedLocaleCaption: function (captionTracks, locale, baseUrl) { + const enCaptionIdx = captionTracks.findIndex(track => + track.languageCode === 'en' && track.kind !== 'asr' + ) + + const enCaptionExists = enCaptionIdx !== -1 + const asrEnabled = captionTracks.some(track => track.kind === 'asr') + + if (enCaptionExists || asrEnabled) { + let label + let url + + if (this.$te('Video.translated from English') && this.$t('Video.translated from English') !== '') { + label = `${this.$t('Locale Name')} (${this.$t('Video.translated from English')})` + } else { + label = `${this.$t('Locale Name')} (translated from English)` + } + + if (enCaptionExists) { + url = new URL(captionTracks[enCaptionIdx].baseUrl) + } else { + url = new URL(baseUrl) + url.searchParams.set('lang', 'en') + url.searchParams.set('kind', 'asr') + } + + url.searchParams.set('tlang', locale) + captionTracks.unshift({ + baseUrl: url.toString(), + name: { simpleText: label }, + languageCode: locale + }) + } + }, + createCaptionPromiseList: function (captionTracks) { return captionTracks.map(caption => new Promise((resolve, reject) => { caption.type = 'text/vtt' caption.charset = 'charset=utf-8' caption.dataSource = 'local' - caption.baseUrl += '&fmt=vtt' - $.get(caption.baseUrl, response => { + const url = new URL(caption.baseUrl) + url.searchParams.set('fmt', 'vtt') + + $.get(url.toString(), response => { // The character '#' needs to be percent-encoded in a (data) URI // because it signals an identifier, which means anything after it // is automatically removed when the URI is used as a source diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 4b969536..7dcf4bee 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -466,6 +466,7 @@ Video: Published on: Published on Streamed on: Streamed on Started streaming on: Started streaming on + translated from English: translated from English # $ is replaced with the number and % with the unit (days, hours, minutes...) Publicationtemplate: $ % ago #& Videos From 39811f6ee4e6ff0f287723aa41cfe426816f1757 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Fri, 19 Mar 2021 19:18:21 +0000 Subject: [PATCH 4/4] Change locale caption function name --- src/renderer/views/Watch/Watch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index cbe1e48f..5d69623f 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -389,7 +389,7 @@ export default Vue.extend({ if (!standardLocale.startsWith('en') && noLocaleCaption) { const baseUrl = result.player_response.captions.playerCaptionsRenderer.baseUrl - this.tryAddingAutoGeneratedLocaleCaption(captionTracks, standardLocale, baseUrl) + this.tryAddingTranslatedLocaleCaption(captionTracks, standardLocale, baseUrl) } this.captionHybridList = this.createCaptionPromiseList(captionTracks) @@ -1051,7 +1051,7 @@ export default Vue.extend({ }) }, - tryAddingAutoGeneratedLocaleCaption: function (captionTracks, locale, baseUrl) { + tryAddingTranslatedLocaleCaption: function (captionTracks, locale, baseUrl) { const enCaptionIdx = captionTracks.findIndex(track => track.languageCode === 'en' && track.kind !== 'asr' )