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'
)