2021-04-01 20:43:35 +05:30
< template >
2022-02-22 11:43:36 +05:30
< div
ref = "container"
data - shaka - player - container
2023-08-13 23:01:57 +05:30
class = "relative max-h-screen w-full flex justify-center"
2022-02-22 11:43:36 +05:30
: class = "{ 'player-container': !isEmbed }"
>
< video ref = "videoEl" class = "w-full" data -shaka -player :autoplay ="shouldAutoPlay" :loop ="selectedAutoLoop" / >
2023-06-27 23:57:43 +05:30
< span
id = "preview-container"
2023-07-27 17:16:05 +05:30
ref = "previewContainer"
2023-08-13 23:01:57 +05:30
class = "absolute bottom-0 z-[2000] mb-[3.5%] hidden flex-col items-center"
2023-06-27 23:57:43 +05:30
>
2023-07-27 17:16:05 +05:30
< canvas id = "preview" ref = "preview" class = "rounded-sm" / >
2023-08-13 23:01:57 +05:30
< 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 23:57:43 +05:30
< / span >
2023-02-02 20:32:04 +05:30
< 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 22:37:30 +05:30
< span
v - if = "error > 0"
v - t = "{ path: 'player.failed', args: [error] }"
2023-08-30 15:29:11 +05:30
class = "absolute top-8 rounded bg-black/80 p-2 text-lg backdrop-blur-sm"
2023-08-29 22:37:30 +05:30
/ >
2021-04-01 20:43:35 +05:30
< / div >
< / template >
< script >
2023-01-26 05:14:07 +05:30
import "shaka-player/dist/controls.css" ;
2023-05-26 00:13:11 +05:30
import { parseTimeParam } from "@/utils/Misc" ;
2021-04-01 20:43:35 +05:30
const shaka = import ( "shaka-player/dist/shaka-player.ui.js" ) ;
2022-10-20 03:32:00 +05:30
if ( ! window . muxjs ) {
import ( "mux.js" ) . then ( muxjs => {
window . muxjs = muxjs ;
} ) ;
}
2022-01-24 00:38:33 +05:30
const hotkeys = import ( "hotkeys-js" ) ;
2021-04-01 20:43:35 +05:30
export default {
2021-05-06 23:00:02 +05:30
props : {
2021-10-09 00:22:51 +05:30
video : {
type : Object ,
default : ( ) => {
return { } ;
} ,
} ,
sponsors : {
type : Object ,
default : ( ) => {
return { } ;
} ,
} ,
2021-05-06 23:00:02 +05:30
selectedAutoPlay : Boolean ,
2021-07-05 00:12:10 +05:30
selectedAutoLoop : Boolean ,
2021-08-04 10:43:22 +05:30
isEmbed : Boolean ,
2021-05-06 23:00:02 +05:30
} ,
2023-08-08 00:15:54 +05:30
emits : [ "timeupdate" , "ended" , "navigateNext" ] ,
2021-07-07 19:48:09 +05:30
data ( ) {
return {
2021-10-27 05:45:37 +05:30
lastUpdate : new Date ( ) . getTime ( ) ,
2022-01-14 04:11:53 +05:30
initialSeekComplete : false ,
2022-04-09 02:59:50 +05:30
destroying : false ,
2023-02-02 20:32:04 +05:30
inSegment : false ,
2023-04-22 15:02:06 +05:30
isHoveringTimebar : false ,
2023-06-27 23:57:43 +05:30
currentTime : 0 ,
seekbarPadding : 2 ,
2023-08-29 22:37:30 +05:30
error : 0 ,
2021-07-07 19:48:09 +05:30
} ;
} ,
2021-05-06 23:00:02 +05:30
computed : {
2021-07-04 23:56:02 +05:30
shouldAutoPlay : _this => {
2021-10-28 23:05:48 +05:30
return _this . getPreferenceBoolean ( "playerAutoPlay" , true ) && ! _this . isEmbed ;
2021-05-06 23:00:02 +05:30
} ,
2021-07-21 16:18:59 +05:30
preferredVideoCodecs : _this => {
var preferredVideoCodecs = [ ] ;
2022-06-06 10:25:01 +05:30
const enabledCodecs = _this . getPreferenceString ( "enabledCodecs" , "vp9,avc" ) . split ( "," ) ;
2021-07-21 16:18:59 +05:30
2021-08-27 13:03:55 +05:30
if (
_this . $refs . videoEl . canPlayType ( 'video/mp4; codecs="av01.0.08M.08"' ) !== "" &&
enabledCodecs . includes ( "av1" )
)
2021-07-21 16:18:59 +05:30
preferredVideoCodecs . push ( "av01" ) ;
2021-08-27 13:03:55 +05:30
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 16:18:59 +05:30
preferredVideoCodecs . push ( "avc1" ) ;
return preferredVideoCodecs ;
} ,
2021-05-06 23:00:02 +05:30
} ,
2021-07-10 02:25:09 +05:30
mounted ( ) {
2022-05-24 01:48:20 +05:30
if ( ! this . $shaka ) this . shakaPromise = shaka . then ( shaka => shaka . default ) . then ( shaka => ( this . $shaka = shaka ) ) ;
2022-01-24 00:38:33 +05:30
if ( ! this . $hotkeys )
this . hotkeysPromise = hotkeys . then ( mod => mod . default ) . then ( hotkeys => ( this . $hotkeys = hotkeys ) ) ;
2021-07-10 02:25:09 +05:30
} ,
2021-10-09 00:22:51 +05:30
activated ( ) {
2022-05-24 01:48:20 +05:30
this . destroying = false ;
2022-06-26 19:45:03 +05:30
this . sponsors ? . segments ? . forEach ( segment => ( segment . skipped = false ) ) ;
2022-01-24 00:38:33 +05:30
this . hotkeysPromise . then ( ( ) => {
var self = this ;
this . $hotkeys (
2023-03-16 21:28:02 +05:30
"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-24 00:38:33 +05:30
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 15:33:31 +05:30
case "shift+n" :
2023-08-08 00:15:54 +05:30
self . $emit ( "navigateNext" ) ;
2022-08-06 15:33:31 +05:30
e . preventDefault ( ) ;
break ;
2022-01-24 00:38:33 +05:30
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 21:28:02 +05:30
case "alt+p" :
document . pictureInPictureElement
? document . exitPictureInPicture ( )
: videoEl . requestPictureInPicture ( ) ;
break ;
2023-02-01 21:07:43 +05:30
case "return" :
self . skipSegment ( videoEl ) ;
break ;
2023-03-16 21:05:23 +05:30
case "." :
videoEl . currentTime += 0.04 ;
e . preventDefault ( ) ;
break ;
case "," :
videoEl . currentTime -= 0.04 ;
e . preventDefault ( ) ;
break ;
2022-01-24 00:38:33 +05:30
}
} ,
) ;
} ) ;
2021-10-09 00:22:51 +05:30
} ,
deactivated ( ) {
2022-04-09 02:59:50 +05:30
this . destroying = true ;
2022-01-24 00:38:33 +05:30
this . destroy ( true ) ;
2021-10-09 00:22:51 +05:30
} ,
unmounted ( ) {
2022-04-09 02:59:50 +05:30
this . destroying = true ;
2022-01-24 00:38:33 +05:30
this . destroy ( true ) ;
2021-10-09 00:22:51 +05:30
} ,
2021-05-06 23:00:02 +05:30
methods : {
2021-09-05 18:42:27 +05:30
async loadVideo ( ) {
2023-03-15 03:38:16 +05:30
this . updateSponsors ( ) ;
2021-06-10 02:51:35 +05:30
const component = this ;
2021-05-06 23:00:02 +05:30
const videoEl = this . $refs . videoEl ;
2021-04-01 20:43:35 +05:30
2021-05-06 23:00:02 +05:30
videoEl . setAttribute ( "poster" , this . video . thumbnailUrl ) ;
2021-04-01 20:43:35 +05:30
2021-10-06 20:03:52 +05:30
const noPrevPlayer = ! this . $player ;
2021-05-06 23:00:02 +05:30
var streams = [ ] ;
streams . push ( ... this . video . audioStreams ) ;
streams . push ( ... this . video . videoStreams ) ;
2021-07-08 01:04:46 +05:30
const MseSupport = window . MediaSource !== undefined ;
2023-03-04 10:12:03 +05:30
const lbry = null ;
2021-07-28 13:32:23 +05:30
2021-06-10 02:51:35 +05:30
var uri ;
2021-09-05 18:42:27 +05:30
var mime ;
2021-06-10 02:51:35 +05:30
2021-08-30 21:18:08 +05:30
if ( this . video . livestream ) {
2021-06-10 02:51:35 +05:30
uri = this . video . hls ;
2021-09-05 18:42:27 +05:30
mime = "application/x-mpegURL" ;
2021-08-30 21:18:08 +05:30
} else if ( this . video . audioStreams . length > 0 && ! lbry && MseSupport ) {
2021-08-16 01:24:34 +05:30
if ( ! this . video . dash ) {
2023-03-02 19:48:53 +05:30
const dash = ( await import ( "../utils/DashUtils.js" ) ) . generate _dash _file _from _formats (
streams ,
this . video . duration ,
) ;
2021-06-10 02:51:35 +05:30
2021-08-16 01:24:34 +05:30
uri = "data:application/dash+xml;charset=utf-8;base64," + btoa ( dash ) ;
2022-12-03 02:41:22 +05:30
} else {
const url = new URL ( this . video . dash ) ;
url . searchParams . set ( "rewrite" , false ) ;
uri = url . toString ( ) ;
}
2021-09-05 18:42:27 +05:30
mime = "application/dash+xml" ;
2021-07-28 13:32:23 +05:30
} else if ( lbry ) {
uri = lbry . url ;
2021-09-02 19:16:27 +05:30
if ( this . getPreferenceBoolean ( "proxyLBRY" , false ) ) {
const url = new URL ( uri ) ;
2021-11-24 23:06:29 +05:30
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 19:16:27 +05:30
url . searchParams . set ( "host" , url . host ) ;
2021-11-24 23:06:29 +05:30
url . protocol = proxyURL . protocol ;
url . host = proxyURL . host ;
url . pathname = proxyPath + url . pathname ;
2021-09-02 19:16:27 +05:30
uri = url . toString ( ) ;
}
2021-09-05 18:42:27 +05:30
const contentType = await fetch ( uri , {
method : "HEAD" ,
2022-05-04 14:49:13 +05:30
} ) . then ( response => {
uri = response . url ;
2022-05-05 23:32:25 +05:30
return response . headers . get ( "Content-Type" ) ;
2022-05-04 14:49:13 +05:30
} ) ;
2021-09-05 18:42:27 +05:30
mime = contentType ;
2021-11-08 00:10:42 +05:30
} else if ( this . video . hls ) {
uri = this . video . hls ;
mime = "application/x-mpegURL" ;
2021-06-18 18:50:41 +05:30
} else {
2023-03-04 13:33:45 +05:30
uri = this . video . videoStreams . findLast ( stream => stream . codec == null ) . url ;
2021-09-05 18:42:27 +05:30
mime = "video/mp4" ;
2021-06-10 02:51:35 +05:30
}
2021-05-06 23:00:02 +05:30
if ( noPrevPlayer )
2023-11-16 20:32:21 +05:30
this . shakaPromise . then ( async ( ) => {
2022-04-09 02:59:50 +05:30
if ( this . destroying ) return ;
2022-05-24 01:48:20 +05:30
this . $shaka . polyfill . installAll ( ) ;
2021-07-10 02:25:09 +05:30
2023-11-16 20:32:21 +05:30
const localPlayer = new this . $shaka . Player ( ) ;
await localPlayer . attach ( videoEl ) ;
2021-11-24 23:06:29 +05:30
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-10 02:25:09 +05:30
localPlayer . getNetworkingEngine ( ) . registerRequestFilter ( ( _type , request ) => {
const uri = request . uris [ 0 ] ;
var url = new URL ( uri ) ;
2021-09-05 18:42:27 +05:30
const headers = request . headers ;
if (
url . host . endsWith ( ".googlevideo.com" ) ||
( url . host . endsWith ( ".lbryplayer.xyz" ) &&
( component . getPreferenceBoolean ( "proxyLBRY" , false ) || headers . Range ) )
) {
2021-07-10 02:25:09 +05:30
url . searchParams . set ( "host" , url . host ) ;
2021-11-24 23:06:29 +05:30
url . protocol = proxyURL . protocol ;
url . host = proxyURL . host ;
url . pathname = proxyPath + url . pathname ;
2021-07-10 02:25:09 +05:30
request . uris [ 0 ] = url . toString ( ) ;
}
2021-11-24 23:06:29 +05:30
if ( url . pathname === proxyPath + "/videoplayback" ) {
2021-08-26 02:02:56 +05:30
if ( headers . Range ) {
url . searchParams . set ( "range" , headers . Range . split ( "=" ) [ 1 ] ) ;
request . headers = { } ;
request . uris [ 0 ] = url . toString ( ) ;
}
}
2021-07-10 02:25:09 +05:30
} ) ;
2021-06-10 02:51:35 +05:30
2021-07-10 02:25:09 +05:30
localPlayer . configure (
"streaming.bufferingGoal" ,
Math . max ( this . getPreferenceNumber ( "bufferGoal" , 10 ) , 10 ) ,
) ;
2021-06-22 16:24:20 +05:30
2022-05-24 01:48:20 +05:30
this . setPlayerAttrs ( localPlayer , videoEl , uri , mime , this . $shaka ) ;
2021-07-10 02:25:09 +05:30
} ) ;
2022-05-24 01:48:20 +05:30
else this . setPlayerAttrs ( this . $player , videoEl , uri , mime , this . $shaka ) ;
2021-05-06 23:00:02 +05:30
if ( noPrevPlayer ) {
2023-03-13 08:50:14 +05:30
videoEl . addEventListener ( "loadeddata" , ( ) => {
if ( document . pictureInPictureElement ) videoEl . requestPictureInPicture ( ) ;
} ) ;
2021-05-06 23:00:02 +05:30
videoEl . addEventListener ( "timeupdate" , ( ) => {
2021-10-27 05:45:37 +05:30
const time = videoEl . currentTime ;
2022-07-19 21:59:03 +05:30
this . $emit ( "timeupdate" , time ) ;
2021-10-27 05:45:37 +05:30
this . updateProgressDatabase ( time ) ;
2021-05-06 23:00:02 +05:30
if ( this . sponsors && this . sponsors . segments ) {
2023-02-01 21:07:43 +05:30
const segment = this . findCurrentSegment ( time ) ;
2023-02-02 20:32:04 +05:30
this . inSegment = ! ! segment ;
2023-02-01 21:07:43 +05:30
if ( segment ? . autoskip && ( ! segment . skipped || this . selectedAutoLoop ) ) {
this . skipSegment ( videoEl , segment ) ;
}
2021-05-06 23:00:02 +05:30
}
} ) ;
videoEl . addEventListener ( "volumechange" , ( ) => {
2022-11-17 00:21:56 +05:30
this . setPreference ( "volume" , videoEl . volume , true ) ;
2021-05-06 23:00:02 +05:30
} ) ;
2022-01-16 14:13:24 +05:30
videoEl . addEventListener ( "ratechange" , e => {
const rate = videoEl . playbackRate ;
if ( rate > 0 && ! isNaN ( videoEl . duration ) && ! isNaN ( videoEl . duration - e . timeStamp / 1000 ) )
2022-11-17 00:21:56 +05:30
this . setPreference ( "rate" , rate , true ) ;
2021-10-08 22:08:01 +05:30
} ) ;
2021-10-04 00:48:04 +05:30
2021-05-06 23:00:02 +05:30
videoEl . addEventListener ( "ended" , ( ) => {
2023-03-03 03:25:23 +05:30
this . $emit ( "ended" ) ;
2021-05-06 23:00:02 +05:30
} ) ;
}
//TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
} ,
2023-02-01 21:07:43 +05:30
findCurrentSegment ( time ) {
return this . sponsors ? . segments ? . find ( s => time >= s . segment [ 0 ] && time < s . segment [ 1 ] ) ;
} ,
2023-02-02 20:32:04 +05:30
onClickSkipSegment ( ) {
const videoEl = this . $refs . videoEl ;
this . skipSegment ( videoEl ) ;
} ,
2023-02-01 21:07:43 +05:30
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 ;
} ,
2021-09-05 18:42:27 +05:30
setPlayerAttrs ( localPlayer , videoEl , uri , mime , shaka ) {
2021-11-11 13:46:00 +05:30
const url = "/watch?v=" + this . video . id ;
2021-10-06 20:03:52 +05:30
if ( ! this . $ui ) {
2022-01-23 04:40:05 +05:30
this . destroy ( ) ;
2021-11-11 13:46:00 +05:30
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 20:03:52 +05:30
this . $ui = new shaka . ui . Overlay ( localPlayer , this . $refs . container , videoEl ) ;
2021-06-08 00:52:29 +05:30
2023-09-14 12:23:48 +05:30
const overflowMenuButtons = [ "quality" , "captions" , "picture_in_picture" , "playback_rate" , "airplay" ] ;
2021-11-11 13:46:00 +05:30
if ( this . isEmbed ) {
overflowMenuButtons . push ( "open_new_tab" ) ;
}
2021-06-08 00:52:29 +05:30
const config = {
2021-11-11 13:46:00 +05:30
overflowMenuButtons : overflowMenuButtons ,
2021-06-08 00:52:29 +05:30
seekBarColors : {
2023-07-20 02:35:00 +05:30
base : "var(--player-base)" ,
buffered : "var(--player-buffered)" ,
played : "var(--player-played)" ,
2021-06-08 00:52:29 +05:30
} ,
} ;
2021-10-06 20:03:52 +05:30
this . $ui . configure ( config ) ;
2021-06-08 00:52:29 +05:30
}
2022-06-06 07:48:47 +05:30
this . updateMarkers ( ) ;
2023-01-22 04:20:56 +05:30
const event = new Event ( "playerInit" ) ;
window . dispatchEvent ( event ) ;
2021-10-06 20:03:52 +05:30
const player = this . $ui . getControls ( ) . getPlayer ( ) ;
2021-06-08 00:52:29 +05:30
2023-03-26 21:26:50 +05:30
this . setupSeekbarPreview ( ) ;
2021-10-06 20:03:52 +05:30
this . $player = player ;
2021-06-08 00:52:29 +05:30
2021-07-04 00:54:09 +05:30
const disableVideo = this . getPreferenceBoolean ( "listen" , false ) && ! this . video . livestream ;
2021-07-15 14:11:36 +05:30
2021-10-06 20:03:52 +05:30
this . $player . configure ( {
2021-07-21 16:18:59 +05:30
preferredVideoCodecs : this . preferredVideoCodecs ,
2021-07-15 14:11:36 +05:30
preferredAudioCodecs : [ "opus" , "mp4a" ] ,
manifest : {
disableVideo : disableVideo ,
} ,
2023-08-31 14:51:57 +05:30
streaming : {
segmentPrefetchLimit : 10 ,
} ,
2021-07-15 14:11:36 +05:30
} ) ;
2021-06-08 02:05:45 +05:30
2021-07-04 23:56:02 +05:30
const quality = this . getPreferenceNumber ( "quality" , 0 ) ;
2021-07-15 14:11:36 +05:30
const qualityConds =
quality > 0 && ( this . video . audioStreams . length > 0 || this . video . livestream ) && ! disableVideo ;
2021-10-06 20:03:52 +05:30
if ( qualityConds ) this . $player . configure ( "abr.enabled" , false ) ;
2021-06-22 01:33:11 +05:30
2023-11-24 00:38:52 +05:30
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 ) ) {
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 ;
}
}
} ;
tx . oncomplete = ( ) => {
this . initialSeekComplete = true ;
} ;
} else {
this . initialSeekComplete = true ;
}
2023-08-29 22:37:30 +05:30
player
2023-11-24 00:38:52 +05:30
. load ( uri , startTime , mime )
2023-08-29 22:37:30 +05:30
. 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 23:01:34 +05:30
}
2023-08-29 22:37:30 +05:30
player . selectAudioLanguage ( lang ) ;
2022-11-16 02:46:19 +05:30
}
2022-05-08 20:55:20 +05:30
2023-09-14 12:23:48 +05:30
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 22:37:30 +05:30
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-22 01:33:11 +05:30
2023-08-29 22:37:30 +05:30
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 12:21:50 +05:30
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 22:37:30 +05:30
} )
. catch ( e => {
console . error ( e ) ;
this . error = e . code ;
2021-05-06 23:00:02 +05:30
} ) ;
2023-01-27 22:43:20 +05:30
// 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 23:00:02 +05:30
} ,
2021-10-27 05:45:37 +05:30
async updateProgressDatabase ( time ) {
// debounce
if ( new Date ( ) . getTime ( ) - this . lastUpdate < 500 ) return ;
this . lastUpdate = new Date ( ) . getTime ( ) ;
2022-01-14 04:11:53 +05:30
if ( ! this . initialSeekComplete || ! this . video . id || ! window . db ) return ;
2021-10-27 05:45:37 +05:30
var tx = window . db . transaction ( "watch_history" , "readwrite" ) ;
var store = tx . objectStore ( "watch_history" ) ;
2021-11-11 13:46:00 +05:30
var request = store . get ( this . video . id ) ;
2022-01-01 20:23:55 +05:30
request . onsuccess = function ( event ) {
2021-10-27 05:45:37 +05:30
var video = event . target . result ;
if ( video ) {
video . currentTime = time ;
store . put ( video ) ;
}
} ;
} ,
2022-01-13 10:22:14 +05:30
seek ( time ) {
if ( this . $refs . videoEl ) {
this . $refs . videoEl . currentTime = time ;
}
} ,
2023-07-20 02:35:00 +05:30
2022-06-06 07:48:47 +05:30
updateMarkers ( ) {
const markers = this . $refs . container . querySelector ( ".shaka-ad-markers" ) ;
const array = [ "to right" ] ;
2022-06-06 08:11:13 +05:30
this . sponsors ? . segments ? . forEach ( segment => {
2022-06-06 07:48:47 +05:30
const start = ( segment . segment [ 0 ] / this . video . duration ) * 100 ;
const end = ( segment . segment [ 1 ] / this . video . duration ) * 100 ;
2023-07-20 02:35:00 +05:30
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 07:48:47 +05:30
array . push ( ` transparent ${ start } % ` ) ;
array . push ( ` ${ color } ${ start } % ` ) ;
array . push ( ` ${ color } ${ end } % ` ) ;
array . push ( ` transparent ${ end } % ` ) ;
} ) ;
if ( array . length <= 1 ) {
return ;
}
2022-06-27 09:56:47 +05:30
if ( markers ) markers . style . background = ` linear-gradient( ${ array . join ( "," ) } ) ` ;
2022-06-06 07:48:47 +05:30
} ,
2023-03-15 03:38:16 +05:30
updateSponsors ( ) {
2022-06-06 07:48:47 +05:30
if ( this . getPreferenceBoolean ( "showMarkers" , true ) ) {
this . shakaPromise . then ( ( ) => {
this . updateMarkers ( ) ;
} ) ;
}
} ,
2023-03-26 21:26:50 +05:30
setupSeekbarPreview ( ) {
if ( ! this . video . previewFrames ) return ;
2023-04-17 04:35:06 +05:30
let seekBar = document . querySelector ( ".shaka-seek-bar" ) ;
2023-03-26 21:26:50 +05:30
// load the thumbnail preview when the user moves over the seekbar
seekBar . addEventListener ( "mousemove" , e => {
2023-04-22 15:02:06 +05:30
this . isHoveringTimebar = true ;
2023-04-17 04:30:32 +05:30
const position = ( e . offsetX / e . target . offsetWidth ) * this . video . duration ;
2023-03-26 21:26:50 +05:30
this . showSeekbarPreview ( position * 1000 ) ;
} ) ;
// hide the preview when the user stops hovering the seekbar
seekBar . addEventListener ( "mouseout" , ( ) => {
2023-04-22 15:02:06 +05:30
this . isHoveringTimebar = false ;
2023-06-27 23:57:43 +05:30
this . $refs . previewContainer . style . display = "none" ;
2023-03-26 21:26:50 +05:30
} ) ;
} ,
async showSeekbarPreview ( position ) {
2023-05-02 03:36:19 +05:30
const frame = this . getFrame ( position ) ;
const originalImage = await this . loadImage ( frame . url ) ;
2023-04-22 15:02:06 +05:30
if ( ! this . isHoveringTimebar ) return ;
2023-05-02 03:36:19 +05:30
const seekBar = document . querySelector ( ".shaka-seek-bar" ) ;
2023-06-27 23:57:43 +05:30
const container = this . $refs . previewContainer ;
const canvas = this . $refs . preview ;
2023-05-02 03:36:19 +05:30
const ctx = canvas . getContext ( "2d" ) ;
2023-03-26 21:26:50 +05:30
2023-06-27 23:57:43 +05:30
const offsetX = frame . positionX * frame . frameWidth ;
const offsetY = frame . positionY * frame . frameHeight ;
2023-05-02 03:36:19 +05:30
canvas . width = frame . frameWidth > 100 ? frame . frameWidth : frame . frameWidth * 2 ;
canvas . height = frame . frameWidth > 100 ? frame . frameHeight : frame . frameHeight * 2 ;
2023-03-26 21:26:50 +05:30
// draw the thumbnail preview into the canvas by cropping only the relevant part
2023-05-02 03:36:19 +05:30
ctx . drawImage (
originalImage ,
offsetX ,
offsetY ,
frame . frameWidth ,
frame . frameHeight ,
0 ,
0 ,
canvas . width ,
canvas . height ,
) ;
2023-03-26 21:26:50 +05:30
// calculate the thumbnail preview offset and display it
const centerOffset = position / this . video . duration / 10 ;
2023-04-22 15:02:06 +05:30
const left = centerOffset - ( ( 0.5 * canvas . width ) / seekBar . clientWidth ) * 100 ;
2023-06-27 23:57:43 +05:30
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 21:26:50 +05:30
} ,
// ineffective algorithm to find the thumbnail corresponding to the currently hovered position in the video
getFrame ( position ) {
let startPosition = 0 ;
2023-05-02 03:36:19 +05:30
const framePage = this . video . previewFrames . at ( - 1 ) ;
2023-03-26 21:26:50 +05:30
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-02 03:36:19 +05:30
frameWidth : framePage . frameWidth ,
frameHeight : framePage . frameHeight ,
2023-03-26 21:26:50 +05:30
} ;
}
startPosition = endPosition ;
}
}
}
return null ;
} ,
// creates a new image from an URL
loadImage ( url ) {
return new Promise ( r => {
2023-05-02 03:36:19 +05:30
const i = new Image ( ) ;
2023-03-26 21:26:50 +05:30
i . onload = ( ) => r ( i ) ;
i . src = url ;
} ) ;
} ,
2022-01-24 00:38:33 +05:30
destroy ( hotkeys ) {
2023-03-13 08:50:14 +05:30
if ( this . $ui && ! document . pictureInPictureElement ) {
2021-10-06 20:03:52 +05:30
this . $ui . destroy ( ) ;
this . $ui = undefined ;
this . $player = undefined ;
2021-09-06 02:23:59 +05:30
}
2021-10-06 20:03:52 +05:30
if ( this . $player ) {
this . $player . destroy ( ) ;
2023-03-13 08:50:14 +05:30
if ( ! document . pictureInPictureElement ) this . $player = undefined ;
2021-09-06 02:23:59 +05:30
}
2022-06-26 19:45:03 +05:30
if ( hotkeys ) this . $hotkeys ? . unbind ( ) ;
this . $refs . container ? . querySelectorAll ( "div" ) . forEach ( node => node . remove ( ) ) ;
2021-09-06 02:23:59 +05:30
} ,
2022-06-06 07:48:47 +05:30
} ,
2021-04-01 20:43:35 +05:30
} ;
< / script >
2021-10-09 00:22:51 +05:30
< style >
2023-07-20 02:35:00 +05:30
: 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-23 04:39:29 +05:30
. player - container {
@ apply max - h - 75 vh min - h - 64 bg - black ;
}
2022-01-31 10:04:27 +05:30
. shaka - video - container . material - icons - round {
@ apply ! text - xl ;
}
. shaka - current - time {
@ apply ! text - base ;
2021-10-09 00:22:51 +05:30
}
. shaka - video - container : - webkit - full - screen {
max - height : none ! important ;
}
2022-07-23 21:45:40 +05:30
/* 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 20:32:04 +05:30
. 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 12:47:18 +05:30
color : # fff ;
2023-02-02 20:32:04 +05:30
line - height : 1.5 em ;
}
. skip - segment - button . material - icons - round {
font - size : 1.6 em ! important ;
line - height : inherit ! important ;
}
2021-10-09 00:22:51 +05:30
< / style >