2021-04-01 15:13:35 +00:00
< template >
2022-02-22 06:13:36 +00:00
< div
ref = "container"
data - shaka - player - container
2023-08-13 17:31:57 +00:00
class = "relative max-h-screen w-full flex justify-center"
2022-02-22 06:13:36 +00:00
: class = "{ 'player-container': !isEmbed }"
>
< video ref = "videoEl" class = "w-full" data -shaka -player :autoplay = "shouldAutoPlay" :loop = "selectedAutoLoop" / >
2023-06-27 18:27:43 +00:00
< span
id = "preview-container"
2023-07-27 11:46:05 +00:00
ref = "previewContainer"
2023-08-13 17:31:57 +00:00
class = "absolute bottom-0 z-[2000] mb-[3.5%] hidden flex-col items-center"
2023-06-27 18:27:43 +00:00
>
2023-07-27 11:46:05 +00:00
< canvas id = "preview" ref = "preview" class = "rounded-sm" / >
2023-08-13 17:31:57 +00:00
< span class = "mt-2 w-min rounded-xl bg-dark-700 px-2 pb-1 pt-1.5 text-sm" v-text = "timeFormat(currentTime)" / >
2023-06-27 18:27:43 +00:00
< / span >
2023-02-02 15:02:04 +00:00
< button
v - if = "inSegment"
class = "skip-segment-button"
type = "button"
: aria - label = "$t('actions.skip_segment')"
aria - pressed = "false"
@ click = "onClickSkipSegment"
>
< span v-t = "'actions.skip_segment'" / >
< i class = "material-icons-round" > skip _next < / i >
< / button >
2023-08-29 17:07:30 +00:00
< span
v - if = "error > 0"
v - t = "{ path: 'player.failed', args: [error] }"
2023-08-30 09:59:11 +00:00
class = "absolute top-8 rounded bg-black/80 p-2 text-lg backdrop-blur-sm"
2023-08-29 17:07:30 +00:00
/ >
2021-04-01 15:13:35 +00:00
< / div >
< / template >
< script >
2023-01-25 23:44:07 +00:00
import "shaka-player/dist/controls.css" ;
2023-05-25 18:43:11 +00:00
import { parseTimeParam } from "@/utils/Misc" ;
2023-11-26 07:33:53 +00:00
2021-04-01 15:13:35 +00:00
const shaka = import ( "shaka-player/dist/shaka-player.ui.js" ) ;
2022-01-23 19:08:33 +00:00
const hotkeys = import ( "hotkeys-js" ) ;
2021-04-01 15:13:35 +00:00
export default {
2021-05-06 17:30:02 +00:00
props : {
2021-10-08 18:52:51 +00:00
video : {
type : Object ,
default : ( ) => {
return { } ;
} ,
} ,
sponsors : {
type : Object ,
default : ( ) => {
return { } ;
} ,
} ,
2021-05-06 17:30:02 +00:00
selectedAutoPlay : Boolean ,
2021-07-04 18:42:10 +00:00
selectedAutoLoop : Boolean ,
2021-08-04 05:13:22 +00:00
isEmbed : Boolean ,
2021-05-06 17:30:02 +00:00
} ,
2023-08-07 18:45:54 +00:00
emits : [ "timeupdate" , "ended" , "navigateNext" ] ,
2021-07-07 14:18:09 +00:00
data ( ) {
return {
2021-10-27 00:15:37 +00:00
lastUpdate : new Date ( ) . getTime ( ) ,
2022-01-13 22:41:53 +00:00
initialSeekComplete : false ,
2022-04-08 21:29:50 +00:00
destroying : false ,
2023-02-02 15:02:04 +00:00
inSegment : false ,
2023-04-22 09:32:06 +00:00
isHoveringTimebar : false ,
2023-06-27 18:27:43 +00:00
currentTime : 0 ,
seekbarPadding : 2 ,
2023-08-29 17:07:30 +00:00
error : 0 ,
2021-07-07 14:18:09 +00:00
} ;
} ,
2021-05-06 17:30:02 +00:00
computed : {
2021-07-04 18:26:02 +00:00
shouldAutoPlay : _this => {
2021-10-28 17:35:48 +00:00
return _this . getPreferenceBoolean ( "playerAutoPlay" , true ) && ! _this . isEmbed ;
2021-05-06 17:30:02 +00:00
} ,
2021-07-21 10:48:59 +00:00
preferredVideoCodecs : _this => {
var preferredVideoCodecs = [ ] ;
2022-06-06 04:55:01 +00:00
const enabledCodecs = _this . getPreferenceString ( "enabledCodecs" , "vp9,avc" ) . split ( "," ) ;
2021-07-21 10:48:59 +00:00
2021-08-27 07:33:55 +00:00
if (
_this . $refs . videoEl . canPlayType ( 'video/mp4; codecs="av01.0.08M.08"' ) !== "" &&
enabledCodecs . includes ( "av1" )
)
2021-07-21 10:48:59 +00:00
preferredVideoCodecs . push ( "av01" ) ;
2021-08-27 07:33:55 +00:00
if ( _this . $refs . videoEl . canPlayType ( 'video/webm; codecs="vp9"' ) !== "" && enabledCodecs . includes ( "vp9" ) )
preferredVideoCodecs . push ( "vp9" ) ;
if (
_this . $refs . videoEl . canPlayType ( 'video/mp4; codecs="avc1.4d401f"' ) !== "" &&
enabledCodecs . includes ( "avc" )
)
2021-07-21 10:48:59 +00:00
preferredVideoCodecs . push ( "avc1" ) ;
return preferredVideoCodecs ;
} ,
2021-05-06 17:30:02 +00:00
} ,
2021-07-09 20:55:09 +00:00
mounted ( ) {
2022-05-23 20:18:20 +00:00
if ( ! this . $shaka ) this . shakaPromise = shaka . then ( shaka => shaka . default ) . then ( shaka => ( this . $shaka = shaka ) ) ;
2022-01-23 19:08:33 +00:00
if ( ! this . $hotkeys )
this . hotkeysPromise = hotkeys . then ( mod => mod . default ) . then ( hotkeys => ( this . $hotkeys = hotkeys ) ) ;
2021-07-09 20:55:09 +00:00
} ,
2021-10-08 18:52:51 +00:00
activated ( ) {
2022-05-23 20:18:20 +00:00
this . destroying = false ;
2022-06-26 14:15:03 +00:00
this . sponsors ? . segments ? . forEach ( segment => ( segment . skipped = false ) ) ;
2022-01-23 19:08:33 +00:00
this . hotkeysPromise . then ( ( ) => {
var self = this ;
this . $hotkeys (
2023-03-16 15:58:02 +00:00
"f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+n,shift+,,shift+.,alt+p,return,.,," ,
2022-01-23 19:08:33 +00:00
function ( e , handler ) {
const videoEl = self . $refs . videoEl ;
switch ( handler . key ) {
case "f" :
self . $ui . getControls ( ) . toggleFullScreen ( ) ;
e . preventDefault ( ) ;
break ;
case "m" :
videoEl . muted = ! videoEl . muted ;
e . preventDefault ( ) ;
break ;
case "j" :
videoEl . currentTime = Math . max ( videoEl . currentTime - 15 , 0 ) ;
e . preventDefault ( ) ;
break ;
case "l" :
videoEl . currentTime = videoEl . currentTime + 15 ;
e . preventDefault ( ) ;
break ;
case "c" :
self . $player . setTextTrackVisibility ( ! self . $player . isTextTrackVisible ( ) ) ;
e . preventDefault ( ) ;
break ;
case "k" :
case "space" :
if ( videoEl . paused ) videoEl . play ( ) ;
else videoEl . pause ( ) ;
e . preventDefault ( ) ;
break ;
case "up" :
videoEl . volume = Math . min ( videoEl . volume + 0.05 , 1 ) ;
e . preventDefault ( ) ;
break ;
case "down" :
videoEl . volume = Math . max ( videoEl . volume - 0.05 , 0 ) ;
e . preventDefault ( ) ;
break ;
case "left" :
videoEl . currentTime = Math . max ( videoEl . currentTime - 5 , 0 ) ;
e . preventDefault ( ) ;
break ;
case "right" :
videoEl . currentTime = videoEl . currentTime + 5 ;
e . preventDefault ( ) ;
break ;
case "0" :
videoEl . currentTime = 0 ;
e . preventDefault ( ) ;
break ;
case "1" :
videoEl . currentTime = videoEl . duration * 0.1 ;
e . preventDefault ( ) ;
break ;
case "2" :
videoEl . currentTime = videoEl . duration * 0.2 ;
e . preventDefault ( ) ;
break ;
case "3" :
videoEl . currentTime = videoEl . duration * 0.3 ;
e . preventDefault ( ) ;
break ;
case "4" :
videoEl . currentTime = videoEl . duration * 0.4 ;
e . preventDefault ( ) ;
break ;
case "5" :
videoEl . currentTime = videoEl . duration * 0.5 ;
e . preventDefault ( ) ;
break ;
case "6" :
videoEl . currentTime = videoEl . duration * 0.6 ;
e . preventDefault ( ) ;
break ;
case "7" :
videoEl . currentTime = videoEl . duration * 0.7 ;
e . preventDefault ( ) ;
break ;
case "8" :
videoEl . currentTime = videoEl . duration * 0.8 ;
e . preventDefault ( ) ;
break ;
case "9" :
videoEl . currentTime = videoEl . duration * 0.9 ;
e . preventDefault ( ) ;
break ;
2022-08-06 10:03:31 +00:00
case "shift+n" :
2023-08-07 18:45:54 +00:00
self . $emit ( "navigateNext" ) ;
2022-08-06 10:03:31 +00:00
e . preventDefault ( ) ;
break ;
2022-01-23 19:08:33 +00:00
case "shift+," :
self . $player . trickPlay ( Math . max ( videoEl . playbackRate - 0.25 , 0.25 ) ) ;
break ;
case "shift+." :
self . $player . trickPlay ( Math . min ( videoEl . playbackRate + 0.25 , 2 ) ) ;
break ;
2023-03-16 15:58:02 +00:00
case "alt+p" :
document . pictureInPictureElement
? document . exitPictureInPicture ( )
: videoEl . requestPictureInPicture ( ) ;
break ;
2023-02-01 15:37:43 +00:00
case "return" :
self . skipSegment ( videoEl ) ;
break ;
2023-03-16 15:35:23 +00:00
case "." :
videoEl . currentTime += 0.04 ;
e . preventDefault ( ) ;
break ;
case "," :
videoEl . currentTime -= 0.04 ;
e . preventDefault ( ) ;
break ;
2022-01-23 19:08:33 +00:00
}
} ,
) ;
} ) ;
2021-10-08 18:52:51 +00:00
} ,
deactivated ( ) {
2022-04-08 21:29:50 +00:00
this . destroying = true ;
2022-01-23 19:08:33 +00:00
this . destroy ( true ) ;
2021-10-08 18:52:51 +00:00
} ,
unmounted ( ) {
2022-04-08 21:29:50 +00:00
this . destroying = true ;
2022-01-23 19:08:33 +00:00
this . destroy ( true ) ;
2021-10-08 18:52:51 +00:00
} ,
2021-05-06 17:30:02 +00:00
methods : {
2021-09-05 13:12:27 +00:00
async loadVideo ( ) {
2023-03-14 22:08:16 +00:00
this . updateSponsors ( ) ;
2021-06-09 21:21:35 +00:00
const component = this ;
2021-05-06 17:30:02 +00:00
const videoEl = this . $refs . videoEl ;
2021-04-01 15:13:35 +00:00
2021-05-06 17:30:02 +00:00
videoEl . setAttribute ( "poster" , this . video . thumbnailUrl ) ;
2021-04-01 15:13:35 +00:00
2021-10-06 14:33:52 +00:00
const noPrevPlayer = ! this . $player ;
2021-05-06 17:30:02 +00:00
var streams = [ ] ;
streams . push ( ... this . video . audioStreams ) ;
streams . push ( ... this . video . videoStreams ) ;
2021-07-07 19:34:46 +00:00
const MseSupport = window . MediaSource !== undefined ;
2023-03-04 04:42:03 +00:00
const lbry = null ;
2021-07-28 08:02:23 +00:00
2021-06-09 21:21:35 +00:00
var uri ;
2021-09-05 13:12:27 +00:00
var mime ;
2021-06-09 21:21:35 +00:00
2021-08-30 15:48:08 +00:00
if ( this . video . livestream ) {
2021-06-09 21:21:35 +00:00
uri = this . video . hls ;
2021-09-05 13:12:27 +00:00
mime = "application/x-mpegURL" ;
2021-08-30 15:48:08 +00:00
} else if ( this . video . audioStreams . length > 0 && ! lbry && MseSupport ) {
2021-08-15 19:54:34 +00:00
if ( ! this . video . dash ) {
2023-03-02 14:18:53 +00:00
const dash = ( await import ( "../utils/DashUtils.js" ) ) . generate _dash _file _from _formats (
streams ,
this . video . duration ,
) ;
2021-06-09 21:21:35 +00:00
2021-08-15 19:54:34 +00:00
uri = "data:application/dash+xml;charset=utf-8;base64," + btoa ( dash ) ;
2022-12-02 21:11:22 +00:00
} else {
const url = new URL ( this . video . dash ) ;
url . searchParams . set ( "rewrite" , false ) ;
uri = url . toString ( ) ;
}
2021-09-05 13:12:27 +00:00
mime = "application/dash+xml" ;
2021-07-28 08:02:23 +00:00
} else if ( lbry ) {
uri = lbry . url ;
2021-09-02 13:46:27 +00:00
if ( this . getPreferenceBoolean ( "proxyLBRY" , false ) ) {
const url = new URL ( uri ) ;
2021-11-24 17:36:29 +00:00
const proxyURL = new URL ( this . video . proxyUrl ) ;
let proxyPath = proxyURL . pathname ;
if ( proxyPath . lastIndexOf ( "/" ) === proxyPath . length - 1 ) {
proxyPath = proxyPath . substring ( 0 , proxyPath . length - 1 ) ;
}
2021-09-02 13:46:27 +00:00
url . searchParams . set ( "host" , url . host ) ;
2021-11-24 17:36:29 +00:00
url . protocol = proxyURL . protocol ;
url . host = proxyURL . host ;
url . pathname = proxyPath + url . pathname ;
2021-09-02 13:46:27 +00:00
uri = url . toString ( ) ;
}
2021-09-05 13:12:27 +00:00
const contentType = await fetch ( uri , {
method : "HEAD" ,
2022-05-04 09:19:13 +00:00
} ) . then ( response => {
uri = response . url ;
2022-05-05 18:02:25 +00:00
return response . headers . get ( "Content-Type" ) ;
2022-05-04 09:19:13 +00:00
} ) ;
2021-09-05 13:12:27 +00:00
mime = contentType ;
2021-11-07 18:40:42 +00:00
} else if ( this . video . hls ) {
uri = this . video . hls ;
mime = "application/x-mpegURL" ;
2021-06-18 13:20:41 +00:00
} else {
2023-03-04 08:03:45 +00:00
uri = this . video . videoStreams . findLast ( stream => stream . codec == null ) . url ;
2021-09-05 13:12:27 +00:00
mime = "video/mp4" ;
2021-06-09 21:21:35 +00:00
}
2021-05-06 17:30:02 +00:00
if ( noPrevPlayer )
2023-11-16 15:02:21 +00:00
this . shakaPromise . then ( async ( ) => {
2022-04-08 21:29:50 +00:00
if ( this . destroying ) return ;
2022-05-23 20:18:20 +00:00
this . $shaka . polyfill . installAll ( ) ;
2021-07-09 20:55:09 +00:00
2023-11-16 15:02:21 +00:00
const localPlayer = new this . $shaka . Player ( ) ;
await localPlayer . attach ( videoEl ) ;
2021-11-24 17:36:29 +00:00
const proxyURL = new URL ( component . video . proxyUrl ) ;
let proxyPath = proxyURL . pathname ;
if ( proxyPath . lastIndexOf ( "/" ) === proxyPath . length - 1 ) {
proxyPath = proxyPath . substring ( 0 , proxyPath . length - 1 ) ;
}
2021-07-09 20:55:09 +00:00
localPlayer . getNetworkingEngine ( ) . registerRequestFilter ( ( _type , request ) => {
const uri = request . uris [ 0 ] ;
var url = new URL ( uri ) ;
2021-09-05 13:12:27 +00:00
const headers = request . headers ;
if (
url . host . endsWith ( ".googlevideo.com" ) ||
( url . host . endsWith ( ".lbryplayer.xyz" ) &&
( component . getPreferenceBoolean ( "proxyLBRY" , false ) || headers . Range ) )
) {
2021-07-09 20:55:09 +00:00
url . searchParams . set ( "host" , url . host ) ;
2021-11-24 17:36:29 +00:00
url . protocol = proxyURL . protocol ;
url . host = proxyURL . host ;
url . pathname = proxyPath + url . pathname ;
2021-07-09 20:55:09 +00:00
request . uris [ 0 ] = url . toString ( ) ;
}
2021-11-24 17:36:29 +00:00
if ( url . pathname === proxyPath + "/videoplayback" ) {
2021-08-25 20:32:56 +00:00
if ( headers . Range ) {
url . searchParams . set ( "range" , headers . Range . split ( "=" ) [ 1 ] ) ;
request . headers = { } ;
request . uris [ 0 ] = url . toString ( ) ;
}
}
2021-07-09 20:55:09 +00:00
} ) ;
2021-06-09 21:21:35 +00:00
2021-07-09 20:55:09 +00:00
localPlayer . configure (
"streaming.bufferingGoal" ,
Math . max ( this . getPreferenceNumber ( "bufferGoal" , 10 ) , 10 ) ,
) ;
2021-06-22 10:54:20 +00:00
2022-05-23 20:18:20 +00:00
this . setPlayerAttrs ( localPlayer , videoEl , uri , mime , this . $shaka ) ;
2021-07-09 20:55:09 +00:00
} ) ;
2022-05-23 20:18:20 +00:00
else this . setPlayerAttrs ( this . $player , videoEl , uri , mime , this . $shaka ) ;
2021-05-06 17:30:02 +00:00
if ( noPrevPlayer ) {
2023-03-13 03:20:14 +00:00
videoEl . addEventListener ( "loadeddata" , ( ) => {
if ( document . pictureInPictureElement ) videoEl . requestPictureInPicture ( ) ;
} ) ;
2021-05-06 17:30:02 +00:00
videoEl . addEventListener ( "timeupdate" , ( ) => {
2021-10-27 00:15:37 +00:00
const time = videoEl . currentTime ;
2022-07-19 16:29:03 +00:00
this . $emit ( "timeupdate" , time ) ;
2021-10-27 00:15:37 +00:00
this . updateProgressDatabase ( time ) ;
2021-05-06 17:30:02 +00:00
if ( this . sponsors && this . sponsors . segments ) {
2023-02-01 15:37:43 +00:00
const segment = this . findCurrentSegment ( time ) ;
2023-02-02 15:02:04 +00:00
this . inSegment = ! ! segment ;
2023-02-01 15:37:43 +00:00
if ( segment ? . autoskip && ( ! segment . skipped || this . selectedAutoLoop ) ) {
this . skipSegment ( videoEl , segment ) ;
}
2021-05-06 17:30:02 +00:00
}
} ) ;
videoEl . addEventListener ( "volumechange" , ( ) => {
2022-11-16 18:51:56 +00:00
this . setPreference ( "volume" , videoEl . volume , true ) ;
2021-05-06 17:30:02 +00:00
} ) ;
2022-01-16 08:43:24 +00:00
videoEl . addEventListener ( "ratechange" , e => {
const rate = videoEl . playbackRate ;
if ( rate > 0 && ! isNaN ( videoEl . duration ) && ! isNaN ( videoEl . duration - e . timeStamp / 1000 ) )
2022-11-16 18:51:56 +00:00
this . setPreference ( "rate" , rate , true ) ;
2021-10-08 16:38:01 +00:00
} ) ;
2021-10-03 19:18:04 +00:00
2021-05-06 17:30:02 +00:00
videoEl . addEventListener ( "ended" , ( ) => {
2023-03-02 21:55:23 +00:00
this . $emit ( "ended" ) ;
2021-05-06 17:30:02 +00:00
} ) ;
}
//TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
} ,
2023-02-01 15:37:43 +00:00
findCurrentSegment ( time ) {
return this . sponsors ? . segments ? . find ( s => time >= s . segment [ 0 ] && time < s . segment [ 1 ] ) ;
} ,
2023-02-02 15:02:04 +00:00
onClickSkipSegment ( ) {
const videoEl = this . $refs . videoEl ;
this . skipSegment ( videoEl ) ;
} ,
2023-02-01 15:37:43 +00:00
skipSegment ( videoEl , segment ) {
const time = videoEl . currentTime ;
if ( ! segment ) segment = this . findCurrentSegment ( time ) ;
if ( ! segment ) return ;
console . log ( "Skipped segment at " + time ) ;
videoEl . currentTime = segment . segment [ 1 ] ;
segment . skipped = true ;
} ,
2023-11-23 21:48:23 +00:00
async setPlayerAttrs ( localPlayer , videoEl , uri , mime , shaka ) {
2021-11-11 08:16:00 +00:00
const url = "/watch?v=" + this . video . id ;
2021-10-06 14:33:52 +00:00
if ( ! this . $ui ) {
2022-01-22 23:10:05 +00:00
this . destroy ( ) ;
2021-11-11 08:16:00 +00:00
const OpenButton = class extends shaka . ui . Element {
constructor ( parent , controls ) {
super ( parent , controls ) ;
this . newTabButton _ = document . createElement ( "button" ) ;
this . newTabButton _ . classList . add ( "shaka-cast-button" ) ;
this . newTabButton _ . classList . add ( "shaka-tooltip" ) ;
this . newTabButton _ . ariaPressed = "false" ;
this . newTabIcon _ = document . createElement ( "i" ) ;
this . newTabIcon _ . classList . add ( "material-icons-round" ) ;
this . newTabIcon _ . textContent = "launch" ;
this . newTabButton _ . appendChild ( this . newTabIcon _ ) ;
const label = document . createElement ( "label" ) ;
label . classList . add ( "shaka-overflow-button-label" ) ;
label . classList . add ( "shaka-overflow-menu-only" ) ;
this . newTabNameSpan _ = document . createElement ( "span" ) ;
this . newTabNameSpan _ . innerText = "Open in new tab" ;
label . appendChild ( this . newTabNameSpan _ ) ;
this . newTabButton _ . appendChild ( label ) ;
this . parent . appendChild ( this . newTabButton _ ) ;
this . eventManager . listen ( this . newTabButton _ , "click" , ( ) => {
this . video . pause ( ) ;
window . open ( url ) ;
} ) ;
}
} ;
OpenButton . Factory = class {
create ( rootElement , controls ) {
return new OpenButton ( rootElement , controls ) ;
}
} ;
shaka . ui . OverflowMenu . registerElement ( "open_new_tab" , new OpenButton . Factory ( ) ) ;
2021-10-06 14:33:52 +00:00
this . $ui = new shaka . ui . Overlay ( localPlayer , this . $refs . container , videoEl ) ;
2021-06-07 19:22:29 +00:00
2023-09-14 06:53:48 +00:00
const overflowMenuButtons = [ "quality" , "captions" , "picture_in_picture" , "playback_rate" , "airplay" ] ;
2021-11-11 08:16:00 +00:00
if ( this . isEmbed ) {
overflowMenuButtons . push ( "open_new_tab" ) ;
}
2021-06-07 19:22:29 +00:00
const config = {
2021-11-11 08:16:00 +00:00
overflowMenuButtons : overflowMenuButtons ,
2021-06-07 19:22:29 +00:00
seekBarColors : {
2023-07-19 21:05:00 +00:00
base : "var(--player-base)" ,
buffered : "var(--player-buffered)" ,
played : "var(--player-played)" ,
2021-06-07 19:22:29 +00:00
} ,
} ;
2021-10-06 14:33:52 +00:00
this . $ui . configure ( config ) ;
2021-06-07 19:22:29 +00:00
}
2022-06-06 02:18:47 +00:00
this . updateMarkers ( ) ;
2023-01-21 22:50:56 +00:00
const event = new Event ( "playerInit" ) ;
window . dispatchEvent ( event ) ;
2021-10-06 14:33:52 +00:00
const player = this . $ui . getControls ( ) . getPlayer ( ) ;
2021-06-07 19:22:29 +00:00
2023-03-26 15:56:50 +00:00
this . setupSeekbarPreview ( ) ;
2021-10-06 14:33:52 +00:00
this . $player = player ;
2021-06-07 19:22:29 +00:00
2021-07-03 19:24:09 +00:00
const disableVideo = this . getPreferenceBoolean ( "listen" , false ) && ! this . video . livestream ;
2021-07-15 08:41:36 +00:00
2023-12-15 21:30:07 +00:00
const prefetchLimit = Math . min ( Math . max ( this . getPreferenceNumber ( "prefetchLimit" , 2 ) , 0 ) , 10 ) ;
2021-10-06 14:33:52 +00:00
this . $player . configure ( {
2021-07-21 10:48:59 +00:00
preferredVideoCodecs : this . preferredVideoCodecs ,
2021-07-15 08:41:36 +00:00
preferredAudioCodecs : [ "opus" , "mp4a" ] ,
manifest : {
disableVideo : disableVideo ,
} ,
2023-08-31 09:21:57 +00:00
streaming : {
2023-12-15 21:30:07 +00:00
segmentPrefetchLimit : prefetchLimit ,
2023-11-23 23:49:34 +00:00
retryParameters : {
maxAttempts : Infinity ,
2023-12-14 13:45:05 +00:00
baseDelay : 250 ,
2023-11-23 23:49:34 +00:00
backoffFactor : 1.5 ,
} ,
2023-08-31 09:21:57 +00:00
} ,
2021-07-15 08:41:36 +00:00
} ) ;
2021-06-07 20:35:45 +00:00
2021-07-04 18:26:02 +00:00
const quality = this . getPreferenceNumber ( "quality" , 0 ) ;
2021-07-15 08:41:36 +00:00
const qualityConds =
quality > 0 && ( this . video . audioStreams . length > 0 || this . video . livestream ) && ! disableVideo ;
2021-10-06 14:33:52 +00:00
if ( qualityConds ) this . $player . configure ( "abr.enabled" , false ) ;
2021-06-21 20:03:11 +00:00
2023-11-23 19:08:52 +00:00
const time = this . $route . query . t ? ? this . $route . query . start ;
var startTime = 0 ;
if ( time ) {
startTime = parseTimeParam ( time ) ;
this . initialSeekComplete = true ;
} else if ( window . db && this . getPreferenceBoolean ( "watchHistory" , false ) ) {
2023-11-23 21:48:23 +00:00
await new Promise ( resolve => {
var tx = window . db . transaction ( "watch_history" , "readonly" ) ;
var store = tx . objectStore ( "watch_history" ) ;
var request = store . get ( this . video . id ) ;
request . onsuccess = function ( event ) {
var video = event . target . result ;
const currentTime = video ? . currentTime ;
if ( currentTime ) {
if ( currentTime < video . duration * 0.9 ) {
startTime = currentTime ;
}
2023-11-23 19:08:52 +00:00
}
2023-11-23 21:48:23 +00:00
resolve ( ) ;
} ;
2023-11-23 19:08:52 +00:00
2023-11-23 21:48:23 +00:00
tx . oncomplete = ( ) => {
this . initialSeekComplete = true ;
} ;
} ) ;
2023-11-23 19:08:52 +00:00
} else {
this . initialSeekComplete = true ;
}
2023-08-29 17:07:30 +00:00
player
2023-11-23 19:08:52 +00:00
. load ( uri , startTime , mime )
2023-08-29 17:07:30 +00:00
. then ( ( ) => {
const isSafari = window . navigator ? . vendor ? . includes ( "Apple" ) ;
if ( ! isSafari ) {
// Set the audio language
const prefLang = this . getPreferenceString ( "hl" , "en" ) . substr ( 0 , 2 ) ;
var lang = "en" ;
for ( var l in player . getAudioLanguages ( ) ) {
if ( l == prefLang ) {
lang = l ;
return ;
}
2022-12-06 17:31:34 +00:00
}
2023-08-29 17:07:30 +00:00
player . selectAudioLanguage ( lang ) ;
2022-11-15 21:16:19 +00:00
}
2022-05-08 15:25:20 +00:00
2023-09-14 06:53:48 +00:00
const audioLanguages = player . getAudioLanguages ( ) ;
if ( audioLanguages . length > 1 ) {
const overflowMenuButtons = this . $ui . getConfiguration ( ) . overflowMenuButtons ;
// append language menu on index 1
const newOverflowMenuButtons = [
... overflowMenuButtons . slice ( 0 , 1 ) ,
"language" ,
... overflowMenuButtons . slice ( 1 ) ,
] ;
this . $ui . configure ( "overflowMenuButtons" , newOverflowMenuButtons ) ;
}
2023-08-29 17:07:30 +00:00
if ( qualityConds ) {
var leastDiff = Number . MAX _VALUE ;
var bestStream = null ;
var bestAudio = 0 ;
const tracks = player
. getVariantTracks ( )
. filter ( track => track . language == lang || track . language == "und" ) ;
// Choose the best audio stream
if ( quality >= 480 )
tracks . forEach ( track => {
const audioBandwidth = track . audioBandwidth ;
if ( audioBandwidth > bestAudio ) bestAudio = audioBandwidth ;
} ) ;
// Find best matching stream based on resolution and bitrate
tracks
. sort ( ( a , b ) => a . bandwidth - b . bandwidth )
. forEach ( stream => {
if ( stream . audioBandwidth < bestAudio ) return ;
const diff = Math . abs ( quality - stream . height ) ;
if ( diff < leastDiff ) {
leastDiff = diff ;
bestStream = stream ;
}
} ) ;
player . selectVariantTrack ( bestStream ) ;
}
2021-06-21 20:03:11 +00:00
2023-08-29 17:07:30 +00:00
this . video . subtitles . map ( subtitle => {
player . addTextTrackAsync (
subtitle . url ,
subtitle . code ,
"subtitles" ,
subtitle . mimeType ,
null ,
subtitle . name ,
) ;
} ) ;
videoEl . volume = this . getPreferenceNumber ( "volume" , 1 ) ;
const rate = this . getPreferenceNumber ( "rate" , 1 ) ;
videoEl . playbackRate = rate ;
videoEl . defaultPlaybackRate = rate ;
const autoDisplayCaptions = this . getPreferenceBoolean ( "autoDisplayCaptions" , false ) ;
this . $player . setTextTrackVisibility ( autoDisplayCaptions ) ;
2023-09-14 06:51:50 +00:00
const prefSubtitles = this . getPreferenceString ( "subtitles" , "" ) ;
if ( prefSubtitles !== "" ) {
const textTracks = this . $player . getTextTracks ( ) ;
const subtitleIdx = textTracks . findIndex ( textTrack => textTrack . language == prefSubtitles ) ;
if ( subtitleIdx != - 1 ) {
this . $player . setTextTrackVisibility ( true ) ;
this . $player . selectTextTrack ( textTracks [ subtitleIdx ] ) ;
}
}
2023-08-29 17:07:30 +00:00
} )
. catch ( e => {
console . error ( e ) ;
this . error = e . code ;
2021-05-06 17:30:02 +00:00
} ) ;
2023-01-27 17:13:20 +00:00
// expand the player to fullscreen when the fullscreen query equals true
if ( this . $route . query . fullscreen === "true" && ! this . $ui . getControls ( ) . isFullScreenEnabled ( ) )
this . $ui . getControls ( ) . toggleFullScreen ( ) ;
2021-05-06 17:30:02 +00:00
} ,
2021-10-27 00:15:37 +00:00
async updateProgressDatabase ( time ) {
// debounce
if ( new Date ( ) . getTime ( ) - this . lastUpdate < 500 ) return ;
this . lastUpdate = new Date ( ) . getTime ( ) ;
2022-01-13 22:41:53 +00:00
if ( ! this . initialSeekComplete || ! this . video . id || ! window . db ) return ;
2021-10-27 00:15:37 +00:00
var tx = window . db . transaction ( "watch_history" , "readwrite" ) ;
var store = tx . objectStore ( "watch_history" ) ;
2021-11-11 08:16:00 +00:00
var request = store . get ( this . video . id ) ;
2022-01-01 14:53:55 +00:00
request . onsuccess = function ( event ) {
2021-10-27 00:15:37 +00:00
var video = event . target . result ;
if ( video ) {
video . currentTime = time ;
store . put ( video ) ;
}
} ;
} ,
2022-01-13 04:52:14 +00:00
seek ( time ) {
if ( this . $refs . videoEl ) {
this . $refs . videoEl . currentTime = time ;
}
} ,
2023-07-19 21:05:00 +00:00
2022-06-06 02:18:47 +00:00
updateMarkers ( ) {
const markers = this . $refs . container . querySelector ( ".shaka-ad-markers" ) ;
const array = [ "to right" ] ;
2022-06-06 02:41:13 +00:00
this . sponsors ? . segments ? . forEach ( segment => {
2022-06-06 02:18:47 +00:00
const start = ( segment . segment [ 0 ] / this . video . duration ) * 100 ;
const end = ( segment . segment [ 1 ] / this . video . duration ) * 100 ;
2023-07-19 21:05:00 +00:00
var color = [
"sponsor" ,
"selfpromo" ,
"interaction" ,
"poi_highlight" ,
"intro" ,
"outro" ,
"preview" ,
"filler" ,
"music_offtopic" ,
] . includes ( segment . category )
? ` var(--spon-seg- ${ segment . category } ) `
: "var(--spon-seg-default)" ;
2022-06-06 02:18:47 +00:00
array . push ( ` transparent ${ start } % ` ) ;
array . push ( ` ${ color } ${ start } % ` ) ;
array . push ( ` ${ color } ${ end } % ` ) ;
array . push ( ` transparent ${ end } % ` ) ;
} ) ;
if ( array . length <= 1 ) {
return ;
}
2022-06-27 04:26:47 +00:00
if ( markers ) markers . style . background = ` linear-gradient( ${ array . join ( "," ) } ) ` ;
2022-06-06 02:18:47 +00:00
} ,
2023-03-14 22:08:16 +00:00
updateSponsors ( ) {
2022-06-06 02:18:47 +00:00
if ( this . getPreferenceBoolean ( "showMarkers" , true ) ) {
this . shakaPromise . then ( ( ) => {
this . updateMarkers ( ) ;
} ) ;
}
} ,
2023-03-26 15:56:50 +00:00
setupSeekbarPreview ( ) {
if ( ! this . video . previewFrames ) return ;
2023-04-16 23:05:06 +00:00
let seekBar = document . querySelector ( ".shaka-seek-bar" ) ;
2023-03-26 15:56:50 +00:00
// load the thumbnail preview when the user moves over the seekbar
seekBar . addEventListener ( "mousemove" , e => {
2023-04-22 09:32:06 +00:00
this . isHoveringTimebar = true ;
2023-04-16 23:00:32 +00:00
const position = ( e . offsetX / e . target . offsetWidth ) * this . video . duration ;
2023-03-26 15:56:50 +00:00
this . showSeekbarPreview ( position * 1000 ) ;
} ) ;
// hide the preview when the user stops hovering the seekbar
seekBar . addEventListener ( "mouseout" , ( ) => {
2023-04-22 09:32:06 +00:00
this . isHoveringTimebar = false ;
2023-06-27 18:27:43 +00:00
this . $refs . previewContainer . style . display = "none" ;
2023-03-26 15:56:50 +00:00
} ) ;
} ,
async showSeekbarPreview ( position ) {
2023-05-01 22:06:19 +00:00
const frame = this . getFrame ( position ) ;
const originalImage = await this . loadImage ( frame . url ) ;
2023-04-22 09:32:06 +00:00
if ( ! this . isHoveringTimebar ) return ;
2023-05-01 22:06:19 +00:00
const seekBar = document . querySelector ( ".shaka-seek-bar" ) ;
2023-06-27 18:27:43 +00:00
const container = this . $refs . previewContainer ;
const canvas = this . $refs . preview ;
2023-05-01 22:06:19 +00:00
const ctx = canvas . getContext ( "2d" ) ;
2023-03-26 15:56:50 +00:00
2023-06-27 18:27:43 +00:00
const offsetX = frame . positionX * frame . frameWidth ;
const offsetY = frame . positionY * frame . frameHeight ;
2023-05-01 22:06:19 +00:00
canvas . width = frame . frameWidth > 100 ? frame . frameWidth : frame . frameWidth * 2 ;
canvas . height = frame . frameWidth > 100 ? frame . frameHeight : frame . frameHeight * 2 ;
2023-03-26 15:56:50 +00:00
// draw the thumbnail preview into the canvas by cropping only the relevant part
2023-05-01 22:06:19 +00:00
ctx . drawImage (
originalImage ,
offsetX ,
offsetY ,
frame . frameWidth ,
frame . frameHeight ,
0 ,
0 ,
canvas . width ,
canvas . height ,
) ;
2023-03-26 15:56:50 +00:00
// calculate the thumbnail preview offset and display it
const centerOffset = position / this . video . duration / 10 ;
2023-04-22 09:32:06 +00:00
const left = centerOffset - ( ( 0.5 * canvas . width ) / seekBar . clientWidth ) * 100 ;
2023-06-27 18:27:43 +00:00
const maxLeft =
( ( seekBar . clientWidth - canvas . clientWidth ) / seekBar . clientWidth ) * 100 - this . seekbarPadding ;
this . currentTime = position / 1000 ;
container . style . left = ` max( ${ this . seekbarPadding } %, min( ${ left } %, ${ maxLeft } %)) ` ;
container . style . display = "flex" ;
2023-03-26 15:56:50 +00:00
} ,
// ineffective algorithm to find the thumbnail corresponding to the currently hovered position in the video
getFrame ( position ) {
let startPosition = 0 ;
2023-05-01 22:06:19 +00:00
const framePage = this . video . previewFrames . at ( - 1 ) ;
2023-03-26 15:56:50 +00:00
for ( let i = 0 ; i < framePage . urls . length ; i ++ ) {
for ( let positionY = 0 ; positionY < framePage . framesPerPageY ; positionY ++ ) {
for ( let positionX = 0 ; positionX < framePage . framesPerPageX ; positionX ++ ) {
const endPosition = startPosition + framePage . durationPerFrame ;
if ( position >= startPosition && position <= endPosition ) {
return {
url : framePage . urls [ i ] ,
positionX : positionX ,
positionY : positionY ,
2023-05-01 22:06:19 +00:00
frameWidth : framePage . frameWidth ,
frameHeight : framePage . frameHeight ,
2023-03-26 15:56:50 +00:00
} ;
}
startPosition = endPosition ;
}
}
}
return null ;
} ,
// creates a new image from an URL
loadImage ( url ) {
return new Promise ( r => {
2023-05-01 22:06:19 +00:00
const i = new Image ( ) ;
2023-03-26 15:56:50 +00:00
i . onload = ( ) => r ( i ) ;
i . src = url ;
} ) ;
} ,
2022-01-23 19:08:33 +00:00
destroy ( hotkeys ) {
2023-03-13 03:20:14 +00:00
if ( this . $ui && ! document . pictureInPictureElement ) {
2021-10-06 14:33:52 +00:00
this . $ui . destroy ( ) ;
this . $ui = undefined ;
this . $player = undefined ;
2021-09-05 20:53:59 +00:00
}
2021-10-06 14:33:52 +00:00
if ( this . $player ) {
this . $player . destroy ( ) ;
2023-03-13 03:20:14 +00:00
if ( ! document . pictureInPictureElement ) this . $player = undefined ;
2021-09-05 20:53:59 +00:00
}
2022-06-26 14:15:03 +00:00
if ( hotkeys ) this . $hotkeys ? . unbind ( ) ;
this . $refs . container ? . querySelectorAll ( "div" ) . forEach ( node => node . remove ( ) ) ;
2021-09-05 20:53:59 +00:00
} ,
2022-06-06 02:18:47 +00:00
} ,
2021-04-01 15:13:35 +00:00
} ;
< / script >
2021-10-08 18:52:51 +00:00
< style >
2023-07-19 21:05:00 +00:00
: root {
-- player - base : rgba ( 255 , 255 , 255 , 0.3 ) ;
-- player - buffered : rgba ( 255 , 255 , 255 , 0.54 ) ;
-- player - played : rgba ( 255 , 0 , 0 ) ;
-- spon - seg - sponsor : # 00 d400 ;
-- spon - seg - selfpromo : # ffff00 ;
-- spon - seg - interaction : # cc00ff ;
-- spon - seg - poi _highlight : # ff1684 ;
-- spon - seg - intro : # 00 ffff ;
-- spon - seg - outro : # 0202 ed ;
-- spon - seg - preview : # 008 fd6 ;
-- spon - seg - filler : # 7300 ff ;
-- spon - seg - music _offtopic : # ff9900 ;
-- spon - seg - default : white ;
}
2022-01-22 23:09:29 +00:00
. player - container {
@ apply max - h - 75 vh min - h - 64 bg - black ;
}
2022-01-31 04:34:27 +00:00
. shaka - video - container . material - icons - round {
@ apply ! text - xl ;
}
. shaka - current - time {
@ apply ! text - base ;
2021-10-08 18:52:51 +00:00
}
. shaka - video - container : - webkit - full - screen {
max - height : none ! important ;
}
2022-07-23 16:15:40 +00:00
/* captions style */
. shaka - text - wrapper * {
text - align : left ! important ;
}
. shaka - text - wrapper > span > span {
background - color : transparent ! important ;
}
/* apply to all spans that don't include multiple other spans to avoid the style being applied to the text container too when the subtitles are two lines */
. shaka - text - wrapper > span > span * : first - child : last - child {
background - color : rgba ( 0 , 0 , 0 , 0.6 ) ! important ;
padding : 0.09 em 0 ;
}
2023-02-02 15:02:04 +00:00
. skip - segment - button {
/* position button above player overlay */
z - index : 1000 ;
position : absolute ;
transform : translate ( 0 , - 50 % ) ;
top : 50 % ;
right : 0 ;
background - color : rgb ( 0 0 0 / 0.5 ) ;
border : 2 px rgba ( 255 , 255 , 255 , 0.75 ) solid ;
border - right : 0 ;
border - radius : 0.75 em ;
border - top - right - radius : 0 ;
border - bottom - right - radius : 0 ;
padding : 0.5 em ;
/* center text vertically */
display : flex ;
align - items : center ;
justify - content : center ;
2023-02-08 07:17:18 +00:00
color : # fff ;
2023-02-02 15:02:04 +00:00
line - height : 1.5 em ;
}
. skip - segment - button . material - icons - round {
font - size : 1.6 em ! important ;
line - height : inherit ! important ;
}
2021-10-08 18:52:51 +00:00
< / style >