Work on video player to support dash and other features

Theatre Mode
Captions
Storyboards
This commit is contained in:
Preston 2020-02-18 15:59:01 -05:00
parent 70f53ad51a
commit 4dc8eab9b8
15 changed files with 478 additions and 26 deletions

143
package-lock.json generated
View File

@ -2428,6 +2428,27 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@silvermine/videojs-quality-selector": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@silvermine/videojs-quality-selector/-/videojs-quality-selector-1.2.3.tgz",
"integrity": "sha512-votXSPzzydjZsBZT37589sTw31csgncWggaYPWKXTygCkzvc8V876iRNJiTykgaiZd/9qQn7pjwEJsOqnfp/pw==",
"requires": {
"class.extend": "0.9.1",
"underscore": "1.9.1"
},
"dependencies": {
"class.extend": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/class.extend/-/class.extend-0.9.1.tgz",
"integrity": "sha1-tO5BfGk3QKRKkqbWTxyVQGQbCXo="
},
"underscore": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
"integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg=="
}
}
},
"@sindresorhus/is": { "@sindresorhus/is": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -6977,6 +6998,11 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true "dev": true
}, },
"fast-xml-parser": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.16.0.tgz",
"integrity": "sha512-U+bpScacfgnfNfIKlWHDu4u6rtOaCyxhblOLJ8sZPkhsjgGqdZmVPBhdOyvdMGCDt8CsAv+cssOP3NzQptNt2w=="
},
"fastq": { "fastq": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz",
@ -14478,11 +14504,110 @@
"xhr": "2.4.0" "xhr": "2.4.0"
} }
}, },
"videojs-contrib-quality-levels": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.9.tgz",
"integrity": "sha512-HJeaJJQdSufi9Y5T7jlyyhkeq+mWPCog86q6ypoTi66boBMMJTo2abiOSHS9KaOGAJjH72gfvrjVY5FRdjlxYA==",
"requires": {
"global": "^4.3.2",
"video.js": "^6 || ^7"
}
},
"videojs-font": { "videojs-font": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
}, },
"videojs-frankly-ttml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/videojs-frankly-ttml/-/videojs-frankly-ttml-1.0.1.tgz",
"integrity": "sha512-lmpTQA7q47V5S2ILpNhHbqOyWBebPGb3OGpTMXzUP1HkhA1ZdGSaBFLUG+manE9ZlONLu8FsoqrEFQoobCR4zA==",
"requires": {
"video.js": "^5.8.5"
},
"dependencies": {
"global": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.3.0.tgz",
"integrity": "sha1-737EvurVebRU9evV5/MD21T0Kis=",
"requires": {
"min-document": "^2.6.1",
"process": "~0.5.1"
}
},
"process": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz",
"integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8="
},
"video.js": {
"version": "5.20.5",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-5.20.5.tgz",
"integrity": "sha1-RFza4gS85Fl4LYajGyWjKv1tjv8=",
"requires": {
"babel-runtime": "^6.9.2",
"global": "4.3.0",
"safe-json-parse": "4.0.0",
"tsml": "1.0.1",
"videojs-font": "2.0.0",
"videojs-ie8": "1.1.2",
"videojs-swf": "5.4.1",
"videojs-vtt.js": "0.12.6",
"xhr": "2.2.2"
}
},
"videojs-font": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-2.0.0.tgz",
"integrity": "sha1-r3Rh751LleAzS/+3iy8v8DZKkDQ="
},
"videojs-vtt.js": {
"version": "0.12.6",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.12.6.tgz",
"integrity": "sha512-XFXeGBQiljnElMhwCcZst0RDbZn2n8LU7ZScXryd3a00OaZsHAjdZu/7/RdSr7Z1jHphd45FnOvOQkGK4YrWCQ==",
"requires": {
"global": "^4.3.1"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
}
}
},
"xhr": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/xhr/-/xhr-2.2.2.tgz",
"integrity": "sha1-LuclcYafhobUFVmp6ihsGJcUNf8=",
"requires": {
"global": "~4.3.0",
"is-function": "^1.0.1",
"parse-headers": "^2.0.0",
"xtend": "^4.0.0"
}
}
}
},
"videojs-http-source-selector": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/videojs-http-source-selector/-/videojs-http-source-selector-1.1.6.tgz",
"integrity": "sha512-6b5MmKTT2cVnrjtdNj4z1VO91udbXkZkTYA6LlD8WN2aHlG2rqFTmtMab4NK4nlkkkbRnm3c2q2IddL3jffLmg==",
"requires": {
"global": "^4.3.2",
"video.js": "^7.0.0",
"videojs-contrib-quality-levels": "^2.0.4"
}
},
"videojs-ie8": { "videojs-ie8": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/videojs-ie8/-/videojs-ie8-1.1.2.tgz", "resolved": "https://registry.npmjs.org/videojs-ie8/-/videojs-ie8-1.1.2.tgz",
@ -14576,6 +14701,16 @@
"resolved": "https://registry.npmjs.org/videojs-swf/-/videojs-swf-5.4.1.tgz", "resolved": "https://registry.npmjs.org/videojs-swf/-/videojs-swf-5.4.1.tgz",
"integrity": "sha1-IHfvccdJ8seCPvSbq65N0qywj4c=" "integrity": "sha1-IHfvccdJ8seCPvSbq65N0qywj4c="
}, },
"videojs-vtt-thumbnails": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/videojs-vtt-thumbnails/-/videojs-vtt-thumbnails-0.0.13.tgz",
"integrity": "sha512-7VGcpTRF+ppIss/NiZcDkVOE02k/GoMltxUumQ2jaTpR1ZieYTM+dPopmTXubLxOgUP3F71uTLMZVnWEtiHjVA==",
"requires": {
"global": "^4.3.2",
"request": "^2.83.0",
"video.js": "^5.19.2 || ^6.6.0 || ^7.2.0"
}
},
"videojs-vtt.js": { "videojs-vtt.js": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.14.1.tgz", "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.14.1.tgz",
@ -15661,6 +15796,14 @@
"node-fetch": "^2.6.0" "node-fetch": "^2.6.0"
} }
}, },
"yt-xml2srt": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/yt-xml2srt/-/yt-xml2srt-1.1.0.tgz",
"integrity": "sha512-6JseclPTdiPEuXZ+cwiYl1xtqYDsyGfJqQfVSWmPXWT3bVEdAYPDXspMqQPRRXHdYEGaMD/oBz2mWMhXanKeOA==",
"requires": {
"fast-xml-parser": "^3.16.0"
}
},
"ytdl-core": { "ytdl-core": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-1.0.7.tgz", "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-1.0.7.tgz",

View File

@ -11,6 +11,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.27", "@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1", "@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/vue-fontawesome": "^0.1.9", "@fortawesome/vue-fontawesome": "^0.1.9",
"@silvermine/videojs-quality-selector": "^1.2.3",
"autolinker": "^3.11.1", "autolinker": "^3.11.1",
"bulma-pro": "^0.1.8", "bulma-pro": "^0.1.8",
"dateformat": "^3.0.3", "dateformat": "^3.0.3",
@ -22,7 +23,10 @@
"nedb": "^1.8.0", "nedb": "^1.8.0",
"opml-to-json": "0.0.3", "opml-to-json": "0.0.3",
"video.js": "^7.6.6", "video.js": "^7.6.6",
"videojs-contrib-quality-levels": "^2.0.9",
"videojs-http-source-selector": "^1.1.6",
"videojs-replay": "^1.1.0", "videojs-replay": "^1.1.0",
"videojs-vtt-thumbnails": "0.0.13",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-electron": "^1.0.6", "vue-electron": "^1.0.6",
"vue-router": "^3.1.5", "vue-router": "^3.1.5",
@ -32,6 +36,7 @@
"youtube-comments-fetch": "^1.0.1", "youtube-comments-fetch": "^1.0.1",
"youtube-comments-task": "^1.3.14", "youtube-comments-task": "^1.3.14",
"youtube-suggest": "^1.1.0", "youtube-suggest": "^1.1.0",
"yt-xml2srt": "^1.1.0",
"ytdl-core": "^1.0.7", "ytdl-core": "^1.0.7",
"ytpl": "^0.1.20", "ytpl": "^0.1.20",
"ytsr": "^0.1.10" "ytsr": "^0.1.10"

View File

@ -5,4 +5,5 @@
.ftVideoPlayer { .ftVideoPlayer {
width: 85%; width: 85%;
max-height: 50vh;
} }

View File

@ -4,6 +4,10 @@ import FtCard from '../ft-card/ft-card.vue'
// I haven't decided which video player I want to use // I haven't decided which video player I want to use
// Need to expirement with both of them to see which one will work best. // Need to expirement with both of them to see which one will work best.
import videojs from 'video.js' import videojs from 'video.js'
import qualitySelector from '@silvermine/videojs-quality-selector'
import 'videojs-vtt-thumbnails'
import 'videojs-contrib-quality-levels'
import 'videojs-http-source-selector'
// import mediaelement from 'mediaelement' // import mediaelement from 'mediaelement'
export default Vue.extend({ export default Vue.extend({
@ -12,21 +16,60 @@ export default Vue.extend({
'ft-card': FtCard 'ft-card': FtCard
}, },
props: { props: {
data: { sourceList: {
type: Array,
required: true
},
dashSrc: {
type: Object,
default: null
},
hlsSrc: {
type: Object,
default: null
},
captionList: {
type: Array, type: Array,
default: () => { return [] } default: () => { return [] }
}, },
src: { storyboardSrc: {
type: String, type: String,
required: true default: ''
} }
}, },
data: function () { data: function () {
return { return {
id: '', id: '',
player: null, player: null,
useDash: false,
useHls: false,
activeSourceList: [],
dataSetup: { dataSetup: {
aspectRatio: '16:9', aspectRatio: '16:9',
nativeTextTracks: false,
plugins: {},
controlBar: {
children: [
'playToggle',
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'liveDisplay',
'seekToLive',
'remainingTimeDisplay',
'customControlSpacer',
'playbackRateMenuButton',
'chaptersButton',
'descriptionsButton',
'subsCapsButton',
'audioTrackButton',
'QualitySelector',
'pictureInPictureToggle',
'fullscreenToggle'
]
},
playbackRates: [ playbackRates: [
0.25, 0.25,
0.5, 0.5,
@ -47,21 +90,79 @@ export default Vue.extend({
computed: { computed: {
listType: function () { listType: function () {
return this.$store.getters.getListType return this.$store.getters.getListType
},
videoFormatPreference: function () {
return this.$store.getters.getVideoFormatPreference
} }
}, },
mounted: function () { mounted: function () {
this.id = this._uid this.id = this._uid
setTimeout(this.initializePlayer, 100)
this.determineFormatType()
}, },
methods: { methods: {
initializePlayer: function () { initializePlayer: function () {
console.log(this.id)
const videoPlayer = document.getElementById(this.id) const videoPlayer = document.getElementById(this.id)
console.log(videoPlayer)
if (videoPlayer !== null) { if (videoPlayer !== null) {
if (!this.useDash && !this.useHls) {
qualitySelector(videojs, { showQualitySelectionLabelInControlBar: true })
}
this.player = videojs(videoPlayer) this.player = videojs(videoPlayer)
console.log(videojs.players)
this.player.vttThumbnails({
src: this.storyboardSrc
})
if (this.useDash) {
this.dataSetup.plugins.httpSourceSelector = {
default: 'auto'
}
this.player.httpSourceSelector()
}
} }
},
determineFormatType: function () {
if (this.hlsSrc === null && this.dashSrc !== null && this.videoFormatPreference === 'dash') {
this.enableDashFormat()
} else {
this.enableLegacyFormat()
}
},
enableDashFormat: function () {
if (this.dashSrc === null) {
console.log('No dash format available.')
return
}
console.log('using dash format')
this.useDash = true
this.useHls = false
this.activeSourceList = this.dashSrc
console.log(this.activeSourceList)
setTimeout(this.initializePlayer, 1000)
},
enableLegacyFormat: function () {
if (this.sourceList.length === 0) {
console.log('No sources available')
return
}
console.log('using legacy format')
this.useDash = false
this.useHls = false
this.activeSourceList = this.sourceList
setTimeout(this.initializePlayer, 100)
} }
} }
}) })

View File

@ -3,15 +3,25 @@
<video <video
:id="id" :id="id"
class="ftVideoPlayer video-js vjs-default-skin" class="ftVideoPlayer video-js vjs-default-skin"
width="800"
height="600"
controls controls
preload="auto" preload="auto"
:data-setup="JSON.stringify(dataSetup)" :data-setup="JSON.stringify(dataSetup)"
> >
<source <source
:src="src" v-for="(source, index) in activeSourceList"
type="video/mp4" :key="index + '_source'"
:src="source.url"
:type="source.type || source.mimeType"
:label="source.qualityLabel"
/>
<track
v-for="(caption, index) in captionList"
:key="index + '_caption'"
kind="subtitles"
:src="caption.baseUrl || caption.url"
:srclang="caption.languageCode"
:label="caption.label || caption.name.simpleText"
:type="caption.type"
> >
</video> </video>
</div> </div>

View File

@ -8,11 +8,22 @@
color: var(--title-color); color: var(--title-color);
} }
.center {
text-align: center;
}
.comment { .comment {
padding: 15px; padding: 15px;
position: relative; position: relative;
} }
.hideComments {
font-size: 13px;
text-decoration: underline;
cursor: pointer;
color: var(--title-color);
}
.commentThumbnail { .commentThumbnail {
float: left; float: left;
width: 60px; width: 60px;
@ -47,7 +58,7 @@
.commentMoreReplies { .commentMoreReplies {
font-size: 11px; font-size: 11px;
margin-left: 110px; margin-left: 120px;
margin-top: -25px; margin-top: -25px;
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;

View File

@ -18,6 +18,7 @@ export default Vue.extend({
data: function () { data: function () {
return { return {
isLoading: false, isLoading: false,
showComments: false,
nextPageToken: null, nextPageToken: null,
commentData: [] commentData: []
} }
@ -87,6 +88,7 @@ export default Vue.extend({
this.commentData = this.commentData.concat(commentData) this.commentData = this.commentData.concat(commentData)
this.nextPageToken = p.nextPageToken this.nextPageToken = p.nextPageToken
this.isLoading = false this.isLoading = false
this.showComments = true
}) })
}, },
@ -127,6 +129,7 @@ export default Vue.extend({
this.commentData = this.commentData.concat(commentData) this.commentData = this.commentData.concat(commentData)
this.nextPageToken = response.continuation this.nextPageToken = response.continuation
this.isLoading = false this.isLoading = false
this.showComments = true
}).catch((xhr) => { }).catch((xhr) => {
console.log('found an error') console.log('found an error')
console.log(xhr) console.log(xhr)

View File

@ -10,13 +10,26 @@
> >
Click to view comments Click to view comments
</h4> </h4>
<h4
v-if="commentData.length > 0 && !isLoading && !showComments"
class="getCommentsTitle"
@click="showComments = true"
>
Click to view comments
</h4>
<h3 <h3
v-if="commentData.length > 0 && !isLoading" v-if="commentData.length > 0 && !isLoading && showComments"
> >
Comments Comments
<span
class="hideComments"
@click="showComments = false"
>
Hide Comments
</span>
</h3> </h3>
<div <div
v-if="commentData.length > 0" v-if="commentData.length > 0 && showComments"
> >
<div <div
v-for="(comment, index) in commentData" v-for="(comment, index) in commentData"
@ -87,8 +100,15 @@
</div> </div>
</div> </div>
</div> </div>
<div
v-else-if="showComments && !isLoading"
>
<h3 class="center">
There are no comments available for this video.
</h3>
</div>
<h4 <h4
v-if="commentData.length > 0 && !isLoading" v-if="commentData.length > 0 && !isLoading && showComments"
class="getMoreComments" class="getMoreComments"
@click="getCommentData" @click="getCommentData"
> >

View File

@ -83,7 +83,14 @@
position: absolute; position: absolute;
right: 15px; right: 15px;
top: 20px; top: 20px;
width: 350px; width: 550px;
}
.theatreModeButton {
height: 30px;
line-height: 10px;
position: relative;
bottom: 5px;
} }
.formatTypeDropdown { .formatTypeDropdown {
@ -93,3 +100,13 @@
.shareDropdown { .shareDropdown {
width: 175px; width: 175px;
} }
@media only screen and (max-width: 1700px) {
.theatreModeButton {
display: none;
}
.videoOptions {
width: 350px;
}
}

View File

@ -27,6 +27,11 @@
/> />
</div> </div>
<ft-flex-box class="videoOptions"> <ft-flex-box class="videoOptions">
<ft-button
label="Toggle Theatre Mode"
class="theatreModeButton"
@click="$emit('theatreMode')"
/>
<ft-list-dropdown <ft-list-dropdown
:title="formatTypeLabel" :title="formatTypeLabel"
:label-names="formatTypeNames" :label-names="formatTypeNames"

View File

@ -14,6 +14,7 @@ const state = {
useClickBaitRemover: true, useClickBaitRemover: true,
clickBaitRemoverPreference: '', clickBaitRemoverPreference: '',
backendFallback: true, backendFallback: true,
videoFormatPreference: 'dash',
autoplay: true, autoplay: true,
useTor: false, useTor: false,
history: true, history: true,
@ -61,6 +62,10 @@ const getters = {
getClickBaitRemoverPreference: () => { getClickBaitRemoverPreference: () => {
return state.clickBaitRemoverPreference return state.clickBaitRemoverPreference
},
getVideoFormatPreference: () => {
return state.videoFormatPreference
} }
} }

View File

@ -890,7 +890,7 @@ body.vjs-full-window {
position: absolute; position: absolute;
top: -3.4em; top: -3.4em;
visibility: hidden; visibility: hidden;
z-index: 1; z-index: 2;
} }
.video-js .vjs-progress-holder:focus .vjs-time-tooltip { .video-js .vjs-progress-holder:focus .vjs-time-tooltip {
@ -1292,7 +1292,7 @@ video::-webkit-media-text-track-display {
.vjs-playback-rate > .vjs-menu-button, .vjs-playback-rate > .vjs-menu-button,
.vjs-playback-rate .vjs-playback-rate-value { .vjs-playback-rate .vjs-playback-rate-value {
position: absolute; position: absolute;
top: 0; top: 30%;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -1301,7 +1301,6 @@ video::-webkit-media-text-track-display {
.vjs-playback-rate .vjs-playback-rate-value { .vjs-playback-rate .vjs-playback-rate-value {
pointer-events: none; pointer-events: none;
font-size: 1.2em; font-size: 1.2em;
line-height: 2.8;
text-align: center; text-align: center;
} }
@ -1895,7 +1894,7 @@ video::-webkit-media-text-track-display {
background-color: transparent; background-color: transparent;
width: 5.5em; width: 5.5em;
left: 1.5em; left: 1.5em;
padding-bottom: .5em padding-bottom: .5em;
} }
.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-item,.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-title { .video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-item,.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-title {
@ -1943,3 +1942,42 @@ video::-webkit-media-text-track-display {
.video-js .vjs-load-progress { .video-js .vjs-load-progress {
background: rgba(255,255,255,0.3); background: rgba(255,255,255,0.3);
} }
.vjs-quality-selector .vjs-menu-button {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
.vjs-quality-selector .vjs-quality-selector-icon {
font-family: 'VideoJS';
font-weight: normal;
font-style: normal;
}
.vjs-quality-selector .vjs-quality-selector-icon:before {
content: '\f110';
}
.vjs-quality-changing .vjs-big-play-button {
display: none;
}
.vjs-quality-changing .vjs-control-bar {
display: flex;
visibility: visible;
opacity: 1;
}
.video-js .vjs-vtt-thumbnails {
display: block;
}
.video-js .vjs-vtt-thumbnail-display {
position: absolute;
transition: transform .1s, opacity .2s;
bottom: 20px;
pointer-events: none;
box-shadow: 0 0 7px rgba(0,0,0,.6);
}

View File

@ -5,6 +5,13 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.theatreWatchVideo {
float: none;
margin: 0 auto;
width: 85%;
margin-bottom: 10px;
}
.videoPlayer { .videoPlayer {
width: calc(65% + 30px); width: calc(65% + 30px);
float: left; float: left;
@ -13,6 +20,13 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.theatrePlayer {
width: calc(85% + 30px);
float: none;
margin: 0 auto;
margin-bottom: 10px;
}
.watchVideoRecommendations { .watchVideoRecommendations {
width: 27%; width: 27%;
max-width: 425px; max-width: 425px;
@ -23,6 +37,14 @@
right: 10px; right: 10px;
} }
.theatreRecommendations {
float: none;
margin: 0 auto;
width: 85%;
max-width: none;
position: static;
}
@media only screen and (max-width: 1700px) { @media only screen and (max-width: 1700px) {
.watchVideo { .watchVideo {
float: none; float: none;

View File

@ -24,9 +24,11 @@ export default Vue.extend({
return { return {
isLoading: false, isLoading: false,
firstLoad: true, firstLoad: true,
useTheatreMode: true,
showDashPlayer: true, showDashPlayer: true,
showLegacyPlayer: false, showLegacyPlayer: false,
showYouTubeNoCookieEmbed: false, showYouTubeNoCookieEmbed: false,
proxyVideos: false,
videoId: '', videoId: '',
videoTitle: '', videoTitle: '',
videoDescription: '', videoDescription: '',
@ -39,9 +41,10 @@ export default Vue.extend({
channelId: '', channelId: '',
channelSubscriptionCountText: '', channelSubscriptionCountText: '',
videoPublished: 0, videoPublished: 0,
videoUrl360p: '', videoStoryboardSrc: '',
videoUrl720p: '',
audioUrl: '', audioUrl: '',
videoSourceList: [],
captionSourceList: [],
recommendedVideos: [] recommendedVideos: []
} }
}, },
@ -64,6 +67,14 @@ export default Vue.extend({
youtubeNoCookieEmbeddedFrame: function () { youtubeNoCookieEmbeddedFrame: function () {
return `<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/${this.videoId}?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>` return `<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/${this.videoId}?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>`
},
dashSrc: function () {
return {
url: `${this.invidiousInstance}/api/manifest/dash/${this.videoId}.mpd`,
type: 'application/dash+xml',
label: 'Dash'
}
} }
}, },
watch: { watch: {
@ -85,6 +96,11 @@ export default Vue.extend({
}, },
mounted: function () { mounted: function () {
this.videoId = this.$route.params.id this.videoId = this.$route.params.id
this.videoStoryboardSrc = `${this.invidiousInstance}/api/v1/storyboards/${this.videoId}?height=90`
if (this.proxyVideos) {
this.dashSrc = this.dashSrc + '?local=true'
}
switch (this.backendPreference) { switch (this.backendPreference) {
case 'local': case 'local':
@ -96,6 +112,10 @@ export default Vue.extend({
} }
}, },
methods: { methods: {
toggleTheatreMode: function () {
this.useTheatreMode = !this.useTheatreMode
},
getVideoInformationLocal: function () { getVideoInformationLocal: function () {
if (this.firstLoad) { if (this.firstLoad) {
this.isLoading = true this.isLoading = true
@ -112,8 +132,45 @@ export default Vue.extend({
this.videoPublished = result.published this.videoPublished = result.published
this.videoDescription = result.player_response.videoDetails.shortDescription this.videoDescription = result.player_response.videoDetails.shortDescription
this.recommendedVideos = result.related_videos this.recommendedVideos = result.related_videos
this.videoSourceList = result.player_response.streamingData.formats
this.videoUrl720p = result.player_response.streamingData.formats[1].url // The response provides a storyboard, however it returns a 403 error.
// Uncomment this line if that ever changes.
// this.videoStoryboardSrc = result.player_response.storyboards.playerStoryboardSpecRenderer.spec
this.captionSourceList = result.player_response.captions.playerCaptionsTracklistRenderer.captionTracks
this.captionSourceList = this.captionSourceList.map((caption) => {
caption.baseUrl = `${this.invidiousInstance}/api/v1/captions/${this.videoId}?label=${encodeURI(caption.name.simpleText)}`
return caption
})
// TODO: The response returns the captions of the video, however they're returned
// in XML / TTML. I haven't found a way to properly convert this for use.
// There may be another URL that we can use to grab an appropriate format as well.
// Video.js requires that the captions are returned in .vtt format. The below code
// Converts it to .srt which may work, but I can't get the player to accept the data.
// this.captionSourceList = this.captionSourceList.map((caption) => {
// caption.type = 'application/ttml+xml'
// caption.dataSource = 'local'
//
// $.get(caption.baseUrl, (response) => {
// console.log('response')
// console.log(response)
// console.log()
// xml2srt.Parse(new XMLSerializer().serializeToString(response))
// .then(srt => {
// caption.track = srt
// }).catch(err => console.log(`Error while converting XML to SRT : ${err}`))
// }).fail((xhr, textStatus, error) => {
// console.log(xhr)
// console.log(textStatus)
// console.log(error)
// })
//
// return caption
// })
this.isLoading = false this.isLoading = false
}).catch((err) => { }).catch((err) => {
@ -147,8 +204,13 @@ export default Vue.extend({
this.videoPublished = result.published * 1000 this.videoPublished = result.published * 1000
this.videoDescriptionHtml = result.descriptionHtml this.videoDescriptionHtml = result.descriptionHtml
this.recommendedVideos = result.recommendedVideos this.recommendedVideos = result.recommendedVideos
this.videoSourceList = result.formatStreams.reverse()
this.videoUrl720p = result.formatStreams[0].url this.captionSourceList = result.captions.map((caption) => {
caption.url = this.invidiousInstance + caption.url
caption.type = ''
caption.dataSource = 'invidious'
return caption
})
this.isLoading = false this.isLoading = false
}).catch((err) => { }).catch((err) => {

View File

@ -6,8 +6,12 @@
/> />
<ft-video-player <ft-video-player
v-if="!isLoading" v-if="!isLoading"
:src="videoUrl720p" :dash-src="dashSrc"
:source-list="videoSourceList"
:caption-list="captionSourceList"
:storyboard-src="videoStoryboardSrc"
class="videoPlayer" class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }"
/> />
<watch-video-info <watch-video-info
v-if="!isLoading" v-if="!isLoading"
@ -20,7 +24,9 @@
:like-count="videoLikeCount" :like-count="videoLikeCount"
:dislike-count="videoDislikeCount" :dislike-count="videoDislikeCount"
:view-count="videoViewCount" :view-count="videoViewCount"
@theatreMode="toggleTheatreMode"
class="watchVideo" class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/> />
<watch-video-description <watch-video-description
v-if="!isLoading" v-if="!isLoading"
@ -28,16 +34,19 @@
:description="videoDescription" :description="videoDescription"
:description-html="videoDescriptionHtml" :description-html="videoDescriptionHtml"
class="watchVideo" class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/> />
<watch-video-comments <watch-video-comments
v-if="!isLoading" v-if="!isLoading"
:id="videoId" :id="videoId"
class="watchVideo" class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/> />
<watch-video-recommendations <watch-video-recommendations
v-if="!isLoading" v-if="!isLoading"
:data="recommendedVideos" :data="recommendedVideos"
class="watchVideoRecommendations" class="watchVideoRecommendations"
:class="{ theatreRecommendations: useTheatreMode }"
/> />
</div> </div>
</template> </template>