core.js 748 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014150151501615017150181501915020150211502215023150241502515026150271502815029150301503115032150331503415035150361503715038150391504015041150421504315044150451504615047150481504915050150511505215053150541505515056150571505815059150601506115062150631506415065150661506715068150691507015071150721507315074150751507615077150781507915080150811508215083150841508515086150871508815089150901509115092150931509415095150961509715098150991510015101151021510315104151051510615107151081510915110151111511215113151141511515116151171511815119151201512115122151231512415125151261512715128151291513015131151321513315134151351513615137151381513915140151411514215143151441514515146151471514815149151501515115152151531515415155151561515715158151591516015161151621516315164151651516615167151681516915170151711517215173151741517515176151771517815179151801518115182151831518415185151861518715188151891519015191151921519315194151951519615197151981519915200152011520215203152041520515206152071520815209152101521115212152131521415215152161521715218152191522015221152221522315224152251522615227152281522915230152311523215233152341523515236152371523815239152401524115242152431524415245152461524715248152491525015251152521525315254152551525615257152581525915260152611526215263152641526515266152671526815269152701527115272152731527415275152761527715278152791528015281152821528315284152851528615287152881528915290152911529215293152941529515296152971529815299153001530115302153031530415305153061530715308153091531015311153121531315314153151531615317153181531915320153211532215323153241532515326153271532815329153301533115332153331533415335153361533715338153391534015341153421534315344153451534615347153481534915350153511535215353153541535515356153571535815359153601536115362153631536415365153661536715368153691537015371153721537315374153751537615377153781537915380153811538215383153841538515386153871538815389153901539115392153931539415395153961539715398153991540015401154021540315404154051540615407154081540915410154111541215413154141541515416154171541815419154201542115422154231542415425154261542715428154291543015431154321543315434154351543615437154381543915440154411544215443154441544515446154471544815449154501545115452154531545415455154561545715458154591546015461154621546315464154651546615467154681546915470154711547215473154741547515476154771547815479154801548115482154831548415485154861548715488154891549015491154921549315494154951549615497154981549915500155011550215503155041550515506155071550815509155101551115512155131551415515155161551715518155191552015521155221552315524155251552615527155281552915530155311553215533155341553515536155371553815539155401554115542155431554415545155461554715548155491555015551155521555315554155551555615557155581555915560155611556215563155641556515566155671556815569155701557115572155731557415575155761557715578155791558015581155821558315584155851558615587155881558915590155911559215593155941559515596155971559815599156001560115602156031560415605156061560715608156091561015611156121561315614156151561615617156181561915620156211562215623156241562515626156271562815629156301563115632156331563415635156361563715638156391564015641156421564315644156451564615647156481564915650156511565215653156541565515656156571565815659156601566115662156631566415665156661566715668156691567015671156721567315674156751567615677156781567915680156811568215683156841568515686156871568815689156901569115692156931569415695156961569715698156991570015701157021570315704157051570615707157081570915710157111571215713157141571515716157171571815719157201572115722157231572415725157261572715728157291573015731157321573315734157351573615737157381573915740157411574215743157441574515746157471574815749157501575115752157531575415755157561575715758157591576015761157621576315764157651576615767157681576915770157711577215773157741577515776157771577815779157801578115782157831578415785157861578715788157891579015791157921579315794157951579615797157981579915800158011580215803158041580515806158071580815809158101581115812158131581415815158161581715818158191582015821158221582315824158251582615827158281582915830158311583215833158341583515836158371583815839158401584115842158431584415845158461584715848158491585015851158521585315854158551585615857158581585915860158611586215863158641586515866158671586815869158701587115872158731587415875158761587715878158791588015881158821588315884158851588615887158881588915890158911589215893158941589515896158971589815899159001590115902159031590415905159061590715908159091591015911159121591315914159151591615917159181591915920159211592215923159241592515926159271592815929159301593115932159331593415935159361593715938159391594015941159421594315944159451594615947159481594915950159511595215953159541595515956159571595815959159601596115962159631596415965159661596715968159691597015971159721597315974159751597615977159781597915980159811598215983159841598515986159871598815989159901599115992159931599415995159961599715998159991600016001160021600316004160051600616007160081600916010160111601216013160141601516016160171601816019160201602116022160231602416025160261602716028160291603016031160321603316034160351603616037160381603916040160411604216043160441604516046160471604816049160501605116052160531605416055160561605716058160591606016061160621606316064160651606616067160681606916070160711607216073160741607516076160771607816079160801608116082160831608416085160861608716088160891609016091160921609316094160951609616097160981609916100161011610216103161041610516106161071610816109161101611116112161131611416115161161611716118161191612016121161221612316124161251612616127161281612916130161311613216133161341613516136161371613816139161401614116142161431614416145161461614716148161491615016151161521615316154161551615616157161581615916160161611616216163161641616516166161671616816169161701617116172161731617416175161761617716178161791618016181161821618316184161851618616187161881618916190161911619216193161941619516196161971619816199162001620116202162031620416205162061620716208162091621016211162121621316214162151621616217162181621916220162211622216223162241622516226162271622816229162301623116232162331623416235162361623716238162391624016241162421624316244162451624616247162481624916250162511625216253162541625516256162571625816259162601626116262162631626416265162661626716268162691627016271162721627316274162751627616277162781627916280162811628216283162841628516286162871628816289162901629116292162931629416295162961629716298162991630016301163021630316304163051630616307163081630916310163111631216313163141631516316163171631816319163201632116322163231632416325163261632716328163291633016331163321633316334163351633616337163381633916340163411634216343163441634516346163471634816349163501635116352163531635416355163561635716358163591636016361163621636316364163651636616367163681636916370163711637216373163741637516376163771637816379163801638116382163831638416385163861638716388163891639016391163921639316394163951639616397163981639916400164011640216403164041640516406164071640816409164101641116412164131641416415164161641716418164191642016421164221642316424164251642616427164281642916430164311643216433164341643516436164371643816439164401644116442164431644416445164461644716448164491645016451164521645316454164551645616457164581645916460164611646216463164641646516466164671646816469164701647116472164731647416475164761647716478164791648016481164821648316484164851648616487164881648916490164911649216493164941649516496164971649816499165001650116502165031650416505165061650716508165091651016511165121651316514165151651616517165181651916520165211652216523165241652516526165271652816529165301653116532165331653416535165361653716538165391654016541165421654316544165451654616547165481654916550165511655216553165541655516556165571655816559165601656116562165631656416565165661656716568165691657016571165721657316574165751657616577165781657916580165811658216583165841658516586165871658816589165901659116592165931659416595165961659716598165991660016601166021660316604166051660616607166081660916610166111661216613166141661516616166171661816619166201662116622166231662416625166261662716628166291663016631166321663316634166351663616637166381663916640166411664216643166441664516646166471664816649166501665116652166531665416655166561665716658166591666016661166621666316664166651666616667166681666916670166711667216673166741667516676166771667816679166801668116682166831668416685166861668716688166891669016691166921669316694166951669616697166981669916700167011670216703167041670516706167071670816709167101671116712167131671416715167161671716718167191672016721167221672316724167251672616727167281672916730167311673216733167341673516736167371673816739167401674116742167431674416745167461674716748167491675016751167521675316754167551675616757167581675916760167611676216763167641676516766167671676816769167701677116772167731677416775167761677716778167791678016781167821678316784167851678616787167881678916790167911679216793167941679516796167971679816799168001680116802168031680416805168061680716808168091681016811168121681316814168151681616817168181681916820168211682216823168241682516826168271682816829168301683116832168331683416835168361683716838168391684016841168421684316844168451684616847168481684916850168511685216853168541685516856168571685816859168601686116862168631686416865168661686716868168691687016871168721687316874168751687616877168781687916880168811688216883168841688516886168871688816889168901689116892168931689416895168961689716898168991690016901169021690316904169051690616907169081690916910169111691216913169141691516916169171691816919169201692116922169231692416925169261692716928169291693016931169321693316934169351693616937169381693916940169411694216943169441694516946169471694816949169501695116952169531695416955169561695716958169591696016961169621696316964169651696616967169681696916970169711697216973169741697516976169771697816979169801698116982169831698416985169861698716988169891699016991169921699316994169951699616997169981699917000170011700217003170041700517006170071700817009170101701117012170131701417015170161701717018170191702017021170221702317024170251702617027170281702917030170311703217033170341703517036170371703817039170401704117042170431704417045170461704717048170491705017051170521705317054170551705617057170581705917060170611706217063170641706517066170671706817069170701707117072170731707417075170761707717078170791708017081170821708317084170851708617087170881708917090170911709217093170941709517096170971709817099171001710117102171031710417105171061710717108171091711017111171121711317114171151711617117171181711917120171211712217123171241712517126171271712817129171301713117132171331713417135171361713717138171391714017141171421714317144171451714617147171481714917150171511715217153171541715517156171571715817159171601716117162171631716417165171661716717168171691717017171171721717317174171751717617177171781717917180171811718217183171841718517186171871718817189171901719117192171931719417195171961719717198171991720017201172021720317204172051720617207172081720917210172111721217213172141721517216172171721817219172201722117222172231722417225172261722717228172291723017231172321723317234172351723617237172381723917240172411724217243172441724517246172471724817249172501725117252172531725417255172561725717258172591726017261172621726317264172651726617267172681726917270172711727217273172741727517276172771727817279172801728117282172831728417285172861728717288172891729017291172921729317294172951729617297172981729917300173011730217303173041730517306173071730817309173101731117312173131731417315173161731717318173191732017321173221732317324173251732617327173281732917330173311733217333173341733517336173371733817339173401734117342173431734417345173461734717348173491735017351173521735317354173551735617357173581735917360173611736217363173641736517366173671736817369173701737117372173731737417375173761737717378173791738017381173821738317384173851738617387173881738917390173911739217393173941739517396173971739817399174001740117402174031740417405174061740717408174091741017411174121741317414174151741617417174181741917420174211742217423174241742517426174271742817429174301743117432174331743417435174361743717438174391744017441174421744317444174451744617447174481744917450174511745217453174541745517456174571745817459174601746117462174631746417465174661746717468174691747017471174721747317474174751747617477174781747917480174811748217483174841748517486174871748817489174901749117492174931749417495174961749717498174991750017501175021750317504175051750617507175081750917510175111751217513175141751517516175171751817519175201752117522175231752417525175261752717528175291753017531175321753317534175351753617537175381753917540175411754217543175441754517546175471754817549175501755117552175531755417555175561755717558175591756017561175621756317564175651756617567175681756917570175711757217573175741757517576175771757817579175801758117582175831758417585175861758717588175891759017591175921759317594175951759617597175981759917600176011760217603176041760517606176071760817609176101761117612176131761417615176161761717618176191762017621176221762317624176251762617627176281762917630176311763217633176341763517636176371763817639176401764117642176431764417645176461764717648176491765017651176521765317654176551765617657176581765917660176611766217663176641766517666176671766817669176701767117672176731767417675176761767717678176791768017681176821768317684176851768617687176881768917690176911769217693176941769517696176971769817699177001770117702177031770417705177061770717708177091771017711177121771317714177151771617717177181771917720177211772217723177241772517726177271772817729177301773117732177331773417735177361773717738177391774017741177421774317744177451774617747177481774917750177511775217753177541775517756177571775817759177601776117762177631776417765177661776717768177691777017771177721777317774177751777617777177781777917780177811778217783177841778517786177871778817789177901779117792177931779417795177961779717798177991780017801178021780317804178051780617807178081780917810178111781217813178141781517816178171781817819178201782117822178231782417825178261782717828178291783017831178321783317834178351783617837178381783917840178411784217843178441784517846178471784817849178501785117852178531785417855178561785717858178591786017861178621786317864178651786617867178681786917870178711787217873178741787517876178771787817879178801788117882178831788417885178861788717888178891789017891178921789317894178951789617897178981789917900179011790217903179041790517906179071790817909179101791117912179131791417915179161791717918179191792017921179221792317924179251792617927179281792917930179311793217933179341793517936179371793817939179401794117942179431794417945179461794717948179491795017951179521795317954179551795617957179581795917960179611796217963179641796517966179671796817969179701797117972179731797417975179761797717978179791798017981179821798317984179851798617987179881798917990179911799217993179941799517996179971799817999180001800118002180031800418005180061800718008180091801018011180121801318014180151801618017180181801918020180211802218023180241802518026180271802818029180301803118032180331803418035180361803718038180391804018041180421804318044180451804618047180481804918050180511805218053180541805518056180571805818059180601806118062180631806418065180661806718068180691807018071180721807318074180751807618077180781807918080180811808218083180841808518086180871808818089180901809118092180931809418095180961809718098180991810018101181021810318104181051810618107181081810918110181111811218113181141811518116181171811818119181201812118122181231812418125181261812718128181291813018131181321813318134181351813618137181381813918140181411814218143181441814518146181471814818149181501815118152181531815418155181561815718158181591816018161181621816318164181651816618167181681816918170181711817218173181741817518176181771817818179181801818118182181831818418185181861818718188181891819018191181921819318194181951819618197181981819918200182011820218203182041820518206182071820818209182101821118212182131821418215182161821718218182191822018221182221822318224182251822618227182281822918230182311823218233182341823518236182371823818239182401824118242182431824418245182461824718248182491825018251182521825318254182551825618257182581825918260182611826218263182641826518266182671826818269182701827118272182731827418275182761827718278182791828018281182821828318284182851828618287182881828918290182911829218293182941829518296182971829818299183001830118302183031830418305183061830718308183091831018311183121831318314183151831618317183181831918320183211832218323183241832518326183271832818329183301833118332183331833418335183361833718338183391834018341183421834318344183451834618347183481834918350183511835218353183541835518356183571835818359183601836118362183631836418365183661836718368183691837018371183721837318374183751837618377183781837918380183811838218383183841838518386183871838818389183901839118392183931839418395183961839718398183991840018401184021840318404184051840618407184081840918410184111841218413184141841518416184171841818419184201842118422184231842418425184261842718428184291843018431184321843318434184351843618437184381843918440184411844218443184441844518446184471844818449184501845118452184531845418455184561845718458184591846018461184621846318464184651846618467184681846918470184711847218473184741847518476184771847818479184801848118482184831848418485184861848718488184891849018491184921849318494184951849618497184981849918500185011850218503185041850518506185071850818509185101851118512185131851418515185161851718518185191852018521185221852318524185251852618527185281852918530185311853218533185341853518536185371853818539185401854118542185431854418545185461854718548185491855018551185521855318554185551855618557185581855918560185611856218563185641856518566185671856818569185701857118572185731857418575185761857718578185791858018581185821858318584185851858618587185881858918590185911859218593185941859518596185971859818599186001860118602186031860418605186061860718608186091861018611186121861318614186151861618617186181861918620186211862218623186241862518626186271862818629186301863118632186331863418635186361863718638186391864018641186421864318644186451864618647186481864918650186511865218653186541865518656186571865818659186601866118662186631866418665186661866718668186691867018671186721867318674186751867618677186781867918680186811868218683186841868518686186871868818689186901869118692186931869418695186961869718698186991870018701187021870318704187051870618707187081870918710187111871218713187141871518716187171871818719187201872118722187231872418725187261872718728187291873018731187321873318734187351873618737187381873918740187411874218743187441874518746187471874818749187501875118752187531875418755187561875718758187591876018761187621876318764187651876618767187681876918770187711877218773187741877518776187771877818779187801878118782187831878418785187861878718788187891879018791187921879318794187951879618797187981879918800188011880218803188041880518806188071880818809188101881118812188131881418815188161881718818188191882018821188221882318824188251882618827188281882918830188311883218833188341883518836188371883818839188401884118842188431884418845188461884718848188491885018851188521885318854188551885618857188581885918860188611886218863188641886518866188671886818869188701887118872188731887418875188761887718878188791888018881188821888318884188851888618887188881888918890188911889218893188941889518896188971889818899189001890118902189031890418905189061890718908189091891018911189121891318914189151891618917189181891918920189211892218923189241892518926189271892818929189301893118932189331893418935189361893718938189391894018941189421894318944189451894618947189481894918950189511895218953189541895518956189571895818959189601896118962189631896418965189661896718968189691897018971189721897318974189751897618977189781897918980189811898218983189841898518986189871898818989189901899118992189931899418995189961899718998189991900019001190021900319004190051900619007190081900919010190111901219013190141901519016190171901819019190201902119022190231902419025190261902719028190291903019031190321903319034190351903619037190381903919040190411904219043190441904519046190471904819049190501905119052190531905419055190561905719058190591906019061190621906319064190651906619067190681906919070190711907219073190741907519076190771907819079190801908119082190831908419085190861908719088190891909019091190921909319094190951909619097190981909919100191011910219103191041910519106191071910819109191101911119112191131911419115191161911719118191191912019121191221912319124191251912619127191281912919130191311913219133191341913519136191371913819139191401914119142191431914419145191461914719148191491915019151191521915319154191551915619157191581915919160191611916219163191641916519166191671916819169191701917119172191731917419175191761917719178191791918019181191821918319184191851918619187191881918919190191911919219193191941919519196191971919819199192001920119202192031920419205192061920719208192091921019211192121921319214192151921619217192181921919220192211922219223192241922519226192271922819229192301923119232192331923419235192361923719238192391924019241192421924319244192451924619247192481924919250192511925219253192541925519256192571925819259192601926119262192631926419265192661926719268192691927019271192721927319274192751927619277192781927919280192811928219283192841928519286192871928819289192901929119292192931929419295192961929719298192991930019301193021930319304193051930619307193081930919310193111931219313193141931519316193171931819319193201932119322193231932419325193261932719328193291933019331193321933319334193351933619337193381933919340193411934219343193441934519346193471934819349193501935119352193531935419355193561935719358193591936019361193621936319364193651936619367193681936919370193711937219373193741937519376193771937819379193801938119382193831938419385193861938719388193891939019391193921939319394193951939619397193981939919400194011940219403194041940519406194071940819409194101941119412194131941419415194161941719418194191942019421194221942319424194251942619427194281942919430194311943219433194341943519436194371943819439194401944119442194431944419445194461944719448194491945019451194521945319454194551945619457194581945919460194611946219463194641946519466194671946819469194701947119472194731947419475194761947719478194791948019481194821948319484194851948619487194881948919490194911949219493194941949519496194971949819499195001950119502195031950419505195061950719508195091951019511195121951319514195151951619517195181951919520195211952219523195241952519526195271952819529195301953119532195331953419535195361953719538195391954019541195421954319544195451954619547195481954919550195511955219553195541955519556195571955819559195601956119562195631956419565195661956719568195691957019571195721957319574195751957619577195781957919580195811958219583195841958519586195871958819589195901959119592195931959419595195961959719598195991960019601196021960319604196051960619607196081960919610196111961219613196141961519616196171961819619196201962119622196231962419625196261962719628196291963019631196321963319634196351963619637196381963919640196411964219643196441964519646196471964819649196501965119652196531965419655196561965719658196591966019661196621966319664196651966619667196681966919670196711967219673196741967519676196771967819679196801968119682196831968419685196861968719688196891969019691196921969319694196951969619697196981969919700197011970219703197041970519706197071970819709197101971119712197131971419715197161971719718197191972019721197221972319724197251972619727197281972919730197311973219733197341973519736197371973819739197401974119742197431974419745197461974719748197491975019751197521975319754197551975619757197581975919760197611976219763197641976519766197671976819769197701977119772197731977419775197761977719778197791978019781197821978319784197851978619787197881978919790197911979219793197941979519796197971979819799198001980119802198031980419805198061980719808198091981019811198121981319814198151981619817198181981919820198211982219823198241982519826198271982819829198301983119832198331983419835198361983719838198391984019841198421984319844198451984619847198481984919850198511985219853198541985519856198571985819859198601986119862198631986419865198661986719868198691987019871198721987319874198751987619877198781987919880198811988219883198841988519886198871988819889198901989119892198931989419895198961989719898198991990019901199021990319904199051990619907199081990919910199111991219913199141991519916199171991819919199201992119922199231992419925199261992719928199291993019931199321993319934199351993619937199381993919940199411994219943199441994519946199471994819949199501995119952199531995419955199561995719958199591996019961199621996319964199651996619967199681996919970199711997219973199741997519976199771997819979199801998119982199831998419985199861998719988199891999019991199921999319994199951999619997199981999920000200012000220003200042000520006200072000820009200102001120012200132001420015200162001720018200192002020021200222002320024200252002620027200282002920030200312003220033200342003520036200372003820039200402004120042200432004420045200462004720048200492005020051200522005320054200552005620057200582005920060200612006220063200642006520066200672006820069200702007120072200732007420075200762007720078200792008020081200822008320084200852008620087200882008920090200912009220093200942009520096200972009820099201002010120102201032010420105201062010720108201092011020111201122011320114201152011620117201182011920120201212012220123201242012520126201272012820129201302013120132201332013420135201362013720138201392014020141201422014320144201452014620147201482014920150201512015220153201542015520156201572015820159201602016120162201632016420165201662016720168201692017020171201722017320174201752017620177201782017920180201812018220183201842018520186201872018820189201902019120192201932019420195201962019720198201992020020201202022020320204202052020620207202082020920210202112021220213202142021520216202172021820219202202022120222202232022420225202262022720228202292023020231202322023320234202352023620237202382023920240202412024220243202442024520246202472024820249202502025120252202532025420255202562025720258202592026020261202622026320264202652026620267202682026920270202712027220273202742027520276202772027820279202802028120282202832028420285202862028720288202892029020291202922029320294202952029620297202982029920300203012030220303203042030520306203072030820309203102031120312203132031420315203162031720318203192032020321203222032320324203252032620327203282032920330203312033220333203342033520336203372033820339203402034120342203432034420345203462034720348203492035020351203522035320354203552035620357203582035920360203612036220363203642036520366203672036820369203702037120372203732037420375203762037720378203792038020381203822038320384203852038620387203882038920390203912039220393203942039520396203972039820399204002040120402204032040420405204062040720408204092041020411204122041320414204152041620417204182041920420204212042220423204242042520426204272042820429204302043120432204332043420435204362043720438204392044020441204422044320444204452044620447204482044920450204512045220453204542045520456204572045820459204602046120462204632046420465204662046720468204692047020471204722047320474204752047620477204782047920480204812048220483204842048520486204872048820489204902049120492204932049420495204962049720498204992050020501205022050320504205052050620507205082050920510205112051220513205142051520516205172051820519205202052120522205232052420525205262052720528205292053020531205322053320534205352053620537205382053920540205412054220543205442054520546205472054820549205502055120552205532055420555205562055720558205592056020561205622056320564205652056620567205682056920570205712057220573205742057520576205772057820579205802058120582205832058420585205862058720588205892059020591205922059320594205952059620597205982059920600206012060220603206042060520606206072060820609206102061120612206132061420615206162061720618206192062020621206222062320624206252062620627206282062920630206312063220633206342063520636206372063820639206402064120642206432064420645206462064720648206492065020651206522065320654206552065620657206582065920660206612066220663206642066520666206672066820669206702067120672206732067420675206762067720678206792068020681206822068320684206852068620687206882068920690206912069220693206942069520696206972069820699207002070120702207032070420705207062070720708207092071020711207122071320714207152071620717207182071920720207212072220723207242072520726207272072820729207302073120732207332073420735207362073720738207392074020741207422074320744207452074620747207482074920750207512075220753207542075520756207572075820759207602076120762207632076420765207662076720768207692077020771207722077320774207752077620777207782077920780207812078220783207842078520786207872078820789207902079120792207932079420795207962079720798207992080020801208022080320804208052080620807208082080920810208112081220813208142081520816208172081820819208202082120822208232082420825208262082720828208292083020831208322083320834208352083620837208382083920840208412084220843208442084520846208472084820849208502085120852208532085420855208562085720858208592086020861208622086320864208652086620867208682086920870208712087220873208742087520876208772087820879208802088120882208832088420885208862088720888208892089020891208922089320894208952089620897208982089920900209012090220903209042090520906209072090820909209102091120912209132091420915209162091720918209192092020921209222092320924209252092620927209282092920930209312093220933209342093520936209372093820939209402094120942209432094420945209462094720948209492095020951209522095320954209552095620957209582095920960209612096220963209642096520966209672096820969209702097120972209732097420975209762097720978209792098020981209822098320984209852098620987209882098920990209912099220993209942099520996209972099820999210002100121002210032100421005210062100721008210092101021011210122101321014210152101621017210182101921020210212102221023210242102521026210272102821029210302103121032210332103421035210362103721038210392104021041210422104321044210452104621047210482104921050210512105221053210542105521056210572105821059210602106121062210632106421065210662106721068210692107021071210722107321074210752107621077210782107921080210812108221083210842108521086210872108821089210902109121092210932109421095210962109721098210992110021101211022110321104211052110621107211082110921110211112111221113211142111521116211172111821119211202112121122211232112421125211262112721128211292113021131211322113321134211352113621137211382113921140211412114221143211442114521146211472114821149211502115121152211532115421155211562115721158211592116021161211622116321164211652116621167211682116921170211712117221173211742117521176211772117821179211802118121182211832118421185211862118721188211892119021191211922119321194211952119621197211982119921200212012120221203212042120521206212072120821209212102121121212212132121421215212162121721218212192122021221212222122321224212252122621227212282122921230212312123221233212342123521236212372123821239212402124121242212432124421245212462124721248212492125021251212522125321254212552125621257212582125921260212612126221263212642126521266212672126821269212702127121272212732127421275212762127721278212792128021281212822128321284212852128621287212882128921290212912129221293212942129521296212972129821299213002130121302213032130421305213062130721308213092131021311213122131321314213152131621317213182131921320213212132221323213242132521326213272132821329213302133121332213332133421335213362133721338213392134021341213422134321344213452134621347213482134921350213512135221353213542135521356213572135821359213602136121362213632136421365213662136721368213692137021371213722137321374213752137621377213782137921380213812138221383213842138521386213872138821389213902139121392213932139421395213962139721398213992140021401214022140321404214052140621407214082140921410214112141221413214142141521416214172141821419214202142121422214232142421425214262142721428214292143021431214322143321434214352143621437214382143921440214412144221443214442144521446214472144821449214502145121452214532145421455214562145721458214592146021461214622146321464214652146621467214682146921470214712147221473214742147521476214772147821479214802148121482214832148421485214862148721488214892149021491214922149321494214952149621497214982149921500215012150221503215042150521506215072150821509215102151121512215132151421515215162151721518215192152021521215222152321524215252152621527215282152921530215312153221533215342153521536215372153821539215402154121542215432154421545215462154721548215492155021551215522155321554215552155621557215582155921560215612156221563215642156521566215672156821569215702157121572215732157421575215762157721578215792158021581215822158321584215852158621587215882158921590215912159221593215942159521596215972159821599216002160121602216032160421605216062160721608216092161021611216122161321614216152161621617216182161921620216212162221623216242162521626216272162821629216302163121632216332163421635216362163721638216392164021641216422164321644216452164621647216482164921650216512165221653216542165521656216572165821659216602166121662216632166421665216662166721668216692167021671216722167321674216752167621677216782167921680216812168221683216842168521686216872168821689216902169121692216932169421695216962169721698216992170021701217022170321704217052170621707217082170921710217112171221713217142171521716217172171821719217202172121722217232172421725217262172721728217292173021731217322173321734217352173621737217382173921740217412174221743217442174521746217472174821749217502175121752217532175421755217562175721758217592176021761217622176321764217652176621767217682176921770217712177221773217742177521776217772177821779217802178121782217832178421785217862178721788217892179021791217922179321794217952179621797217982179921800218012180221803218042180521806218072180821809218102181121812218132181421815218162181721818218192182021821218222182321824218252182621827218282182921830218312183221833218342183521836218372183821839218402184121842218432184421845218462184721848218492185021851218522185321854218552185621857218582185921860218612186221863218642186521866218672186821869218702187121872218732187421875218762187721878218792188021881218822188321884218852188621887218882188921890218912189221893218942189521896218972189821899219002190121902219032190421905219062190721908219092191021911219122191321914219152191621917219182191921920219212192221923219242192521926219272192821929219302193121932219332193421935219362193721938219392194021941219422194321944219452194621947219482194921950219512195221953219542195521956219572195821959219602196121962219632196421965219662196721968219692197021971219722197321974219752197621977219782197921980219812198221983219842198521986219872198821989219902199121992219932199421995219962199721998219992200022001220022200322004220052200622007220082200922010220112201222013220142201522016220172201822019220202202122022220232202422025220262202722028220292203022031220322203322034220352203622037220382203922040220412204222043220442204522046220472204822049220502205122052220532205422055220562205722058220592206022061220622206322064220652206622067220682206922070220712207222073220742207522076220772207822079220802208122082220832208422085220862208722088220892209022091220922209322094220952209622097220982209922100221012210222103221042210522106221072210822109221102211122112221132211422115221162211722118221192212022121221222212322124221252212622127221282212922130221312213222133221342213522136221372213822139221402214122142221432214422145221462214722148221492215022151221522215322154221552215622157221582215922160221612216222163221642216522166221672216822169221702217122172221732217422175221762217722178221792218022181221822218322184221852218622187221882218922190221912219222193221942219522196221972219822199222002220122202222032220422205222062220722208222092221022211222122221322214222152221622217222182221922220222212222222223222242222522226222272222822229222302223122232222332223422235222362223722238222392224022241222422224322244222452224622247222482224922250222512225222253222542225522256222572225822259222602226122262222632226422265222662226722268222692227022271222722227322274222752227622277222782227922280222812228222283222842228522286222872228822289222902229122292222932229422295222962229722298222992230022301223022230322304223052230622307223082230922310223112231222313223142231522316223172231822319223202232122322223232232422325223262232722328223292233022331223322233322334223352233622337223382233922340223412234222343223442234522346223472234822349223502235122352223532235422355223562235722358223592236022361223622236322364223652236622367223682236922370223712237222373223742237522376223772237822379223802238122382223832238422385223862238722388223892239022391223922239322394223952239622397223982239922400224012240222403224042240522406224072240822409224102241122412224132241422415224162241722418224192242022421224222242322424224252242622427224282242922430224312243222433224342243522436224372243822439224402244122442224432244422445224462244722448224492245022451224522245322454224552245622457224582245922460224612246222463224642246522466224672246822469224702247122472224732247422475224762247722478224792248022481224822248322484224852248622487224882248922490224912249222493224942249522496224972249822499225002250122502225032250422505225062250722508225092251022511225122251322514225152251622517225182251922520225212252222523225242252522526225272252822529225302253122532225332253422535225362253722538225392254022541225422254322544225452254622547225482254922550225512255222553225542255522556225572255822559225602256122562225632256422565225662256722568225692257022571225722257322574225752257622577225782257922580225812258222583225842258522586225872258822589225902259122592225932259422595225962259722598225992260022601226022260322604226052260622607226082260922610226112261222613226142261522616226172261822619226202262122622226232262422625226262262722628226292263022631226322263322634226352263622637226382263922640226412264222643226442264522646226472264822649226502265122652226532265422655226562265722658226592266022661226622266322664226652266622667226682266922670226712267222673226742267522676226772267822679226802268122682226832268422685226862268722688226892269022691226922269322694226952269622697226982269922700227012270222703227042270522706227072270822709227102271122712227132271422715227162271722718227192272022721227222272322724227252272622727227282272922730227312273222733227342273522736227372273822739227402274122742227432274422745227462274722748227492275022751227522275322754227552275622757227582275922760227612276222763227642276522766227672276822769227702277122772227732277422775227762277722778227792278022781227822278322784227852278622787227882278922790227912279222793227942279522796227972279822799228002280122802228032280422805228062280722808228092281022811228122281322814228152281622817228182281922820228212282222823228242282522826228272282822829228302283122832228332283422835228362283722838228392284022841228422284322844228452284622847228482284922850228512285222853228542285522856228572285822859228602286122862228632286422865228662286722868228692287022871228722287322874228752287622877228782287922880228812288222883228842288522886228872288822889228902289122892228932289422895228962289722898228992290022901229022290322904229052290622907229082290922910229112291222913229142291522916229172291822919229202292122922229232292422925229262292722928229292293022931229322293322934229352293622937229382293922940229412294222943229442294522946229472294822949229502295122952229532295422955229562295722958229592296022961229622296322964229652296622967229682296922970229712297222973229742297522976229772297822979229802298122982229832298422985229862298722988229892299022991229922299322994229952299622997229982299923000230012300223003230042300523006230072300823009230102301123012230132301423015230162301723018230192302023021230222302323024230252302623027230282302923030230312303223033230342303523036230372303823039230402304123042230432304423045230462304723048230492305023051230522305323054230552305623057230582305923060230612306223063230642306523066230672306823069230702307123072230732307423075230762307723078230792308023081230822308323084230852308623087230882308923090230912309223093230942309523096230972309823099231002310123102231032310423105231062310723108231092311023111231122311323114231152311623117231182311923120231212312223123231242312523126231272312823129231302313123132231332313423135231362313723138231392314023141231422314323144231452314623147231482314923150231512315223153231542315523156231572315823159231602316123162231632316423165231662316723168231692317023171231722317323174231752317623177231782317923180231812318223183231842318523186231872318823189231902319123192231932319423195231962319723198231992320023201232022320323204232052320623207232082320923210232112321223213232142321523216232172321823219232202322123222232232322423225232262322723228232292323023231232322323323234232352323623237232382323923240232412324223243232442324523246232472324823249232502325123252232532325423255232562325723258232592326023261232622326323264232652326623267232682326923270232712327223273232742327523276232772327823279232802328123282232832328423285232862328723288232892329023291232922329323294232952329623297232982329923300233012330223303233042330523306233072330823309233102331123312233132331423315233162331723318233192332023321233222332323324233252332623327233282332923330233312333223333233342333523336233372333823339233402334123342233432334423345233462334723348233492335023351233522335323354233552335623357233582335923360233612336223363233642336523366233672336823369233702337123372233732337423375233762337723378233792338023381233822338323384233852338623387233882338923390233912339223393233942339523396233972339823399234002340123402234032340423405234062340723408234092341023411234122341323414234152341623417234182341923420234212342223423234242342523426234272342823429234302343123432234332343423435234362343723438234392344023441234422344323444234452344623447234482344923450234512345223453234542345523456234572345823459234602346123462234632346423465234662346723468234692347023471234722347323474234752347623477234782347923480234812348223483234842348523486234872348823489234902349123492234932349423495234962349723498234992350023501235022350323504235052350623507235082350923510235112351223513235142351523516235172351823519235202352123522235232352423525235262352723528235292353023531235322353323534235352353623537235382353923540235412354223543235442354523546235472354823549235502355123552235532355423555235562355723558235592356023561235622356323564235652356623567235682356923570235712357223573235742357523576235772357823579235802358123582235832358423585235862358723588235892359023591235922359323594235952359623597235982359923600236012360223603236042360523606236072360823609236102361123612236132361423615236162361723618236192362023621236222362323624236252362623627236282362923630236312363223633236342363523636236372363823639236402364123642236432364423645236462364723648236492365023651236522365323654236552365623657236582365923660236612366223663236642366523666236672366823669236702367123672236732367423675236762367723678236792368023681236822368323684236852368623687236882368923690236912369223693236942369523696236972369823699237002370123702237032370423705237062370723708237092371023711237122371323714237152371623717237182371923720237212372223723237242372523726237272372823729237302373123732237332373423735237362373723738237392374023741237422374323744237452374623747237482374923750237512375223753237542375523756237572375823759237602376123762237632376423765237662376723768237692377023771237722377323774237752377623777237782377923780237812378223783237842378523786237872378823789237902379123792237932379423795237962379723798237992380023801238022380323804238052380623807238082380923810238112381223813238142381523816238172381823819238202382123822238232382423825238262382723828238292383023831238322383323834238352383623837238382383923840238412384223843238442384523846238472384823849238502385123852238532385423855238562385723858238592386023861238622386323864238652386623867238682386923870238712387223873238742387523876238772387823879238802388123882238832388423885238862388723888238892389023891238922389323894238952389623897238982389923900239012390223903239042390523906239072390823909239102391123912239132391423915239162391723918239192392023921239222392323924239252392623927239282392923930239312393223933239342393523936239372393823939239402394123942239432394423945239462394723948239492395023951239522395323954239552395623957239582395923960239612396223963239642396523966239672396823969239702397123972239732397423975239762397723978239792398023981239822398323984239852398623987239882398923990239912399223993239942399523996239972399823999240002400124002240032400424005240062400724008240092401024011240122401324014240152401624017240182401924020240212402224023240242402524026240272402824029240302403124032240332403424035240362403724038240392404024041240422404324044240452404624047240482404924050240512405224053240542405524056240572405824059240602406124062240632406424065240662406724068240692407024071240722407324074240752407624077240782407924080240812408224083240842408524086240872408824089240902409124092240932409424095240962409724098240992410024101241022410324104241052410624107241082410924110241112411224113241142411524116241172411824119241202412124122241232412424125241262412724128241292413024131241322413324134241352413624137241382413924140241412414224143241442414524146241472414824149241502415124152241532415424155241562415724158241592416024161241622416324164241652416624167241682416924170241712417224173241742417524176241772417824179241802418124182241832418424185241862418724188241892419024191241922419324194241952419624197241982419924200242012420224203242042420524206242072420824209242102421124212242132421424215242162421724218242192422024221242222422324224242252422624227242282422924230242312423224233242342423524236242372423824239242402424124242242432424424245242462424724248242492425024251242522425324254242552425624257242582425924260242612426224263242642426524266242672426824269242702427124272242732427424275242762427724278242792428024281242822428324284242852428624287242882428924290242912429224293242942429524296242972429824299243002430124302243032430424305243062430724308243092431024311243122431324314243152431624317243182431924320243212432224323243242432524326243272432824329243302433124332243332433424335243362433724338243392434024341243422434324344243452434624347243482434924350243512435224353243542435524356243572435824359243602436124362243632436424365243662436724368243692437024371243722437324374243752437624377243782437924380243812438224383243842438524386243872438824389243902439124392243932439424395243962439724398243992440024401244022440324404244052440624407244082440924410244112441224413244142441524416244172441824419244202442124422244232442424425244262442724428244292443024431244322443324434244352443624437244382443924440244412444224443244442444524446244472444824449244502445124452244532445424455244562445724458244592446024461244622446324464244652446624467244682446924470244712447224473244742447524476244772447824479244802448124482244832448424485244862448724488244892449024491244922449324494244952449624497244982449924500245012450224503245042450524506245072450824509245102451124512245132451424515245162451724518245192452024521245222452324524245252452624527245282452924530245312453224533245342453524536245372453824539245402454124542245432454424545245462454724548245492455024551245522455324554245552455624557245582455924560245612456224563245642456524566245672456824569245702457124572245732457424575245762457724578245792458024581245822458324584245852458624587245882458924590245912459224593245942459524596245972459824599246002460124602246032460424605246062460724608246092461024611246122461324614246152461624617246182461924620246212462224623246242462524626246272462824629246302463124632246332463424635246362463724638246392464024641246422464324644246452464624647246482464924650246512465224653246542465524656246572465824659246602466124662246632466424665246662466724668246692467024671246722467324674246752467624677246782467924680246812468224683246842468524686246872468824689246902469124692246932469424695246962469724698246992470024701247022470324704247052470624707247082470924710247112471224713247142471524716247172471824719247202472124722247232472424725247262472724728247292473024731247322473324734247352473624737247382473924740247412474224743247442474524746247472474824749247502475124752247532475424755247562475724758247592476024761247622476324764247652476624767247682476924770247712477224773247742477524776247772477824779247802478124782247832478424785247862478724788247892479024791247922479324794247952479624797247982479924800248012480224803248042480524806248072480824809248102481124812248132481424815248162481724818248192482024821248222482324824248252482624827248282482924830248312483224833248342483524836248372483824839248402484124842248432484424845248462484724848248492485024851248522485324854248552485624857248582485924860248612486224863248642486524866248672486824869248702487124872248732487424875248762487724878248792488024881248822488324884248852488624887248882488924890248912489224893248942489524896248972489824899249002490124902249032490424905249062490724908249092491024911249122491324914249152491624917249182491924920249212492224923249242492524926249272492824929249302493124932249332493424935249362493724938249392494024941249422494324944249452494624947249482494924950249512495224953249542495524956249572495824959249602496124962249632496424965249662496724968249692497024971249722497324974249752497624977249782497924980249812498224983249842498524986249872498824989249902499124992249932499424995249962499724998249992500025001250022500325004250052500625007250082500925010250112501225013250142501525016250172501825019250202502125022250232502425025250262502725028250292503025031250322503325034250352503625037250382503925040250412504225043250442504525046250472504825049250502505125052250532505425055250562505725058250592506025061250622506325064250652506625067250682506925070250712507225073250742507525076250772507825079250802508125082250832508425085250862508725088250892509025091250922509325094250952509625097250982509925100251012510225103251042510525106251072510825109251102511125112251132511425115251162511725118251192512025121251222512325124251252512625127251282512925130251312513225133251342513525136251372513825139251402514125142251432514425145251462514725148251492515025151251522515325154251552515625157251582515925160251612516225163251642516525166251672516825169251702517125172251732517425175251762517725178251792518025181251822518325184251852518625187251882518925190251912519225193251942519525196251972519825199252002520125202252032520425205252062520725208252092521025211252122521325214252152521625217252182521925220252212522225223252242522525226252272522825229252302523125232252332523425235252362523725238252392524025241252422524325244252452524625247252482524925250252512525225253252542525525256252572525825259252602526125262252632526425265252662526725268252692527025271252722527325274252752527625277252782527925280252812528225283252842528525286252872528825289252902529125292252932529425295252962529725298252992530025301253022530325304253052530625307253082530925310253112531225313253142531525316253172531825319253202532125322253232532425325253262532725328253292533025331253322533325334253352533625337253382533925340253412534225343253442534525346253472534825349253502535125352253532535425355253562535725358253592536025361253622536325364253652536625367253682536925370253712537225373253742537525376253772537825379253802538125382253832538425385253862538725388253892539025391253922539325394253952539625397253982539925400254012540225403254042540525406254072540825409254102541125412254132541425415254162541725418254192542025421254222542325424254252542625427254282542925430254312543225433254342543525436254372543825439254402544125442254432544425445254462544725448254492545025451254522545325454254552545625457254582545925460254612546225463254642546525466254672546825469254702547125472254732547425475254762547725478254792548025481254822548325484254852548625487254882548925490254912549225493254942549525496254972549825499255002550125502255032550425505255062550725508255092551025511255122551325514255152551625517255182551925520255212552225523255242552525526255272552825529255302553125532255332553425535255362553725538255392554025541255422554325544255452554625547255482554925550255512555225553255542555525556255572555825559255602556125562255632556425565255662556725568255692557025571255722557325574255752557625577255782557925580255812558225583255842558525586255872558825589255902559125592255932559425595255962559725598255992560025601256022560325604256052560625607256082560925610256112561225613256142561525616256172561825619256202562125622256232562425625256262562725628256292563025631256322563325634256352563625637256382563925640256412564225643256442564525646256472564825649256502565125652256532565425655256562565725658256592566025661256622566325664256652566625667256682566925670256712567225673256742567525676256772567825679256802568125682256832568425685256862568725688256892569025691256922569325694256952569625697256982569925700257012570225703257042570525706257072570825709257102571125712257132571425715257162571725718257192572025721257222572325724257252572625727257282572925730257312573225733257342573525736257372573825739257402574125742257432574425745257462574725748257492575025751257522575325754257552575625757257582575925760
  1. /**
  2. * @license
  3. * Video.js 8.3.0 <http://videojs.com/>
  4. * Copyright Brightcove, Inc. <https://www.brightcove.com/>
  5. * Available under Apache License Version 2.0
  6. * <https://github.com/videojs/video.js/blob/main/LICENSE>
  7. *
  8. * Includes vtt.js <https://github.com/mozilla/vtt.js>
  9. * Available under Apache License Version 2.0
  10. * <https://github.com/mozilla/vtt.js/blob/main/LICENSE>
  11. */
  12. 'use strict';
  13. var window = require('global/window');
  14. var document = require('global/document');
  15. var keycode = require('keycode');
  16. var safeParseTuple = require('safe-json-parse/tuple');
  17. var XHR = require('@videojs/xhr');
  18. var vtt = require('videojs-vtt.js');
  19. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  20. var window__default = /*#__PURE__*/_interopDefaultLegacy(window);
  21. var document__default = /*#__PURE__*/_interopDefaultLegacy(document);
  22. var keycode__default = /*#__PURE__*/_interopDefaultLegacy(keycode);
  23. var safeParseTuple__default = /*#__PURE__*/_interopDefaultLegacy(safeParseTuple);
  24. var XHR__default = /*#__PURE__*/_interopDefaultLegacy(XHR);
  25. var vtt__default = /*#__PURE__*/_interopDefaultLegacy(vtt);
  26. var version = "8.3.0";
  27. /**
  28. * An Object that contains lifecycle hooks as keys which point to an array
  29. * of functions that are run when a lifecycle is triggered
  30. *
  31. * @private
  32. */
  33. const hooks_ = {};
  34. /**
  35. * Get a list of hooks for a specific lifecycle
  36. *
  37. * @param {string} type
  38. * the lifecycle to get hooks from
  39. *
  40. * @param {Function|Function[]} [fn]
  41. * Optionally add a hook (or hooks) to the lifecycle that your are getting.
  42. *
  43. * @return {Array}
  44. * an array of hooks, or an empty array if there are none.
  45. */
  46. const hooks = function (type, fn) {
  47. hooks_[type] = hooks_[type] || [];
  48. if (fn) {
  49. hooks_[type] = hooks_[type].concat(fn);
  50. }
  51. return hooks_[type];
  52. };
  53. /**
  54. * Add a function hook to a specific videojs lifecycle.
  55. *
  56. * @param {string} type
  57. * the lifecycle to hook the function to.
  58. *
  59. * @param {Function|Function[]}
  60. * The function or array of functions to attach.
  61. */
  62. const hook = function (type, fn) {
  63. hooks(type, fn);
  64. };
  65. /**
  66. * Remove a hook from a specific videojs lifecycle.
  67. *
  68. * @param {string} type
  69. * the lifecycle that the function hooked to
  70. *
  71. * @param {Function} fn
  72. * The hooked function to remove
  73. *
  74. * @return {boolean}
  75. * The function that was removed or undef
  76. */
  77. const removeHook = function (type, fn) {
  78. const index = hooks(type).indexOf(fn);
  79. if (index <= -1) {
  80. return false;
  81. }
  82. hooks_[type] = hooks_[type].slice();
  83. hooks_[type].splice(index, 1);
  84. return true;
  85. };
  86. /**
  87. * Add a function hook that will only run once to a specific videojs lifecycle.
  88. *
  89. * @param {string} type
  90. * the lifecycle to hook the function to.
  91. *
  92. * @param {Function|Function[]}
  93. * The function or array of functions to attach.
  94. */
  95. const hookOnce = function (type, fn) {
  96. hooks(type, [].concat(fn).map(original => {
  97. const wrapper = (...args) => {
  98. removeHook(type, wrapper);
  99. return original(...args);
  100. };
  101. return wrapper;
  102. }));
  103. };
  104. /**
  105. * @file fullscreen-api.js
  106. * @module fullscreen-api
  107. */
  108. /**
  109. * Store the browser-specific methods for the fullscreen API.
  110. *
  111. * @type {Object}
  112. * @see [Specification]{@link https://fullscreen.spec.whatwg.org}
  113. * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
  114. */
  115. const FullscreenApi = {
  116. prefixed: true
  117. };
  118. // browser API methods
  119. const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
  120. // WebKit
  121. ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen'],
  122. // Mozilla
  123. ['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror', '-moz-full-screen'],
  124. // Microsoft
  125. ['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError', '-ms-fullscreen']];
  126. const specApi = apiMap[0];
  127. let browserApi;
  128. // determine the supported set of functions
  129. for (let i = 0; i < apiMap.length; i++) {
  130. // check for exitFullscreen function
  131. if (apiMap[i][1] in document__default["default"]) {
  132. browserApi = apiMap[i];
  133. break;
  134. }
  135. }
  136. // map the browser API names to the spec API names
  137. if (browserApi) {
  138. for (let i = 0; i < browserApi.length; i++) {
  139. FullscreenApi[specApi[i]] = browserApi[i];
  140. }
  141. FullscreenApi.prefixed = browserApi[0] !== specApi[0];
  142. }
  143. /**
  144. * @file create-logger.js
  145. * @module create-logger
  146. */
  147. // This is the private tracking variable for the logging history.
  148. let history = [];
  149. /**
  150. * Log messages to the console and history based on the type of message
  151. *
  152. * @private
  153. * @param {string} type
  154. * The name of the console method to use.
  155. *
  156. * @param {Array} args
  157. * The arguments to be passed to the matching console method.
  158. */
  159. const LogByTypeFactory = (name, log) => (type, level, args) => {
  160. const lvl = log.levels[level];
  161. const lvlRegExp = new RegExp(`^(${lvl})$`);
  162. if (type !== 'log') {
  163. // Add the type to the front of the message when it's not "log".
  164. args.unshift(type.toUpperCase() + ':');
  165. }
  166. // Add console prefix after adding to history.
  167. args.unshift(name + ':');
  168. // Add a clone of the args at this point to history.
  169. if (history) {
  170. history.push([].concat(args));
  171. // only store 1000 history entries
  172. const splice = history.length - 1000;
  173. history.splice(0, splice > 0 ? splice : 0);
  174. }
  175. // If there's no console then don't try to output messages, but they will
  176. // still be stored in history.
  177. if (!window__default["default"].console) {
  178. return;
  179. }
  180. // Was setting these once outside of this function, but containing them
  181. // in the function makes it easier to test cases where console doesn't exist
  182. // when the module is executed.
  183. let fn = window__default["default"].console[type];
  184. if (!fn && type === 'debug') {
  185. // Certain browsers don't have support for console.debug. For those, we
  186. // should default to the closest comparable log.
  187. fn = window__default["default"].console.info || window__default["default"].console.log;
  188. }
  189. // Bail out if there's no console or if this type is not allowed by the
  190. // current logging level.
  191. if (!fn || !lvl || !lvlRegExp.test(type)) {
  192. return;
  193. }
  194. fn[Array.isArray(args) ? 'apply' : 'call'](window__default["default"].console, args);
  195. };
  196. function createLogger$1(name) {
  197. // This is the private tracking variable for logging level.
  198. let level = 'info';
  199. // the curried logByType bound to the specific log and history
  200. let logByType;
  201. /**
  202. * Logs plain debug messages. Similar to `console.log`.
  203. *
  204. * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
  205. * of our JSDoc template, we cannot properly document this as both a function
  206. * and a namespace, so its function signature is documented here.
  207. *
  208. * #### Arguments
  209. * ##### *args
  210. * *[]
  211. *
  212. * Any combination of values that could be passed to `console.log()`.
  213. *
  214. * #### Return Value
  215. *
  216. * `undefined`
  217. *
  218. * @namespace
  219. * @param {...*} args
  220. * One or more messages or objects that should be logged.
  221. */
  222. const log = function (...args) {
  223. logByType('log', level, args);
  224. };
  225. // This is the logByType helper that the logging methods below use
  226. logByType = LogByTypeFactory(name, log);
  227. /**
  228. * Create a new sublogger which chains the old name to the new name.
  229. *
  230. * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
  231. * ```js
  232. * mylogger('foo');
  233. * // > VIDEOJS: player: foo
  234. * ```
  235. *
  236. * @param {string} name
  237. * The name to add call the new logger
  238. * @return {Object}
  239. */
  240. log.createLogger = subname => createLogger$1(name + ': ' + subname);
  241. /**
  242. * Enumeration of available logging levels, where the keys are the level names
  243. * and the values are `|`-separated strings containing logging methods allowed
  244. * in that logging level. These strings are used to create a regular expression
  245. * matching the function name being called.
  246. *
  247. * Levels provided by Video.js are:
  248. *
  249. * - `off`: Matches no calls. Any value that can be cast to `false` will have
  250. * this effect. The most restrictive.
  251. * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
  252. * `log.warn`, and `log.error`).
  253. * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
  254. * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
  255. * - `warn`: Matches `log.warn` and `log.error` calls.
  256. * - `error`: Matches only `log.error` calls.
  257. *
  258. * @type {Object}
  259. */
  260. log.levels = {
  261. all: 'debug|log|warn|error',
  262. off: '',
  263. debug: 'debug|log|warn|error',
  264. info: 'log|warn|error',
  265. warn: 'warn|error',
  266. error: 'error',
  267. DEFAULT: level
  268. };
  269. /**
  270. * Get or set the current logging level.
  271. *
  272. * If a string matching a key from {@link module:log.levels} is provided, acts
  273. * as a setter.
  274. *
  275. * @param {string} [lvl]
  276. * Pass a valid level to set a new logging level.
  277. *
  278. * @return {string}
  279. * The current logging level.
  280. */
  281. log.level = lvl => {
  282. if (typeof lvl === 'string') {
  283. if (!log.levels.hasOwnProperty(lvl)) {
  284. throw new Error(`"${lvl}" in not a valid log level`);
  285. }
  286. level = lvl;
  287. }
  288. return level;
  289. };
  290. /**
  291. * Returns an array containing everything that has been logged to the history.
  292. *
  293. * This array is a shallow clone of the internal history record. However, its
  294. * contents are _not_ cloned; so, mutating objects inside this array will
  295. * mutate them in history.
  296. *
  297. * @return {Array}
  298. */
  299. log.history = () => history ? [].concat(history) : [];
  300. /**
  301. * Allows you to filter the history by the given logger name
  302. *
  303. * @param {string} fname
  304. * The name to filter by
  305. *
  306. * @return {Array}
  307. * The filtered list to return
  308. */
  309. log.history.filter = fname => {
  310. return (history || []).filter(historyItem => {
  311. // if the first item in each historyItem includes `fname`, then it's a match
  312. return new RegExp(`.*${fname}.*`).test(historyItem[0]);
  313. });
  314. };
  315. /**
  316. * Clears the internal history tracking, but does not prevent further history
  317. * tracking.
  318. */
  319. log.history.clear = () => {
  320. if (history) {
  321. history.length = 0;
  322. }
  323. };
  324. /**
  325. * Disable history tracking if it is currently enabled.
  326. */
  327. log.history.disable = () => {
  328. if (history !== null) {
  329. history.length = 0;
  330. history = null;
  331. }
  332. };
  333. /**
  334. * Enable history tracking if it is currently disabled.
  335. */
  336. log.history.enable = () => {
  337. if (history === null) {
  338. history = [];
  339. }
  340. };
  341. /**
  342. * Logs error messages. Similar to `console.error`.
  343. *
  344. * @param {...*} args
  345. * One or more messages or objects that should be logged as an error
  346. */
  347. log.error = (...args) => logByType('error', level, args);
  348. /**
  349. * Logs warning messages. Similar to `console.warn`.
  350. *
  351. * @param {...*} args
  352. * One or more messages or objects that should be logged as a warning.
  353. */
  354. log.warn = (...args) => logByType('warn', level, args);
  355. /**
  356. * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
  357. * log if `console.debug` is not available
  358. *
  359. * @param {...*} args
  360. * One or more messages or objects that should be logged as debug.
  361. */
  362. log.debug = (...args) => logByType('debug', level, args);
  363. return log;
  364. }
  365. /**
  366. * @file log.js
  367. * @module log
  368. */
  369. const log = createLogger$1('VIDEOJS');
  370. const createLogger = log.createLogger;
  371. /**
  372. * @file obj.js
  373. * @module obj
  374. */
  375. /**
  376. * @callback obj:EachCallback
  377. *
  378. * @param {*} value
  379. * The current key for the object that is being iterated over.
  380. *
  381. * @param {string} key
  382. * The current key-value for object that is being iterated over
  383. */
  384. /**
  385. * @callback obj:ReduceCallback
  386. *
  387. * @param {*} accum
  388. * The value that is accumulating over the reduce loop.
  389. *
  390. * @param {*} value
  391. * The current key for the object that is being iterated over.
  392. *
  393. * @param {string} key
  394. * The current key-value for object that is being iterated over
  395. *
  396. * @return {*}
  397. * The new accumulated value.
  398. */
  399. const toString = Object.prototype.toString;
  400. /**
  401. * Get the keys of an Object
  402. *
  403. * @param {Object}
  404. * The Object to get the keys from
  405. *
  406. * @return {string[]}
  407. * An array of the keys from the object. Returns an empty array if the
  408. * object passed in was invalid or had no keys.
  409. *
  410. * @private
  411. */
  412. const keys = function (object) {
  413. return isObject(object) ? Object.keys(object) : [];
  414. };
  415. /**
  416. * Array-like iteration for objects.
  417. *
  418. * @param {Object} object
  419. * The object to iterate over
  420. *
  421. * @param {obj:EachCallback} fn
  422. * The callback function which is called for each key in the object.
  423. */
  424. function each(object, fn) {
  425. keys(object).forEach(key => fn(object[key], key));
  426. }
  427. /**
  428. * Array-like reduce for objects.
  429. *
  430. * @param {Object} object
  431. * The Object that you want to reduce.
  432. *
  433. * @param {Function} fn
  434. * A callback function which is called for each key in the object. It
  435. * receives the accumulated value and the per-iteration value and key
  436. * as arguments.
  437. *
  438. * @param {*} [initial = 0]
  439. * Starting value
  440. *
  441. * @return {*}
  442. * The final accumulated value.
  443. */
  444. function reduce(object, fn, initial = 0) {
  445. return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
  446. }
  447. /**
  448. * Returns whether a value is an object of any kind - including DOM nodes,
  449. * arrays, regular expressions, etc. Not functions, though.
  450. *
  451. * This avoids the gotcha where using `typeof` on a `null` value
  452. * results in `'object'`.
  453. *
  454. * @param {Object} value
  455. * @return {boolean}
  456. */
  457. function isObject(value) {
  458. return !!value && typeof value === 'object';
  459. }
  460. /**
  461. * Returns whether an object appears to be a "plain" object - that is, a
  462. * direct instance of `Object`.
  463. *
  464. * @param {Object} value
  465. * @return {boolean}
  466. */
  467. function isPlain(value) {
  468. return isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object;
  469. }
  470. /**
  471. * Merge two objects recursively.
  472. *
  473. * Performs a deep merge like
  474. * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
  475. * plain objects (not arrays, elements, or anything else).
  476. *
  477. * Non-plain object values will be copied directly from the right-most
  478. * argument.
  479. *
  480. * @param {Object[]} sources
  481. * One or more objects to merge into a new object.
  482. *
  483. * @return {Object}
  484. * A new object that is the merged result of all sources.
  485. */
  486. function merge(...sources) {
  487. const result = {};
  488. sources.forEach(source => {
  489. if (!source) {
  490. return;
  491. }
  492. each(source, (value, key) => {
  493. if (!isPlain(value)) {
  494. result[key] = value;
  495. return;
  496. }
  497. if (!isPlain(result[key])) {
  498. result[key] = {};
  499. }
  500. result[key] = merge(result[key], value);
  501. });
  502. });
  503. return result;
  504. }
  505. /**
  506. * Object.defineProperty but "lazy", which means that the value is only set after
  507. * it is retrieved the first time, rather than being set right away.
  508. *
  509. * @param {Object} obj the object to set the property on
  510. * @param {string} key the key for the property to set
  511. * @param {Function} getValue the function used to get the value when it is needed.
  512. * @param {boolean} setter whether a setter should be allowed or not
  513. */
  514. function defineLazyProperty(obj, key, getValue, setter = true) {
  515. const set = value => Object.defineProperty(obj, key, {
  516. value,
  517. enumerable: true,
  518. writable: true
  519. });
  520. const options = {
  521. configurable: true,
  522. enumerable: true,
  523. get() {
  524. const value = getValue();
  525. set(value);
  526. return value;
  527. }
  528. };
  529. if (setter) {
  530. options.set = set;
  531. }
  532. return Object.defineProperty(obj, key, options);
  533. }
  534. var Obj = /*#__PURE__*/Object.freeze({
  535. __proto__: null,
  536. each: each,
  537. reduce: reduce,
  538. isObject: isObject,
  539. isPlain: isPlain,
  540. merge: merge,
  541. defineLazyProperty: defineLazyProperty
  542. });
  543. /**
  544. * @file browser.js
  545. * @module browser
  546. */
  547. /**
  548. * Whether or not this device is an iPod.
  549. *
  550. * @static
  551. * @type {Boolean}
  552. */
  553. let IS_IPOD = false;
  554. /**
  555. * The detected iOS version - or `null`.
  556. *
  557. * @static
  558. * @type {string|null}
  559. */
  560. let IOS_VERSION = null;
  561. /**
  562. * Whether or not this is an Android device.
  563. *
  564. * @static
  565. * @type {Boolean}
  566. */
  567. let IS_ANDROID = false;
  568. /**
  569. * The detected Android version - or `null` if not Android or indeterminable.
  570. *
  571. * @static
  572. * @type {number|string|null}
  573. */
  574. let ANDROID_VERSION;
  575. /**
  576. * Whether or not this is Mozilla Firefox.
  577. *
  578. * @static
  579. * @type {Boolean}
  580. */
  581. let IS_FIREFOX = false;
  582. /**
  583. * Whether or not this is Microsoft Edge.
  584. *
  585. * @static
  586. * @type {Boolean}
  587. */
  588. let IS_EDGE = false;
  589. /**
  590. * Whether or not this is any Chromium Browser
  591. *
  592. * @static
  593. * @type {Boolean}
  594. */
  595. let IS_CHROMIUM = false;
  596. /**
  597. * Whether or not this is any Chromium browser that is not Edge.
  598. *
  599. * This will also be `true` for Chrome on iOS, which will have different support
  600. * as it is actually Safari under the hood.
  601. *
  602. * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
  603. * IS_CHROMIUM should be used instead.
  604. * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
  605. *
  606. * @static
  607. * @deprecated
  608. * @type {Boolean}
  609. */
  610. let IS_CHROME = false;
  611. /**
  612. * The detected Chromium version - or `null`.
  613. *
  614. * @static
  615. * @type {number|null}
  616. */
  617. let CHROMIUM_VERSION = null;
  618. /**
  619. * The detected Google Chrome version - or `null`.
  620. * This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
  621. * Deprecated, use CHROMIUM_VERSION instead.
  622. *
  623. * @static
  624. * @deprecated
  625. * @type {number|null}
  626. */
  627. let CHROME_VERSION = null;
  628. /**
  629. * The detected Internet Explorer version - or `null`.
  630. *
  631. * @static
  632. * @deprecated
  633. * @type {number|null}
  634. */
  635. let IE_VERSION = null;
  636. /**
  637. * Whether or not this is desktop Safari.
  638. *
  639. * @static
  640. * @type {Boolean}
  641. */
  642. let IS_SAFARI = false;
  643. /**
  644. * Whether or not this is a Windows machine.
  645. *
  646. * @static
  647. * @type {Boolean}
  648. */
  649. let IS_WINDOWS = false;
  650. /**
  651. * Whether or not this device is an iPad.
  652. *
  653. * @static
  654. * @type {Boolean}
  655. */
  656. let IS_IPAD = false;
  657. /**
  658. * Whether or not this device is an iPhone.
  659. *
  660. * @static
  661. * @type {Boolean}
  662. */
  663. // The Facebook app's UIWebView identifies as both an iPhone and iPad, so
  664. // to identify iPhones, we need to exclude iPads.
  665. // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
  666. let IS_IPHONE = false;
  667. /**
  668. * Whether or not this device is touch-enabled.
  669. *
  670. * @static
  671. * @const
  672. * @type {Boolean}
  673. */
  674. const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window__default["default"] || window__default["default"].navigator.maxTouchPoints || window__default["default"].DocumentTouch && window__default["default"].document instanceof window__default["default"].DocumentTouch));
  675. const UAD = window__default["default"].navigator && window__default["default"].navigator.userAgentData;
  676. if (UAD) {
  677. // If userAgentData is present, use it instead of userAgent to avoid warnings
  678. // Currently only implemented on Chromium
  679. // userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
  680. IS_ANDROID = UAD.platform === 'Android';
  681. IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
  682. IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
  683. IS_CHROME = !IS_EDGE && IS_CHROMIUM;
  684. CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
  685. IS_WINDOWS = UAD.platform === 'Windows';
  686. }
  687. // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
  688. // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
  689. // the checks need to be made agiainst the regular userAgent string.
  690. if (!IS_CHROMIUM) {
  691. const USER_AGENT = window__default["default"].navigator && window__default["default"].navigator.userAgent || '';
  692. IS_IPOD = /iPod/i.test(USER_AGENT);
  693. IOS_VERSION = function () {
  694. const match = USER_AGENT.match(/OS (\d+)_/i);
  695. if (match && match[1]) {
  696. return match[1];
  697. }
  698. return null;
  699. }();
  700. IS_ANDROID = /Android/i.test(USER_AGENT);
  701. ANDROID_VERSION = function () {
  702. // This matches Android Major.Minor.Patch versions
  703. // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
  704. const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
  705. if (!match) {
  706. return null;
  707. }
  708. const major = match[1] && parseFloat(match[1]);
  709. const minor = match[2] && parseFloat(match[2]);
  710. if (major && minor) {
  711. return parseFloat(match[1] + '.' + match[2]);
  712. } else if (major) {
  713. return major;
  714. }
  715. return null;
  716. }();
  717. IS_FIREFOX = /Firefox/i.test(USER_AGENT);
  718. IS_EDGE = /Edg/i.test(USER_AGENT);
  719. IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
  720. IS_CHROME = !IS_EDGE && IS_CHROMIUM;
  721. CHROMIUM_VERSION = CHROME_VERSION = function () {
  722. const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
  723. if (match && match[2]) {
  724. return parseFloat(match[2]);
  725. }
  726. return null;
  727. }();
  728. IE_VERSION = function () {
  729. const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
  730. let version = result && parseFloat(result[1]);
  731. if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
  732. // IE 11 has a different user agent string than other IE versions
  733. version = 11.0;
  734. }
  735. return version;
  736. }();
  737. IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE;
  738. IS_WINDOWS = /Windows/i.test(USER_AGENT);
  739. IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
  740. IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
  741. }
  742. /**
  743. * Whether or not this is an iOS device.
  744. *
  745. * @static
  746. * @const
  747. * @type {Boolean}
  748. */
  749. const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
  750. /**
  751. * Whether or not this is any flavor of Safari - including iOS.
  752. *
  753. * @static
  754. * @const
  755. * @type {Boolean}
  756. */
  757. const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
  758. var browser = /*#__PURE__*/Object.freeze({
  759. __proto__: null,
  760. get IS_IPOD () { return IS_IPOD; },
  761. get IOS_VERSION () { return IOS_VERSION; },
  762. get IS_ANDROID () { return IS_ANDROID; },
  763. get ANDROID_VERSION () { return ANDROID_VERSION; },
  764. get IS_FIREFOX () { return IS_FIREFOX; },
  765. get IS_EDGE () { return IS_EDGE; },
  766. get IS_CHROMIUM () { return IS_CHROMIUM; },
  767. get IS_CHROME () { return IS_CHROME; },
  768. get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
  769. get CHROME_VERSION () { return CHROME_VERSION; },
  770. get IE_VERSION () { return IE_VERSION; },
  771. get IS_SAFARI () { return IS_SAFARI; },
  772. get IS_WINDOWS () { return IS_WINDOWS; },
  773. get IS_IPAD () { return IS_IPAD; },
  774. get IS_IPHONE () { return IS_IPHONE; },
  775. TOUCH_ENABLED: TOUCH_ENABLED,
  776. IS_IOS: IS_IOS,
  777. IS_ANY_SAFARI: IS_ANY_SAFARI
  778. });
  779. /**
  780. * @file dom.js
  781. * @module dom
  782. */
  783. /**
  784. * Detect if a value is a string with any non-whitespace characters.
  785. *
  786. * @private
  787. * @param {string} str
  788. * The string to check
  789. *
  790. * @return {boolean}
  791. * Will be `true` if the string is non-blank, `false` otherwise.
  792. *
  793. */
  794. function isNonBlankString(str) {
  795. // we use str.trim as it will trim any whitespace characters
  796. // from the front or back of non-whitespace characters. aka
  797. // Any string that contains non-whitespace characters will
  798. // still contain them after `trim` but whitespace only strings
  799. // will have a length of 0, failing this check.
  800. return typeof str === 'string' && Boolean(str.trim());
  801. }
  802. /**
  803. * Throws an error if the passed string has whitespace. This is used by
  804. * class methods to be relatively consistent with the classList API.
  805. *
  806. * @private
  807. * @param {string} str
  808. * The string to check for whitespace.
  809. *
  810. * @throws {Error}
  811. * Throws an error if there is whitespace in the string.
  812. */
  813. function throwIfWhitespace(str) {
  814. // str.indexOf instead of regex because str.indexOf is faster performance wise.
  815. if (str.indexOf(' ') >= 0) {
  816. throw new Error('class has illegal whitespace characters');
  817. }
  818. }
  819. /**
  820. * Whether the current DOM interface appears to be real (i.e. not simulated).
  821. *
  822. * @return {boolean}
  823. * Will be `true` if the DOM appears to be real, `false` otherwise.
  824. */
  825. function isReal() {
  826. // Both document and window will never be undefined thanks to `global`.
  827. return document__default["default"] === window__default["default"].document;
  828. }
  829. /**
  830. * Determines, via duck typing, whether or not a value is a DOM element.
  831. *
  832. * @param {*} value
  833. * The value to check.
  834. *
  835. * @return {boolean}
  836. * Will be `true` if the value is a DOM element, `false` otherwise.
  837. */
  838. function isEl(value) {
  839. return isObject(value) && value.nodeType === 1;
  840. }
  841. /**
  842. * Determines if the current DOM is embedded in an iframe.
  843. *
  844. * @return {boolean}
  845. * Will be `true` if the DOM is embedded in an iframe, `false`
  846. * otherwise.
  847. */
  848. function isInFrame() {
  849. // We need a try/catch here because Safari will throw errors when attempting
  850. // to get either `parent` or `self`
  851. try {
  852. return window__default["default"].parent !== window__default["default"].self;
  853. } catch (x) {
  854. return true;
  855. }
  856. }
  857. /**
  858. * Creates functions to query the DOM using a given method.
  859. *
  860. * @private
  861. * @param {string} method
  862. * The method to create the query with.
  863. *
  864. * @return {Function}
  865. * The query method
  866. */
  867. function createQuerier(method) {
  868. return function (selector, context) {
  869. if (!isNonBlankString(selector)) {
  870. return document__default["default"][method](null);
  871. }
  872. if (isNonBlankString(context)) {
  873. context = document__default["default"].querySelector(context);
  874. }
  875. const ctx = isEl(context) ? context : document__default["default"];
  876. return ctx[method] && ctx[method](selector);
  877. };
  878. }
  879. /**
  880. * Creates an element and applies properties, attributes, and inserts content.
  881. *
  882. * @param {string} [tagName='div']
  883. * Name of tag to be created.
  884. *
  885. * @param {Object} [properties={}]
  886. * Element properties to be applied.
  887. *
  888. * @param {Object} [attributes={}]
  889. * Element attributes to be applied.
  890. *
  891. * @param {ContentDescriptor} [content]
  892. * A content descriptor object.
  893. *
  894. * @return {Element}
  895. * The element that was created.
  896. */
  897. function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
  898. const el = document__default["default"].createElement(tagName);
  899. Object.getOwnPropertyNames(properties).forEach(function (propName) {
  900. const val = properties[propName];
  901. // Handle textContent since it's not supported everywhere and we have a
  902. // method for it.
  903. if (propName === 'textContent') {
  904. textContent(el, val);
  905. } else if (el[propName] !== val || propName === 'tabIndex') {
  906. el[propName] = val;
  907. }
  908. });
  909. Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
  910. el.setAttribute(attrName, attributes[attrName]);
  911. });
  912. if (content) {
  913. appendContent(el, content);
  914. }
  915. return el;
  916. }
  917. /**
  918. * Injects text into an element, replacing any existing contents entirely.
  919. *
  920. * @param {Element} el
  921. * The element to add text content into
  922. *
  923. * @param {string} text
  924. * The text content to add.
  925. *
  926. * @return {Element}
  927. * The element with added text content.
  928. */
  929. function textContent(el, text) {
  930. if (typeof el.textContent === 'undefined') {
  931. el.innerText = text;
  932. } else {
  933. el.textContent = text;
  934. }
  935. return el;
  936. }
  937. /**
  938. * Insert an element as the first child node of another
  939. *
  940. * @param {Element} child
  941. * Element to insert
  942. *
  943. * @param {Element} parent
  944. * Element to insert child into
  945. */
  946. function prependTo(child, parent) {
  947. if (parent.firstChild) {
  948. parent.insertBefore(child, parent.firstChild);
  949. } else {
  950. parent.appendChild(child);
  951. }
  952. }
  953. /**
  954. * Check if an element has a class name.
  955. *
  956. * @param {Element} element
  957. * Element to check
  958. *
  959. * @param {string} classToCheck
  960. * Class name to check for
  961. *
  962. * @return {boolean}
  963. * Will be `true` if the element has a class, `false` otherwise.
  964. *
  965. * @throws {Error}
  966. * Throws an error if `classToCheck` has white space.
  967. */
  968. function hasClass(element, classToCheck) {
  969. throwIfWhitespace(classToCheck);
  970. return element.classList.contains(classToCheck);
  971. }
  972. /**
  973. * Add a class name to an element.
  974. *
  975. * @param {Element} element
  976. * Element to add class name to.
  977. *
  978. * @param {...string} classesToAdd
  979. * One or more class name to add.
  980. *
  981. * @return {Element}
  982. * The DOM element with the added class name.
  983. */
  984. function addClass(element, ...classesToAdd) {
  985. element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
  986. return element;
  987. }
  988. /**
  989. * Remove a class name from an element.
  990. *
  991. * @param {Element} element
  992. * Element to remove a class name from.
  993. *
  994. * @param {...string} classesToRemove
  995. * One or more class name to remove.
  996. *
  997. * @return {Element}
  998. * The DOM element with class name removed.
  999. */
  1000. function removeClass(element, ...classesToRemove) {
  1001. // Protect in case the player gets disposed
  1002. if (!element) {
  1003. log.warn("removeClass was called with an element that doesn't exist");
  1004. return null;
  1005. }
  1006. element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
  1007. return element;
  1008. }
  1009. /**
  1010. * The callback definition for toggleClass.
  1011. *
  1012. * @callback module:dom~PredicateCallback
  1013. * @param {Element} element
  1014. * The DOM element of the Component.
  1015. *
  1016. * @param {string} classToToggle
  1017. * The `className` that wants to be toggled
  1018. *
  1019. * @return {boolean|undefined}
  1020. * If `true` is returned, the `classToToggle` will be added to the
  1021. * `element`. If `false`, the `classToToggle` will be removed from
  1022. * the `element`. If `undefined`, the callback will be ignored.
  1023. */
  1024. /**
  1025. * Adds or removes a class name to/from an element depending on an optional
  1026. * condition or the presence/absence of the class name.
  1027. *
  1028. * @param {Element} element
  1029. * The element to toggle a class name on.
  1030. *
  1031. * @param {string} classToToggle
  1032. * The class that should be toggled.
  1033. *
  1034. * @param {boolean|module:dom~PredicateCallback} [predicate]
  1035. * See the return value for {@link module:dom~PredicateCallback}
  1036. *
  1037. * @return {Element}
  1038. * The element with a class that has been toggled.
  1039. */
  1040. function toggleClass(element, classToToggle, predicate) {
  1041. if (typeof predicate === 'function') {
  1042. predicate = predicate(element, classToToggle);
  1043. }
  1044. if (typeof predicate !== 'boolean') {
  1045. predicate = undefined;
  1046. }
  1047. classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
  1048. return element;
  1049. }
  1050. /**
  1051. * Apply attributes to an HTML element.
  1052. *
  1053. * @param {Element} el
  1054. * Element to add attributes to.
  1055. *
  1056. * @param {Object} [attributes]
  1057. * Attributes to be applied.
  1058. */
  1059. function setAttributes(el, attributes) {
  1060. Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
  1061. const attrValue = attributes[attrName];
  1062. if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
  1063. el.removeAttribute(attrName);
  1064. } else {
  1065. el.setAttribute(attrName, attrValue === true ? '' : attrValue);
  1066. }
  1067. });
  1068. }
  1069. /**
  1070. * Get an element's attribute values, as defined on the HTML tag.
  1071. *
  1072. * Attributes are not the same as properties. They're defined on the tag
  1073. * or with setAttribute.
  1074. *
  1075. * @param {Element} tag
  1076. * Element from which to get tag attributes.
  1077. *
  1078. * @return {Object}
  1079. * All attributes of the element. Boolean attributes will be `true` or
  1080. * `false`, others will be strings.
  1081. */
  1082. function getAttributes(tag) {
  1083. const obj = {};
  1084. // known boolean attributes
  1085. // we can check for matching boolean properties, but not all browsers
  1086. // and not all tags know about these attributes, so, we still want to check them manually
  1087. const knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ',';
  1088. if (tag && tag.attributes && tag.attributes.length > 0) {
  1089. const attrs = tag.attributes;
  1090. for (let i = attrs.length - 1; i >= 0; i--) {
  1091. const attrName = attrs[i].name;
  1092. let attrVal = attrs[i].value;
  1093. // check for known booleans
  1094. // the matching element property will return a value for typeof
  1095. if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) {
  1096. // the value of an included boolean attribute is typically an empty
  1097. // string ('') which would equal false if we just check for a false value.
  1098. // we also don't want support bad code like autoplay='false'
  1099. attrVal = attrVal !== null ? true : false;
  1100. }
  1101. obj[attrName] = attrVal;
  1102. }
  1103. }
  1104. return obj;
  1105. }
  1106. /**
  1107. * Get the value of an element's attribute.
  1108. *
  1109. * @param {Element} el
  1110. * A DOM element.
  1111. *
  1112. * @param {string} attribute
  1113. * Attribute to get the value of.
  1114. *
  1115. * @return {string}
  1116. * The value of the attribute.
  1117. */
  1118. function getAttribute(el, attribute) {
  1119. return el.getAttribute(attribute);
  1120. }
  1121. /**
  1122. * Set the value of an element's attribute.
  1123. *
  1124. * @param {Element} el
  1125. * A DOM element.
  1126. *
  1127. * @param {string} attribute
  1128. * Attribute to set.
  1129. *
  1130. * @param {string} value
  1131. * Value to set the attribute to.
  1132. */
  1133. function setAttribute(el, attribute, value) {
  1134. el.setAttribute(attribute, value);
  1135. }
  1136. /**
  1137. * Remove an element's attribute.
  1138. *
  1139. * @param {Element} el
  1140. * A DOM element.
  1141. *
  1142. * @param {string} attribute
  1143. * Attribute to remove.
  1144. */
  1145. function removeAttribute(el, attribute) {
  1146. el.removeAttribute(attribute);
  1147. }
  1148. /**
  1149. * Attempt to block the ability to select text.
  1150. */
  1151. function blockTextSelection() {
  1152. document__default["default"].body.focus();
  1153. document__default["default"].onselectstart = function () {
  1154. return false;
  1155. };
  1156. }
  1157. /**
  1158. * Turn off text selection blocking.
  1159. */
  1160. function unblockTextSelection() {
  1161. document__default["default"].onselectstart = function () {
  1162. return true;
  1163. };
  1164. }
  1165. /**
  1166. * Identical to the native `getBoundingClientRect` function, but ensures that
  1167. * the method is supported at all (it is in all browsers we claim to support)
  1168. * and that the element is in the DOM before continuing.
  1169. *
  1170. * This wrapper function also shims properties which are not provided by some
  1171. * older browsers (namely, IE8).
  1172. *
  1173. * Additionally, some browsers do not support adding properties to a
  1174. * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
  1175. * properties (except `x` and `y` which are not widely supported). This helps
  1176. * avoid implementations where keys are non-enumerable.
  1177. *
  1178. * @param {Element} el
  1179. * Element whose `ClientRect` we want to calculate.
  1180. *
  1181. * @return {Object|undefined}
  1182. * Always returns a plain object - or `undefined` if it cannot.
  1183. */
  1184. function getBoundingClientRect(el) {
  1185. if (el && el.getBoundingClientRect && el.parentNode) {
  1186. const rect = el.getBoundingClientRect();
  1187. const result = {};
  1188. ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
  1189. if (rect[k] !== undefined) {
  1190. result[k] = rect[k];
  1191. }
  1192. });
  1193. if (!result.height) {
  1194. result.height = parseFloat(computedStyle(el, 'height'));
  1195. }
  1196. if (!result.width) {
  1197. result.width = parseFloat(computedStyle(el, 'width'));
  1198. }
  1199. return result;
  1200. }
  1201. }
  1202. /**
  1203. * Represents the position of a DOM element on the page.
  1204. *
  1205. * @typedef {Object} module:dom~Position
  1206. *
  1207. * @property {number} left
  1208. * Pixels to the left.
  1209. *
  1210. * @property {number} top
  1211. * Pixels from the top.
  1212. */
  1213. /**
  1214. * Get the position of an element in the DOM.
  1215. *
  1216. * Uses `getBoundingClientRect` technique from John Resig.
  1217. *
  1218. * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
  1219. *
  1220. * @param {Element} el
  1221. * Element from which to get offset.
  1222. *
  1223. * @return {module:dom~Position}
  1224. * The position of the element that was passed in.
  1225. */
  1226. function findPosition(el) {
  1227. if (!el || el && !el.offsetParent) {
  1228. return {
  1229. left: 0,
  1230. top: 0,
  1231. width: 0,
  1232. height: 0
  1233. };
  1234. }
  1235. const width = el.offsetWidth;
  1236. const height = el.offsetHeight;
  1237. let left = 0;
  1238. let top = 0;
  1239. while (el.offsetParent && el !== document__default["default"][FullscreenApi.fullscreenElement]) {
  1240. left += el.offsetLeft;
  1241. top += el.offsetTop;
  1242. el = el.offsetParent;
  1243. }
  1244. return {
  1245. left,
  1246. top,
  1247. width,
  1248. height
  1249. };
  1250. }
  1251. /**
  1252. * Represents x and y coordinates for a DOM element or mouse pointer.
  1253. *
  1254. * @typedef {Object} module:dom~Coordinates
  1255. *
  1256. * @property {number} x
  1257. * x coordinate in pixels
  1258. *
  1259. * @property {number} y
  1260. * y coordinate in pixels
  1261. */
  1262. /**
  1263. * Get the pointer position within an element.
  1264. *
  1265. * The base on the coordinates are the bottom left of the element.
  1266. *
  1267. * @param {Element} el
  1268. * Element on which to get the pointer position on.
  1269. *
  1270. * @param {Event} event
  1271. * Event object.
  1272. *
  1273. * @return {module:dom~Coordinates}
  1274. * A coordinates object corresponding to the mouse position.
  1275. *
  1276. */
  1277. function getPointerPosition(el, event) {
  1278. const translated = {
  1279. x: 0,
  1280. y: 0
  1281. };
  1282. if (IS_IOS) {
  1283. let item = el;
  1284. while (item && item.nodeName.toLowerCase() !== 'html') {
  1285. const transform = computedStyle(item, 'transform');
  1286. if (/^matrix/.test(transform)) {
  1287. const values = transform.slice(7, -1).split(/,\s/).map(Number);
  1288. translated.x += values[4];
  1289. translated.y += values[5];
  1290. } else if (/^matrix3d/.test(transform)) {
  1291. const values = transform.slice(9, -1).split(/,\s/).map(Number);
  1292. translated.x += values[12];
  1293. translated.y += values[13];
  1294. }
  1295. item = item.parentNode;
  1296. }
  1297. }
  1298. const position = {};
  1299. const boxTarget = findPosition(event.target);
  1300. const box = findPosition(el);
  1301. const boxW = box.width;
  1302. const boxH = box.height;
  1303. let offsetY = event.offsetY - (box.top - boxTarget.top);
  1304. let offsetX = event.offsetX - (box.left - boxTarget.left);
  1305. if (event.changedTouches) {
  1306. offsetX = event.changedTouches[0].pageX - box.left;
  1307. offsetY = event.changedTouches[0].pageY + box.top;
  1308. if (IS_IOS) {
  1309. offsetX -= translated.x;
  1310. offsetY -= translated.y;
  1311. }
  1312. }
  1313. position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
  1314. position.x = Math.max(0, Math.min(1, offsetX / boxW));
  1315. return position;
  1316. }
  1317. /**
  1318. * Determines, via duck typing, whether or not a value is a text node.
  1319. *
  1320. * @param {*} value
  1321. * Check if this value is a text node.
  1322. *
  1323. * @return {boolean}
  1324. * Will be `true` if the value is a text node, `false` otherwise.
  1325. */
  1326. function isTextNode(value) {
  1327. return isObject(value) && value.nodeType === 3;
  1328. }
  1329. /**
  1330. * Empties the contents of an element.
  1331. *
  1332. * @param {Element} el
  1333. * The element to empty children from
  1334. *
  1335. * @return {Element}
  1336. * The element with no children
  1337. */
  1338. function emptyEl(el) {
  1339. while (el.firstChild) {
  1340. el.removeChild(el.firstChild);
  1341. }
  1342. return el;
  1343. }
  1344. /**
  1345. * This is a mixed value that describes content to be injected into the DOM
  1346. * via some method. It can be of the following types:
  1347. *
  1348. * Type | Description
  1349. * -----------|-------------
  1350. * `string` | The value will be normalized into a text node.
  1351. * `Element` | The value will be accepted as-is.
  1352. * `Text` | A TextNode. The value will be accepted as-is.
  1353. * `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
  1354. * `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
  1355. *
  1356. * @typedef {string|Element|Text|Array|Function} ContentDescriptor
  1357. */
  1358. /**
  1359. * Normalizes content for eventual insertion into the DOM.
  1360. *
  1361. * This allows a wide range of content definition methods, but helps protect
  1362. * from falling into the trap of simply writing to `innerHTML`, which could
  1363. * be an XSS concern.
  1364. *
  1365. * The content for an element can be passed in multiple types and
  1366. * combinations, whose behavior is as follows:
  1367. *
  1368. * @param {ContentDescriptor} content
  1369. * A content descriptor value.
  1370. *
  1371. * @return {Array}
  1372. * All of the content that was passed in, normalized to an array of
  1373. * elements or text nodes.
  1374. */
  1375. function normalizeContent(content) {
  1376. // First, invoke content if it is a function. If it produces an array,
  1377. // that needs to happen before normalization.
  1378. if (typeof content === 'function') {
  1379. content = content();
  1380. }
  1381. // Next up, normalize to an array, so one or many items can be normalized,
  1382. // filtered, and returned.
  1383. return (Array.isArray(content) ? content : [content]).map(value => {
  1384. // First, invoke value if it is a function to produce a new value,
  1385. // which will be subsequently normalized to a Node of some kind.
  1386. if (typeof value === 'function') {
  1387. value = value();
  1388. }
  1389. if (isEl(value) || isTextNode(value)) {
  1390. return value;
  1391. }
  1392. if (typeof value === 'string' && /\S/.test(value)) {
  1393. return document__default["default"].createTextNode(value);
  1394. }
  1395. }).filter(value => value);
  1396. }
  1397. /**
  1398. * Normalizes and appends content to an element.
  1399. *
  1400. * @param {Element} el
  1401. * Element to append normalized content to.
  1402. *
  1403. * @param {ContentDescriptor} content
  1404. * A content descriptor value.
  1405. *
  1406. * @return {Element}
  1407. * The element with appended normalized content.
  1408. */
  1409. function appendContent(el, content) {
  1410. normalizeContent(content).forEach(node => el.appendChild(node));
  1411. return el;
  1412. }
  1413. /**
  1414. * Normalizes and inserts content into an element; this is identical to
  1415. * `appendContent()`, except it empties the element first.
  1416. *
  1417. * @param {Element} el
  1418. * Element to insert normalized content into.
  1419. *
  1420. * @param {ContentDescriptor} content
  1421. * A content descriptor value.
  1422. *
  1423. * @return {Element}
  1424. * The element with inserted normalized content.
  1425. */
  1426. function insertContent(el, content) {
  1427. return appendContent(emptyEl(el), content);
  1428. }
  1429. /**
  1430. * Check if an event was a single left click.
  1431. *
  1432. * @param {Event} event
  1433. * Event object.
  1434. *
  1435. * @return {boolean}
  1436. * Will be `true` if a single left click, `false` otherwise.
  1437. */
  1438. function isSingleLeftClick(event) {
  1439. // Note: if you create something draggable, be sure to
  1440. // call it on both `mousedown` and `mousemove` event,
  1441. // otherwise `mousedown` should be enough for a button
  1442. if (event.button === undefined && event.buttons === undefined) {
  1443. // Why do we need `buttons` ?
  1444. // Because, middle mouse sometimes have this:
  1445. // e.button === 0 and e.buttons === 4
  1446. // Furthermore, we want to prevent combination click, something like
  1447. // HOLD middlemouse then left click, that would be
  1448. // e.button === 0, e.buttons === 5
  1449. // just `button` is not gonna work
  1450. // Alright, then what this block does ?
  1451. // this is for chrome `simulate mobile devices`
  1452. // I want to support this as well
  1453. return true;
  1454. }
  1455. if (event.button === 0 && event.buttons === undefined) {
  1456. // Touch screen, sometimes on some specific device, `buttons`
  1457. // doesn't have anything (safari on ios, blackberry...)
  1458. return true;
  1459. }
  1460. // `mouseup` event on a single left click has
  1461. // `button` and `buttons` equal to 0
  1462. if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
  1463. return true;
  1464. }
  1465. if (event.button !== 0 || event.buttons !== 1) {
  1466. // This is the reason we have those if else block above
  1467. // if any special case we can catch and let it slide
  1468. // we do it above, when get to here, this definitely
  1469. // is-not-left-click
  1470. return false;
  1471. }
  1472. return true;
  1473. }
  1474. /**
  1475. * Finds a single DOM element matching `selector` within the optional
  1476. * `context` of another DOM element (defaulting to `document`).
  1477. *
  1478. * @param {string} selector
  1479. * A valid CSS selector, which will be passed to `querySelector`.
  1480. *
  1481. * @param {Element|String} [context=document]
  1482. * A DOM element within which to query. Can also be a selector
  1483. * string in which case the first matching element will be used
  1484. * as context. If missing (or no element matches selector), falls
  1485. * back to `document`.
  1486. *
  1487. * @return {Element|null}
  1488. * The element that was found or null.
  1489. */
  1490. const $ = createQuerier('querySelector');
  1491. /**
  1492. * Finds a all DOM elements matching `selector` within the optional
  1493. * `context` of another DOM element (defaulting to `document`).
  1494. *
  1495. * @param {string} selector
  1496. * A valid CSS selector, which will be passed to `querySelectorAll`.
  1497. *
  1498. * @param {Element|String} [context=document]
  1499. * A DOM element within which to query. Can also be a selector
  1500. * string in which case the first matching element will be used
  1501. * as context. If missing (or no element matches selector), falls
  1502. * back to `document`.
  1503. *
  1504. * @return {NodeList}
  1505. * A element list of elements that were found. Will be empty if none
  1506. * were found.
  1507. *
  1508. */
  1509. const $$ = createQuerier('querySelectorAll');
  1510. /**
  1511. * A safe getComputedStyle.
  1512. *
  1513. * This is needed because in Firefox, if the player is loaded in an iframe with
  1514. * `display:none`, then `getComputedStyle` returns `null`, so, we do a
  1515. * null-check to make sure that the player doesn't break in these cases.
  1516. *
  1517. * @param {Element} el
  1518. * The element you want the computed style of
  1519. *
  1520. * @param {string} prop
  1521. * The property name you want
  1522. *
  1523. * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
  1524. */
  1525. function computedStyle(el, prop) {
  1526. if (!el || !prop) {
  1527. return '';
  1528. }
  1529. if (typeof window__default["default"].getComputedStyle === 'function') {
  1530. let computedStyleValue;
  1531. try {
  1532. computedStyleValue = window__default["default"].getComputedStyle(el);
  1533. } catch (e) {
  1534. return '';
  1535. }
  1536. return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
  1537. }
  1538. return '';
  1539. }
  1540. var Dom = /*#__PURE__*/Object.freeze({
  1541. __proto__: null,
  1542. isReal: isReal,
  1543. isEl: isEl,
  1544. isInFrame: isInFrame,
  1545. createEl: createEl,
  1546. textContent: textContent,
  1547. prependTo: prependTo,
  1548. hasClass: hasClass,
  1549. addClass: addClass,
  1550. removeClass: removeClass,
  1551. toggleClass: toggleClass,
  1552. setAttributes: setAttributes,
  1553. getAttributes: getAttributes,
  1554. getAttribute: getAttribute,
  1555. setAttribute: setAttribute,
  1556. removeAttribute: removeAttribute,
  1557. blockTextSelection: blockTextSelection,
  1558. unblockTextSelection: unblockTextSelection,
  1559. getBoundingClientRect: getBoundingClientRect,
  1560. findPosition: findPosition,
  1561. getPointerPosition: getPointerPosition,
  1562. isTextNode: isTextNode,
  1563. emptyEl: emptyEl,
  1564. normalizeContent: normalizeContent,
  1565. appendContent: appendContent,
  1566. insertContent: insertContent,
  1567. isSingleLeftClick: isSingleLeftClick,
  1568. $: $,
  1569. $$: $$,
  1570. computedStyle: computedStyle
  1571. });
  1572. /**
  1573. * @file setup.js - Functions for setting up a player without
  1574. * user interaction based on the data-setup `attribute` of the video tag.
  1575. *
  1576. * @module setup
  1577. */
  1578. let _windowLoaded = false;
  1579. let videojs$1;
  1580. /**
  1581. * Set up any tags that have a data-setup `attribute` when the player is started.
  1582. */
  1583. const autoSetup = function () {
  1584. if (videojs$1.options.autoSetup === false) {
  1585. return;
  1586. }
  1587. const vids = Array.prototype.slice.call(document__default["default"].getElementsByTagName('video'));
  1588. const audios = Array.prototype.slice.call(document__default["default"].getElementsByTagName('audio'));
  1589. const divs = Array.prototype.slice.call(document__default["default"].getElementsByTagName('video-js'));
  1590. const mediaEls = vids.concat(audios, divs);
  1591. // Check if any media elements exist
  1592. if (mediaEls && mediaEls.length > 0) {
  1593. for (let i = 0, e = mediaEls.length; i < e; i++) {
  1594. const mediaEl = mediaEls[i];
  1595. // Check if element exists, has getAttribute func.
  1596. if (mediaEl && mediaEl.getAttribute) {
  1597. // Make sure this player hasn't already been set up.
  1598. if (mediaEl.player === undefined) {
  1599. const options = mediaEl.getAttribute('data-setup');
  1600. // Check if data-setup attr exists.
  1601. // We only auto-setup if they've added the data-setup attr.
  1602. if (options !== null) {
  1603. // Create new video.js instance.
  1604. videojs$1(mediaEl);
  1605. }
  1606. }
  1607. // If getAttribute isn't defined, we need to wait for the DOM.
  1608. } else {
  1609. autoSetupTimeout(1);
  1610. break;
  1611. }
  1612. }
  1613. // No videos were found, so keep looping unless page is finished loading.
  1614. } else if (!_windowLoaded) {
  1615. autoSetupTimeout(1);
  1616. }
  1617. };
  1618. /**
  1619. * Wait until the page is loaded before running autoSetup. This will be called in
  1620. * autoSetup if `hasLoaded` returns false.
  1621. *
  1622. * @param {number} wait
  1623. * How long to wait in ms
  1624. *
  1625. * @param {module:videojs} [vjs]
  1626. * The videojs library function
  1627. */
  1628. function autoSetupTimeout(wait, vjs) {
  1629. // Protect against breakage in non-browser environments
  1630. if (!isReal()) {
  1631. return;
  1632. }
  1633. if (vjs) {
  1634. videojs$1 = vjs;
  1635. }
  1636. window__default["default"].setTimeout(autoSetup, wait);
  1637. }
  1638. /**
  1639. * Used to set the internal tracking of window loaded state to true.
  1640. *
  1641. * @private
  1642. */
  1643. function setWindowLoaded() {
  1644. _windowLoaded = true;
  1645. window__default["default"].removeEventListener('load', setWindowLoaded);
  1646. }
  1647. if (isReal()) {
  1648. if (document__default["default"].readyState === 'complete') {
  1649. setWindowLoaded();
  1650. } else {
  1651. /**
  1652. * Listen for the load event on window, and set _windowLoaded to true.
  1653. *
  1654. * We use a standard event listener here to avoid incrementing the GUID
  1655. * before any players are created.
  1656. *
  1657. * @listens load
  1658. */
  1659. window__default["default"].addEventListener('load', setWindowLoaded);
  1660. }
  1661. }
  1662. /**
  1663. * @file stylesheet.js
  1664. * @module stylesheet
  1665. */
  1666. /**
  1667. * Create a DOM style element given a className for it.
  1668. *
  1669. * @param {string} className
  1670. * The className to add to the created style element.
  1671. *
  1672. * @return {Element}
  1673. * The element that was created.
  1674. */
  1675. const createStyleElement = function (className) {
  1676. const style = document__default["default"].createElement('style');
  1677. style.className = className;
  1678. return style;
  1679. };
  1680. /**
  1681. * Add text to a DOM element.
  1682. *
  1683. * @param {Element} el
  1684. * The Element to add text content to.
  1685. *
  1686. * @param {string} content
  1687. * The text to add to the element.
  1688. */
  1689. const setTextContent = function (el, content) {
  1690. if (el.styleSheet) {
  1691. el.styleSheet.cssText = content;
  1692. } else {
  1693. el.textContent = content;
  1694. }
  1695. };
  1696. /**
  1697. * @file dom-data.js
  1698. * @module dom-data
  1699. */
  1700. /**
  1701. * Element Data Store.
  1702. *
  1703. * Allows for binding data to an element without putting it directly on the
  1704. * element. Ex. Event listeners are stored here.
  1705. * (also from jsninja.com, slightly modified and updated for closure compiler)
  1706. *
  1707. * @type {Object}
  1708. * @private
  1709. */
  1710. var DomData = new WeakMap();
  1711. /**
  1712. * @file guid.js
  1713. * @module guid
  1714. */
  1715. // Default value for GUIDs. This allows us to reset the GUID counter in tests.
  1716. //
  1717. // The initial GUID is 3 because some users have come to rely on the first
  1718. // default player ID ending up as `vjs_video_3`.
  1719. //
  1720. // See: https://github.com/videojs/video.js/pull/6216
  1721. const _initialGuid = 3;
  1722. /**
  1723. * Unique ID for an element or function
  1724. *
  1725. * @type {Number}
  1726. */
  1727. let _guid = _initialGuid;
  1728. /**
  1729. * Get a unique auto-incrementing ID by number that has not been returned before.
  1730. *
  1731. * @return {number}
  1732. * A new unique ID.
  1733. */
  1734. function newGUID() {
  1735. return _guid++;
  1736. }
  1737. /**
  1738. * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
  1739. * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
  1740. * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
  1741. * robust as jquery's, so there's probably some differences.
  1742. *
  1743. * @file events.js
  1744. * @module events
  1745. */
  1746. /**
  1747. * Clean up the listener cache and dispatchers
  1748. *
  1749. * @param {Element|Object} elem
  1750. * Element to clean up
  1751. *
  1752. * @param {string} type
  1753. * Type of event to clean up
  1754. */
  1755. function _cleanUpEvents(elem, type) {
  1756. if (!DomData.has(elem)) {
  1757. return;
  1758. }
  1759. const data = DomData.get(elem);
  1760. // Remove the events of a particular type if there are none left
  1761. if (data.handlers[type].length === 0) {
  1762. delete data.handlers[type];
  1763. // data.handlers[type] = null;
  1764. // Setting to null was causing an error with data.handlers
  1765. // Remove the meta-handler from the element
  1766. if (elem.removeEventListener) {
  1767. elem.removeEventListener(type, data.dispatcher, false);
  1768. } else if (elem.detachEvent) {
  1769. elem.detachEvent('on' + type, data.dispatcher);
  1770. }
  1771. }
  1772. // Remove the events object if there are no types left
  1773. if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
  1774. delete data.handlers;
  1775. delete data.dispatcher;
  1776. delete data.disabled;
  1777. }
  1778. // Finally remove the element data if there is no data left
  1779. if (Object.getOwnPropertyNames(data).length === 0) {
  1780. DomData.delete(elem);
  1781. }
  1782. }
  1783. /**
  1784. * Loops through an array of event types and calls the requested method for each type.
  1785. *
  1786. * @param {Function} fn
  1787. * The event method we want to use.
  1788. *
  1789. * @param {Element|Object} elem
  1790. * Element or object to bind listeners to
  1791. *
  1792. * @param {string} type
  1793. * Type of event to bind to.
  1794. *
  1795. * @param {Function} callback
  1796. * Event listener.
  1797. */
  1798. function _handleMultipleEvents(fn, elem, types, callback) {
  1799. types.forEach(function (type) {
  1800. // Call the event method for each one of the types
  1801. fn(elem, type, callback);
  1802. });
  1803. }
  1804. /**
  1805. * Fix a native event to have standard property values
  1806. *
  1807. * @param {Object} event
  1808. * Event object to fix.
  1809. *
  1810. * @return {Object}
  1811. * Fixed event object.
  1812. */
  1813. function fixEvent(event) {
  1814. if (event.fixed_) {
  1815. return event;
  1816. }
  1817. function returnTrue() {
  1818. return true;
  1819. }
  1820. function returnFalse() {
  1821. return false;
  1822. }
  1823. // Test if fixing up is needed
  1824. // Used to check if !event.stopPropagation instead of isPropagationStopped
  1825. // But native events return true for stopPropagation, but don't have
  1826. // other expected methods like isPropagationStopped. Seems to be a problem
  1827. // with the Javascript Ninja code. So we're just overriding all events now.
  1828. if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
  1829. const old = event || window__default["default"].event;
  1830. event = {};
  1831. // Clone the old object so that we can modify the values event = {};
  1832. // IE8 Doesn't like when you mess with native event properties
  1833. // Firefox returns false for event.hasOwnProperty('type') and other props
  1834. // which makes copying more difficult.
  1835. // TODO: Probably best to create a whitelist of event props
  1836. for (const key in old) {
  1837. // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
  1838. // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
  1839. // and webkitMovementX/Y
  1840. // Lighthouse complains if Event.path is copied
  1841. if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
  1842. // Chrome 32+ warns if you try to copy deprecated returnValue, but
  1843. // we still want to if preventDefault isn't supported (IE8).
  1844. if (!(key === 'returnValue' && old.preventDefault)) {
  1845. event[key] = old[key];
  1846. }
  1847. }
  1848. }
  1849. // The event occurred on this element
  1850. if (!event.target) {
  1851. event.target = event.srcElement || document__default["default"];
  1852. }
  1853. // Handle which other element the event is related to
  1854. if (!event.relatedTarget) {
  1855. event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
  1856. }
  1857. // Stop the default browser action
  1858. event.preventDefault = function () {
  1859. if (old.preventDefault) {
  1860. old.preventDefault();
  1861. }
  1862. event.returnValue = false;
  1863. old.returnValue = false;
  1864. event.defaultPrevented = true;
  1865. };
  1866. event.defaultPrevented = false;
  1867. // Stop the event from bubbling
  1868. event.stopPropagation = function () {
  1869. if (old.stopPropagation) {
  1870. old.stopPropagation();
  1871. }
  1872. event.cancelBubble = true;
  1873. old.cancelBubble = true;
  1874. event.isPropagationStopped = returnTrue;
  1875. };
  1876. event.isPropagationStopped = returnFalse;
  1877. // Stop the event from bubbling and executing other handlers
  1878. event.stopImmediatePropagation = function () {
  1879. if (old.stopImmediatePropagation) {
  1880. old.stopImmediatePropagation();
  1881. }
  1882. event.isImmediatePropagationStopped = returnTrue;
  1883. event.stopPropagation();
  1884. };
  1885. event.isImmediatePropagationStopped = returnFalse;
  1886. // Handle mouse position
  1887. if (event.clientX !== null && event.clientX !== undefined) {
  1888. const doc = document__default["default"].documentElement;
  1889. const body = document__default["default"].body;
  1890. event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
  1891. event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
  1892. }
  1893. // Handle key presses
  1894. event.which = event.charCode || event.keyCode;
  1895. // Fix button for mouse clicks:
  1896. // 0 == left; 1 == middle; 2 == right
  1897. if (event.button !== null && event.button !== undefined) {
  1898. // The following is disabled because it does not pass videojs-standard
  1899. // and... yikes.
  1900. /* eslint-disable */
  1901. event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
  1902. /* eslint-enable */
  1903. }
  1904. }
  1905. event.fixed_ = true;
  1906. // Returns fixed-up instance
  1907. return event;
  1908. }
  1909. /**
  1910. * Whether passive event listeners are supported
  1911. */
  1912. let _supportsPassive;
  1913. const supportsPassive = function () {
  1914. if (typeof _supportsPassive !== 'boolean') {
  1915. _supportsPassive = false;
  1916. try {
  1917. const opts = Object.defineProperty({}, 'passive', {
  1918. get() {
  1919. _supportsPassive = true;
  1920. }
  1921. });
  1922. window__default["default"].addEventListener('test', null, opts);
  1923. window__default["default"].removeEventListener('test', null, opts);
  1924. } catch (e) {
  1925. // disregard
  1926. }
  1927. }
  1928. return _supportsPassive;
  1929. };
  1930. /**
  1931. * Touch events Chrome expects to be passive
  1932. */
  1933. const passiveEvents = ['touchstart', 'touchmove'];
  1934. /**
  1935. * Add an event listener to element
  1936. * It stores the handler function in a separate cache object
  1937. * and adds a generic handler to the element's event,
  1938. * along with a unique id (guid) to the element.
  1939. *
  1940. * @param {Element|Object} elem
  1941. * Element or object to bind listeners to
  1942. *
  1943. * @param {string|string[]} type
  1944. * Type of event to bind to.
  1945. *
  1946. * @param {Function} fn
  1947. * Event listener.
  1948. */
  1949. function on(elem, type, fn) {
  1950. if (Array.isArray(type)) {
  1951. return _handleMultipleEvents(on, elem, type, fn);
  1952. }
  1953. if (!DomData.has(elem)) {
  1954. DomData.set(elem, {});
  1955. }
  1956. const data = DomData.get(elem);
  1957. // We need a place to store all our handler data
  1958. if (!data.handlers) {
  1959. data.handlers = {};
  1960. }
  1961. if (!data.handlers[type]) {
  1962. data.handlers[type] = [];
  1963. }
  1964. if (!fn.guid) {
  1965. fn.guid = newGUID();
  1966. }
  1967. data.handlers[type].push(fn);
  1968. if (!data.dispatcher) {
  1969. data.disabled = false;
  1970. data.dispatcher = function (event, hash) {
  1971. if (data.disabled) {
  1972. return;
  1973. }
  1974. event = fixEvent(event);
  1975. const handlers = data.handlers[event.type];
  1976. if (handlers) {
  1977. // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
  1978. const handlersCopy = handlers.slice(0);
  1979. for (let m = 0, n = handlersCopy.length; m < n; m++) {
  1980. if (event.isImmediatePropagationStopped()) {
  1981. break;
  1982. } else {
  1983. try {
  1984. handlersCopy[m].call(elem, event, hash);
  1985. } catch (e) {
  1986. log.error(e);
  1987. }
  1988. }
  1989. }
  1990. }
  1991. };
  1992. }
  1993. if (data.handlers[type].length === 1) {
  1994. if (elem.addEventListener) {
  1995. let options = false;
  1996. if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
  1997. options = {
  1998. passive: true
  1999. };
  2000. }
  2001. elem.addEventListener(type, data.dispatcher, options);
  2002. } else if (elem.attachEvent) {
  2003. elem.attachEvent('on' + type, data.dispatcher);
  2004. }
  2005. }
  2006. }
  2007. /**
  2008. * Removes event listeners from an element
  2009. *
  2010. * @param {Element|Object} elem
  2011. * Object to remove listeners from.
  2012. *
  2013. * @param {string|string[]} [type]
  2014. * Type of listener to remove. Don't include to remove all events from element.
  2015. *
  2016. * @param {Function} [fn]
  2017. * Specific listener to remove. Don't include to remove listeners for an event
  2018. * type.
  2019. */
  2020. function off(elem, type, fn) {
  2021. // Don't want to add a cache object through getElData if not needed
  2022. if (!DomData.has(elem)) {
  2023. return;
  2024. }
  2025. const data = DomData.get(elem);
  2026. // If no events exist, nothing to unbind
  2027. if (!data.handlers) {
  2028. return;
  2029. }
  2030. if (Array.isArray(type)) {
  2031. return _handleMultipleEvents(off, elem, type, fn);
  2032. }
  2033. // Utility function
  2034. const removeType = function (el, t) {
  2035. data.handlers[t] = [];
  2036. _cleanUpEvents(el, t);
  2037. };
  2038. // Are we removing all bound events?
  2039. if (type === undefined) {
  2040. for (const t in data.handlers) {
  2041. if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
  2042. removeType(elem, t);
  2043. }
  2044. }
  2045. return;
  2046. }
  2047. const handlers = data.handlers[type];
  2048. // If no handlers exist, nothing to unbind
  2049. if (!handlers) {
  2050. return;
  2051. }
  2052. // If no listener was provided, remove all listeners for type
  2053. if (!fn) {
  2054. removeType(elem, type);
  2055. return;
  2056. }
  2057. // We're only removing a single handler
  2058. if (fn.guid) {
  2059. for (let n = 0; n < handlers.length; n++) {
  2060. if (handlers[n].guid === fn.guid) {
  2061. handlers.splice(n--, 1);
  2062. }
  2063. }
  2064. }
  2065. _cleanUpEvents(elem, type);
  2066. }
  2067. /**
  2068. * Trigger an event for an element
  2069. *
  2070. * @param {Element|Object} elem
  2071. * Element to trigger an event on
  2072. *
  2073. * @param {EventTarget~Event|string} event
  2074. * A string (the type) or an event object with a type attribute
  2075. *
  2076. * @param {Object} [hash]
  2077. * data hash to pass along with the event
  2078. *
  2079. * @return {boolean|undefined}
  2080. * Returns the opposite of `defaultPrevented` if default was
  2081. * prevented. Otherwise, returns `undefined`
  2082. */
  2083. function trigger(elem, event, hash) {
  2084. // Fetches element data and a reference to the parent (for bubbling).
  2085. // Don't want to add a data object to cache for every parent,
  2086. // so checking hasElData first.
  2087. const elemData = DomData.has(elem) ? DomData.get(elem) : {};
  2088. const parent = elem.parentNode || elem.ownerDocument;
  2089. // type = event.type || event,
  2090. // handler;
  2091. // If an event name was passed as a string, creates an event out of it
  2092. if (typeof event === 'string') {
  2093. event = {
  2094. type: event,
  2095. target: elem
  2096. };
  2097. } else if (!event.target) {
  2098. event.target = elem;
  2099. }
  2100. // Normalizes the event properties.
  2101. event = fixEvent(event);
  2102. // If the passed element has a dispatcher, executes the established handlers.
  2103. if (elemData.dispatcher) {
  2104. elemData.dispatcher.call(elem, event, hash);
  2105. }
  2106. // Unless explicitly stopped or the event does not bubble (e.g. media events)
  2107. // recursively calls this function to bubble the event up the DOM.
  2108. if (parent && !event.isPropagationStopped() && event.bubbles === true) {
  2109. trigger.call(null, parent, event, hash);
  2110. // If at the top of the DOM, triggers the default action unless disabled.
  2111. } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
  2112. if (!DomData.has(event.target)) {
  2113. DomData.set(event.target, {});
  2114. }
  2115. const targetData = DomData.get(event.target);
  2116. // Checks if the target has a default action for this event.
  2117. if (event.target[event.type]) {
  2118. // Temporarily disables event dispatching on the target as we have already executed the handler.
  2119. targetData.disabled = true;
  2120. // Executes the default action.
  2121. if (typeof event.target[event.type] === 'function') {
  2122. event.target[event.type]();
  2123. }
  2124. // Re-enables event dispatching.
  2125. targetData.disabled = false;
  2126. }
  2127. }
  2128. // Inform the triggerer if the default was prevented by returning false
  2129. return !event.defaultPrevented;
  2130. }
  2131. /**
  2132. * Trigger a listener only once for an event.
  2133. *
  2134. * @param {Element|Object} elem
  2135. * Element or object to bind to.
  2136. *
  2137. * @param {string|string[]} type
  2138. * Name/type of event
  2139. *
  2140. * @param {Event~EventListener} fn
  2141. * Event listener function
  2142. */
  2143. function one(elem, type, fn) {
  2144. if (Array.isArray(type)) {
  2145. return _handleMultipleEvents(one, elem, type, fn);
  2146. }
  2147. const func = function () {
  2148. off(elem, type, func);
  2149. fn.apply(this, arguments);
  2150. };
  2151. // copy the guid to the new function so it can removed using the original function's ID
  2152. func.guid = fn.guid = fn.guid || newGUID();
  2153. on(elem, type, func);
  2154. }
  2155. /**
  2156. * Trigger a listener only once and then turn if off for all
  2157. * configured events
  2158. *
  2159. * @param {Element|Object} elem
  2160. * Element or object to bind to.
  2161. *
  2162. * @param {string|string[]} type
  2163. * Name/type of event
  2164. *
  2165. * @param {Event~EventListener} fn
  2166. * Event listener function
  2167. */
  2168. function any(elem, type, fn) {
  2169. const func = function () {
  2170. off(elem, type, func);
  2171. fn.apply(this, arguments);
  2172. };
  2173. // copy the guid to the new function so it can removed using the original function's ID
  2174. func.guid = fn.guid = fn.guid || newGUID();
  2175. // multiple ons, but one off for everything
  2176. on(elem, type, func);
  2177. }
  2178. var Events = /*#__PURE__*/Object.freeze({
  2179. __proto__: null,
  2180. fixEvent: fixEvent,
  2181. on: on,
  2182. off: off,
  2183. trigger: trigger,
  2184. one: one,
  2185. any: any
  2186. });
  2187. /**
  2188. * @file fn.js
  2189. * @module fn
  2190. */
  2191. const UPDATE_REFRESH_INTERVAL = 30;
  2192. /**
  2193. * A private, internal-only function for changing the context of a function.
  2194. *
  2195. * It also stores a unique id on the function so it can be easily removed from
  2196. * events.
  2197. *
  2198. * @private
  2199. * @function
  2200. * @param {*} context
  2201. * The object to bind as scope.
  2202. *
  2203. * @param {Function} fn
  2204. * The function to be bound to a scope.
  2205. *
  2206. * @param {number} [uid]
  2207. * An optional unique ID for the function to be set
  2208. *
  2209. * @return {Function}
  2210. * The new function that will be bound into the context given
  2211. */
  2212. const bind_ = function (context, fn, uid) {
  2213. // Make sure the function has a unique ID
  2214. if (!fn.guid) {
  2215. fn.guid = newGUID();
  2216. }
  2217. // Create the new function that changes the context
  2218. const bound = fn.bind(context);
  2219. // Allow for the ability to individualize this function
  2220. // Needed in the case where multiple objects might share the same prototype
  2221. // IF both items add an event listener with the same function, then you try to remove just one
  2222. // it will remove both because they both have the same guid.
  2223. // when using this, you need to use the bind method when you remove the listener as well.
  2224. // currently used in text tracks
  2225. bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
  2226. return bound;
  2227. };
  2228. /**
  2229. * Wraps the given function, `fn`, with a new function that only invokes `fn`
  2230. * at most once per every `wait` milliseconds.
  2231. *
  2232. * @function
  2233. * @param {Function} fn
  2234. * The function to be throttled.
  2235. *
  2236. * @param {number} wait
  2237. * The number of milliseconds by which to throttle.
  2238. *
  2239. * @return {Function}
  2240. */
  2241. const throttle = function (fn, wait) {
  2242. let last = window__default["default"].performance.now();
  2243. const throttled = function (...args) {
  2244. const now = window__default["default"].performance.now();
  2245. if (now - last >= wait) {
  2246. fn(...args);
  2247. last = now;
  2248. }
  2249. };
  2250. return throttled;
  2251. };
  2252. /**
  2253. * Creates a debounced function that delays invoking `func` until after `wait`
  2254. * milliseconds have elapsed since the last time the debounced function was
  2255. * invoked.
  2256. *
  2257. * Inspired by lodash and underscore implementations.
  2258. *
  2259. * @function
  2260. * @param {Function} func
  2261. * The function to wrap with debounce behavior.
  2262. *
  2263. * @param {number} wait
  2264. * The number of milliseconds to wait after the last invocation.
  2265. *
  2266. * @param {boolean} [immediate]
  2267. * Whether or not to invoke the function immediately upon creation.
  2268. *
  2269. * @param {Object} [context=window]
  2270. * The "context" in which the debounced function should debounce. For
  2271. * example, if this function should be tied to a Video.js player,
  2272. * the player can be passed here. Alternatively, defaults to the
  2273. * global `window` object.
  2274. *
  2275. * @return {Function}
  2276. * A debounced function.
  2277. */
  2278. const debounce = function (func, wait, immediate, context = window__default["default"]) {
  2279. let timeout;
  2280. const cancel = () => {
  2281. context.clearTimeout(timeout);
  2282. timeout = null;
  2283. };
  2284. /* eslint-disable consistent-this */
  2285. const debounced = function () {
  2286. const self = this;
  2287. const args = arguments;
  2288. let later = function () {
  2289. timeout = null;
  2290. later = null;
  2291. if (!immediate) {
  2292. func.apply(self, args);
  2293. }
  2294. };
  2295. if (!timeout && immediate) {
  2296. func.apply(self, args);
  2297. }
  2298. context.clearTimeout(timeout);
  2299. timeout = context.setTimeout(later, wait);
  2300. };
  2301. /* eslint-enable consistent-this */
  2302. debounced.cancel = cancel;
  2303. return debounced;
  2304. };
  2305. var Fn = /*#__PURE__*/Object.freeze({
  2306. __proto__: null,
  2307. UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
  2308. bind_: bind_,
  2309. throttle: throttle,
  2310. debounce: debounce
  2311. });
  2312. /**
  2313. * @file src/js/event-target.js
  2314. */
  2315. let EVENT_MAP;
  2316. /**
  2317. * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
  2318. * adds shorthand functions that wrap around lengthy functions. For example:
  2319. * the `on` function is a wrapper around `addEventListener`.
  2320. *
  2321. * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
  2322. * @class EventTarget
  2323. */
  2324. class EventTarget {
  2325. /**
  2326. * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
  2327. * function that will get called when an event with a certain name gets triggered.
  2328. *
  2329. * @param {string|string[]} type
  2330. * An event name or an array of event names.
  2331. *
  2332. * @param {Function} fn
  2333. * The function to call with `EventTarget`s
  2334. */
  2335. on(type, fn) {
  2336. // Remove the addEventListener alias before calling Events.on
  2337. // so we don't get into an infinite type loop
  2338. const ael = this.addEventListener;
  2339. this.addEventListener = () => {};
  2340. on(this, type, fn);
  2341. this.addEventListener = ael;
  2342. }
  2343. /**
  2344. * Removes an `event listener` for a specific event from an instance of `EventTarget`.
  2345. * This makes it so that the `event listener` will no longer get called when the
  2346. * named event happens.
  2347. *
  2348. * @param {string|string[]} type
  2349. * An event name or an array of event names.
  2350. *
  2351. * @param {Function} fn
  2352. * The function to remove.
  2353. */
  2354. off(type, fn) {
  2355. off(this, type, fn);
  2356. }
  2357. /**
  2358. * This function will add an `event listener` that gets triggered only once. After the
  2359. * first trigger it will get removed. This is like adding an `event listener`
  2360. * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
  2361. *
  2362. * @param {string|string[]} type
  2363. * An event name or an array of event names.
  2364. *
  2365. * @param {Function} fn
  2366. * The function to be called once for each event name.
  2367. */
  2368. one(type, fn) {
  2369. // Remove the addEventListener aliasing Events.on
  2370. // so we don't get into an infinite type loop
  2371. const ael = this.addEventListener;
  2372. this.addEventListener = () => {};
  2373. one(this, type, fn);
  2374. this.addEventListener = ael;
  2375. }
  2376. /**
  2377. * This function will add an `event listener` that gets triggered only once and is
  2378. * removed from all events. This is like adding an array of `event listener`s
  2379. * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
  2380. * first time it is triggered.
  2381. *
  2382. * @param {string|string[]} type
  2383. * An event name or an array of event names.
  2384. *
  2385. * @param {Function} fn
  2386. * The function to be called once for each event name.
  2387. */
  2388. any(type, fn) {
  2389. // Remove the addEventListener aliasing Events.on
  2390. // so we don't get into an infinite type loop
  2391. const ael = this.addEventListener;
  2392. this.addEventListener = () => {};
  2393. any(this, type, fn);
  2394. this.addEventListener = ael;
  2395. }
  2396. /**
  2397. * This function causes an event to happen. This will then cause any `event listeners`
  2398. * that are waiting for that event, to get called. If there are no `event listeners`
  2399. * for an event then nothing will happen.
  2400. *
  2401. * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
  2402. * Trigger will also call the `on` + `uppercaseEventName` function.
  2403. *
  2404. * Example:
  2405. * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
  2406. * `onClick` if it exists.
  2407. *
  2408. * @param {string|EventTarget~Event|Object} event
  2409. * The name of the event, an `Event`, or an object with a key of type set to
  2410. * an event name.
  2411. */
  2412. trigger(event) {
  2413. const type = event.type || event;
  2414. // deprecation
  2415. // In a future version we should default target to `this`
  2416. // similar to how we default the target to `elem` in
  2417. // `Events.trigger`. Right now the default `target` will be
  2418. // `document` due to the `Event.fixEvent` call.
  2419. if (typeof event === 'string') {
  2420. event = {
  2421. type
  2422. };
  2423. }
  2424. event = fixEvent(event);
  2425. if (this.allowedEvents_[type] && this['on' + type]) {
  2426. this['on' + type](event);
  2427. }
  2428. trigger(this, event);
  2429. }
  2430. queueTrigger(event) {
  2431. // only set up EVENT_MAP if it'll be used
  2432. if (!EVENT_MAP) {
  2433. EVENT_MAP = new Map();
  2434. }
  2435. const type = event.type || event;
  2436. let map = EVENT_MAP.get(this);
  2437. if (!map) {
  2438. map = new Map();
  2439. EVENT_MAP.set(this, map);
  2440. }
  2441. const oldTimeout = map.get(type);
  2442. map.delete(type);
  2443. window__default["default"].clearTimeout(oldTimeout);
  2444. const timeout = window__default["default"].setTimeout(() => {
  2445. map.delete(type);
  2446. // if we cleared out all timeouts for the current target, delete its map
  2447. if (map.size === 0) {
  2448. map = null;
  2449. EVENT_MAP.delete(this);
  2450. }
  2451. this.trigger(event);
  2452. }, 0);
  2453. map.set(type, timeout);
  2454. }
  2455. }
  2456. /**
  2457. * A Custom DOM event.
  2458. *
  2459. * @typedef {CustomEvent} Event
  2460. * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
  2461. */
  2462. /**
  2463. * All event listeners should follow the following format.
  2464. *
  2465. * @callback EventTarget~EventListener
  2466. * @this {EventTarget}
  2467. *
  2468. * @param {Event} event
  2469. * the event that triggered this function
  2470. *
  2471. * @param {Object} [hash]
  2472. * hash of data sent during the event
  2473. */
  2474. /**
  2475. * An object containing event names as keys and booleans as values.
  2476. *
  2477. * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
  2478. * will have extra functionality. See that function for more information.
  2479. *
  2480. * @property EventTarget.prototype.allowedEvents_
  2481. * @private
  2482. */
  2483. EventTarget.prototype.allowedEvents_ = {};
  2484. /**
  2485. * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
  2486. * the standard DOM API.
  2487. *
  2488. * @function
  2489. * @see {@link EventTarget#on}
  2490. */
  2491. EventTarget.prototype.addEventListener = EventTarget.prototype.on;
  2492. /**
  2493. * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
  2494. * the standard DOM API.
  2495. *
  2496. * @function
  2497. * @see {@link EventTarget#off}
  2498. */
  2499. EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
  2500. /**
  2501. * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
  2502. * the standard DOM API.
  2503. *
  2504. * @function
  2505. * @see {@link EventTarget#trigger}
  2506. */
  2507. EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
  2508. /**
  2509. * @file mixins/evented.js
  2510. * @module evented
  2511. */
  2512. const objName = obj => {
  2513. if (typeof obj.name === 'function') {
  2514. return obj.name();
  2515. }
  2516. if (typeof obj.name === 'string') {
  2517. return obj.name;
  2518. }
  2519. if (obj.name_) {
  2520. return obj.name_;
  2521. }
  2522. if (obj.constructor && obj.constructor.name) {
  2523. return obj.constructor.name;
  2524. }
  2525. return typeof obj;
  2526. };
  2527. /**
  2528. * Returns whether or not an object has had the evented mixin applied.
  2529. *
  2530. * @param {Object} object
  2531. * An object to test.
  2532. *
  2533. * @return {boolean}
  2534. * Whether or not the object appears to be evented.
  2535. */
  2536. const isEvented = object => object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
  2537. /**
  2538. * Adds a callback to run after the evented mixin applied.
  2539. *
  2540. * @param {Object} object
  2541. * An object to Add
  2542. * @param {Function} callback
  2543. * The callback to run.
  2544. */
  2545. const addEventedCallback = (target, callback) => {
  2546. if (isEvented(target)) {
  2547. callback();
  2548. } else {
  2549. if (!target.eventedCallbacks) {
  2550. target.eventedCallbacks = [];
  2551. }
  2552. target.eventedCallbacks.push(callback);
  2553. }
  2554. };
  2555. /**
  2556. * Whether a value is a valid event type - non-empty string or array.
  2557. *
  2558. * @private
  2559. * @param {string|Array} type
  2560. * The type value to test.
  2561. *
  2562. * @return {boolean}
  2563. * Whether or not the type is a valid event type.
  2564. */
  2565. const isValidEventType = type =>
  2566. // The regex here verifies that the `type` contains at least one non-
  2567. // whitespace character.
  2568. typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
  2569. /**
  2570. * Validates a value to determine if it is a valid event target. Throws if not.
  2571. *
  2572. * @private
  2573. * @throws {Error}
  2574. * If the target does not appear to be a valid event target.
  2575. *
  2576. * @param {Object} target
  2577. * The object to test.
  2578. *
  2579. * @param {Object} obj
  2580. * The evented object we are validating for
  2581. *
  2582. * @param {string} fnName
  2583. * The name of the evented mixin function that called this.
  2584. */
  2585. const validateTarget = (target, obj, fnName) => {
  2586. if (!target || !target.nodeName && !isEvented(target)) {
  2587. throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
  2588. }
  2589. };
  2590. /**
  2591. * Validates a value to determine if it is a valid event target. Throws if not.
  2592. *
  2593. * @private
  2594. * @throws {Error}
  2595. * If the type does not appear to be a valid event type.
  2596. *
  2597. * @param {string|Array} type
  2598. * The type to test.
  2599. *
  2600. * @param {Object} obj
  2601. * The evented object we are validating for
  2602. *
  2603. * @param {string} fnName
  2604. * The name of the evented mixin function that called this.
  2605. */
  2606. const validateEventType = (type, obj, fnName) => {
  2607. if (!isValidEventType(type)) {
  2608. throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
  2609. }
  2610. };
  2611. /**
  2612. * Validates a value to determine if it is a valid listener. Throws if not.
  2613. *
  2614. * @private
  2615. * @throws {Error}
  2616. * If the listener is not a function.
  2617. *
  2618. * @param {Function} listener
  2619. * The listener to test.
  2620. *
  2621. * @param {Object} obj
  2622. * The evented object we are validating for
  2623. *
  2624. * @param {string} fnName
  2625. * The name of the evented mixin function that called this.
  2626. */
  2627. const validateListener = (listener, obj, fnName) => {
  2628. if (typeof listener !== 'function') {
  2629. throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
  2630. }
  2631. };
  2632. /**
  2633. * Takes an array of arguments given to `on()` or `one()`, validates them, and
  2634. * normalizes them into an object.
  2635. *
  2636. * @private
  2637. * @param {Object} self
  2638. * The evented object on which `on()` or `one()` was called. This
  2639. * object will be bound as the `this` value for the listener.
  2640. *
  2641. * @param {Array} args
  2642. * An array of arguments passed to `on()` or `one()`.
  2643. *
  2644. * @param {string} fnName
  2645. * The name of the evented mixin function that called this.
  2646. *
  2647. * @return {Object}
  2648. * An object containing useful values for `on()` or `one()` calls.
  2649. */
  2650. const normalizeListenArgs = (self, args, fnName) => {
  2651. // If the number of arguments is less than 3, the target is always the
  2652. // evented object itself.
  2653. const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
  2654. let target;
  2655. let type;
  2656. let listener;
  2657. if (isTargetingSelf) {
  2658. target = self.eventBusEl_;
  2659. // Deal with cases where we got 3 arguments, but we are still listening to
  2660. // the evented object itself.
  2661. if (args.length >= 3) {
  2662. args.shift();
  2663. }
  2664. [type, listener] = args;
  2665. } else {
  2666. [target, type, listener] = args;
  2667. }
  2668. validateTarget(target, self, fnName);
  2669. validateEventType(type, self, fnName);
  2670. validateListener(listener, self, fnName);
  2671. listener = bind_(self, listener);
  2672. return {
  2673. isTargetingSelf,
  2674. target,
  2675. type,
  2676. listener
  2677. };
  2678. };
  2679. /**
  2680. * Adds the listener to the event type(s) on the target, normalizing for
  2681. * the type of target.
  2682. *
  2683. * @private
  2684. * @param {Element|Object} target
  2685. * A DOM node or evented object.
  2686. *
  2687. * @param {string} method
  2688. * The event binding method to use ("on" or "one").
  2689. *
  2690. * @param {string|Array} type
  2691. * One or more event type(s).
  2692. *
  2693. * @param {Function} listener
  2694. * A listener function.
  2695. */
  2696. const listen = (target, method, type, listener) => {
  2697. validateTarget(target, target, method);
  2698. if (target.nodeName) {
  2699. Events[method](target, type, listener);
  2700. } else {
  2701. target[method](type, listener);
  2702. }
  2703. };
  2704. /**
  2705. * Contains methods that provide event capabilities to an object which is passed
  2706. * to {@link module:evented|evented}.
  2707. *
  2708. * @mixin EventedMixin
  2709. */
  2710. const EventedMixin = {
  2711. /**
  2712. * Add a listener to an event (or events) on this object or another evented
  2713. * object.
  2714. *
  2715. * @param {string|Array|Element|Object} targetOrType
  2716. * If this is a string or array, it represents the event type(s)
  2717. * that will trigger the listener.
  2718. *
  2719. * Another evented object can be passed here instead, which will
  2720. * cause the listener to listen for events on _that_ object.
  2721. *
  2722. * In either case, the listener's `this` value will be bound to
  2723. * this object.
  2724. *
  2725. * @param {string|Array|Function} typeOrListener
  2726. * If the first argument was a string or array, this should be the
  2727. * listener function. Otherwise, this is a string or array of event
  2728. * type(s).
  2729. *
  2730. * @param {Function} [listener]
  2731. * If the first argument was another evented object, this will be
  2732. * the listener function.
  2733. */
  2734. on(...args) {
  2735. const {
  2736. isTargetingSelf,
  2737. target,
  2738. type,
  2739. listener
  2740. } = normalizeListenArgs(this, args, 'on');
  2741. listen(target, 'on', type, listener);
  2742. // If this object is listening to another evented object.
  2743. if (!isTargetingSelf) {
  2744. // If this object is disposed, remove the listener.
  2745. const removeListenerOnDispose = () => this.off(target, type, listener);
  2746. // Use the same function ID as the listener so we can remove it later it
  2747. // using the ID of the original listener.
  2748. removeListenerOnDispose.guid = listener.guid;
  2749. // Add a listener to the target's dispose event as well. This ensures
  2750. // that if the target is disposed BEFORE this object, we remove the
  2751. // removal listener that was just added. Otherwise, we create a memory leak.
  2752. const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
  2753. // Use the same function ID as the listener so we can remove it later
  2754. // it using the ID of the original listener.
  2755. removeRemoverOnTargetDispose.guid = listener.guid;
  2756. listen(this, 'on', 'dispose', removeListenerOnDispose);
  2757. listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
  2758. }
  2759. },
  2760. /**
  2761. * Add a listener to an event (or events) on this object or another evented
  2762. * object. The listener will be called once per event and then removed.
  2763. *
  2764. * @param {string|Array|Element|Object} targetOrType
  2765. * If this is a string or array, it represents the event type(s)
  2766. * that will trigger the listener.
  2767. *
  2768. * Another evented object can be passed here instead, which will
  2769. * cause the listener to listen for events on _that_ object.
  2770. *
  2771. * In either case, the listener's `this` value will be bound to
  2772. * this object.
  2773. *
  2774. * @param {string|Array|Function} typeOrListener
  2775. * If the first argument was a string or array, this should be the
  2776. * listener function. Otherwise, this is a string or array of event
  2777. * type(s).
  2778. *
  2779. * @param {Function} [listener]
  2780. * If the first argument was another evented object, this will be
  2781. * the listener function.
  2782. */
  2783. one(...args) {
  2784. const {
  2785. isTargetingSelf,
  2786. target,
  2787. type,
  2788. listener
  2789. } = normalizeListenArgs(this, args, 'one');
  2790. // Targeting this evented object.
  2791. if (isTargetingSelf) {
  2792. listen(target, 'one', type, listener);
  2793. // Targeting another evented object.
  2794. } else {
  2795. // TODO: This wrapper is incorrect! It should only
  2796. // remove the wrapper for the event type that called it.
  2797. // Instead all listeners are removed on the first trigger!
  2798. // see https://github.com/videojs/video.js/issues/5962
  2799. const wrapper = (...largs) => {
  2800. this.off(target, type, wrapper);
  2801. listener.apply(null, largs);
  2802. };
  2803. // Use the same function ID as the listener so we can remove it later
  2804. // it using the ID of the original listener.
  2805. wrapper.guid = listener.guid;
  2806. listen(target, 'one', type, wrapper);
  2807. }
  2808. },
  2809. /**
  2810. * Add a listener to an event (or events) on this object or another evented
  2811. * object. The listener will only be called once for the first event that is triggered
  2812. * then removed.
  2813. *
  2814. * @param {string|Array|Element|Object} targetOrType
  2815. * If this is a string or array, it represents the event type(s)
  2816. * that will trigger the listener.
  2817. *
  2818. * Another evented object can be passed here instead, which will
  2819. * cause the listener to listen for events on _that_ object.
  2820. *
  2821. * In either case, the listener's `this` value will be bound to
  2822. * this object.
  2823. *
  2824. * @param {string|Array|Function} typeOrListener
  2825. * If the first argument was a string or array, this should be the
  2826. * listener function. Otherwise, this is a string or array of event
  2827. * type(s).
  2828. *
  2829. * @param {Function} [listener]
  2830. * If the first argument was another evented object, this will be
  2831. * the listener function.
  2832. */
  2833. any(...args) {
  2834. const {
  2835. isTargetingSelf,
  2836. target,
  2837. type,
  2838. listener
  2839. } = normalizeListenArgs(this, args, 'any');
  2840. // Targeting this evented object.
  2841. if (isTargetingSelf) {
  2842. listen(target, 'any', type, listener);
  2843. // Targeting another evented object.
  2844. } else {
  2845. const wrapper = (...largs) => {
  2846. this.off(target, type, wrapper);
  2847. listener.apply(null, largs);
  2848. };
  2849. // Use the same function ID as the listener so we can remove it later
  2850. // it using the ID of the original listener.
  2851. wrapper.guid = listener.guid;
  2852. listen(target, 'any', type, wrapper);
  2853. }
  2854. },
  2855. /**
  2856. * Removes listener(s) from event(s) on an evented object.
  2857. *
  2858. * @param {string|Array|Element|Object} [targetOrType]
  2859. * If this is a string or array, it represents the event type(s).
  2860. *
  2861. * Another evented object can be passed here instead, in which case
  2862. * ALL 3 arguments are _required_.
  2863. *
  2864. * @param {string|Array|Function} [typeOrListener]
  2865. * If the first argument was a string or array, this may be the
  2866. * listener function. Otherwise, this is a string or array of event
  2867. * type(s).
  2868. *
  2869. * @param {Function} [listener]
  2870. * If the first argument was another evented object, this will be
  2871. * the listener function; otherwise, _all_ listeners bound to the
  2872. * event type(s) will be removed.
  2873. */
  2874. off(targetOrType, typeOrListener, listener) {
  2875. // Targeting this evented object.
  2876. if (!targetOrType || isValidEventType(targetOrType)) {
  2877. off(this.eventBusEl_, targetOrType, typeOrListener);
  2878. // Targeting another evented object.
  2879. } else {
  2880. const target = targetOrType;
  2881. const type = typeOrListener;
  2882. // Fail fast and in a meaningful way!
  2883. validateTarget(target, this, 'off');
  2884. validateEventType(type, this, 'off');
  2885. validateListener(listener, this, 'off');
  2886. // Ensure there's at least a guid, even if the function hasn't been used
  2887. listener = bind_(this, listener);
  2888. // Remove the dispose listener on this evented object, which was given
  2889. // the same guid as the event listener in on().
  2890. this.off('dispose', listener);
  2891. if (target.nodeName) {
  2892. off(target, type, listener);
  2893. off(target, 'dispose', listener);
  2894. } else if (isEvented(target)) {
  2895. target.off(type, listener);
  2896. target.off('dispose', listener);
  2897. }
  2898. }
  2899. },
  2900. /**
  2901. * Fire an event on this evented object, causing its listeners to be called.
  2902. *
  2903. * @param {string|Object} event
  2904. * An event type or an object with a type property.
  2905. *
  2906. * @param {Object} [hash]
  2907. * An additional object to pass along to listeners.
  2908. *
  2909. * @return {boolean}
  2910. * Whether or not the default behavior was prevented.
  2911. */
  2912. trigger(event, hash) {
  2913. validateTarget(this.eventBusEl_, this, 'trigger');
  2914. const type = event && typeof event !== 'string' ? event.type : event;
  2915. if (!isValidEventType(type)) {
  2916. throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
  2917. }
  2918. return trigger(this.eventBusEl_, event, hash);
  2919. }
  2920. };
  2921. /**
  2922. * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
  2923. *
  2924. * @param {Object} target
  2925. * The object to which to add event methods.
  2926. *
  2927. * @param {Object} [options={}]
  2928. * Options for customizing the mixin behavior.
  2929. *
  2930. * @param {string} [options.eventBusKey]
  2931. * By default, adds a `eventBusEl_` DOM element to the target object,
  2932. * which is used as an event bus. If the target object already has a
  2933. * DOM element that should be used, pass its key here.
  2934. *
  2935. * @return {Object}
  2936. * The target object.
  2937. */
  2938. function evented(target, options = {}) {
  2939. const {
  2940. eventBusKey
  2941. } = options;
  2942. // Set or create the eventBusEl_.
  2943. if (eventBusKey) {
  2944. if (!target[eventBusKey].nodeName) {
  2945. throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
  2946. }
  2947. target.eventBusEl_ = target[eventBusKey];
  2948. } else {
  2949. target.eventBusEl_ = createEl('span', {
  2950. className: 'vjs-event-bus'
  2951. });
  2952. }
  2953. Object.assign(target, EventedMixin);
  2954. if (target.eventedCallbacks) {
  2955. target.eventedCallbacks.forEach(callback => {
  2956. callback();
  2957. });
  2958. }
  2959. // When any evented object is disposed, it removes all its listeners.
  2960. target.on('dispose', () => {
  2961. target.off();
  2962. [target, target.el_, target.eventBusEl_].forEach(function (val) {
  2963. if (val && DomData.has(val)) {
  2964. DomData.delete(val);
  2965. }
  2966. });
  2967. window__default["default"].setTimeout(() => {
  2968. target.eventBusEl_ = null;
  2969. }, 0);
  2970. });
  2971. return target;
  2972. }
  2973. /**
  2974. * @file mixins/stateful.js
  2975. * @module stateful
  2976. */
  2977. /**
  2978. * Contains methods that provide statefulness to an object which is passed
  2979. * to {@link module:stateful}.
  2980. *
  2981. * @mixin StatefulMixin
  2982. */
  2983. const StatefulMixin = {
  2984. /**
  2985. * A hash containing arbitrary keys and values representing the state of
  2986. * the object.
  2987. *
  2988. * @type {Object}
  2989. */
  2990. state: {},
  2991. /**
  2992. * Set the state of an object by mutating its
  2993. * {@link module:stateful~StatefulMixin.state|state} object in place.
  2994. *
  2995. * @fires module:stateful~StatefulMixin#statechanged
  2996. * @param {Object|Function} stateUpdates
  2997. * A new set of properties to shallow-merge into the plugin state.
  2998. * Can be a plain object or a function returning a plain object.
  2999. *
  3000. * @return {Object|undefined}
  3001. * An object containing changes that occurred. If no changes
  3002. * occurred, returns `undefined`.
  3003. */
  3004. setState(stateUpdates) {
  3005. // Support providing the `stateUpdates` state as a function.
  3006. if (typeof stateUpdates === 'function') {
  3007. stateUpdates = stateUpdates();
  3008. }
  3009. let changes;
  3010. each(stateUpdates, (value, key) => {
  3011. // Record the change if the value is different from what's in the
  3012. // current state.
  3013. if (this.state[key] !== value) {
  3014. changes = changes || {};
  3015. changes[key] = {
  3016. from: this.state[key],
  3017. to: value
  3018. };
  3019. }
  3020. this.state[key] = value;
  3021. });
  3022. // Only trigger "statechange" if there were changes AND we have a trigger
  3023. // function. This allows us to not require that the target object be an
  3024. // evented object.
  3025. if (changes && isEvented(this)) {
  3026. /**
  3027. * An event triggered on an object that is both
  3028. * {@link module:stateful|stateful} and {@link module:evented|evented}
  3029. * indicating that its state has changed.
  3030. *
  3031. * @event module:stateful~StatefulMixin#statechanged
  3032. * @type {Object}
  3033. * @property {Object} changes
  3034. * A hash containing the properties that were changed and
  3035. * the values they were changed `from` and `to`.
  3036. */
  3037. this.trigger({
  3038. changes,
  3039. type: 'statechanged'
  3040. });
  3041. }
  3042. return changes;
  3043. }
  3044. };
  3045. /**
  3046. * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
  3047. * object.
  3048. *
  3049. * If the target object is {@link module:evented|evented} and has a
  3050. * `handleStateChanged` method, that method will be automatically bound to the
  3051. * `statechanged` event on itself.
  3052. *
  3053. * @param {Object} target
  3054. * The object to be made stateful.
  3055. *
  3056. * @param {Object} [defaultState]
  3057. * A default set of properties to populate the newly-stateful object's
  3058. * `state` property.
  3059. *
  3060. * @return {Object}
  3061. * Returns the `target`.
  3062. */
  3063. function stateful(target, defaultState) {
  3064. Object.assign(target, StatefulMixin);
  3065. // This happens after the mixing-in because we need to replace the `state`
  3066. // added in that step.
  3067. target.state = Object.assign({}, target.state, defaultState);
  3068. // Auto-bind the `handleStateChanged` method of the target object if it exists.
  3069. if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
  3070. target.on('statechanged', target.handleStateChanged);
  3071. }
  3072. return target;
  3073. }
  3074. /**
  3075. * @file str.js
  3076. * @module to-lower-case
  3077. */
  3078. /**
  3079. * Lowercase the first letter of a string.
  3080. *
  3081. * @param {string} string
  3082. * String to be lowercased
  3083. *
  3084. * @return {string}
  3085. * The string with a lowercased first letter
  3086. */
  3087. const toLowerCase = function (string) {
  3088. if (typeof string !== 'string') {
  3089. return string;
  3090. }
  3091. return string.replace(/./, w => w.toLowerCase());
  3092. };
  3093. /**
  3094. * Uppercase the first letter of a string.
  3095. *
  3096. * @param {string} string
  3097. * String to be uppercased
  3098. *
  3099. * @return {string}
  3100. * The string with an uppercased first letter
  3101. */
  3102. const toTitleCase = function (string) {
  3103. if (typeof string !== 'string') {
  3104. return string;
  3105. }
  3106. return string.replace(/./, w => w.toUpperCase());
  3107. };
  3108. /**
  3109. * Compares the TitleCase versions of the two strings for equality.
  3110. *
  3111. * @param {string} str1
  3112. * The first string to compare
  3113. *
  3114. * @param {string} str2
  3115. * The second string to compare
  3116. *
  3117. * @return {boolean}
  3118. * Whether the TitleCase versions of the strings are equal
  3119. */
  3120. const titleCaseEquals = function (str1, str2) {
  3121. return toTitleCase(str1) === toTitleCase(str2);
  3122. };
  3123. var Str = /*#__PURE__*/Object.freeze({
  3124. __proto__: null,
  3125. toLowerCase: toLowerCase,
  3126. toTitleCase: toTitleCase,
  3127. titleCaseEquals: titleCaseEquals
  3128. });
  3129. /**
  3130. * Player Component - Base class for all UI objects
  3131. *
  3132. * @file component.js
  3133. */
  3134. /**
  3135. * Base class for all UI Components.
  3136. * Components are UI objects which represent both a javascript object and an element
  3137. * in the DOM. They can be children of other components, and can have
  3138. * children themselves.
  3139. *
  3140. * Components can also use methods from {@link EventTarget}
  3141. */
  3142. class Component {
  3143. /**
  3144. * A callback that is called when a component is ready. Does not have any
  3145. * parameters and any callback value will be ignored.
  3146. *
  3147. * @callback ReadyCallback
  3148. * @this Component
  3149. */
  3150. /**
  3151. * Creates an instance of this class.
  3152. *
  3153. * @param { import('./player').default } player
  3154. * The `Player` that this class should be attached to.
  3155. *
  3156. * @param {Object} [options]
  3157. * The key/value store of component options.
  3158. *
  3159. * @param {Object[]} [options.children]
  3160. * An array of children objects to initialize this component with. Children objects have
  3161. * a name property that will be used if more than one component of the same type needs to be
  3162. * added.
  3163. *
  3164. * @param {string} [options.className]
  3165. * A class or space separated list of classes to add the component
  3166. *
  3167. * @param {ReadyCallback} [ready]
  3168. * Function that gets called when the `Component` is ready.
  3169. */
  3170. constructor(player, options, ready) {
  3171. // The component might be the player itself and we can't pass `this` to super
  3172. if (!player && this.play) {
  3173. this.player_ = player = this; // eslint-disable-line
  3174. } else {
  3175. this.player_ = player;
  3176. }
  3177. this.isDisposed_ = false;
  3178. // Hold the reference to the parent component via `addChild` method
  3179. this.parentComponent_ = null;
  3180. // Make a copy of prototype.options_ to protect against overriding defaults
  3181. this.options_ = merge({}, this.options_);
  3182. // Updated options with supplied options
  3183. options = this.options_ = merge(this.options_, options);
  3184. // Get ID from options or options element if one is supplied
  3185. this.id_ = options.id || options.el && options.el.id;
  3186. // If there was no ID from the options, generate one
  3187. if (!this.id_) {
  3188. // Don't require the player ID function in the case of mock players
  3189. const id = player && player.id && player.id() || 'no_player';
  3190. this.id_ = `${id}_component_${newGUID()}`;
  3191. }
  3192. this.name_ = options.name || null;
  3193. // Create element if one wasn't provided in options
  3194. if (options.el) {
  3195. this.el_ = options.el;
  3196. } else if (options.createEl !== false) {
  3197. this.el_ = this.createEl();
  3198. }
  3199. if (options.className && this.el_) {
  3200. options.className.split(' ').forEach(c => this.addClass(c));
  3201. }
  3202. // Remove the placeholder event methods. If the component is evented, the
  3203. // real methods are added next
  3204. ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
  3205. this[fn] = undefined;
  3206. });
  3207. // if evented is anything except false, we want to mixin in evented
  3208. if (options.evented !== false) {
  3209. // Make this an evented object and use `el_`, if available, as its event bus
  3210. evented(this, {
  3211. eventBusKey: this.el_ ? 'el_' : null
  3212. });
  3213. this.handleLanguagechange = this.handleLanguagechange.bind(this);
  3214. this.on(this.player_, 'languagechange', this.handleLanguagechange);
  3215. }
  3216. stateful(this, this.constructor.defaultState);
  3217. this.children_ = [];
  3218. this.childIndex_ = {};
  3219. this.childNameIndex_ = {};
  3220. this.setTimeoutIds_ = new Set();
  3221. this.setIntervalIds_ = new Set();
  3222. this.rafIds_ = new Set();
  3223. this.namedRafs_ = new Map();
  3224. this.clearingTimersOnDispose_ = false;
  3225. // Add any child components in options
  3226. if (options.initChildren !== false) {
  3227. this.initChildren();
  3228. }
  3229. // Don't want to trigger ready here or it will go before init is actually
  3230. // finished for all children that run this constructor
  3231. this.ready(ready);
  3232. if (options.reportTouchActivity !== false) {
  3233. this.enableTouchActivity();
  3234. }
  3235. }
  3236. // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
  3237. // They are replaced or removed in the constructor
  3238. /**
  3239. * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
  3240. * function that will get called when an event with a certain name gets triggered.
  3241. *
  3242. * @param {string|string[]} type
  3243. * An event name or an array of event names.
  3244. *
  3245. * @param {Function} fn
  3246. * The function to call with `EventTarget`s
  3247. */
  3248. on(type, fn) {}
  3249. /**
  3250. * Removes an `event listener` for a specific event from an instance of `EventTarget`.
  3251. * This makes it so that the `event listener` will no longer get called when the
  3252. * named event happens.
  3253. *
  3254. * @param {string|string[]} type
  3255. * An event name or an array of event names.
  3256. *
  3257. * @param {Function} fn
  3258. * The function to remove.
  3259. */
  3260. off(type, fn) {}
  3261. /**
  3262. * This function will add an `event listener` that gets triggered only once. After the
  3263. * first trigger it will get removed. This is like adding an `event listener`
  3264. * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
  3265. *
  3266. * @param {string|string[]} type
  3267. * An event name or an array of event names.
  3268. *
  3269. * @param {Function} fn
  3270. * The function to be called once for each event name.
  3271. */
  3272. one(type, fn) {}
  3273. /**
  3274. * This function will add an `event listener` that gets triggered only once and is
  3275. * removed from all events. This is like adding an array of `event listener`s
  3276. * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
  3277. * first time it is triggered.
  3278. *
  3279. * @param {string|string[]} type
  3280. * An event name or an array of event names.
  3281. *
  3282. * @param {Function} fn
  3283. * The function to be called once for each event name.
  3284. */
  3285. any(type, fn) {}
  3286. /**
  3287. * This function causes an event to happen. This will then cause any `event listeners`
  3288. * that are waiting for that event, to get called. If there are no `event listeners`
  3289. * for an event then nothing will happen.
  3290. *
  3291. * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
  3292. * Trigger will also call the `on` + `uppercaseEventName` function.
  3293. *
  3294. * Example:
  3295. * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
  3296. * `onClick` if it exists.
  3297. *
  3298. * @param {string|Event|Object} event
  3299. * The name of the event, an `Event`, or an object with a key of type set to
  3300. * an event name.
  3301. */
  3302. trigger(event) {}
  3303. /**
  3304. * Dispose of the `Component` and all child components.
  3305. *
  3306. * @fires Component#dispose
  3307. *
  3308. * @param {Object} options
  3309. * @param {Element} options.originalEl element with which to replace player element
  3310. */
  3311. dispose(options = {}) {
  3312. // Bail out if the component has already been disposed.
  3313. if (this.isDisposed_) {
  3314. return;
  3315. }
  3316. if (this.readyQueue_) {
  3317. this.readyQueue_.length = 0;
  3318. }
  3319. /**
  3320. * Triggered when a `Component` is disposed.
  3321. *
  3322. * @event Component#dispose
  3323. * @type {Event}
  3324. *
  3325. * @property {boolean} [bubbles=false]
  3326. * set to false so that the dispose event does not
  3327. * bubble up
  3328. */
  3329. this.trigger({
  3330. type: 'dispose',
  3331. bubbles: false
  3332. });
  3333. this.isDisposed_ = true;
  3334. // Dispose all children.
  3335. if (this.children_) {
  3336. for (let i = this.children_.length - 1; i >= 0; i--) {
  3337. if (this.children_[i].dispose) {
  3338. this.children_[i].dispose();
  3339. }
  3340. }
  3341. }
  3342. // Delete child references
  3343. this.children_ = null;
  3344. this.childIndex_ = null;
  3345. this.childNameIndex_ = null;
  3346. this.parentComponent_ = null;
  3347. if (this.el_) {
  3348. // Remove element from DOM
  3349. if (this.el_.parentNode) {
  3350. if (options.restoreEl) {
  3351. this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
  3352. } else {
  3353. this.el_.parentNode.removeChild(this.el_);
  3354. }
  3355. }
  3356. this.el_ = null;
  3357. }
  3358. // remove reference to the player after disposing of the element
  3359. this.player_ = null;
  3360. }
  3361. /**
  3362. * Determine whether or not this component has been disposed.
  3363. *
  3364. * @return {boolean}
  3365. * If the component has been disposed, will be `true`. Otherwise, `false`.
  3366. */
  3367. isDisposed() {
  3368. return Boolean(this.isDisposed_);
  3369. }
  3370. /**
  3371. * Return the {@link Player} that the `Component` has attached to.
  3372. *
  3373. * @return { import('./player').default }
  3374. * The player that this `Component` has attached to.
  3375. */
  3376. player() {
  3377. return this.player_;
  3378. }
  3379. /**
  3380. * Deep merge of options objects with new options.
  3381. * > Note: When both `obj` and `options` contain properties whose values are objects.
  3382. * The two properties get merged using {@link module:obj.merge}
  3383. *
  3384. * @param {Object} obj
  3385. * The object that contains new options.
  3386. *
  3387. * @return {Object}
  3388. * A new object of `this.options_` and `obj` merged together.
  3389. */
  3390. options(obj) {
  3391. if (!obj) {
  3392. return this.options_;
  3393. }
  3394. this.options_ = merge(this.options_, obj);
  3395. return this.options_;
  3396. }
  3397. /**
  3398. * Get the `Component`s DOM element
  3399. *
  3400. * @return {Element}
  3401. * The DOM element for this `Component`.
  3402. */
  3403. el() {
  3404. return this.el_;
  3405. }
  3406. /**
  3407. * Create the `Component`s DOM element.
  3408. *
  3409. * @param {string} [tagName]
  3410. * Element's DOM node type. e.g. 'div'
  3411. *
  3412. * @param {Object} [properties]
  3413. * An object of properties that should be set.
  3414. *
  3415. * @param {Object} [attributes]
  3416. * An object of attributes that should be set.
  3417. *
  3418. * @return {Element}
  3419. * The element that gets created.
  3420. */
  3421. createEl(tagName, properties, attributes) {
  3422. return createEl(tagName, properties, attributes);
  3423. }
  3424. /**
  3425. * Localize a string given the string in english.
  3426. *
  3427. * If tokens are provided, it'll try and run a simple token replacement on the provided string.
  3428. * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
  3429. *
  3430. * If a `defaultValue` is provided, it'll use that over `string`,
  3431. * if a value isn't found in provided language files.
  3432. * This is useful if you want to have a descriptive key for token replacement
  3433. * but have a succinct localized string and not require `en.json` to be included.
  3434. *
  3435. * Currently, it is used for the progress bar timing.
  3436. * ```js
  3437. * {
  3438. * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
  3439. * }
  3440. * ```
  3441. * It is then used like so:
  3442. * ```js
  3443. * this.localize('progress bar timing: currentTime={1} duration{2}',
  3444. * [this.player_.currentTime(), this.player_.duration()],
  3445. * '{1} of {2}');
  3446. * ```
  3447. *
  3448. * Which outputs something like: `01:23 of 24:56`.
  3449. *
  3450. *
  3451. * @param {string} string
  3452. * The string to localize and the key to lookup in the language files.
  3453. * @param {string[]} [tokens]
  3454. * If the current item has token replacements, provide the tokens here.
  3455. * @param {string} [defaultValue]
  3456. * Defaults to `string`. Can be a default value to use for token replacement
  3457. * if the lookup key is needed to be separate.
  3458. *
  3459. * @return {string}
  3460. * The localized string or if no localization exists the english string.
  3461. */
  3462. localize(string, tokens, defaultValue = string) {
  3463. const code = this.player_.language && this.player_.language();
  3464. const languages = this.player_.languages && this.player_.languages();
  3465. const language = languages && languages[code];
  3466. const primaryCode = code && code.split('-')[0];
  3467. const primaryLang = languages && languages[primaryCode];
  3468. let localizedString = defaultValue;
  3469. if (language && language[string]) {
  3470. localizedString = language[string];
  3471. } else if (primaryLang && primaryLang[string]) {
  3472. localizedString = primaryLang[string];
  3473. }
  3474. if (tokens) {
  3475. localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
  3476. const value = tokens[index - 1];
  3477. let ret = value;
  3478. if (typeof value === 'undefined') {
  3479. ret = match;
  3480. }
  3481. return ret;
  3482. });
  3483. }
  3484. return localizedString;
  3485. }
  3486. /**
  3487. * Handles language change for the player in components. Should be overridden by sub-components.
  3488. *
  3489. * @abstract
  3490. */
  3491. handleLanguagechange() {}
  3492. /**
  3493. * Return the `Component`s DOM element. This is where children get inserted.
  3494. * This will usually be the the same as the element returned in {@link Component#el}.
  3495. *
  3496. * @return {Element}
  3497. * The content element for this `Component`.
  3498. */
  3499. contentEl() {
  3500. return this.contentEl_ || this.el_;
  3501. }
  3502. /**
  3503. * Get this `Component`s ID
  3504. *
  3505. * @return {string}
  3506. * The id of this `Component`
  3507. */
  3508. id() {
  3509. return this.id_;
  3510. }
  3511. /**
  3512. * Get the `Component`s name. The name gets used to reference the `Component`
  3513. * and is set during registration.
  3514. *
  3515. * @return {string}
  3516. * The name of this `Component`.
  3517. */
  3518. name() {
  3519. return this.name_;
  3520. }
  3521. /**
  3522. * Get an array of all child components
  3523. *
  3524. * @return {Array}
  3525. * The children
  3526. */
  3527. children() {
  3528. return this.children_;
  3529. }
  3530. /**
  3531. * Returns the child `Component` with the given `id`.
  3532. *
  3533. * @param {string} id
  3534. * The id of the child `Component` to get.
  3535. *
  3536. * @return {Component|undefined}
  3537. * The child `Component` with the given `id` or undefined.
  3538. */
  3539. getChildById(id) {
  3540. return this.childIndex_[id];
  3541. }
  3542. /**
  3543. * Returns the child `Component` with the given `name`.
  3544. *
  3545. * @param {string} name
  3546. * The name of the child `Component` to get.
  3547. *
  3548. * @return {Component|undefined}
  3549. * The child `Component` with the given `name` or undefined.
  3550. */
  3551. getChild(name) {
  3552. if (!name) {
  3553. return;
  3554. }
  3555. return this.childNameIndex_[name];
  3556. }
  3557. /**
  3558. * Returns the descendant `Component` following the givent
  3559. * descendant `names`. For instance ['foo', 'bar', 'baz'] would
  3560. * try to get 'foo' on the current component, 'bar' on the 'foo'
  3561. * component and 'baz' on the 'bar' component and return undefined
  3562. * if any of those don't exist.
  3563. *
  3564. * @param {...string[]|...string} names
  3565. * The name of the child `Component` to get.
  3566. *
  3567. * @return {Component|undefined}
  3568. * The descendant `Component` following the given descendant
  3569. * `names` or undefined.
  3570. */
  3571. getDescendant(...names) {
  3572. // flatten array argument into the main array
  3573. names = names.reduce((acc, n) => acc.concat(n), []);
  3574. let currentChild = this;
  3575. for (let i = 0; i < names.length; i++) {
  3576. currentChild = currentChild.getChild(names[i]);
  3577. if (!currentChild || !currentChild.getChild) {
  3578. return;
  3579. }
  3580. }
  3581. return currentChild;
  3582. }
  3583. /**
  3584. * Add a child `Component` inside the current `Component`.
  3585. *
  3586. *
  3587. * @param {string|Component} child
  3588. * The name or instance of a child to add.
  3589. *
  3590. * @param {Object} [options={}]
  3591. * The key/value store of options that will get passed to children of
  3592. * the child.
  3593. *
  3594. * @param {number} [index=this.children_.length]
  3595. * The index to attempt to add a child into.
  3596. *
  3597. * @return {Component}
  3598. * The `Component` that gets added as a child. When using a string the
  3599. * `Component` will get created by this process.
  3600. */
  3601. addChild(child, options = {}, index = this.children_.length) {
  3602. let component;
  3603. let componentName;
  3604. // If child is a string, create component with options
  3605. if (typeof child === 'string') {
  3606. componentName = toTitleCase(child);
  3607. const componentClassName = options.componentClass || componentName;
  3608. // Set name through options
  3609. options.name = componentName;
  3610. // Create a new object & element for this controls set
  3611. // If there's no .player_, this is a player
  3612. const ComponentClass = Component.getComponent(componentClassName);
  3613. if (!ComponentClass) {
  3614. throw new Error(`Component ${componentClassName} does not exist`);
  3615. }
  3616. // data stored directly on the videojs object may be
  3617. // misidentified as a component to retain
  3618. // backwards-compatibility with 4.x. check to make sure the
  3619. // component class can be instantiated.
  3620. if (typeof ComponentClass !== 'function') {
  3621. return null;
  3622. }
  3623. component = new ComponentClass(this.player_ || this, options);
  3624. // child is a component instance
  3625. } else {
  3626. component = child;
  3627. }
  3628. if (component.parentComponent_) {
  3629. component.parentComponent_.removeChild(component);
  3630. }
  3631. this.children_.splice(index, 0, component);
  3632. component.parentComponent_ = this;
  3633. if (typeof component.id === 'function') {
  3634. this.childIndex_[component.id()] = component;
  3635. }
  3636. // If a name wasn't used to create the component, check if we can use the
  3637. // name function of the component
  3638. componentName = componentName || component.name && toTitleCase(component.name());
  3639. if (componentName) {
  3640. this.childNameIndex_[componentName] = component;
  3641. this.childNameIndex_[toLowerCase(componentName)] = component;
  3642. }
  3643. // Add the UI object's element to the container div (box)
  3644. // Having an element is not required
  3645. if (typeof component.el === 'function' && component.el()) {
  3646. // If inserting before a component, insert before that component's element
  3647. let refNode = null;
  3648. if (this.children_[index + 1]) {
  3649. // Most children are components, but the video tech is an HTML element
  3650. if (this.children_[index + 1].el_) {
  3651. refNode = this.children_[index + 1].el_;
  3652. } else if (isEl(this.children_[index + 1])) {
  3653. refNode = this.children_[index + 1];
  3654. }
  3655. }
  3656. this.contentEl().insertBefore(component.el(), refNode);
  3657. }
  3658. // Return so it can stored on parent object if desired.
  3659. return component;
  3660. }
  3661. /**
  3662. * Remove a child `Component` from this `Component`s list of children. Also removes
  3663. * the child `Component`s element from this `Component`s element.
  3664. *
  3665. * @param {Component} component
  3666. * The child `Component` to remove.
  3667. */
  3668. removeChild(component) {
  3669. if (typeof component === 'string') {
  3670. component = this.getChild(component);
  3671. }
  3672. if (!component || !this.children_) {
  3673. return;
  3674. }
  3675. let childFound = false;
  3676. for (let i = this.children_.length - 1; i >= 0; i--) {
  3677. if (this.children_[i] === component) {
  3678. childFound = true;
  3679. this.children_.splice(i, 1);
  3680. break;
  3681. }
  3682. }
  3683. if (!childFound) {
  3684. return;
  3685. }
  3686. component.parentComponent_ = null;
  3687. this.childIndex_[component.id()] = null;
  3688. this.childNameIndex_[toTitleCase(component.name())] = null;
  3689. this.childNameIndex_[toLowerCase(component.name())] = null;
  3690. const compEl = component.el();
  3691. if (compEl && compEl.parentNode === this.contentEl()) {
  3692. this.contentEl().removeChild(component.el());
  3693. }
  3694. }
  3695. /**
  3696. * Add and initialize default child `Component`s based upon options.
  3697. */
  3698. initChildren() {
  3699. const children = this.options_.children;
  3700. if (children) {
  3701. // `this` is `parent`
  3702. const parentOptions = this.options_;
  3703. const handleAdd = child => {
  3704. const name = child.name;
  3705. let opts = child.opts;
  3706. // Allow options for children to be set at the parent options
  3707. // e.g. videojs(id, { controlBar: false });
  3708. // instead of videojs(id, { children: { controlBar: false });
  3709. if (parentOptions[name] !== undefined) {
  3710. opts = parentOptions[name];
  3711. }
  3712. // Allow for disabling default components
  3713. // e.g. options['children']['posterImage'] = false
  3714. if (opts === false) {
  3715. return;
  3716. }
  3717. // Allow options to be passed as a simple boolean if no configuration
  3718. // is necessary.
  3719. if (opts === true) {
  3720. opts = {};
  3721. }
  3722. // We also want to pass the original player options
  3723. // to each component as well so they don't need to
  3724. // reach back into the player for options later.
  3725. opts.playerOptions = this.options_.playerOptions;
  3726. // Create and add the child component.
  3727. // Add a direct reference to the child by name on the parent instance.
  3728. // If two of the same component are used, different names should be supplied
  3729. // for each
  3730. const newChild = this.addChild(name, opts);
  3731. if (newChild) {
  3732. this[name] = newChild;
  3733. }
  3734. };
  3735. // Allow for an array of children details to passed in the options
  3736. let workingChildren;
  3737. const Tech = Component.getComponent('Tech');
  3738. if (Array.isArray(children)) {
  3739. workingChildren = children;
  3740. } else {
  3741. workingChildren = Object.keys(children);
  3742. }
  3743. workingChildren
  3744. // children that are in this.options_ but also in workingChildren would
  3745. // give us extra children we do not want. So, we want to filter them out.
  3746. .concat(Object.keys(this.options_).filter(function (child) {
  3747. return !workingChildren.some(function (wchild) {
  3748. if (typeof wchild === 'string') {
  3749. return child === wchild;
  3750. }
  3751. return child === wchild.name;
  3752. });
  3753. })).map(child => {
  3754. let name;
  3755. let opts;
  3756. if (typeof child === 'string') {
  3757. name = child;
  3758. opts = children[name] || this.options_[name] || {};
  3759. } else {
  3760. name = child.name;
  3761. opts = child;
  3762. }
  3763. return {
  3764. name,
  3765. opts
  3766. };
  3767. }).filter(child => {
  3768. // we have to make sure that child.name isn't in the techOrder since
  3769. // techs are registered as Components but can't aren't compatible
  3770. // See https://github.com/videojs/video.js/issues/2772
  3771. const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name));
  3772. return c && !Tech.isTech(c);
  3773. }).forEach(handleAdd);
  3774. }
  3775. }
  3776. /**
  3777. * Builds the default DOM class name. Should be overridden by sub-components.
  3778. *
  3779. * @return {string}
  3780. * The DOM class name for this object.
  3781. *
  3782. * @abstract
  3783. */
  3784. buildCSSClass() {
  3785. // Child classes can include a function that does:
  3786. // return 'CLASS NAME' + this._super();
  3787. return '';
  3788. }
  3789. /**
  3790. * Bind a listener to the component's ready state.
  3791. * Different from event listeners in that if the ready event has already happened
  3792. * it will trigger the function immediately.
  3793. *
  3794. * @param {ReadyCallback} fn
  3795. * Function that gets called when the `Component` is ready.
  3796. *
  3797. * @return {Component}
  3798. * Returns itself; method can be chained.
  3799. */
  3800. ready(fn, sync = false) {
  3801. if (!fn) {
  3802. return;
  3803. }
  3804. if (!this.isReady_) {
  3805. this.readyQueue_ = this.readyQueue_ || [];
  3806. this.readyQueue_.push(fn);
  3807. return;
  3808. }
  3809. if (sync) {
  3810. fn.call(this);
  3811. } else {
  3812. // Call the function asynchronously by default for consistency
  3813. this.setTimeout(fn, 1);
  3814. }
  3815. }
  3816. /**
  3817. * Trigger all the ready listeners for this `Component`.
  3818. *
  3819. * @fires Component#ready
  3820. */
  3821. triggerReady() {
  3822. this.isReady_ = true;
  3823. // Ensure ready is triggered asynchronously
  3824. this.setTimeout(function () {
  3825. const readyQueue = this.readyQueue_;
  3826. // Reset Ready Queue
  3827. this.readyQueue_ = [];
  3828. if (readyQueue && readyQueue.length > 0) {
  3829. readyQueue.forEach(function (fn) {
  3830. fn.call(this);
  3831. }, this);
  3832. }
  3833. // Allow for using event listeners also
  3834. /**
  3835. * Triggered when a `Component` is ready.
  3836. *
  3837. * @event Component#ready
  3838. * @type {Event}
  3839. */
  3840. this.trigger('ready');
  3841. }, 1);
  3842. }
  3843. /**
  3844. * Find a single DOM element matching a `selector`. This can be within the `Component`s
  3845. * `contentEl()` or another custom context.
  3846. *
  3847. * @param {string} selector
  3848. * A valid CSS selector, which will be passed to `querySelector`.
  3849. *
  3850. * @param {Element|string} [context=this.contentEl()]
  3851. * A DOM element within which to query. Can also be a selector string in
  3852. * which case the first matching element will get used as context. If
  3853. * missing `this.contentEl()` gets used. If `this.contentEl()` returns
  3854. * nothing it falls back to `document`.
  3855. *
  3856. * @return {Element|null}
  3857. * the dom element that was found, or null
  3858. *
  3859. * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
  3860. */
  3861. $(selector, context) {
  3862. return $(selector, context || this.contentEl());
  3863. }
  3864. /**
  3865. * Finds all DOM element matching a `selector`. This can be within the `Component`s
  3866. * `contentEl()` or another custom context.
  3867. *
  3868. * @param {string} selector
  3869. * A valid CSS selector, which will be passed to `querySelectorAll`.
  3870. *
  3871. * @param {Element|string} [context=this.contentEl()]
  3872. * A DOM element within which to query. Can also be a selector string in
  3873. * which case the first matching element will get used as context. If
  3874. * missing `this.contentEl()` gets used. If `this.contentEl()` returns
  3875. * nothing it falls back to `document`.
  3876. *
  3877. * @return {NodeList}
  3878. * a list of dom elements that were found
  3879. *
  3880. * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
  3881. */
  3882. $$(selector, context) {
  3883. return $$(selector, context || this.contentEl());
  3884. }
  3885. /**
  3886. * Check if a component's element has a CSS class name.
  3887. *
  3888. * @param {string} classToCheck
  3889. * CSS class name to check.
  3890. *
  3891. * @return {boolean}
  3892. * - True if the `Component` has the class.
  3893. * - False if the `Component` does not have the class`
  3894. */
  3895. hasClass(classToCheck) {
  3896. return hasClass(this.el_, classToCheck);
  3897. }
  3898. /**
  3899. * Add a CSS class name to the `Component`s element.
  3900. *
  3901. * @param {...string} classesToAdd
  3902. * One or more CSS class name to add.
  3903. */
  3904. addClass(...classesToAdd) {
  3905. addClass(this.el_, ...classesToAdd);
  3906. }
  3907. /**
  3908. * Remove a CSS class name from the `Component`s element.
  3909. *
  3910. * @param {...string} classesToRemove
  3911. * One or more CSS class name to remove.
  3912. */
  3913. removeClass(...classesToRemove) {
  3914. removeClass(this.el_, ...classesToRemove);
  3915. }
  3916. /**
  3917. * Add or remove a CSS class name from the component's element.
  3918. * - `classToToggle` gets added when {@link Component#hasClass} would return false.
  3919. * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
  3920. *
  3921. * @param {string} classToToggle
  3922. * The class to add or remove based on (@link Component#hasClass}
  3923. *
  3924. * @param {boolean|Dom~predicate} [predicate]
  3925. * An {@link Dom~predicate} function or a boolean
  3926. */
  3927. toggleClass(classToToggle, predicate) {
  3928. toggleClass(this.el_, classToToggle, predicate);
  3929. }
  3930. /**
  3931. * Show the `Component`s element if it is hidden by removing the
  3932. * 'vjs-hidden' class name from it.
  3933. */
  3934. show() {
  3935. this.removeClass('vjs-hidden');
  3936. }
  3937. /**
  3938. * Hide the `Component`s element if it is currently showing by adding the
  3939. * 'vjs-hidden` class name to it.
  3940. */
  3941. hide() {
  3942. this.addClass('vjs-hidden');
  3943. }
  3944. /**
  3945. * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
  3946. * class name to it. Used during fadeIn/fadeOut.
  3947. *
  3948. * @private
  3949. */
  3950. lockShowing() {
  3951. this.addClass('vjs-lock-showing');
  3952. }
  3953. /**
  3954. * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
  3955. * class name from it. Used during fadeIn/fadeOut.
  3956. *
  3957. * @private
  3958. */
  3959. unlockShowing() {
  3960. this.removeClass('vjs-lock-showing');
  3961. }
  3962. /**
  3963. * Get the value of an attribute on the `Component`s element.
  3964. *
  3965. * @param {string} attribute
  3966. * Name of the attribute to get the value from.
  3967. *
  3968. * @return {string|null}
  3969. * - The value of the attribute that was asked for.
  3970. * - Can be an empty string on some browsers if the attribute does not exist
  3971. * or has no value
  3972. * - Most browsers will return null if the attribute does not exist or has
  3973. * no value.
  3974. *
  3975. * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
  3976. */
  3977. getAttribute(attribute) {
  3978. return getAttribute(this.el_, attribute);
  3979. }
  3980. /**
  3981. * Set the value of an attribute on the `Component`'s element
  3982. *
  3983. * @param {string} attribute
  3984. * Name of the attribute to set.
  3985. *
  3986. * @param {string} value
  3987. * Value to set the attribute to.
  3988. *
  3989. * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
  3990. */
  3991. setAttribute(attribute, value) {
  3992. setAttribute(this.el_, attribute, value);
  3993. }
  3994. /**
  3995. * Remove an attribute from the `Component`s element.
  3996. *
  3997. * @param {string} attribute
  3998. * Name of the attribute to remove.
  3999. *
  4000. * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
  4001. */
  4002. removeAttribute(attribute) {
  4003. removeAttribute(this.el_, attribute);
  4004. }
  4005. /**
  4006. * Get or set the width of the component based upon the CSS styles.
  4007. * See {@link Component#dimension} for more detailed information.
  4008. *
  4009. * @param {number|string} [num]
  4010. * The width that you want to set postfixed with '%', 'px' or nothing.
  4011. *
  4012. * @param {boolean} [skipListeners]
  4013. * Skip the componentresize event trigger
  4014. *
  4015. * @return {number|string}
  4016. * The width when getting, zero if there is no width. Can be a string
  4017. * postpixed with '%' or 'px'.
  4018. */
  4019. width(num, skipListeners) {
  4020. return this.dimension('width', num, skipListeners);
  4021. }
  4022. /**
  4023. * Get or set the height of the component based upon the CSS styles.
  4024. * See {@link Component#dimension} for more detailed information.
  4025. *
  4026. * @param {number|string} [num]
  4027. * The height that you want to set postfixed with '%', 'px' or nothing.
  4028. *
  4029. * @param {boolean} [skipListeners]
  4030. * Skip the componentresize event trigger
  4031. *
  4032. * @return {number|string}
  4033. * The width when getting, zero if there is no width. Can be a string
  4034. * postpixed with '%' or 'px'.
  4035. */
  4036. height(num, skipListeners) {
  4037. return this.dimension('height', num, skipListeners);
  4038. }
  4039. /**
  4040. * Set both the width and height of the `Component` element at the same time.
  4041. *
  4042. * @param {number|string} width
  4043. * Width to set the `Component`s element to.
  4044. *
  4045. * @param {number|string} height
  4046. * Height to set the `Component`s element to.
  4047. */
  4048. dimensions(width, height) {
  4049. // Skip componentresize listeners on width for optimization
  4050. this.width(width, true);
  4051. this.height(height);
  4052. }
  4053. /**
  4054. * Get or set width or height of the `Component` element. This is the shared code
  4055. * for the {@link Component#width} and {@link Component#height}.
  4056. *
  4057. * Things to know:
  4058. * - If the width or height in an number this will return the number postfixed with 'px'.
  4059. * - If the width/height is a percent this will return the percent postfixed with '%'
  4060. * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
  4061. * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
  4062. * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
  4063. * for more information
  4064. * - If you want the computed style of the component, use {@link Component#currentWidth}
  4065. * and {@link {Component#currentHeight}
  4066. *
  4067. * @fires Component#componentresize
  4068. *
  4069. * @param {string} widthOrHeight
  4070. 8 'width' or 'height'
  4071. *
  4072. * @param {number|string} [num]
  4073. 8 New dimension
  4074. *
  4075. * @param {boolean} [skipListeners]
  4076. * Skip componentresize event trigger
  4077. *
  4078. * @return {number}
  4079. * The dimension when getting or 0 if unset
  4080. */
  4081. dimension(widthOrHeight, num, skipListeners) {
  4082. if (num !== undefined) {
  4083. // Set to zero if null or literally NaN (NaN !== NaN)
  4084. if (num === null || num !== num) {
  4085. num = 0;
  4086. }
  4087. // Check if using css width/height (% or px) and adjust
  4088. if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
  4089. this.el_.style[widthOrHeight] = num;
  4090. } else if (num === 'auto') {
  4091. this.el_.style[widthOrHeight] = '';
  4092. } else {
  4093. this.el_.style[widthOrHeight] = num + 'px';
  4094. }
  4095. // skipListeners allows us to avoid triggering the resize event when setting both width and height
  4096. if (!skipListeners) {
  4097. /**
  4098. * Triggered when a component is resized.
  4099. *
  4100. * @event Component#componentresize
  4101. * @type {Event}
  4102. */
  4103. this.trigger('componentresize');
  4104. }
  4105. return;
  4106. }
  4107. // Not setting a value, so getting it
  4108. // Make sure element exists
  4109. if (!this.el_) {
  4110. return 0;
  4111. }
  4112. // Get dimension value from style
  4113. const val = this.el_.style[widthOrHeight];
  4114. const pxIndex = val.indexOf('px');
  4115. if (pxIndex !== -1) {
  4116. // Return the pixel value with no 'px'
  4117. return parseInt(val.slice(0, pxIndex), 10);
  4118. }
  4119. // No px so using % or no style was set, so falling back to offsetWidth/height
  4120. // If component has display:none, offset will return 0
  4121. // TODO: handle display:none and no dimension style using px
  4122. return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
  4123. }
  4124. /**
  4125. * Get the computed width or the height of the component's element.
  4126. *
  4127. * Uses `window.getComputedStyle`.
  4128. *
  4129. * @param {string} widthOrHeight
  4130. * A string containing 'width' or 'height'. Whichever one you want to get.
  4131. *
  4132. * @return {number}
  4133. * The dimension that gets asked for or 0 if nothing was set
  4134. * for that dimension.
  4135. */
  4136. currentDimension(widthOrHeight) {
  4137. let computedWidthOrHeight = 0;
  4138. if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
  4139. throw new Error('currentDimension only accepts width or height value');
  4140. }
  4141. computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
  4142. // remove 'px' from variable and parse as integer
  4143. computedWidthOrHeight = parseFloat(computedWidthOrHeight);
  4144. // if the computed value is still 0, it's possible that the browser is lying
  4145. // and we want to check the offset values.
  4146. // This code also runs wherever getComputedStyle doesn't exist.
  4147. if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
  4148. const rule = `offset${toTitleCase(widthOrHeight)}`;
  4149. computedWidthOrHeight = this.el_[rule];
  4150. }
  4151. return computedWidthOrHeight;
  4152. }
  4153. /**
  4154. * An object that contains width and height values of the `Component`s
  4155. * computed style. Uses `window.getComputedStyle`.
  4156. *
  4157. * @typedef {Object} Component~DimensionObject
  4158. *
  4159. * @property {number} width
  4160. * The width of the `Component`s computed style.
  4161. *
  4162. * @property {number} height
  4163. * The height of the `Component`s computed style.
  4164. */
  4165. /**
  4166. * Get an object that contains computed width and height values of the
  4167. * component's element.
  4168. *
  4169. * Uses `window.getComputedStyle`.
  4170. *
  4171. * @return {Component~DimensionObject}
  4172. * The computed dimensions of the component's element.
  4173. */
  4174. currentDimensions() {
  4175. return {
  4176. width: this.currentDimension('width'),
  4177. height: this.currentDimension('height')
  4178. };
  4179. }
  4180. /**
  4181. * Get the computed width of the component's element.
  4182. *
  4183. * Uses `window.getComputedStyle`.
  4184. *
  4185. * @return {number}
  4186. * The computed width of the component's element.
  4187. */
  4188. currentWidth() {
  4189. return this.currentDimension('width');
  4190. }
  4191. /**
  4192. * Get the computed height of the component's element.
  4193. *
  4194. * Uses `window.getComputedStyle`.
  4195. *
  4196. * @return {number}
  4197. * The computed height of the component's element.
  4198. */
  4199. currentHeight() {
  4200. return this.currentDimension('height');
  4201. }
  4202. /**
  4203. * Set the focus to this component
  4204. */
  4205. focus() {
  4206. this.el_.focus();
  4207. }
  4208. /**
  4209. * Remove the focus from this component
  4210. */
  4211. blur() {
  4212. this.el_.blur();
  4213. }
  4214. /**
  4215. * When this Component receives a `keydown` event which it does not process,
  4216. * it passes the event to the Player for handling.
  4217. *
  4218. * @param {KeyboardEvent} event
  4219. * The `keydown` event that caused this function to be called.
  4220. */
  4221. handleKeyDown(event) {
  4222. if (this.player_) {
  4223. // We only stop propagation here because we want unhandled events to fall
  4224. // back to the browser. Exclude Tab for focus trapping.
  4225. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  4226. event.stopPropagation();
  4227. }
  4228. this.player_.handleKeyDown(event);
  4229. }
  4230. }
  4231. /**
  4232. * Many components used to have a `handleKeyPress` method, which was poorly
  4233. * named because it listened to a `keydown` event. This method name now
  4234. * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
  4235. * will not see their method calls stop working.
  4236. *
  4237. * @param {Event} event
  4238. * The event that caused this function to be called.
  4239. */
  4240. handleKeyPress(event) {
  4241. this.handleKeyDown(event);
  4242. }
  4243. /**
  4244. * Emit a 'tap' events when touch event support gets detected. This gets used to
  4245. * support toggling the controls through a tap on the video. They get enabled
  4246. * because every sub-component would have extra overhead otherwise.
  4247. *
  4248. * @private
  4249. * @fires Component#tap
  4250. * @listens Component#touchstart
  4251. * @listens Component#touchmove
  4252. * @listens Component#touchleave
  4253. * @listens Component#touchcancel
  4254. * @listens Component#touchend
  4255. */
  4256. emitTapEvents() {
  4257. // Track the start time so we can determine how long the touch lasted
  4258. let touchStart = 0;
  4259. let firstTouch = null;
  4260. // Maximum movement allowed during a touch event to still be considered a tap
  4261. // Other popular libs use anywhere from 2 (hammer.js) to 15,
  4262. // so 10 seems like a nice, round number.
  4263. const tapMovementThreshold = 10;
  4264. // The maximum length a touch can be while still being considered a tap
  4265. const touchTimeThreshold = 200;
  4266. let couldBeTap;
  4267. this.on('touchstart', function (event) {
  4268. // If more than one finger, don't consider treating this as a click
  4269. if (event.touches.length === 1) {
  4270. // Copy pageX/pageY from the object
  4271. firstTouch = {
  4272. pageX: event.touches[0].pageX,
  4273. pageY: event.touches[0].pageY
  4274. };
  4275. // Record start time so we can detect a tap vs. "touch and hold"
  4276. touchStart = window__default["default"].performance.now();
  4277. // Reset couldBeTap tracking
  4278. couldBeTap = true;
  4279. }
  4280. });
  4281. this.on('touchmove', function (event) {
  4282. // If more than one finger, don't consider treating this as a click
  4283. if (event.touches.length > 1) {
  4284. couldBeTap = false;
  4285. } else if (firstTouch) {
  4286. // Some devices will throw touchmoves for all but the slightest of taps.
  4287. // So, if we moved only a small distance, this could still be a tap
  4288. const xdiff = event.touches[0].pageX - firstTouch.pageX;
  4289. const ydiff = event.touches[0].pageY - firstTouch.pageY;
  4290. const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
  4291. if (touchDistance > tapMovementThreshold) {
  4292. couldBeTap = false;
  4293. }
  4294. }
  4295. });
  4296. const noTap = function () {
  4297. couldBeTap = false;
  4298. };
  4299. // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
  4300. this.on('touchleave', noTap);
  4301. this.on('touchcancel', noTap);
  4302. // When the touch ends, measure how long it took and trigger the appropriate
  4303. // event
  4304. this.on('touchend', function (event) {
  4305. firstTouch = null;
  4306. // Proceed only if the touchmove/leave/cancel event didn't happen
  4307. if (couldBeTap === true) {
  4308. // Measure how long the touch lasted
  4309. const touchTime = window__default["default"].performance.now() - touchStart;
  4310. // Make sure the touch was less than the threshold to be considered a tap
  4311. if (touchTime < touchTimeThreshold) {
  4312. // Don't let browser turn this into a click
  4313. event.preventDefault();
  4314. /**
  4315. * Triggered when a `Component` is tapped.
  4316. *
  4317. * @event Component#tap
  4318. * @type {MouseEvent}
  4319. */
  4320. this.trigger('tap');
  4321. // It may be good to copy the touchend event object and change the
  4322. // type to tap, if the other event properties aren't exact after
  4323. // Events.fixEvent runs (e.g. event.target)
  4324. }
  4325. }
  4326. });
  4327. }
  4328. /**
  4329. * This function reports user activity whenever touch events happen. This can get
  4330. * turned off by any sub-components that wants touch events to act another way.
  4331. *
  4332. * Report user touch activity when touch events occur. User activity gets used to
  4333. * determine when controls should show/hide. It is simple when it comes to mouse
  4334. * events, because any mouse event should show the controls. So we capture mouse
  4335. * events that bubble up to the player and report activity when that happens.
  4336. * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
  4337. * controls. So touch events can't help us at the player level either.
  4338. *
  4339. * User activity gets checked asynchronously. So what could happen is a tap event
  4340. * on the video turns the controls off. Then the `touchend` event bubbles up to
  4341. * the player. Which, if it reported user activity, would turn the controls right
  4342. * back on. We also don't want to completely block touch events from bubbling up.
  4343. * Furthermore a `touchmove` event and anything other than a tap, should not turn
  4344. * controls back on.
  4345. *
  4346. * @listens Component#touchstart
  4347. * @listens Component#touchmove
  4348. * @listens Component#touchend
  4349. * @listens Component#touchcancel
  4350. */
  4351. enableTouchActivity() {
  4352. // Don't continue if the root player doesn't support reporting user activity
  4353. if (!this.player() || !this.player().reportUserActivity) {
  4354. return;
  4355. }
  4356. // listener for reporting that the user is active
  4357. const report = bind_(this.player(), this.player().reportUserActivity);
  4358. let touchHolding;
  4359. this.on('touchstart', function () {
  4360. report();
  4361. // For as long as the they are touching the device or have their mouse down,
  4362. // we consider them active even if they're not moving their finger or mouse.
  4363. // So we want to continue to update that they are active
  4364. this.clearInterval(touchHolding);
  4365. // report at the same interval as activityCheck
  4366. touchHolding = this.setInterval(report, 250);
  4367. });
  4368. const touchEnd = function (event) {
  4369. report();
  4370. // stop the interval that maintains activity if the touch is holding
  4371. this.clearInterval(touchHolding);
  4372. };
  4373. this.on('touchmove', report);
  4374. this.on('touchend', touchEnd);
  4375. this.on('touchcancel', touchEnd);
  4376. }
  4377. /**
  4378. * A callback that has no parameters and is bound into `Component`s context.
  4379. *
  4380. * @callback Component~GenericCallback
  4381. * @this Component
  4382. */
  4383. /**
  4384. * Creates a function that runs after an `x` millisecond timeout. This function is a
  4385. * wrapper around `window.setTimeout`. There are a few reasons to use this one
  4386. * instead though:
  4387. * 1. It gets cleared via {@link Component#clearTimeout} when
  4388. * {@link Component#dispose} gets called.
  4389. * 2. The function callback will gets turned into a {@link Component~GenericCallback}
  4390. *
  4391. * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
  4392. * will cause its dispose listener not to get cleaned up! Please use
  4393. * {@link Component#clearTimeout} or {@link Component#dispose} instead.
  4394. *
  4395. * @param {Component~GenericCallback} fn
  4396. * The function that will be run after `timeout`.
  4397. *
  4398. * @param {number} timeout
  4399. * Timeout in milliseconds to delay before executing the specified function.
  4400. *
  4401. * @return {number}
  4402. * Returns a timeout ID that gets used to identify the timeout. It can also
  4403. * get used in {@link Component#clearTimeout} to clear the timeout that
  4404. * was set.
  4405. *
  4406. * @listens Component#dispose
  4407. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
  4408. */
  4409. setTimeout(fn, timeout) {
  4410. // declare as variables so they are properly available in timeout function
  4411. // eslint-disable-next-line
  4412. var timeoutId;
  4413. fn = bind_(this, fn);
  4414. this.clearTimersOnDispose_();
  4415. timeoutId = window__default["default"].setTimeout(() => {
  4416. if (this.setTimeoutIds_.has(timeoutId)) {
  4417. this.setTimeoutIds_.delete(timeoutId);
  4418. }
  4419. fn();
  4420. }, timeout);
  4421. this.setTimeoutIds_.add(timeoutId);
  4422. return timeoutId;
  4423. }
  4424. /**
  4425. * Clears a timeout that gets created via `window.setTimeout` or
  4426. * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
  4427. * use this function instead of `window.clearTimout`. If you don't your dispose
  4428. * listener will not get cleaned up until {@link Component#dispose}!
  4429. *
  4430. * @param {number} timeoutId
  4431. * The id of the timeout to clear. The return value of
  4432. * {@link Component#setTimeout} or `window.setTimeout`.
  4433. *
  4434. * @return {number}
  4435. * Returns the timeout id that was cleared.
  4436. *
  4437. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
  4438. */
  4439. clearTimeout(timeoutId) {
  4440. if (this.setTimeoutIds_.has(timeoutId)) {
  4441. this.setTimeoutIds_.delete(timeoutId);
  4442. window__default["default"].clearTimeout(timeoutId);
  4443. }
  4444. return timeoutId;
  4445. }
  4446. /**
  4447. * Creates a function that gets run every `x` milliseconds. This function is a wrapper
  4448. * around `window.setInterval`. There are a few reasons to use this one instead though.
  4449. * 1. It gets cleared via {@link Component#clearInterval} when
  4450. * {@link Component#dispose} gets called.
  4451. * 2. The function callback will be a {@link Component~GenericCallback}
  4452. *
  4453. * @param {Component~GenericCallback} fn
  4454. * The function to run every `x` seconds.
  4455. *
  4456. * @param {number} interval
  4457. * Execute the specified function every `x` milliseconds.
  4458. *
  4459. * @return {number}
  4460. * Returns an id that can be used to identify the interval. It can also be be used in
  4461. * {@link Component#clearInterval} to clear the interval.
  4462. *
  4463. * @listens Component#dispose
  4464. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
  4465. */
  4466. setInterval(fn, interval) {
  4467. fn = bind_(this, fn);
  4468. this.clearTimersOnDispose_();
  4469. const intervalId = window__default["default"].setInterval(fn, interval);
  4470. this.setIntervalIds_.add(intervalId);
  4471. return intervalId;
  4472. }
  4473. /**
  4474. * Clears an interval that gets created via `window.setInterval` or
  4475. * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
  4476. * use this function instead of `window.clearInterval`. If you don't your dispose
  4477. * listener will not get cleaned up until {@link Component#dispose}!
  4478. *
  4479. * @param {number} intervalId
  4480. * The id of the interval to clear. The return value of
  4481. * {@link Component#setInterval} or `window.setInterval`.
  4482. *
  4483. * @return {number}
  4484. * Returns the interval id that was cleared.
  4485. *
  4486. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
  4487. */
  4488. clearInterval(intervalId) {
  4489. if (this.setIntervalIds_.has(intervalId)) {
  4490. this.setIntervalIds_.delete(intervalId);
  4491. window__default["default"].clearInterval(intervalId);
  4492. }
  4493. return intervalId;
  4494. }
  4495. /**
  4496. * Queues up a callback to be passed to requestAnimationFrame (rAF), but
  4497. * with a few extra bonuses:
  4498. *
  4499. * - Supports browsers that do not support rAF by falling back to
  4500. * {@link Component#setTimeout}.
  4501. *
  4502. * - The callback is turned into a {@link Component~GenericCallback} (i.e.
  4503. * bound to the component).
  4504. *
  4505. * - Automatic cancellation of the rAF callback is handled if the component
  4506. * is disposed before it is called.
  4507. *
  4508. * @param {Component~GenericCallback} fn
  4509. * A function that will be bound to this component and executed just
  4510. * before the browser's next repaint.
  4511. *
  4512. * @return {number}
  4513. * Returns an rAF ID that gets used to identify the timeout. It can
  4514. * also be used in {@link Component#cancelAnimationFrame} to cancel
  4515. * the animation frame callback.
  4516. *
  4517. * @listens Component#dispose
  4518. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
  4519. */
  4520. requestAnimationFrame(fn) {
  4521. this.clearTimersOnDispose_();
  4522. // declare as variables so they are properly available in rAF function
  4523. // eslint-disable-next-line
  4524. var id;
  4525. fn = bind_(this, fn);
  4526. id = window__default["default"].requestAnimationFrame(() => {
  4527. if (this.rafIds_.has(id)) {
  4528. this.rafIds_.delete(id);
  4529. }
  4530. fn();
  4531. });
  4532. this.rafIds_.add(id);
  4533. return id;
  4534. }
  4535. /**
  4536. * Request an animation frame, but only one named animation
  4537. * frame will be queued. Another will never be added until
  4538. * the previous one finishes.
  4539. *
  4540. * @param {string} name
  4541. * The name to give this requestAnimationFrame
  4542. *
  4543. * @param {Component~GenericCallback} fn
  4544. * A function that will be bound to this component and executed just
  4545. * before the browser's next repaint.
  4546. */
  4547. requestNamedAnimationFrame(name, fn) {
  4548. if (this.namedRafs_.has(name)) {
  4549. return;
  4550. }
  4551. this.clearTimersOnDispose_();
  4552. fn = bind_(this, fn);
  4553. const id = this.requestAnimationFrame(() => {
  4554. fn();
  4555. if (this.namedRafs_.has(name)) {
  4556. this.namedRafs_.delete(name);
  4557. }
  4558. });
  4559. this.namedRafs_.set(name, id);
  4560. return name;
  4561. }
  4562. /**
  4563. * Cancels a current named animation frame if it exists.
  4564. *
  4565. * @param {string} name
  4566. * The name of the requestAnimationFrame to cancel.
  4567. */
  4568. cancelNamedAnimationFrame(name) {
  4569. if (!this.namedRafs_.has(name)) {
  4570. return;
  4571. }
  4572. this.cancelAnimationFrame(this.namedRafs_.get(name));
  4573. this.namedRafs_.delete(name);
  4574. }
  4575. /**
  4576. * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
  4577. * (rAF).
  4578. *
  4579. * If you queue an rAF callback via {@link Component#requestAnimationFrame},
  4580. * use this function instead of `window.cancelAnimationFrame`. If you don't,
  4581. * your dispose listener will not get cleaned up until {@link Component#dispose}!
  4582. *
  4583. * @param {number} id
  4584. * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
  4585. *
  4586. * @return {number}
  4587. * Returns the rAF ID that was cleared.
  4588. *
  4589. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
  4590. */
  4591. cancelAnimationFrame(id) {
  4592. if (this.rafIds_.has(id)) {
  4593. this.rafIds_.delete(id);
  4594. window__default["default"].cancelAnimationFrame(id);
  4595. }
  4596. return id;
  4597. }
  4598. /**
  4599. * A function to setup `requestAnimationFrame`, `setTimeout`,
  4600. * and `setInterval`, clearing on dispose.
  4601. *
  4602. * > Previously each timer added and removed dispose listeners on it's own.
  4603. * For better performance it was decided to batch them all, and use `Set`s
  4604. * to track outstanding timer ids.
  4605. *
  4606. * @private
  4607. */
  4608. clearTimersOnDispose_() {
  4609. if (this.clearingTimersOnDispose_) {
  4610. return;
  4611. }
  4612. this.clearingTimersOnDispose_ = true;
  4613. this.one('dispose', () => {
  4614. [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
  4615. // for a `Set` key will actually be the value again
  4616. // so forEach((val, val) =>` but for maps we want to use
  4617. // the key.
  4618. this[idName].forEach((val, key) => this[cancelName](key));
  4619. });
  4620. this.clearingTimersOnDispose_ = false;
  4621. });
  4622. }
  4623. /**
  4624. * Register a `Component` with `videojs` given the name and the component.
  4625. *
  4626. * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
  4627. * should be registered using {@link Tech.registerTech} or
  4628. * {@link videojs:videojs.registerTech}.
  4629. *
  4630. * > NOTE: This function can also be seen on videojs as
  4631. * {@link videojs:videojs.registerComponent}.
  4632. *
  4633. * @param {string} name
  4634. * The name of the `Component` to register.
  4635. *
  4636. * @param {Component} ComponentToRegister
  4637. * The `Component` class to register.
  4638. *
  4639. * @return {Component}
  4640. * The `Component` that was registered.
  4641. */
  4642. static registerComponent(name, ComponentToRegister) {
  4643. if (typeof name !== 'string' || !name) {
  4644. throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
  4645. }
  4646. const Tech = Component.getComponent('Tech');
  4647. // We need to make sure this check is only done if Tech has been registered.
  4648. const isTech = Tech && Tech.isTech(ComponentToRegister);
  4649. const isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
  4650. if (isTech || !isComp) {
  4651. let reason;
  4652. if (isTech) {
  4653. reason = 'techs must be registered using Tech.registerTech()';
  4654. } else {
  4655. reason = 'must be a Component subclass';
  4656. }
  4657. throw new Error(`Illegal component, "${name}"; ${reason}.`);
  4658. }
  4659. name = toTitleCase(name);
  4660. if (!Component.components_) {
  4661. Component.components_ = {};
  4662. }
  4663. const Player = Component.getComponent('Player');
  4664. if (name === 'Player' && Player && Player.players) {
  4665. const players = Player.players;
  4666. const playerNames = Object.keys(players);
  4667. // If we have players that were disposed, then their name will still be
  4668. // in Players.players. So, we must loop through and verify that the value
  4669. // for each item is not null. This allows registration of the Player component
  4670. // after all players have been disposed or before any were created.
  4671. if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
  4672. throw new Error('Can not register Player component after player has been created.');
  4673. }
  4674. }
  4675. Component.components_[name] = ComponentToRegister;
  4676. Component.components_[toLowerCase(name)] = ComponentToRegister;
  4677. return ComponentToRegister;
  4678. }
  4679. /**
  4680. * Get a `Component` based on the name it was registered with.
  4681. *
  4682. * @param {string} name
  4683. * The Name of the component to get.
  4684. *
  4685. * @return {Component}
  4686. * The `Component` that got registered under the given name.
  4687. */
  4688. static getComponent(name) {
  4689. if (!name || !Component.components_) {
  4690. return;
  4691. }
  4692. return Component.components_[name];
  4693. }
  4694. }
  4695. Component.registerComponent('Component', Component);
  4696. /**
  4697. * @file time.js
  4698. * @module time
  4699. */
  4700. /**
  4701. * Returns the time for the specified index at the start or end
  4702. * of a TimeRange object.
  4703. *
  4704. * @typedef {Function} TimeRangeIndex
  4705. *
  4706. * @param {number} [index=0]
  4707. * The range number to return the time for.
  4708. *
  4709. * @return {number}
  4710. * The time offset at the specified index.
  4711. *
  4712. * @deprecated The index argument must be provided.
  4713. * In the future, leaving it out will throw an error.
  4714. */
  4715. /**
  4716. * An object that contains ranges of time, which mimics {@link TimeRanges}.
  4717. *
  4718. * @typedef {Object} TimeRange
  4719. *
  4720. * @property {number} length
  4721. * The number of time ranges represented by this object.
  4722. *
  4723. * @property {module:time~TimeRangeIndex} start
  4724. * Returns the time offset at which a specified time range begins.
  4725. *
  4726. * @property {module:time~TimeRangeIndex} end
  4727. * Returns the time offset at which a specified time range ends.
  4728. *
  4729. * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
  4730. */
  4731. /**
  4732. * Check if any of the time ranges are over the maximum index.
  4733. *
  4734. * @private
  4735. * @param {string} fnName
  4736. * The function name to use for logging
  4737. *
  4738. * @param {number} index
  4739. * The index to check
  4740. *
  4741. * @param {number} maxIndex
  4742. * The maximum possible index
  4743. *
  4744. * @throws {Error} if the timeRanges provided are over the maxIndex
  4745. */
  4746. function rangeCheck(fnName, index, maxIndex) {
  4747. if (typeof index !== 'number' || index < 0 || index > maxIndex) {
  4748. throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
  4749. }
  4750. }
  4751. /**
  4752. * Get the time for the specified index at the start or end
  4753. * of a TimeRange object.
  4754. *
  4755. * @private
  4756. * @param {string} fnName
  4757. * The function name to use for logging
  4758. *
  4759. * @param {string} valueIndex
  4760. * The property that should be used to get the time. should be
  4761. * 'start' or 'end'
  4762. *
  4763. * @param {Array} ranges
  4764. * An array of time ranges
  4765. *
  4766. * @param {Array} [rangeIndex=0]
  4767. * The index to start the search at
  4768. *
  4769. * @return {number}
  4770. * The time that offset at the specified index.
  4771. *
  4772. * @deprecated rangeIndex must be set to a value, in the future this will throw an error.
  4773. * @throws {Error} if rangeIndex is more than the length of ranges
  4774. */
  4775. function getRange(fnName, valueIndex, ranges, rangeIndex) {
  4776. rangeCheck(fnName, rangeIndex, ranges.length - 1);
  4777. return ranges[rangeIndex][valueIndex];
  4778. }
  4779. /**
  4780. * Create a time range object given ranges of time.
  4781. *
  4782. * @private
  4783. * @param {Array} [ranges]
  4784. * An array of time ranges.
  4785. *
  4786. * @return {TimeRange}
  4787. */
  4788. function createTimeRangesObj(ranges) {
  4789. let timeRangesObj;
  4790. if (ranges === undefined || ranges.length === 0) {
  4791. timeRangesObj = {
  4792. length: 0,
  4793. start() {
  4794. throw new Error('This TimeRanges object is empty');
  4795. },
  4796. end() {
  4797. throw new Error('This TimeRanges object is empty');
  4798. }
  4799. };
  4800. } else {
  4801. timeRangesObj = {
  4802. length: ranges.length,
  4803. start: getRange.bind(null, 'start', 0, ranges),
  4804. end: getRange.bind(null, 'end', 1, ranges)
  4805. };
  4806. }
  4807. if (window__default["default"].Symbol && window__default["default"].Symbol.iterator) {
  4808. timeRangesObj[window__default["default"].Symbol.iterator] = () => (ranges || []).values();
  4809. }
  4810. return timeRangesObj;
  4811. }
  4812. /**
  4813. * Create a `TimeRange` object which mimics an
  4814. * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
  4815. *
  4816. * @param {number|Array[]} start
  4817. * The start of a single range (a number) or an array of ranges (an
  4818. * array of arrays of two numbers each).
  4819. *
  4820. * @param {number} end
  4821. * The end of a single range. Cannot be used with the array form of
  4822. * the `start` argument.
  4823. *
  4824. * @return {TimeRange}
  4825. */
  4826. function createTimeRanges(start, end) {
  4827. if (Array.isArray(start)) {
  4828. return createTimeRangesObj(start);
  4829. } else if (start === undefined || end === undefined) {
  4830. return createTimeRangesObj();
  4831. }
  4832. return createTimeRangesObj([[start, end]]);
  4833. }
  4834. /**
  4835. * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
  4836. * seconds) will force a number of leading zeros to cover the length of the
  4837. * guide.
  4838. *
  4839. * @private
  4840. * @param {number} seconds
  4841. * Number of seconds to be turned into a string
  4842. *
  4843. * @param {number} guide
  4844. * Number (in seconds) to model the string after
  4845. *
  4846. * @return {string}
  4847. * Time formatted as H:MM:SS or M:SS
  4848. */
  4849. const defaultImplementation = function (seconds, guide) {
  4850. seconds = seconds < 0 ? 0 : seconds;
  4851. let s = Math.floor(seconds % 60);
  4852. let m = Math.floor(seconds / 60 % 60);
  4853. let h = Math.floor(seconds / 3600);
  4854. const gm = Math.floor(guide / 60 % 60);
  4855. const gh = Math.floor(guide / 3600);
  4856. // handle invalid times
  4857. if (isNaN(seconds) || seconds === Infinity) {
  4858. // '-' is false for all relational operators (e.g. <, >=) so this setting
  4859. // will add the minimum number of fields specified by the guide
  4860. h = m = s = '-';
  4861. }
  4862. // Check if we need to show hours
  4863. h = h > 0 || gh > 0 ? h + ':' : '';
  4864. // If hours are showing, we may need to add a leading zero.
  4865. // Always show at least one digit of minutes.
  4866. m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
  4867. // Check if leading zero is need for seconds
  4868. s = s < 10 ? '0' + s : s;
  4869. return h + m + s;
  4870. };
  4871. // Internal pointer to the current implementation.
  4872. let implementation = defaultImplementation;
  4873. /**
  4874. * Replaces the default formatTime implementation with a custom implementation.
  4875. *
  4876. * @param {Function} customImplementation
  4877. * A function which will be used in place of the default formatTime
  4878. * implementation. Will receive the current time in seconds and the
  4879. * guide (in seconds) as arguments.
  4880. */
  4881. function setFormatTime(customImplementation) {
  4882. implementation = customImplementation;
  4883. }
  4884. /**
  4885. * Resets formatTime to the default implementation.
  4886. */
  4887. function resetFormatTime() {
  4888. implementation = defaultImplementation;
  4889. }
  4890. /**
  4891. * Delegates to either the default time formatting function or a custom
  4892. * function supplied via `setFormatTime`.
  4893. *
  4894. * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
  4895. * guide (in seconds) will force a number of leading zeros to cover the
  4896. * length of the guide.
  4897. *
  4898. * @example formatTime(125, 600) === "02:05"
  4899. * @param {number} seconds
  4900. * Number of seconds to be turned into a string
  4901. *
  4902. * @param {number} guide
  4903. * Number (in seconds) to model the string after
  4904. *
  4905. * @return {string}
  4906. * Time formatted as H:MM:SS or M:SS
  4907. */
  4908. function formatTime(seconds, guide = seconds) {
  4909. return implementation(seconds, guide);
  4910. }
  4911. var Time = /*#__PURE__*/Object.freeze({
  4912. __proto__: null,
  4913. createTimeRanges: createTimeRanges,
  4914. createTimeRange: createTimeRanges,
  4915. setFormatTime: setFormatTime,
  4916. resetFormatTime: resetFormatTime,
  4917. formatTime: formatTime
  4918. });
  4919. /**
  4920. * @file buffer.js
  4921. * @module buffer
  4922. */
  4923. /**
  4924. * Compute the percentage of the media that has been buffered.
  4925. *
  4926. * @param { import('./time').TimeRange } buffered
  4927. * The current `TimeRanges` object representing buffered time ranges
  4928. *
  4929. * @param {number} duration
  4930. * Total duration of the media
  4931. *
  4932. * @return {number}
  4933. * Percent buffered of the total duration in decimal form.
  4934. */
  4935. function bufferedPercent(buffered, duration) {
  4936. let bufferedDuration = 0;
  4937. let start;
  4938. let end;
  4939. if (!duration) {
  4940. return 0;
  4941. }
  4942. if (!buffered || !buffered.length) {
  4943. buffered = createTimeRanges(0, 0);
  4944. }
  4945. for (let i = 0; i < buffered.length; i++) {
  4946. start = buffered.start(i);
  4947. end = buffered.end(i);
  4948. // buffered end can be bigger than duration by a very small fraction
  4949. if (end > duration) {
  4950. end = duration;
  4951. }
  4952. bufferedDuration += end - start;
  4953. }
  4954. return bufferedDuration / duration;
  4955. }
  4956. /**
  4957. * @file media-error.js
  4958. */
  4959. /**
  4960. * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
  4961. *
  4962. * @param {number|string|Object|MediaError} value
  4963. * This can be of multiple types:
  4964. * - number: should be a standard error code
  4965. * - string: an error message (the code will be 0)
  4966. * - Object: arbitrary properties
  4967. * - `MediaError` (native): used to populate a video.js `MediaError` object
  4968. * - `MediaError` (video.js): will return itself if it's already a
  4969. * video.js `MediaError` object.
  4970. *
  4971. * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
  4972. * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
  4973. *
  4974. * @class MediaError
  4975. */
  4976. function MediaError(value) {
  4977. // Allow redundant calls to this constructor to avoid having `instanceof`
  4978. // checks peppered around the code.
  4979. if (value instanceof MediaError) {
  4980. return value;
  4981. }
  4982. if (typeof value === 'number') {
  4983. this.code = value;
  4984. } else if (typeof value === 'string') {
  4985. // default code is zero, so this is a custom error
  4986. this.message = value;
  4987. } else if (isObject(value)) {
  4988. // We assign the `code` property manually because native `MediaError` objects
  4989. // do not expose it as an own/enumerable property of the object.
  4990. if (typeof value.code === 'number') {
  4991. this.code = value.code;
  4992. }
  4993. Object.assign(this, value);
  4994. }
  4995. if (!this.message) {
  4996. this.message = MediaError.defaultMessages[this.code] || '';
  4997. }
  4998. }
  4999. /**
  5000. * The error code that refers two one of the defined `MediaError` types
  5001. *
  5002. * @type {Number}
  5003. */
  5004. MediaError.prototype.code = 0;
  5005. /**
  5006. * An optional message that to show with the error. Message is not part of the HTML5
  5007. * video spec but allows for more informative custom errors.
  5008. *
  5009. * @type {String}
  5010. */
  5011. MediaError.prototype.message = '';
  5012. /**
  5013. * An optional status code that can be set by plugins to allow even more detail about
  5014. * the error. For example a plugin might provide a specific HTTP status code and an
  5015. * error message for that code. Then when the plugin gets that error this class will
  5016. * know how to display an error message for it. This allows a custom message to show
  5017. * up on the `Player` error overlay.
  5018. *
  5019. * @type {Array}
  5020. */
  5021. MediaError.prototype.status = null;
  5022. /**
  5023. * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
  5024. * specification listed under {@link MediaError} for more information.
  5025. *
  5026. * @enum {array}
  5027. * @readonly
  5028. * @property {string} 0 - MEDIA_ERR_CUSTOM
  5029. * @property {string} 1 - MEDIA_ERR_ABORTED
  5030. * @property {string} 2 - MEDIA_ERR_NETWORK
  5031. * @property {string} 3 - MEDIA_ERR_DECODE
  5032. * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
  5033. * @property {string} 5 - MEDIA_ERR_ENCRYPTED
  5034. */
  5035. MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
  5036. /**
  5037. * The default `MediaError` messages based on the {@link MediaError.errorTypes}.
  5038. *
  5039. * @type {Array}
  5040. * @constant
  5041. */
  5042. MediaError.defaultMessages = {
  5043. 1: 'You aborted the media playback',
  5044. 2: 'A network error caused the media download to fail part-way.',
  5045. 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
  5046. 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
  5047. 5: 'The media is encrypted and we do not have the keys to decrypt it.'
  5048. };
  5049. // Add types as properties on MediaError
  5050. // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
  5051. for (let errNum = 0; errNum < MediaError.errorTypes.length; errNum++) {
  5052. MediaError[MediaError.errorTypes[errNum]] = errNum;
  5053. // values should be accessible on both the class and instance
  5054. MediaError.prototype[MediaError.errorTypes[errNum]] = errNum;
  5055. }
  5056. /**
  5057. * Returns whether an object is `Promise`-like (i.e. has a `then` method).
  5058. *
  5059. * @param {Object} value
  5060. * An object that may or may not be `Promise`-like.
  5061. *
  5062. * @return {boolean}
  5063. * Whether or not the object is `Promise`-like.
  5064. */
  5065. function isPromise(value) {
  5066. return value !== undefined && value !== null && typeof value.then === 'function';
  5067. }
  5068. /**
  5069. * Silence a Promise-like object.
  5070. *
  5071. * This is useful for avoiding non-harmful, but potentially confusing "uncaught
  5072. * play promise" rejection error messages.
  5073. *
  5074. * @param {Object} value
  5075. * An object that may or may not be `Promise`-like.
  5076. */
  5077. function silencePromise(value) {
  5078. if (isPromise(value)) {
  5079. value.then(null, e => {});
  5080. }
  5081. }
  5082. /**
  5083. * @file text-track-list-converter.js Utilities for capturing text track state and
  5084. * re-creating tracks based on a capture.
  5085. *
  5086. * @module text-track-list-converter
  5087. */
  5088. /**
  5089. * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
  5090. * represents the {@link TextTrack}'s state.
  5091. *
  5092. * @param {TextTrack} track
  5093. * The text track to query.
  5094. *
  5095. * @return {Object}
  5096. * A serializable javascript representation of the TextTrack.
  5097. * @private
  5098. */
  5099. const trackToJson_ = function (track) {
  5100. const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
  5101. if (track[prop]) {
  5102. acc[prop] = track[prop];
  5103. }
  5104. return acc;
  5105. }, {
  5106. cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
  5107. return {
  5108. startTime: cue.startTime,
  5109. endTime: cue.endTime,
  5110. text: cue.text,
  5111. id: cue.id
  5112. };
  5113. })
  5114. });
  5115. return ret;
  5116. };
  5117. /**
  5118. * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
  5119. * state of all {@link TextTrack}s currently configured. The return array is compatible with
  5120. * {@link text-track-list-converter:jsonToTextTracks}.
  5121. *
  5122. * @param { import('../tech/tech').default } tech
  5123. * The tech object to query
  5124. *
  5125. * @return {Array}
  5126. * A serializable javascript representation of the {@link Tech}s
  5127. * {@link TextTrackList}.
  5128. */
  5129. const textTracksToJson = function (tech) {
  5130. const trackEls = tech.$$('track');
  5131. const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
  5132. const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
  5133. const json = trackToJson_(trackEl.track);
  5134. if (trackEl.src) {
  5135. json.src = trackEl.src;
  5136. }
  5137. return json;
  5138. });
  5139. return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
  5140. return trackObjs.indexOf(track) === -1;
  5141. }).map(trackToJson_));
  5142. };
  5143. /**
  5144. * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
  5145. * object {@link TextTrack} representations.
  5146. *
  5147. * @param {Array} json
  5148. * An array of `TextTrack` representation objects, like those that would be
  5149. * produced by `textTracksToJson`.
  5150. *
  5151. * @param {Tech} tech
  5152. * The `Tech` to create the `TextTrack`s on.
  5153. */
  5154. const jsonToTextTracks = function (json, tech) {
  5155. json.forEach(function (track) {
  5156. const addedTrack = tech.addRemoteTextTrack(track).track;
  5157. if (!track.src && track.cues) {
  5158. track.cues.forEach(cue => addedTrack.addCue(cue));
  5159. }
  5160. });
  5161. return tech.textTracks();
  5162. };
  5163. var textTrackConverter = {
  5164. textTracksToJson,
  5165. jsonToTextTracks,
  5166. trackToJson_
  5167. };
  5168. /**
  5169. * @file modal-dialog.js
  5170. */
  5171. const MODAL_CLASS_NAME = 'vjs-modal-dialog';
  5172. /**
  5173. * The `ModalDialog` displays over the video and its controls, which blocks
  5174. * interaction with the player until it is closed.
  5175. *
  5176. * Modal dialogs include a "Close" button and will close when that button
  5177. * is activated - or when ESC is pressed anywhere.
  5178. *
  5179. * @extends Component
  5180. */
  5181. class ModalDialog extends Component {
  5182. /**
  5183. * Create an instance of this class.
  5184. *
  5185. * @param { import('./player').default } player
  5186. * The `Player` that this class should be attached to.
  5187. *
  5188. * @param {Object} [options]
  5189. * The key/value store of player options.
  5190. *
  5191. * @param { import('./utils/dom').ContentDescriptor} [options.content=undefined]
  5192. * Provide customized content for this modal.
  5193. *
  5194. * @param {string} [options.description]
  5195. * A text description for the modal, primarily for accessibility.
  5196. *
  5197. * @param {boolean} [options.fillAlways=false]
  5198. * Normally, modals are automatically filled only the first time
  5199. * they open. This tells the modal to refresh its content
  5200. * every time it opens.
  5201. *
  5202. * @param {string} [options.label]
  5203. * A text label for the modal, primarily for accessibility.
  5204. *
  5205. * @param {boolean} [options.pauseOnOpen=true]
  5206. * If `true`, playback will will be paused if playing when
  5207. * the modal opens, and resumed when it closes.
  5208. *
  5209. * @param {boolean} [options.temporary=true]
  5210. * If `true`, the modal can only be opened once; it will be
  5211. * disposed as soon as it's closed.
  5212. *
  5213. * @param {boolean} [options.uncloseable=false]
  5214. * If `true`, the user will not be able to close the modal
  5215. * through the UI in the normal ways. Programmatic closing is
  5216. * still possible.
  5217. */
  5218. constructor(player, options) {
  5219. super(player, options);
  5220. this.handleKeyDown_ = e => this.handleKeyDown(e);
  5221. this.close_ = e => this.close(e);
  5222. this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
  5223. this.closeable(!this.options_.uncloseable);
  5224. this.content(this.options_.content);
  5225. // Make sure the contentEl is defined AFTER any children are initialized
  5226. // because we only want the contents of the modal in the contentEl
  5227. // (not the UI elements like the close button).
  5228. this.contentEl_ = createEl('div', {
  5229. className: `${MODAL_CLASS_NAME}-content`
  5230. }, {
  5231. role: 'document'
  5232. });
  5233. this.descEl_ = createEl('p', {
  5234. className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
  5235. id: this.el().getAttribute('aria-describedby')
  5236. });
  5237. textContent(this.descEl_, this.description());
  5238. this.el_.appendChild(this.descEl_);
  5239. this.el_.appendChild(this.contentEl_);
  5240. }
  5241. /**
  5242. * Create the `ModalDialog`'s DOM element
  5243. *
  5244. * @return {Element}
  5245. * The DOM element that gets created.
  5246. */
  5247. createEl() {
  5248. return super.createEl('div', {
  5249. className: this.buildCSSClass(),
  5250. tabIndex: -1
  5251. }, {
  5252. 'aria-describedby': `${this.id()}_description`,
  5253. 'aria-hidden': 'true',
  5254. 'aria-label': this.label(),
  5255. 'role': 'dialog'
  5256. });
  5257. }
  5258. dispose() {
  5259. this.contentEl_ = null;
  5260. this.descEl_ = null;
  5261. this.previouslyActiveEl_ = null;
  5262. super.dispose();
  5263. }
  5264. /**
  5265. * Builds the default DOM `className`.
  5266. *
  5267. * @return {string}
  5268. * The DOM `className` for this object.
  5269. */
  5270. buildCSSClass() {
  5271. return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
  5272. }
  5273. /**
  5274. * Returns the label string for this modal. Primarily used for accessibility.
  5275. *
  5276. * @return {string}
  5277. * the localized or raw label of this modal.
  5278. */
  5279. label() {
  5280. return this.localize(this.options_.label || 'Modal Window');
  5281. }
  5282. /**
  5283. * Returns the description string for this modal. Primarily used for
  5284. * accessibility.
  5285. *
  5286. * @return {string}
  5287. * The localized or raw description of this modal.
  5288. */
  5289. description() {
  5290. let desc = this.options_.description || this.localize('This is a modal window.');
  5291. // Append a universal closeability message if the modal is closeable.
  5292. if (this.closeable()) {
  5293. desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
  5294. }
  5295. return desc;
  5296. }
  5297. /**
  5298. * Opens the modal.
  5299. *
  5300. * @fires ModalDialog#beforemodalopen
  5301. * @fires ModalDialog#modalopen
  5302. */
  5303. open() {
  5304. if (!this.opened_) {
  5305. const player = this.player();
  5306. /**
  5307. * Fired just before a `ModalDialog` is opened.
  5308. *
  5309. * @event ModalDialog#beforemodalopen
  5310. * @type {Event}
  5311. */
  5312. this.trigger('beforemodalopen');
  5313. this.opened_ = true;
  5314. // Fill content if the modal has never opened before and
  5315. // never been filled.
  5316. if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
  5317. this.fill();
  5318. }
  5319. // If the player was playing, pause it and take note of its previously
  5320. // playing state.
  5321. this.wasPlaying_ = !player.paused();
  5322. if (this.options_.pauseOnOpen && this.wasPlaying_) {
  5323. player.pause();
  5324. }
  5325. this.on('keydown', this.handleKeyDown_);
  5326. // Hide controls and note if they were enabled.
  5327. this.hadControls_ = player.controls();
  5328. player.controls(false);
  5329. this.show();
  5330. this.conditionalFocus_();
  5331. this.el().setAttribute('aria-hidden', 'false');
  5332. /**
  5333. * Fired just after a `ModalDialog` is opened.
  5334. *
  5335. * @event ModalDialog#modalopen
  5336. * @type {Event}
  5337. */
  5338. this.trigger('modalopen');
  5339. this.hasBeenOpened_ = true;
  5340. }
  5341. }
  5342. /**
  5343. * If the `ModalDialog` is currently open or closed.
  5344. *
  5345. * @param {boolean} [value]
  5346. * If given, it will open (`true`) or close (`false`) the modal.
  5347. *
  5348. * @return {boolean}
  5349. * the current open state of the modaldialog
  5350. */
  5351. opened(value) {
  5352. if (typeof value === 'boolean') {
  5353. this[value ? 'open' : 'close']();
  5354. }
  5355. return this.opened_;
  5356. }
  5357. /**
  5358. * Closes the modal, does nothing if the `ModalDialog` is
  5359. * not open.
  5360. *
  5361. * @fires ModalDialog#beforemodalclose
  5362. * @fires ModalDialog#modalclose
  5363. */
  5364. close() {
  5365. if (!this.opened_) {
  5366. return;
  5367. }
  5368. const player = this.player();
  5369. /**
  5370. * Fired just before a `ModalDialog` is closed.
  5371. *
  5372. * @event ModalDialog#beforemodalclose
  5373. * @type {Event}
  5374. */
  5375. this.trigger('beforemodalclose');
  5376. this.opened_ = false;
  5377. if (this.wasPlaying_ && this.options_.pauseOnOpen) {
  5378. player.play();
  5379. }
  5380. this.off('keydown', this.handleKeyDown_);
  5381. if (this.hadControls_) {
  5382. player.controls(true);
  5383. }
  5384. this.hide();
  5385. this.el().setAttribute('aria-hidden', 'true');
  5386. /**
  5387. * Fired just after a `ModalDialog` is closed.
  5388. *
  5389. * @event ModalDialog#modalclose
  5390. * @type {Event}
  5391. */
  5392. this.trigger('modalclose');
  5393. this.conditionalBlur_();
  5394. if (this.options_.temporary) {
  5395. this.dispose();
  5396. }
  5397. }
  5398. /**
  5399. * Check to see if the `ModalDialog` is closeable via the UI.
  5400. *
  5401. * @param {boolean} [value]
  5402. * If given as a boolean, it will set the `closeable` option.
  5403. *
  5404. * @return {boolean}
  5405. * Returns the final value of the closable option.
  5406. */
  5407. closeable(value) {
  5408. if (typeof value === 'boolean') {
  5409. const closeable = this.closeable_ = !!value;
  5410. let close = this.getChild('closeButton');
  5411. // If this is being made closeable and has no close button, add one.
  5412. if (closeable && !close) {
  5413. // The close button should be a child of the modal - not its
  5414. // content element, so temporarily change the content element.
  5415. const temp = this.contentEl_;
  5416. this.contentEl_ = this.el_;
  5417. close = this.addChild('closeButton', {
  5418. controlText: 'Close Modal Dialog'
  5419. });
  5420. this.contentEl_ = temp;
  5421. this.on(close, 'close', this.close_);
  5422. }
  5423. // If this is being made uncloseable and has a close button, remove it.
  5424. if (!closeable && close) {
  5425. this.off(close, 'close', this.close_);
  5426. this.removeChild(close);
  5427. close.dispose();
  5428. }
  5429. }
  5430. return this.closeable_;
  5431. }
  5432. /**
  5433. * Fill the modal's content element with the modal's "content" option.
  5434. * The content element will be emptied before this change takes place.
  5435. */
  5436. fill() {
  5437. this.fillWith(this.content());
  5438. }
  5439. /**
  5440. * Fill the modal's content element with arbitrary content.
  5441. * The content element will be emptied before this change takes place.
  5442. *
  5443. * @fires ModalDialog#beforemodalfill
  5444. * @fires ModalDialog#modalfill
  5445. *
  5446. * @param { import('./utils/dom').ContentDescriptor} [content]
  5447. * The same rules apply to this as apply to the `content` option.
  5448. */
  5449. fillWith(content) {
  5450. const contentEl = this.contentEl();
  5451. const parentEl = contentEl.parentNode;
  5452. const nextSiblingEl = contentEl.nextSibling;
  5453. /**
  5454. * Fired just before a `ModalDialog` is filled with content.
  5455. *
  5456. * @event ModalDialog#beforemodalfill
  5457. * @type {Event}
  5458. */
  5459. this.trigger('beforemodalfill');
  5460. this.hasBeenFilled_ = true;
  5461. // Detach the content element from the DOM before performing
  5462. // manipulation to avoid modifying the live DOM multiple times.
  5463. parentEl.removeChild(contentEl);
  5464. this.empty();
  5465. insertContent(contentEl, content);
  5466. /**
  5467. * Fired just after a `ModalDialog` is filled with content.
  5468. *
  5469. * @event ModalDialog#modalfill
  5470. * @type {Event}
  5471. */
  5472. this.trigger('modalfill');
  5473. // Re-inject the re-filled content element.
  5474. if (nextSiblingEl) {
  5475. parentEl.insertBefore(contentEl, nextSiblingEl);
  5476. } else {
  5477. parentEl.appendChild(contentEl);
  5478. }
  5479. // make sure that the close button is last in the dialog DOM
  5480. const closeButton = this.getChild('closeButton');
  5481. if (closeButton) {
  5482. parentEl.appendChild(closeButton.el_);
  5483. }
  5484. }
  5485. /**
  5486. * Empties the content element. This happens anytime the modal is filled.
  5487. *
  5488. * @fires ModalDialog#beforemodalempty
  5489. * @fires ModalDialog#modalempty
  5490. */
  5491. empty() {
  5492. /**
  5493. * Fired just before a `ModalDialog` is emptied.
  5494. *
  5495. * @event ModalDialog#beforemodalempty
  5496. * @type {Event}
  5497. */
  5498. this.trigger('beforemodalempty');
  5499. emptyEl(this.contentEl());
  5500. /**
  5501. * Fired just after a `ModalDialog` is emptied.
  5502. *
  5503. * @event ModalDialog#modalempty
  5504. * @type {Event}
  5505. */
  5506. this.trigger('modalempty');
  5507. }
  5508. /**
  5509. * Gets or sets the modal content, which gets normalized before being
  5510. * rendered into the DOM.
  5511. *
  5512. * This does not update the DOM or fill the modal, but it is called during
  5513. * that process.
  5514. *
  5515. * @param { import('./utils/dom').ContentDescriptor} [value]
  5516. * If defined, sets the internal content value to be used on the
  5517. * next call(s) to `fill`. This value is normalized before being
  5518. * inserted. To "clear" the internal content value, pass `null`.
  5519. *
  5520. * @return { import('./utils/dom').ContentDescriptor}
  5521. * The current content of the modal dialog
  5522. */
  5523. content(value) {
  5524. if (typeof value !== 'undefined') {
  5525. this.content_ = value;
  5526. }
  5527. return this.content_;
  5528. }
  5529. /**
  5530. * conditionally focus the modal dialog if focus was previously on the player.
  5531. *
  5532. * @private
  5533. */
  5534. conditionalFocus_() {
  5535. const activeEl = document__default["default"].activeElement;
  5536. const playerEl = this.player_.el_;
  5537. this.previouslyActiveEl_ = null;
  5538. if (playerEl.contains(activeEl) || playerEl === activeEl) {
  5539. this.previouslyActiveEl_ = activeEl;
  5540. this.focus();
  5541. }
  5542. }
  5543. /**
  5544. * conditionally blur the element and refocus the last focused element
  5545. *
  5546. * @private
  5547. */
  5548. conditionalBlur_() {
  5549. if (this.previouslyActiveEl_) {
  5550. this.previouslyActiveEl_.focus();
  5551. this.previouslyActiveEl_ = null;
  5552. }
  5553. }
  5554. /**
  5555. * Keydown handler. Attached when modal is focused.
  5556. *
  5557. * @listens keydown
  5558. */
  5559. handleKeyDown(event) {
  5560. // Do not allow keydowns to reach out of the modal dialog.
  5561. event.stopPropagation();
  5562. if (keycode__default["default"].isEventKey(event, 'Escape') && this.closeable()) {
  5563. event.preventDefault();
  5564. this.close();
  5565. return;
  5566. }
  5567. // exit early if it isn't a tab key
  5568. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  5569. return;
  5570. }
  5571. const focusableEls = this.focusableEls_();
  5572. const activeEl = this.el_.querySelector(':focus');
  5573. let focusIndex;
  5574. for (let i = 0; i < focusableEls.length; i++) {
  5575. if (activeEl === focusableEls[i]) {
  5576. focusIndex = i;
  5577. break;
  5578. }
  5579. }
  5580. if (document__default["default"].activeElement === this.el_) {
  5581. focusIndex = 0;
  5582. }
  5583. if (event.shiftKey && focusIndex === 0) {
  5584. focusableEls[focusableEls.length - 1].focus();
  5585. event.preventDefault();
  5586. } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
  5587. focusableEls[0].focus();
  5588. event.preventDefault();
  5589. }
  5590. }
  5591. /**
  5592. * get all focusable elements
  5593. *
  5594. * @private
  5595. */
  5596. focusableEls_() {
  5597. const allChildren = this.el_.querySelectorAll('*');
  5598. return Array.prototype.filter.call(allChildren, child => {
  5599. return (child instanceof window__default["default"].HTMLAnchorElement || child instanceof window__default["default"].HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window__default["default"].HTMLInputElement || child instanceof window__default["default"].HTMLSelectElement || child instanceof window__default["default"].HTMLTextAreaElement || child instanceof window__default["default"].HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window__default["default"].HTMLIFrameElement || child instanceof window__default["default"].HTMLObjectElement || child instanceof window__default["default"].HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
  5600. });
  5601. }
  5602. }
  5603. /**
  5604. * Default options for `ModalDialog` default options.
  5605. *
  5606. * @type {Object}
  5607. * @private
  5608. */
  5609. ModalDialog.prototype.options_ = {
  5610. pauseOnOpen: true,
  5611. temporary: true
  5612. };
  5613. Component.registerComponent('ModalDialog', ModalDialog);
  5614. /**
  5615. * @file track-list.js
  5616. */
  5617. /**
  5618. * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
  5619. * {@link VideoTrackList}
  5620. *
  5621. * @extends EventTarget
  5622. */
  5623. class TrackList extends EventTarget {
  5624. /**
  5625. * Create an instance of this class
  5626. *
  5627. * @param { import('./track').default[] } tracks
  5628. * A list of tracks to initialize the list with.
  5629. *
  5630. * @abstract
  5631. */
  5632. constructor(tracks = []) {
  5633. super();
  5634. this.tracks_ = [];
  5635. /**
  5636. * @memberof TrackList
  5637. * @member {number} length
  5638. * The current number of `Track`s in the this Trackist.
  5639. * @instance
  5640. */
  5641. Object.defineProperty(this, 'length', {
  5642. get() {
  5643. return this.tracks_.length;
  5644. }
  5645. });
  5646. for (let i = 0; i < tracks.length; i++) {
  5647. this.addTrack(tracks[i]);
  5648. }
  5649. }
  5650. /**
  5651. * Add a {@link Track} to the `TrackList`
  5652. *
  5653. * @param { import('./track').default } track
  5654. * The audio, video, or text track to add to the list.
  5655. *
  5656. * @fires TrackList#addtrack
  5657. */
  5658. addTrack(track) {
  5659. const index = this.tracks_.length;
  5660. if (!('' + index in this)) {
  5661. Object.defineProperty(this, index, {
  5662. get() {
  5663. return this.tracks_[index];
  5664. }
  5665. });
  5666. }
  5667. // Do not add duplicate tracks
  5668. if (this.tracks_.indexOf(track) === -1) {
  5669. this.tracks_.push(track);
  5670. /**
  5671. * Triggered when a track is added to a track list.
  5672. *
  5673. * @event TrackList#addtrack
  5674. * @type {Event}
  5675. * @property {Track} track
  5676. * A reference to track that was added.
  5677. */
  5678. this.trigger({
  5679. track,
  5680. type: 'addtrack',
  5681. target: this
  5682. });
  5683. }
  5684. /**
  5685. * Triggered when a track label is changed.
  5686. *
  5687. * @event TrackList#addtrack
  5688. * @type {Event}
  5689. * @property {Track} track
  5690. * A reference to track that was added.
  5691. */
  5692. track.labelchange_ = () => {
  5693. this.trigger({
  5694. track,
  5695. type: 'labelchange',
  5696. target: this
  5697. });
  5698. };
  5699. if (isEvented(track)) {
  5700. track.addEventListener('labelchange', track.labelchange_);
  5701. }
  5702. }
  5703. /**
  5704. * Remove a {@link Track} from the `TrackList`
  5705. *
  5706. * @param { import('./track').default } rtrack
  5707. * The audio, video, or text track to remove from the list.
  5708. *
  5709. * @fires TrackList#removetrack
  5710. */
  5711. removeTrack(rtrack) {
  5712. let track;
  5713. for (let i = 0, l = this.length; i < l; i++) {
  5714. if (this[i] === rtrack) {
  5715. track = this[i];
  5716. if (track.off) {
  5717. track.off();
  5718. }
  5719. this.tracks_.splice(i, 1);
  5720. break;
  5721. }
  5722. }
  5723. if (!track) {
  5724. return;
  5725. }
  5726. /**
  5727. * Triggered when a track is removed from track list.
  5728. *
  5729. * @event TrackList#removetrack
  5730. * @type {Event}
  5731. * @property {Track} track
  5732. * A reference to track that was removed.
  5733. */
  5734. this.trigger({
  5735. track,
  5736. type: 'removetrack',
  5737. target: this
  5738. });
  5739. }
  5740. /**
  5741. * Get a Track from the TrackList by a tracks id
  5742. *
  5743. * @param {string} id - the id of the track to get
  5744. * @method getTrackById
  5745. * @return { import('./track').default }
  5746. * @private
  5747. */
  5748. getTrackById(id) {
  5749. let result = null;
  5750. for (let i = 0, l = this.length; i < l; i++) {
  5751. const track = this[i];
  5752. if (track.id === id) {
  5753. result = track;
  5754. break;
  5755. }
  5756. }
  5757. return result;
  5758. }
  5759. }
  5760. /**
  5761. * Triggered when a different track is selected/enabled.
  5762. *
  5763. * @event TrackList#change
  5764. * @type {Event}
  5765. */
  5766. /**
  5767. * Events that can be called with on + eventName. See {@link EventHandler}.
  5768. *
  5769. * @property {Object} TrackList#allowedEvents_
  5770. * @private
  5771. */
  5772. TrackList.prototype.allowedEvents_ = {
  5773. change: 'change',
  5774. addtrack: 'addtrack',
  5775. removetrack: 'removetrack',
  5776. labelchange: 'labelchange'
  5777. };
  5778. // emulate attribute EventHandler support to allow for feature detection
  5779. for (const event in TrackList.prototype.allowedEvents_) {
  5780. TrackList.prototype['on' + event] = null;
  5781. }
  5782. /**
  5783. * @file audio-track-list.js
  5784. */
  5785. /**
  5786. * Anywhere we call this function we diverge from the spec
  5787. * as we only support one enabled audiotrack at a time
  5788. *
  5789. * @param {AudioTrackList} list
  5790. * list to work on
  5791. *
  5792. * @param { import('./audio-track').default } track
  5793. * The track to skip
  5794. *
  5795. * @private
  5796. */
  5797. const disableOthers$1 = function (list, track) {
  5798. for (let i = 0; i < list.length; i++) {
  5799. if (!Object.keys(list[i]).length || track.id === list[i].id) {
  5800. continue;
  5801. }
  5802. // another audio track is enabled, disable it
  5803. list[i].enabled = false;
  5804. }
  5805. };
  5806. /**
  5807. * The current list of {@link AudioTrack} for a media file.
  5808. *
  5809. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
  5810. * @extends TrackList
  5811. */
  5812. class AudioTrackList extends TrackList {
  5813. /**
  5814. * Create an instance of this class.
  5815. *
  5816. * @param {AudioTrack[]} [tracks=[]]
  5817. * A list of `AudioTrack` to instantiate the list with.
  5818. */
  5819. constructor(tracks = []) {
  5820. // make sure only 1 track is enabled
  5821. // sorted from last index to first index
  5822. for (let i = tracks.length - 1; i >= 0; i--) {
  5823. if (tracks[i].enabled) {
  5824. disableOthers$1(tracks, tracks[i]);
  5825. break;
  5826. }
  5827. }
  5828. super(tracks);
  5829. this.changing_ = false;
  5830. }
  5831. /**
  5832. * Add an {@link AudioTrack} to the `AudioTrackList`.
  5833. *
  5834. * @param { import('./audio-track').default } track
  5835. * The AudioTrack to add to the list
  5836. *
  5837. * @fires TrackList#addtrack
  5838. */
  5839. addTrack(track) {
  5840. if (track.enabled) {
  5841. disableOthers$1(this, track);
  5842. }
  5843. super.addTrack(track);
  5844. // native tracks don't have this
  5845. if (!track.addEventListener) {
  5846. return;
  5847. }
  5848. track.enabledChange_ = () => {
  5849. // when we are disabling other tracks (since we don't support
  5850. // more than one track at a time) we will set changing_
  5851. // to true so that we don't trigger additional change events
  5852. if (this.changing_) {
  5853. return;
  5854. }
  5855. this.changing_ = true;
  5856. disableOthers$1(this, track);
  5857. this.changing_ = false;
  5858. this.trigger('change');
  5859. };
  5860. /**
  5861. * @listens AudioTrack#enabledchange
  5862. * @fires TrackList#change
  5863. */
  5864. track.addEventListener('enabledchange', track.enabledChange_);
  5865. }
  5866. removeTrack(rtrack) {
  5867. super.removeTrack(rtrack);
  5868. if (rtrack.removeEventListener && rtrack.enabledChange_) {
  5869. rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
  5870. rtrack.enabledChange_ = null;
  5871. }
  5872. }
  5873. }
  5874. /**
  5875. * @file video-track-list.js
  5876. */
  5877. /**
  5878. * Un-select all other {@link VideoTrack}s that are selected.
  5879. *
  5880. * @param {VideoTrackList} list
  5881. * list to work on
  5882. *
  5883. * @param { import('./video-track').default } track
  5884. * The track to skip
  5885. *
  5886. * @private
  5887. */
  5888. const disableOthers = function (list, track) {
  5889. for (let i = 0; i < list.length; i++) {
  5890. if (!Object.keys(list[i]).length || track.id === list[i].id) {
  5891. continue;
  5892. }
  5893. // another video track is enabled, disable it
  5894. list[i].selected = false;
  5895. }
  5896. };
  5897. /**
  5898. * The current list of {@link VideoTrack} for a video.
  5899. *
  5900. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
  5901. * @extends TrackList
  5902. */
  5903. class VideoTrackList extends TrackList {
  5904. /**
  5905. * Create an instance of this class.
  5906. *
  5907. * @param {VideoTrack[]} [tracks=[]]
  5908. * A list of `VideoTrack` to instantiate the list with.
  5909. */
  5910. constructor(tracks = []) {
  5911. // make sure only 1 track is enabled
  5912. // sorted from last index to first index
  5913. for (let i = tracks.length - 1; i >= 0; i--) {
  5914. if (tracks[i].selected) {
  5915. disableOthers(tracks, tracks[i]);
  5916. break;
  5917. }
  5918. }
  5919. super(tracks);
  5920. this.changing_ = false;
  5921. /**
  5922. * @member {number} VideoTrackList#selectedIndex
  5923. * The current index of the selected {@link VideoTrack`}.
  5924. */
  5925. Object.defineProperty(this, 'selectedIndex', {
  5926. get() {
  5927. for (let i = 0; i < this.length; i++) {
  5928. if (this[i].selected) {
  5929. return i;
  5930. }
  5931. }
  5932. return -1;
  5933. },
  5934. set() {}
  5935. });
  5936. }
  5937. /**
  5938. * Add a {@link VideoTrack} to the `VideoTrackList`.
  5939. *
  5940. * @param { import('./video-track').default } track
  5941. * The VideoTrack to add to the list
  5942. *
  5943. * @fires TrackList#addtrack
  5944. */
  5945. addTrack(track) {
  5946. if (track.selected) {
  5947. disableOthers(this, track);
  5948. }
  5949. super.addTrack(track);
  5950. // native tracks don't have this
  5951. if (!track.addEventListener) {
  5952. return;
  5953. }
  5954. track.selectedChange_ = () => {
  5955. if (this.changing_) {
  5956. return;
  5957. }
  5958. this.changing_ = true;
  5959. disableOthers(this, track);
  5960. this.changing_ = false;
  5961. this.trigger('change');
  5962. };
  5963. /**
  5964. * @listens VideoTrack#selectedchange
  5965. * @fires TrackList#change
  5966. */
  5967. track.addEventListener('selectedchange', track.selectedChange_);
  5968. }
  5969. removeTrack(rtrack) {
  5970. super.removeTrack(rtrack);
  5971. if (rtrack.removeEventListener && rtrack.selectedChange_) {
  5972. rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
  5973. rtrack.selectedChange_ = null;
  5974. }
  5975. }
  5976. }
  5977. /**
  5978. * @file text-track-list.js
  5979. */
  5980. /**
  5981. * The current list of {@link TextTrack} for a media file.
  5982. *
  5983. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
  5984. * @extends TrackList
  5985. */
  5986. class TextTrackList extends TrackList {
  5987. /**
  5988. * Add a {@link TextTrack} to the `TextTrackList`
  5989. *
  5990. * @param { import('./text-track').default } track
  5991. * The text track to add to the list.
  5992. *
  5993. * @fires TrackList#addtrack
  5994. */
  5995. addTrack(track) {
  5996. super.addTrack(track);
  5997. if (!this.queueChange_) {
  5998. this.queueChange_ = () => this.queueTrigger('change');
  5999. }
  6000. if (!this.triggerSelectedlanguagechange) {
  6001. this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
  6002. }
  6003. /**
  6004. * @listens TextTrack#modechange
  6005. * @fires TrackList#change
  6006. */
  6007. track.addEventListener('modechange', this.queueChange_);
  6008. const nonLanguageTextTrackKind = ['metadata', 'chapters'];
  6009. if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
  6010. track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
  6011. }
  6012. }
  6013. removeTrack(rtrack) {
  6014. super.removeTrack(rtrack);
  6015. // manually remove the event handlers we added
  6016. if (rtrack.removeEventListener) {
  6017. if (this.queueChange_) {
  6018. rtrack.removeEventListener('modechange', this.queueChange_);
  6019. }
  6020. if (this.selectedlanguagechange_) {
  6021. rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
  6022. }
  6023. }
  6024. }
  6025. }
  6026. /**
  6027. * @file html-track-element-list.js
  6028. */
  6029. /**
  6030. * The current list of {@link HtmlTrackElement}s.
  6031. */
  6032. class HtmlTrackElementList {
  6033. /**
  6034. * Create an instance of this class.
  6035. *
  6036. * @param {HtmlTrackElement[]} [tracks=[]]
  6037. * A list of `HtmlTrackElement` to instantiate the list with.
  6038. */
  6039. constructor(trackElements = []) {
  6040. this.trackElements_ = [];
  6041. /**
  6042. * @memberof HtmlTrackElementList
  6043. * @member {number} length
  6044. * The current number of `Track`s in the this Trackist.
  6045. * @instance
  6046. */
  6047. Object.defineProperty(this, 'length', {
  6048. get() {
  6049. return this.trackElements_.length;
  6050. }
  6051. });
  6052. for (let i = 0, length = trackElements.length; i < length; i++) {
  6053. this.addTrackElement_(trackElements[i]);
  6054. }
  6055. }
  6056. /**
  6057. * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
  6058. *
  6059. * @param {HtmlTrackElement} trackElement
  6060. * The track element to add to the list.
  6061. *
  6062. * @private
  6063. */
  6064. addTrackElement_(trackElement) {
  6065. const index = this.trackElements_.length;
  6066. if (!('' + index in this)) {
  6067. Object.defineProperty(this, index, {
  6068. get() {
  6069. return this.trackElements_[index];
  6070. }
  6071. });
  6072. }
  6073. // Do not add duplicate elements
  6074. if (this.trackElements_.indexOf(trackElement) === -1) {
  6075. this.trackElements_.push(trackElement);
  6076. }
  6077. }
  6078. /**
  6079. * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
  6080. * {@link TextTrack}.
  6081. *
  6082. * @param {TextTrack} track
  6083. * The track associated with a track element.
  6084. *
  6085. * @return {HtmlTrackElement|undefined}
  6086. * The track element that was found or undefined.
  6087. *
  6088. * @private
  6089. */
  6090. getTrackElementByTrack_(track) {
  6091. let trackElement_;
  6092. for (let i = 0, length = this.trackElements_.length; i < length; i++) {
  6093. if (track === this.trackElements_[i].track) {
  6094. trackElement_ = this.trackElements_[i];
  6095. break;
  6096. }
  6097. }
  6098. return trackElement_;
  6099. }
  6100. /**
  6101. * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
  6102. *
  6103. * @param {HtmlTrackElement} trackElement
  6104. * The track element to remove from the list.
  6105. *
  6106. * @private
  6107. */
  6108. removeTrackElement_(trackElement) {
  6109. for (let i = 0, length = this.trackElements_.length; i < length; i++) {
  6110. if (trackElement === this.trackElements_[i]) {
  6111. if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
  6112. this.trackElements_[i].track.off();
  6113. }
  6114. if (typeof this.trackElements_[i].off === 'function') {
  6115. this.trackElements_[i].off();
  6116. }
  6117. this.trackElements_.splice(i, 1);
  6118. break;
  6119. }
  6120. }
  6121. }
  6122. }
  6123. /**
  6124. * @file text-track-cue-list.js
  6125. */
  6126. /**
  6127. * @typedef {Object} TextTrackCueList~TextTrackCue
  6128. *
  6129. * @property {string} id
  6130. * The unique id for this text track cue
  6131. *
  6132. * @property {number} startTime
  6133. * The start time for this text track cue
  6134. *
  6135. * @property {number} endTime
  6136. * The end time for this text track cue
  6137. *
  6138. * @property {boolean} pauseOnExit
  6139. * Pause when the end time is reached if true.
  6140. *
  6141. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
  6142. */
  6143. /**
  6144. * A List of TextTrackCues.
  6145. *
  6146. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
  6147. */
  6148. class TextTrackCueList {
  6149. /**
  6150. * Create an instance of this class..
  6151. *
  6152. * @param {Array} cues
  6153. * A list of cues to be initialized with
  6154. */
  6155. constructor(cues) {
  6156. TextTrackCueList.prototype.setCues_.call(this, cues);
  6157. /**
  6158. * @memberof TextTrackCueList
  6159. * @member {number} length
  6160. * The current number of `TextTrackCue`s in the TextTrackCueList.
  6161. * @instance
  6162. */
  6163. Object.defineProperty(this, 'length', {
  6164. get() {
  6165. return this.length_;
  6166. }
  6167. });
  6168. }
  6169. /**
  6170. * A setter for cues in this list. Creates getters
  6171. * an an index for the cues.
  6172. *
  6173. * @param {Array} cues
  6174. * An array of cues to set
  6175. *
  6176. * @private
  6177. */
  6178. setCues_(cues) {
  6179. const oldLength = this.length || 0;
  6180. let i = 0;
  6181. const l = cues.length;
  6182. this.cues_ = cues;
  6183. this.length_ = cues.length;
  6184. const defineProp = function (index) {
  6185. if (!('' + index in this)) {
  6186. Object.defineProperty(this, '' + index, {
  6187. get() {
  6188. return this.cues_[index];
  6189. }
  6190. });
  6191. }
  6192. };
  6193. if (oldLength < l) {
  6194. i = oldLength;
  6195. for (; i < l; i++) {
  6196. defineProp.call(this, i);
  6197. }
  6198. }
  6199. }
  6200. /**
  6201. * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
  6202. *
  6203. * @param {string} id
  6204. * The id of the cue that should be searched for.
  6205. *
  6206. * @return {TextTrackCueList~TextTrackCue|null}
  6207. * A single cue or null if none was found.
  6208. */
  6209. getCueById(id) {
  6210. let result = null;
  6211. for (let i = 0, l = this.length; i < l; i++) {
  6212. const cue = this[i];
  6213. if (cue.id === id) {
  6214. result = cue;
  6215. break;
  6216. }
  6217. }
  6218. return result;
  6219. }
  6220. }
  6221. /**
  6222. * @file track-kinds.js
  6223. */
  6224. /**
  6225. * All possible `VideoTrackKind`s
  6226. *
  6227. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
  6228. * @typedef VideoTrack~Kind
  6229. * @enum
  6230. */
  6231. const VideoTrackKind = {
  6232. alternative: 'alternative',
  6233. captions: 'captions',
  6234. main: 'main',
  6235. sign: 'sign',
  6236. subtitles: 'subtitles',
  6237. commentary: 'commentary'
  6238. };
  6239. /**
  6240. * All possible `AudioTrackKind`s
  6241. *
  6242. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
  6243. * @typedef AudioTrack~Kind
  6244. * @enum
  6245. */
  6246. const AudioTrackKind = {
  6247. 'alternative': 'alternative',
  6248. 'descriptions': 'descriptions',
  6249. 'main': 'main',
  6250. 'main-desc': 'main-desc',
  6251. 'translation': 'translation',
  6252. 'commentary': 'commentary'
  6253. };
  6254. /**
  6255. * All possible `TextTrackKind`s
  6256. *
  6257. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
  6258. * @typedef TextTrack~Kind
  6259. * @enum
  6260. */
  6261. const TextTrackKind = {
  6262. subtitles: 'subtitles',
  6263. captions: 'captions',
  6264. descriptions: 'descriptions',
  6265. chapters: 'chapters',
  6266. metadata: 'metadata'
  6267. };
  6268. /**
  6269. * All possible `TextTrackMode`s
  6270. *
  6271. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
  6272. * @typedef TextTrack~Mode
  6273. * @enum
  6274. */
  6275. const TextTrackMode = {
  6276. disabled: 'disabled',
  6277. hidden: 'hidden',
  6278. showing: 'showing'
  6279. };
  6280. /**
  6281. * @file track.js
  6282. */
  6283. /**
  6284. * A Track class that contains all of the common functionality for {@link AudioTrack},
  6285. * {@link VideoTrack}, and {@link TextTrack}.
  6286. *
  6287. * > Note: This class should not be used directly
  6288. *
  6289. * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
  6290. * @extends EventTarget
  6291. * @abstract
  6292. */
  6293. class Track extends EventTarget {
  6294. /**
  6295. * Create an instance of this class.
  6296. *
  6297. * @param {Object} [options={}]
  6298. * Object of option names and values
  6299. *
  6300. * @param {string} [options.kind='']
  6301. * A valid kind for the track type you are creating.
  6302. *
  6303. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  6304. * A unique id for this AudioTrack.
  6305. *
  6306. * @param {string} [options.label='']
  6307. * The menu label for this track.
  6308. *
  6309. * @param {string} [options.language='']
  6310. * A valid two character language code.
  6311. *
  6312. * @abstract
  6313. */
  6314. constructor(options = {}) {
  6315. super();
  6316. const trackProps = {
  6317. id: options.id || 'vjs_track_' + newGUID(),
  6318. kind: options.kind || '',
  6319. language: options.language || ''
  6320. };
  6321. let label = options.label || '';
  6322. /**
  6323. * @memberof Track
  6324. * @member {string} id
  6325. * The id of this track. Cannot be changed after creation.
  6326. * @instance
  6327. *
  6328. * @readonly
  6329. */
  6330. /**
  6331. * @memberof Track
  6332. * @member {string} kind
  6333. * The kind of track that this is. Cannot be changed after creation.
  6334. * @instance
  6335. *
  6336. * @readonly
  6337. */
  6338. /**
  6339. * @memberof Track
  6340. * @member {string} language
  6341. * The two letter language code for this track. Cannot be changed after
  6342. * creation.
  6343. * @instance
  6344. *
  6345. * @readonly
  6346. */
  6347. for (const key in trackProps) {
  6348. Object.defineProperty(this, key, {
  6349. get() {
  6350. return trackProps[key];
  6351. },
  6352. set() {}
  6353. });
  6354. }
  6355. /**
  6356. * @memberof Track
  6357. * @member {string} label
  6358. * The label of this track. Cannot be changed after creation.
  6359. * @instance
  6360. *
  6361. * @fires Track#labelchange
  6362. */
  6363. Object.defineProperty(this, 'label', {
  6364. get() {
  6365. return label;
  6366. },
  6367. set(newLabel) {
  6368. if (newLabel !== label) {
  6369. label = newLabel;
  6370. /**
  6371. * An event that fires when label changes on this track.
  6372. *
  6373. * > Note: This is not part of the spec!
  6374. *
  6375. * @event Track#labelchange
  6376. * @type {Event}
  6377. */
  6378. this.trigger('labelchange');
  6379. }
  6380. }
  6381. });
  6382. }
  6383. }
  6384. /**
  6385. * @file url.js
  6386. * @module url
  6387. */
  6388. /**
  6389. * @typedef {Object} url:URLObject
  6390. *
  6391. * @property {string} protocol
  6392. * The protocol of the url that was parsed.
  6393. *
  6394. * @property {string} hostname
  6395. * The hostname of the url that was parsed.
  6396. *
  6397. * @property {string} port
  6398. * The port of the url that was parsed.
  6399. *
  6400. * @property {string} pathname
  6401. * The pathname of the url that was parsed.
  6402. *
  6403. * @property {string} search
  6404. * The search query of the url that was parsed.
  6405. *
  6406. * @property {string} hash
  6407. * The hash of the url that was parsed.
  6408. *
  6409. * @property {string} host
  6410. * The host of the url that was parsed.
  6411. */
  6412. /**
  6413. * Resolve and parse the elements of a URL.
  6414. *
  6415. * @function
  6416. * @param {String} url
  6417. * The url to parse
  6418. *
  6419. * @return {url:URLObject}
  6420. * An object of url details
  6421. */
  6422. const parseUrl = function (url) {
  6423. // This entire method can be replace with URL once we are able to drop IE11
  6424. const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
  6425. // add the url to an anchor and let the browser parse the URL
  6426. const a = document__default["default"].createElement('a');
  6427. a.href = url;
  6428. // Copy the specific URL properties to a new object
  6429. // This is also needed for IE because the anchor loses its
  6430. // properties when it's removed from the dom
  6431. const details = {};
  6432. for (let i = 0; i < props.length; i++) {
  6433. details[props[i]] = a[props[i]];
  6434. }
  6435. // IE adds the port to the host property unlike everyone else. If
  6436. // a port identifier is added for standard ports, strip it.
  6437. if (details.protocol === 'http:') {
  6438. details.host = details.host.replace(/:80$/, '');
  6439. }
  6440. if (details.protocol === 'https:') {
  6441. details.host = details.host.replace(/:443$/, '');
  6442. }
  6443. if (!details.protocol) {
  6444. details.protocol = window__default["default"].location.protocol;
  6445. }
  6446. /* istanbul ignore if */
  6447. if (!details.host) {
  6448. details.host = window__default["default"].location.host;
  6449. }
  6450. return details;
  6451. };
  6452. /**
  6453. * Get absolute version of relative URL.
  6454. *
  6455. * @function
  6456. * @param {string} url
  6457. * URL to make absolute
  6458. *
  6459. * @return {string}
  6460. * Absolute URL
  6461. *
  6462. * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
  6463. */
  6464. const getAbsoluteURL = function (url) {
  6465. // Check if absolute URL
  6466. if (!url.match(/^https?:\/\//)) {
  6467. // Add the url to an anchor and let the browser parse it to convert to an absolute url
  6468. const a = document__default["default"].createElement('a');
  6469. a.href = url;
  6470. url = a.href;
  6471. }
  6472. return url;
  6473. };
  6474. /**
  6475. * Returns the extension of the passed file name. It will return an empty string
  6476. * if passed an invalid path.
  6477. *
  6478. * @function
  6479. * @param {string} path
  6480. * The fileName path like '/path/to/file.mp4'
  6481. *
  6482. * @return {string}
  6483. * The extension in lower case or an empty string if no
  6484. * extension could be found.
  6485. */
  6486. const getFileExtension = function (path) {
  6487. if (typeof path === 'string') {
  6488. const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
  6489. const pathParts = splitPathRe.exec(path);
  6490. if (pathParts) {
  6491. return pathParts.pop().toLowerCase();
  6492. }
  6493. }
  6494. return '';
  6495. };
  6496. /**
  6497. * Returns whether the url passed is a cross domain request or not.
  6498. *
  6499. * @function
  6500. * @param {string} url
  6501. * The url to check.
  6502. *
  6503. * @param {Object} [winLoc]
  6504. * the domain to check the url against, defaults to window.location
  6505. *
  6506. * @param {string} [winLoc.protocol]
  6507. * The window location protocol defaults to window.location.protocol
  6508. *
  6509. * @param {string} [winLoc.host]
  6510. * The window location host defaults to window.location.host
  6511. *
  6512. * @return {boolean}
  6513. * Whether it is a cross domain request or not.
  6514. */
  6515. const isCrossOrigin = function (url, winLoc = window__default["default"].location) {
  6516. const urlInfo = parseUrl(url);
  6517. // IE8 protocol relative urls will return ':' for protocol
  6518. const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
  6519. // Check if url is for another domain/origin
  6520. // IE8 doesn't know location.origin, so we won't rely on it here
  6521. const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
  6522. return crossOrigin;
  6523. };
  6524. var Url = /*#__PURE__*/Object.freeze({
  6525. __proto__: null,
  6526. parseUrl: parseUrl,
  6527. getAbsoluteURL: getAbsoluteURL,
  6528. getFileExtension: getFileExtension,
  6529. isCrossOrigin: isCrossOrigin
  6530. });
  6531. /**
  6532. * @file text-track.js
  6533. */
  6534. /**
  6535. * Takes a webvtt file contents and parses it into cues
  6536. *
  6537. * @param {string} srcContent
  6538. * webVTT file contents
  6539. *
  6540. * @param {TextTrack} track
  6541. * TextTrack to add cues to. Cues come from the srcContent.
  6542. *
  6543. * @private
  6544. */
  6545. const parseCues = function (srcContent, track) {
  6546. const parser = new window__default["default"].WebVTT.Parser(window__default["default"], window__default["default"].vttjs, window__default["default"].WebVTT.StringDecoder());
  6547. const errors = [];
  6548. parser.oncue = function (cue) {
  6549. track.addCue(cue);
  6550. };
  6551. parser.onparsingerror = function (error) {
  6552. errors.push(error);
  6553. };
  6554. parser.onflush = function () {
  6555. track.trigger({
  6556. type: 'loadeddata',
  6557. target: track
  6558. });
  6559. };
  6560. parser.parse(srcContent);
  6561. if (errors.length > 0) {
  6562. if (window__default["default"].console && window__default["default"].console.groupCollapsed) {
  6563. window__default["default"].console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
  6564. }
  6565. errors.forEach(error => log.error(error));
  6566. if (window__default["default"].console && window__default["default"].console.groupEnd) {
  6567. window__default["default"].console.groupEnd();
  6568. }
  6569. }
  6570. parser.flush();
  6571. };
  6572. /**
  6573. * Load a `TextTrack` from a specified url.
  6574. *
  6575. * @param {string} src
  6576. * Url to load track from.
  6577. *
  6578. * @param {TextTrack} track
  6579. * Track to add cues to. Comes from the content at the end of `url`.
  6580. *
  6581. * @private
  6582. */
  6583. const loadTrack = function (src, track) {
  6584. const opts = {
  6585. uri: src
  6586. };
  6587. const crossOrigin = isCrossOrigin(src);
  6588. if (crossOrigin) {
  6589. opts.cors = crossOrigin;
  6590. }
  6591. const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
  6592. if (withCredentials) {
  6593. opts.withCredentials = withCredentials;
  6594. }
  6595. XHR__default["default"](opts, bind_(this, function (err, response, responseBody) {
  6596. if (err) {
  6597. return log.error(err, response);
  6598. }
  6599. track.loaded_ = true;
  6600. // Make sure that vttjs has loaded, otherwise, wait till it finished loading
  6601. // NOTE: this is only used for the alt/video.novtt.js build
  6602. if (typeof window__default["default"].WebVTT !== 'function') {
  6603. if (track.tech_) {
  6604. // to prevent use before define eslint error, we define loadHandler
  6605. // as a let here
  6606. track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
  6607. if (event.type === 'vttjserror') {
  6608. log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
  6609. return;
  6610. }
  6611. return parseCues(responseBody, track);
  6612. });
  6613. }
  6614. } else {
  6615. parseCues(responseBody, track);
  6616. }
  6617. }));
  6618. };
  6619. /**
  6620. * A representation of a single `TextTrack`.
  6621. *
  6622. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
  6623. * @extends Track
  6624. */
  6625. class TextTrack extends Track {
  6626. /**
  6627. * Create an instance of this class.
  6628. *
  6629. * @param {Object} options={}
  6630. * Object of option names and values
  6631. *
  6632. * @param { import('../tech/tech').default } options.tech
  6633. * A reference to the tech that owns this TextTrack.
  6634. *
  6635. * @param {TextTrack~Kind} [options.kind='subtitles']
  6636. * A valid text track kind.
  6637. *
  6638. * @param {TextTrack~Mode} [options.mode='disabled']
  6639. * A valid text track mode.
  6640. *
  6641. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  6642. * A unique id for this TextTrack.
  6643. *
  6644. * @param {string} [options.label='']
  6645. * The menu label for this track.
  6646. *
  6647. * @param {string} [options.language='']
  6648. * A valid two character language code.
  6649. *
  6650. * @param {string} [options.srclang='']
  6651. * A valid two character language code. An alternative, but deprioritized
  6652. * version of `options.language`
  6653. *
  6654. * @param {string} [options.src]
  6655. * A url to TextTrack cues.
  6656. *
  6657. * @param {boolean} [options.default]
  6658. * If this track should default to on or off.
  6659. */
  6660. constructor(options = {}) {
  6661. if (!options.tech) {
  6662. throw new Error('A tech was not provided.');
  6663. }
  6664. const settings = merge(options, {
  6665. kind: TextTrackKind[options.kind] || 'subtitles',
  6666. language: options.language || options.srclang || ''
  6667. });
  6668. let mode = TextTrackMode[settings.mode] || 'disabled';
  6669. const default_ = settings.default;
  6670. if (settings.kind === 'metadata' || settings.kind === 'chapters') {
  6671. mode = 'hidden';
  6672. }
  6673. super(settings);
  6674. this.tech_ = settings.tech;
  6675. this.cues_ = [];
  6676. this.activeCues_ = [];
  6677. this.preload_ = this.tech_.preloadTextTracks !== false;
  6678. const cues = new TextTrackCueList(this.cues_);
  6679. const activeCues = new TextTrackCueList(this.activeCues_);
  6680. let changed = false;
  6681. this.timeupdateHandler = bind_(this, function (event = {}) {
  6682. if (this.tech_.isDisposed()) {
  6683. return;
  6684. }
  6685. if (!this.tech_.isReady_) {
  6686. if (event.type !== 'timeupdate') {
  6687. this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
  6688. }
  6689. return;
  6690. }
  6691. // Accessing this.activeCues for the side-effects of updating itself
  6692. // due to its nature as a getter function. Do not remove or cues will
  6693. // stop updating!
  6694. // Use the setter to prevent deletion from uglify (pure_getters rule)
  6695. this.activeCues = this.activeCues;
  6696. if (changed) {
  6697. this.trigger('cuechange');
  6698. changed = false;
  6699. }
  6700. if (event.type !== 'timeupdate') {
  6701. this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
  6702. }
  6703. });
  6704. const disposeHandler = () => {
  6705. this.stopTracking();
  6706. };
  6707. this.tech_.one('dispose', disposeHandler);
  6708. if (mode !== 'disabled') {
  6709. this.startTracking();
  6710. }
  6711. Object.defineProperties(this, {
  6712. /**
  6713. * @memberof TextTrack
  6714. * @member {boolean} default
  6715. * If this track was set to be on or off by default. Cannot be changed after
  6716. * creation.
  6717. * @instance
  6718. *
  6719. * @readonly
  6720. */
  6721. default: {
  6722. get() {
  6723. return default_;
  6724. },
  6725. set() {}
  6726. },
  6727. /**
  6728. * @memberof TextTrack
  6729. * @member {string} mode
  6730. * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
  6731. * not be set if setting to an invalid mode.
  6732. * @instance
  6733. *
  6734. * @fires TextTrack#modechange
  6735. */
  6736. mode: {
  6737. get() {
  6738. return mode;
  6739. },
  6740. set(newMode) {
  6741. if (!TextTrackMode[newMode]) {
  6742. return;
  6743. }
  6744. if (mode === newMode) {
  6745. return;
  6746. }
  6747. mode = newMode;
  6748. if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
  6749. // On-demand load.
  6750. loadTrack(this.src, this);
  6751. }
  6752. this.stopTracking();
  6753. if (mode !== 'disabled') {
  6754. this.startTracking();
  6755. }
  6756. /**
  6757. * An event that fires when mode changes on this track. This allows
  6758. * the TextTrackList that holds this track to act accordingly.
  6759. *
  6760. * > Note: This is not part of the spec!
  6761. *
  6762. * @event TextTrack#modechange
  6763. * @type {Event}
  6764. */
  6765. this.trigger('modechange');
  6766. }
  6767. },
  6768. /**
  6769. * @memberof TextTrack
  6770. * @member {TextTrackCueList} cues
  6771. * The text track cue list for this TextTrack.
  6772. * @instance
  6773. */
  6774. cues: {
  6775. get() {
  6776. if (!this.loaded_) {
  6777. return null;
  6778. }
  6779. return cues;
  6780. },
  6781. set() {}
  6782. },
  6783. /**
  6784. * @memberof TextTrack
  6785. * @member {TextTrackCueList} activeCues
  6786. * The list text track cues that are currently active for this TextTrack.
  6787. * @instance
  6788. */
  6789. activeCues: {
  6790. get() {
  6791. if (!this.loaded_) {
  6792. return null;
  6793. }
  6794. // nothing to do
  6795. if (this.cues.length === 0) {
  6796. return activeCues;
  6797. }
  6798. const ct = this.tech_.currentTime();
  6799. const active = [];
  6800. for (let i = 0, l = this.cues.length; i < l; i++) {
  6801. const cue = this.cues[i];
  6802. if (cue.startTime <= ct && cue.endTime >= ct) {
  6803. active.push(cue);
  6804. }
  6805. }
  6806. changed = false;
  6807. if (active.length !== this.activeCues_.length) {
  6808. changed = true;
  6809. } else {
  6810. for (let i = 0; i < active.length; i++) {
  6811. if (this.activeCues_.indexOf(active[i]) === -1) {
  6812. changed = true;
  6813. }
  6814. }
  6815. }
  6816. this.activeCues_ = active;
  6817. activeCues.setCues_(this.activeCues_);
  6818. return activeCues;
  6819. },
  6820. // /!\ Keep this setter empty (see the timeupdate handler above)
  6821. set() {}
  6822. }
  6823. });
  6824. if (settings.src) {
  6825. this.src = settings.src;
  6826. if (!this.preload_) {
  6827. // Tracks will load on-demand.
  6828. // Act like we're loaded for other purposes.
  6829. this.loaded_ = true;
  6830. }
  6831. if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
  6832. loadTrack(this.src, this);
  6833. }
  6834. } else {
  6835. this.loaded_ = true;
  6836. }
  6837. }
  6838. startTracking() {
  6839. // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
  6840. this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
  6841. // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
  6842. this.tech_.on('timeupdate', this.timeupdateHandler);
  6843. }
  6844. stopTracking() {
  6845. if (this.rvf_) {
  6846. this.tech_.cancelVideoFrameCallback(this.rvf_);
  6847. this.rvf_ = undefined;
  6848. }
  6849. this.tech_.off('timeupdate', this.timeupdateHandler);
  6850. }
  6851. /**
  6852. * Add a cue to the internal list of cues.
  6853. *
  6854. * @param {TextTrack~Cue} cue
  6855. * The cue to add to our internal list
  6856. */
  6857. addCue(originalCue) {
  6858. let cue = originalCue;
  6859. if (window__default["default"].vttjs && !(originalCue instanceof window__default["default"].vttjs.VTTCue)) {
  6860. cue = new window__default["default"].vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
  6861. for (const prop in originalCue) {
  6862. if (!(prop in cue)) {
  6863. cue[prop] = originalCue[prop];
  6864. }
  6865. }
  6866. // make sure that `id` is copied over
  6867. cue.id = originalCue.id;
  6868. cue.originalCue_ = originalCue;
  6869. }
  6870. const tracks = this.tech_.textTracks();
  6871. for (let i = 0; i < tracks.length; i++) {
  6872. if (tracks[i] !== this) {
  6873. tracks[i].removeCue(cue);
  6874. }
  6875. }
  6876. this.cues_.push(cue);
  6877. this.cues.setCues_(this.cues_);
  6878. }
  6879. /**
  6880. * Remove a cue from our internal list
  6881. *
  6882. * @param {TextTrack~Cue} removeCue
  6883. * The cue to remove from our internal list
  6884. */
  6885. removeCue(removeCue) {
  6886. let i = this.cues_.length;
  6887. while (i--) {
  6888. const cue = this.cues_[i];
  6889. if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
  6890. this.cues_.splice(i, 1);
  6891. this.cues.setCues_(this.cues_);
  6892. break;
  6893. }
  6894. }
  6895. }
  6896. }
  6897. /**
  6898. * cuechange - One or more cues in the track have become active or stopped being active.
  6899. */
  6900. TextTrack.prototype.allowedEvents_ = {
  6901. cuechange: 'cuechange'
  6902. };
  6903. /**
  6904. * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
  6905. * only one `AudioTrack` in the list will be enabled at a time.
  6906. *
  6907. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
  6908. * @extends Track
  6909. */
  6910. class AudioTrack extends Track {
  6911. /**
  6912. * Create an instance of this class.
  6913. *
  6914. * @param {Object} [options={}]
  6915. * Object of option names and values
  6916. *
  6917. * @param {AudioTrack~Kind} [options.kind='']
  6918. * A valid audio track kind
  6919. *
  6920. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  6921. * A unique id for this AudioTrack.
  6922. *
  6923. * @param {string} [options.label='']
  6924. * The menu label for this track.
  6925. *
  6926. * @param {string} [options.language='']
  6927. * A valid two character language code.
  6928. *
  6929. * @param {boolean} [options.enabled]
  6930. * If this track is the one that is currently playing. If this track is part of
  6931. * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
  6932. */
  6933. constructor(options = {}) {
  6934. const settings = merge(options, {
  6935. kind: AudioTrackKind[options.kind] || ''
  6936. });
  6937. super(settings);
  6938. let enabled = false;
  6939. /**
  6940. * @memberof AudioTrack
  6941. * @member {boolean} enabled
  6942. * If this `AudioTrack` is enabled or not. When setting this will
  6943. * fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
  6944. * @instance
  6945. *
  6946. * @fires VideoTrack#selectedchange
  6947. */
  6948. Object.defineProperty(this, 'enabled', {
  6949. get() {
  6950. return enabled;
  6951. },
  6952. set(newEnabled) {
  6953. // an invalid or unchanged value
  6954. if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
  6955. return;
  6956. }
  6957. enabled = newEnabled;
  6958. /**
  6959. * An event that fires when enabled changes on this track. This allows
  6960. * the AudioTrackList that holds this track to act accordingly.
  6961. *
  6962. * > Note: This is not part of the spec! Native tracks will do
  6963. * this internally without an event.
  6964. *
  6965. * @event AudioTrack#enabledchange
  6966. * @type {Event}
  6967. */
  6968. this.trigger('enabledchange');
  6969. }
  6970. });
  6971. // if the user sets this track to selected then
  6972. // set selected to that true value otherwise
  6973. // we keep it false
  6974. if (settings.enabled) {
  6975. this.enabled = settings.enabled;
  6976. }
  6977. this.loaded_ = true;
  6978. }
  6979. }
  6980. /**
  6981. * A representation of a single `VideoTrack`.
  6982. *
  6983. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
  6984. * @extends Track
  6985. */
  6986. class VideoTrack extends Track {
  6987. /**
  6988. * Create an instance of this class.
  6989. *
  6990. * @param {Object} [options={}]
  6991. * Object of option names and values
  6992. *
  6993. * @param {string} [options.kind='']
  6994. * A valid {@link VideoTrack~Kind}
  6995. *
  6996. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  6997. * A unique id for this AudioTrack.
  6998. *
  6999. * @param {string} [options.label='']
  7000. * The menu label for this track.
  7001. *
  7002. * @param {string} [options.language='']
  7003. * A valid two character language code.
  7004. *
  7005. * @param {boolean} [options.selected]
  7006. * If this track is the one that is currently playing.
  7007. */
  7008. constructor(options = {}) {
  7009. const settings = merge(options, {
  7010. kind: VideoTrackKind[options.kind] || ''
  7011. });
  7012. super(settings);
  7013. let selected = false;
  7014. /**
  7015. * @memberof VideoTrack
  7016. * @member {boolean} selected
  7017. * If this `VideoTrack` is selected or not. When setting this will
  7018. * fire {@link VideoTrack#selectedchange} if the state of selected changed.
  7019. * @instance
  7020. *
  7021. * @fires VideoTrack#selectedchange
  7022. */
  7023. Object.defineProperty(this, 'selected', {
  7024. get() {
  7025. return selected;
  7026. },
  7027. set(newSelected) {
  7028. // an invalid or unchanged value
  7029. if (typeof newSelected !== 'boolean' || newSelected === selected) {
  7030. return;
  7031. }
  7032. selected = newSelected;
  7033. /**
  7034. * An event that fires when selected changes on this track. This allows
  7035. * the VideoTrackList that holds this track to act accordingly.
  7036. *
  7037. * > Note: This is not part of the spec! Native tracks will do
  7038. * this internally without an event.
  7039. *
  7040. * @event VideoTrack#selectedchange
  7041. * @type {Event}
  7042. */
  7043. this.trigger('selectedchange');
  7044. }
  7045. });
  7046. // if the user sets this track to selected then
  7047. // set selected to that true value otherwise
  7048. // we keep it false
  7049. if (settings.selected) {
  7050. this.selected = settings.selected;
  7051. }
  7052. }
  7053. }
  7054. /**
  7055. * @file html-track-element.js
  7056. */
  7057. /**
  7058. * A single track represented in the DOM.
  7059. *
  7060. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
  7061. * @extends EventTarget
  7062. */
  7063. class HTMLTrackElement extends EventTarget {
  7064. /**
  7065. * Create an instance of this class.
  7066. *
  7067. * @param {Object} options={}
  7068. * Object of option names and values
  7069. *
  7070. * @param { import('../tech/tech').default } options.tech
  7071. * A reference to the tech that owns this HTMLTrackElement.
  7072. *
  7073. * @param {TextTrack~Kind} [options.kind='subtitles']
  7074. * A valid text track kind.
  7075. *
  7076. * @param {TextTrack~Mode} [options.mode='disabled']
  7077. * A valid text track mode.
  7078. *
  7079. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  7080. * A unique id for this TextTrack.
  7081. *
  7082. * @param {string} [options.label='']
  7083. * The menu label for this track.
  7084. *
  7085. * @param {string} [options.language='']
  7086. * A valid two character language code.
  7087. *
  7088. * @param {string} [options.srclang='']
  7089. * A valid two character language code. An alternative, but deprioritized
  7090. * version of `options.language`
  7091. *
  7092. * @param {string} [options.src]
  7093. * A url to TextTrack cues.
  7094. *
  7095. * @param {boolean} [options.default]
  7096. * If this track should default to on or off.
  7097. */
  7098. constructor(options = {}) {
  7099. super();
  7100. let readyState;
  7101. const track = new TextTrack(options);
  7102. this.kind = track.kind;
  7103. this.src = track.src;
  7104. this.srclang = track.language;
  7105. this.label = track.label;
  7106. this.default = track.default;
  7107. Object.defineProperties(this, {
  7108. /**
  7109. * @memberof HTMLTrackElement
  7110. * @member {HTMLTrackElement~ReadyState} readyState
  7111. * The current ready state of the track element.
  7112. * @instance
  7113. */
  7114. readyState: {
  7115. get() {
  7116. return readyState;
  7117. }
  7118. },
  7119. /**
  7120. * @memberof HTMLTrackElement
  7121. * @member {TextTrack} track
  7122. * The underlying TextTrack object.
  7123. * @instance
  7124. *
  7125. */
  7126. track: {
  7127. get() {
  7128. return track;
  7129. }
  7130. }
  7131. });
  7132. readyState = HTMLTrackElement.NONE;
  7133. /**
  7134. * @listens TextTrack#loadeddata
  7135. * @fires HTMLTrackElement#load
  7136. */
  7137. track.addEventListener('loadeddata', () => {
  7138. readyState = HTMLTrackElement.LOADED;
  7139. this.trigger({
  7140. type: 'load',
  7141. target: this
  7142. });
  7143. });
  7144. }
  7145. }
  7146. HTMLTrackElement.prototype.allowedEvents_ = {
  7147. load: 'load'
  7148. };
  7149. /**
  7150. * The text track not loaded state.
  7151. *
  7152. * @type {number}
  7153. * @static
  7154. */
  7155. HTMLTrackElement.NONE = 0;
  7156. /**
  7157. * The text track loading state.
  7158. *
  7159. * @type {number}
  7160. * @static
  7161. */
  7162. HTMLTrackElement.LOADING = 1;
  7163. /**
  7164. * The text track loaded state.
  7165. *
  7166. * @type {number}
  7167. * @static
  7168. */
  7169. HTMLTrackElement.LOADED = 2;
  7170. /**
  7171. * The text track failed to load state.
  7172. *
  7173. * @type {number}
  7174. * @static
  7175. */
  7176. HTMLTrackElement.ERROR = 3;
  7177. /*
  7178. * This file contains all track properties that are used in
  7179. * player.js, tech.js, html5.js and possibly other techs in the future.
  7180. */
  7181. const NORMAL = {
  7182. audio: {
  7183. ListClass: AudioTrackList,
  7184. TrackClass: AudioTrack,
  7185. capitalName: 'Audio'
  7186. },
  7187. video: {
  7188. ListClass: VideoTrackList,
  7189. TrackClass: VideoTrack,
  7190. capitalName: 'Video'
  7191. },
  7192. text: {
  7193. ListClass: TextTrackList,
  7194. TrackClass: TextTrack,
  7195. capitalName: 'Text'
  7196. }
  7197. };
  7198. Object.keys(NORMAL).forEach(function (type) {
  7199. NORMAL[type].getterName = `${type}Tracks`;
  7200. NORMAL[type].privateName = `${type}Tracks_`;
  7201. });
  7202. const REMOTE = {
  7203. remoteText: {
  7204. ListClass: TextTrackList,
  7205. TrackClass: TextTrack,
  7206. capitalName: 'RemoteText',
  7207. getterName: 'remoteTextTracks',
  7208. privateName: 'remoteTextTracks_'
  7209. },
  7210. remoteTextEl: {
  7211. ListClass: HtmlTrackElementList,
  7212. TrackClass: HTMLTrackElement,
  7213. capitalName: 'RemoteTextTrackEls',
  7214. getterName: 'remoteTextTrackEls',
  7215. privateName: 'remoteTextTrackEls_'
  7216. }
  7217. };
  7218. const ALL = Object.assign({}, NORMAL, REMOTE);
  7219. REMOTE.names = Object.keys(REMOTE);
  7220. NORMAL.names = Object.keys(NORMAL);
  7221. ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
  7222. /**
  7223. * @file tech.js
  7224. */
  7225. /**
  7226. * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
  7227. * that just contains the src url alone.
  7228. * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
  7229. * `var SourceString = 'http://example.com/some-video.mp4';`
  7230. *
  7231. * @typedef {Object|string} Tech~SourceObject
  7232. *
  7233. * @property {string} src
  7234. * The url to the source
  7235. *
  7236. * @property {string} type
  7237. * The mime type of the source
  7238. */
  7239. /**
  7240. * A function used by {@link Tech} to create a new {@link TextTrack}.
  7241. *
  7242. * @private
  7243. *
  7244. * @param {Tech} self
  7245. * An instance of the Tech class.
  7246. *
  7247. * @param {string} kind
  7248. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
  7249. *
  7250. * @param {string} [label]
  7251. * Label to identify the text track
  7252. *
  7253. * @param {string} [language]
  7254. * Two letter language abbreviation
  7255. *
  7256. * @param {Object} [options={}]
  7257. * An object with additional text track options
  7258. *
  7259. * @return {TextTrack}
  7260. * The text track that was created.
  7261. */
  7262. function createTrackHelper(self, kind, label, language, options = {}) {
  7263. const tracks = self.textTracks();
  7264. options.kind = kind;
  7265. if (label) {
  7266. options.label = label;
  7267. }
  7268. if (language) {
  7269. options.language = language;
  7270. }
  7271. options.tech = self;
  7272. const track = new ALL.text.TrackClass(options);
  7273. tracks.addTrack(track);
  7274. return track;
  7275. }
  7276. /**
  7277. * This is the base class for media playback technology controllers, such as
  7278. * {@link HTML5}
  7279. *
  7280. * @extends Component
  7281. */
  7282. class Tech extends Component {
  7283. /**
  7284. * Create an instance of this Tech.
  7285. *
  7286. * @param {Object} [options]
  7287. * The key/value store of player options.
  7288. *
  7289. * @param {Function} [ready]
  7290. * Callback function to call when the `HTML5` Tech is ready.
  7291. */
  7292. constructor(options = {}, ready = function () {}) {
  7293. // we don't want the tech to report user activity automatically.
  7294. // This is done manually in addControlsListeners
  7295. options.reportTouchActivity = false;
  7296. super(null, options, ready);
  7297. this.onDurationChange_ = e => this.onDurationChange(e);
  7298. this.trackProgress_ = e => this.trackProgress(e);
  7299. this.trackCurrentTime_ = e => this.trackCurrentTime(e);
  7300. this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
  7301. this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
  7302. this.queuedHanders_ = new Set();
  7303. // keep track of whether the current source has played at all to
  7304. // implement a very limited played()
  7305. this.hasStarted_ = false;
  7306. this.on('playing', function () {
  7307. this.hasStarted_ = true;
  7308. });
  7309. this.on('loadstart', function () {
  7310. this.hasStarted_ = false;
  7311. });
  7312. ALL.names.forEach(name => {
  7313. const props = ALL[name];
  7314. if (options && options[props.getterName]) {
  7315. this[props.privateName] = options[props.getterName];
  7316. }
  7317. });
  7318. // Manually track progress in cases where the browser/tech doesn't report it.
  7319. if (!this.featuresProgressEvents) {
  7320. this.manualProgressOn();
  7321. }
  7322. // Manually track timeupdates in cases where the browser/tech doesn't report it.
  7323. if (!this.featuresTimeupdateEvents) {
  7324. this.manualTimeUpdatesOn();
  7325. }
  7326. ['Text', 'Audio', 'Video'].forEach(track => {
  7327. if (options[`native${track}Tracks`] === false) {
  7328. this[`featuresNative${track}Tracks`] = false;
  7329. }
  7330. });
  7331. if (options.nativeCaptions === false || options.nativeTextTracks === false) {
  7332. this.featuresNativeTextTracks = false;
  7333. } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
  7334. this.featuresNativeTextTracks = true;
  7335. }
  7336. if (!this.featuresNativeTextTracks) {
  7337. this.emulateTextTracks();
  7338. }
  7339. this.preloadTextTracks = options.preloadTextTracks !== false;
  7340. this.autoRemoteTextTracks_ = new ALL.text.ListClass();
  7341. this.initTrackListeners();
  7342. // Turn on component tap events only if not using native controls
  7343. if (!options.nativeControlsForTouch) {
  7344. this.emitTapEvents();
  7345. }
  7346. if (this.constructor) {
  7347. this.name_ = this.constructor.name || 'Unknown Tech';
  7348. }
  7349. }
  7350. /**
  7351. * A special function to trigger source set in a way that will allow player
  7352. * to re-trigger if the player or tech are not ready yet.
  7353. *
  7354. * @fires Tech#sourceset
  7355. * @param {string} src The source string at the time of the source changing.
  7356. */
  7357. triggerSourceset(src) {
  7358. if (!this.isReady_) {
  7359. // on initial ready we have to trigger source set
  7360. // 1ms after ready so that player can watch for it.
  7361. this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
  7362. }
  7363. /**
  7364. * Fired when the source is set on the tech causing the media element
  7365. * to reload.
  7366. *
  7367. * @see {@link Player#event:sourceset}
  7368. * @event Tech#sourceset
  7369. * @type {Event}
  7370. */
  7371. this.trigger({
  7372. src,
  7373. type: 'sourceset'
  7374. });
  7375. }
  7376. /* Fallbacks for unsupported event types
  7377. ================================================================================ */
  7378. /**
  7379. * Polyfill the `progress` event for browsers that don't support it natively.
  7380. *
  7381. * @see {@link Tech#trackProgress}
  7382. */
  7383. manualProgressOn() {
  7384. this.on('durationchange', this.onDurationChange_);
  7385. this.manualProgress = true;
  7386. // Trigger progress watching when a source begins loading
  7387. this.one('ready', this.trackProgress_);
  7388. }
  7389. /**
  7390. * Turn off the polyfill for `progress` events that was created in
  7391. * {@link Tech#manualProgressOn}
  7392. */
  7393. manualProgressOff() {
  7394. this.manualProgress = false;
  7395. this.stopTrackingProgress();
  7396. this.off('durationchange', this.onDurationChange_);
  7397. }
  7398. /**
  7399. * This is used to trigger a `progress` event when the buffered percent changes. It
  7400. * sets an interval function that will be called every 500 milliseconds to check if the
  7401. * buffer end percent has changed.
  7402. *
  7403. * > This function is called by {@link Tech#manualProgressOn}
  7404. *
  7405. * @param {Event} event
  7406. * The `ready` event that caused this to run.
  7407. *
  7408. * @listens Tech#ready
  7409. * @fires Tech#progress
  7410. */
  7411. trackProgress(event) {
  7412. this.stopTrackingProgress();
  7413. this.progressInterval = this.setInterval(bind_(this, function () {
  7414. // Don't trigger unless buffered amount is greater than last time
  7415. const numBufferedPercent = this.bufferedPercent();
  7416. if (this.bufferedPercent_ !== numBufferedPercent) {
  7417. /**
  7418. * See {@link Player#progress}
  7419. *
  7420. * @event Tech#progress
  7421. * @type {Event}
  7422. */
  7423. this.trigger('progress');
  7424. }
  7425. this.bufferedPercent_ = numBufferedPercent;
  7426. if (numBufferedPercent === 1) {
  7427. this.stopTrackingProgress();
  7428. }
  7429. }), 500);
  7430. }
  7431. /**
  7432. * Update our internal duration on a `durationchange` event by calling
  7433. * {@link Tech#duration}.
  7434. *
  7435. * @param {Event} event
  7436. * The `durationchange` event that caused this to run.
  7437. *
  7438. * @listens Tech#durationchange
  7439. */
  7440. onDurationChange(event) {
  7441. this.duration_ = this.duration();
  7442. }
  7443. /**
  7444. * Get and create a `TimeRange` object for buffering.
  7445. *
  7446. * @return { import('../utils/time').TimeRange }
  7447. * The time range object that was created.
  7448. */
  7449. buffered() {
  7450. return createTimeRanges(0, 0);
  7451. }
  7452. /**
  7453. * Get the percentage of the current video that is currently buffered.
  7454. *
  7455. * @return {number}
  7456. * A number from 0 to 1 that represents the decimal percentage of the
  7457. * video that is buffered.
  7458. *
  7459. */
  7460. bufferedPercent() {
  7461. return bufferedPercent(this.buffered(), this.duration_);
  7462. }
  7463. /**
  7464. * Turn off the polyfill for `progress` events that was created in
  7465. * {@link Tech#manualProgressOn}
  7466. * Stop manually tracking progress events by clearing the interval that was set in
  7467. * {@link Tech#trackProgress}.
  7468. */
  7469. stopTrackingProgress() {
  7470. this.clearInterval(this.progressInterval);
  7471. }
  7472. /**
  7473. * Polyfill the `timeupdate` event for browsers that don't support it.
  7474. *
  7475. * @see {@link Tech#trackCurrentTime}
  7476. */
  7477. manualTimeUpdatesOn() {
  7478. this.manualTimeUpdates = true;
  7479. this.on('play', this.trackCurrentTime_);
  7480. this.on('pause', this.stopTrackingCurrentTime_);
  7481. }
  7482. /**
  7483. * Turn off the polyfill for `timeupdate` events that was created in
  7484. * {@link Tech#manualTimeUpdatesOn}
  7485. */
  7486. manualTimeUpdatesOff() {
  7487. this.manualTimeUpdates = false;
  7488. this.stopTrackingCurrentTime();
  7489. this.off('play', this.trackCurrentTime_);
  7490. this.off('pause', this.stopTrackingCurrentTime_);
  7491. }
  7492. /**
  7493. * Sets up an interval function to track current time and trigger `timeupdate` every
  7494. * 250 milliseconds.
  7495. *
  7496. * @listens Tech#play
  7497. * @triggers Tech#timeupdate
  7498. */
  7499. trackCurrentTime() {
  7500. if (this.currentTimeInterval) {
  7501. this.stopTrackingCurrentTime();
  7502. }
  7503. this.currentTimeInterval = this.setInterval(function () {
  7504. /**
  7505. * Triggered at an interval of 250ms to indicated that time is passing in the video.
  7506. *
  7507. * @event Tech#timeupdate
  7508. * @type {Event}
  7509. */
  7510. this.trigger({
  7511. type: 'timeupdate',
  7512. target: this,
  7513. manuallyTriggered: true
  7514. });
  7515. // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
  7516. }, 250);
  7517. }
  7518. /**
  7519. * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
  7520. * `timeupdate` event is no longer triggered.
  7521. *
  7522. * @listens {Tech#pause}
  7523. */
  7524. stopTrackingCurrentTime() {
  7525. this.clearInterval(this.currentTimeInterval);
  7526. // #1002 - if the video ends right before the next timeupdate would happen,
  7527. // the progress bar won't make it all the way to the end
  7528. this.trigger({
  7529. type: 'timeupdate',
  7530. target: this,
  7531. manuallyTriggered: true
  7532. });
  7533. }
  7534. /**
  7535. * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
  7536. * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
  7537. *
  7538. * @fires Component#dispose
  7539. */
  7540. dispose() {
  7541. // clear out all tracks because we can't reuse them between techs
  7542. this.clearTracks(NORMAL.names);
  7543. // Turn off any manual progress or timeupdate tracking
  7544. if (this.manualProgress) {
  7545. this.manualProgressOff();
  7546. }
  7547. if (this.manualTimeUpdates) {
  7548. this.manualTimeUpdatesOff();
  7549. }
  7550. super.dispose();
  7551. }
  7552. /**
  7553. * Clear out a single `TrackList` or an array of `TrackLists` given their names.
  7554. *
  7555. * > Note: Techs without source handlers should call this between sources for `video`
  7556. * & `audio` tracks. You don't want to use them between tracks!
  7557. *
  7558. * @param {string[]|string} types
  7559. * TrackList names to clear, valid names are `video`, `audio`, and
  7560. * `text`.
  7561. */
  7562. clearTracks(types) {
  7563. types = [].concat(types);
  7564. // clear out all tracks because we can't reuse them between techs
  7565. types.forEach(type => {
  7566. const list = this[`${type}Tracks`]() || [];
  7567. let i = list.length;
  7568. while (i--) {
  7569. const track = list[i];
  7570. if (type === 'text') {
  7571. this.removeRemoteTextTrack(track);
  7572. }
  7573. list.removeTrack(track);
  7574. }
  7575. });
  7576. }
  7577. /**
  7578. * Remove any TextTracks added via addRemoteTextTrack that are
  7579. * flagged for automatic garbage collection
  7580. */
  7581. cleanupAutoTextTracks() {
  7582. const list = this.autoRemoteTextTracks_ || [];
  7583. let i = list.length;
  7584. while (i--) {
  7585. const track = list[i];
  7586. this.removeRemoteTextTrack(track);
  7587. }
  7588. }
  7589. /**
  7590. * Reset the tech, which will removes all sources and reset the internal readyState.
  7591. *
  7592. * @abstract
  7593. */
  7594. reset() {}
  7595. /**
  7596. * Get the value of `crossOrigin` from the tech.
  7597. *
  7598. * @abstract
  7599. *
  7600. * @see {Html5#crossOrigin}
  7601. */
  7602. crossOrigin() {}
  7603. /**
  7604. * Set the value of `crossOrigin` on the tech.
  7605. *
  7606. * @abstract
  7607. *
  7608. * @param {string} crossOrigin the crossOrigin value
  7609. * @see {Html5#setCrossOrigin}
  7610. */
  7611. setCrossOrigin() {}
  7612. /**
  7613. * Get or set an error on the Tech.
  7614. *
  7615. * @param {MediaError} [err]
  7616. * Error to set on the Tech
  7617. *
  7618. * @return {MediaError|null}
  7619. * The current error object on the tech, or null if there isn't one.
  7620. */
  7621. error(err) {
  7622. if (err !== undefined) {
  7623. this.error_ = new MediaError(err);
  7624. this.trigger('error');
  7625. }
  7626. return this.error_;
  7627. }
  7628. /**
  7629. * Returns the `TimeRange`s that have been played through for the current source.
  7630. *
  7631. * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
  7632. * It only checks whether the source has played at all or not.
  7633. *
  7634. * @return {TimeRange}
  7635. * - A single time range if this video has played
  7636. * - An empty set of ranges if not.
  7637. */
  7638. played() {
  7639. if (this.hasStarted_) {
  7640. return createTimeRanges(0, 0);
  7641. }
  7642. return createTimeRanges();
  7643. }
  7644. /**
  7645. * Start playback
  7646. *
  7647. * @abstract
  7648. *
  7649. * @see {Html5#play}
  7650. */
  7651. play() {}
  7652. /**
  7653. * Set whether we are scrubbing or not
  7654. *
  7655. * @abstract
  7656. * @param {boolean} _isScrubbing
  7657. * - true for we are currently scrubbing
  7658. * - false for we are no longer scrubbing
  7659. *
  7660. * @see {Html5#setScrubbing}
  7661. */
  7662. setScrubbing(_isScrubbing) {}
  7663. /**
  7664. * Get whether we are scrubbing or not
  7665. *
  7666. * @abstract
  7667. *
  7668. * @see {Html5#scrubbing}
  7669. */
  7670. scrubbing() {}
  7671. /**
  7672. * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
  7673. * previously called.
  7674. *
  7675. * @param {number} _seconds
  7676. * Set the current time of the media to this.
  7677. * @fires Tech#timeupdate
  7678. */
  7679. setCurrentTime(_seconds) {
  7680. // improve the accuracy of manual timeupdates
  7681. if (this.manualTimeUpdates) {
  7682. /**
  7683. * A manual `timeupdate` event.
  7684. *
  7685. * @event Tech#timeupdate
  7686. * @type {Event}
  7687. */
  7688. this.trigger({
  7689. type: 'timeupdate',
  7690. target: this,
  7691. manuallyTriggered: true
  7692. });
  7693. }
  7694. }
  7695. /**
  7696. * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
  7697. * {@link TextTrackList} events.
  7698. *
  7699. * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
  7700. *
  7701. * @fires Tech#audiotrackchange
  7702. * @fires Tech#videotrackchange
  7703. * @fires Tech#texttrackchange
  7704. */
  7705. initTrackListeners() {
  7706. /**
  7707. * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
  7708. *
  7709. * @event Tech#audiotrackchange
  7710. * @type {Event}
  7711. */
  7712. /**
  7713. * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
  7714. *
  7715. * @event Tech#videotrackchange
  7716. * @type {Event}
  7717. */
  7718. /**
  7719. * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
  7720. *
  7721. * @event Tech#texttrackchange
  7722. * @type {Event}
  7723. */
  7724. NORMAL.names.forEach(name => {
  7725. const props = NORMAL[name];
  7726. const trackListChanges = () => {
  7727. this.trigger(`${name}trackchange`);
  7728. };
  7729. const tracks = this[props.getterName]();
  7730. tracks.addEventListener('removetrack', trackListChanges);
  7731. tracks.addEventListener('addtrack', trackListChanges);
  7732. this.on('dispose', () => {
  7733. tracks.removeEventListener('removetrack', trackListChanges);
  7734. tracks.removeEventListener('addtrack', trackListChanges);
  7735. });
  7736. });
  7737. }
  7738. /**
  7739. * Emulate TextTracks using vtt.js if necessary
  7740. *
  7741. * @fires Tech#vttjsloaded
  7742. * @fires Tech#vttjserror
  7743. */
  7744. addWebVttScript_() {
  7745. if (window__default["default"].WebVTT) {
  7746. return;
  7747. }
  7748. // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
  7749. // signals that the Tech is ready at which point Tech.el_ is part of the DOM
  7750. // before inserting the WebVTT script
  7751. if (document__default["default"].body.contains(this.el())) {
  7752. // load via require if available and vtt.js script location was not passed in
  7753. // as an option. novtt builds will turn the above require call into an empty object
  7754. // which will cause this if check to always fail.
  7755. if (!this.options_['vtt.js'] && isPlain(vtt__default["default"]) && Object.keys(vtt__default["default"]).length > 0) {
  7756. this.trigger('vttjsloaded');
  7757. return;
  7758. }
  7759. // load vtt.js via the script location option or the cdn of no location was
  7760. // passed in
  7761. const script = document__default["default"].createElement('script');
  7762. script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
  7763. script.onload = () => {
  7764. /**
  7765. * Fired when vtt.js is loaded.
  7766. *
  7767. * @event Tech#vttjsloaded
  7768. * @type {Event}
  7769. */
  7770. this.trigger('vttjsloaded');
  7771. };
  7772. script.onerror = () => {
  7773. /**
  7774. * Fired when vtt.js was not loaded due to an error
  7775. *
  7776. * @event Tech#vttjsloaded
  7777. * @type {Event}
  7778. */
  7779. this.trigger('vttjserror');
  7780. };
  7781. this.on('dispose', () => {
  7782. script.onload = null;
  7783. script.onerror = null;
  7784. });
  7785. // but have not loaded yet and we set it to true before the inject so that
  7786. // we don't overwrite the injected window.WebVTT if it loads right away
  7787. window__default["default"].WebVTT = true;
  7788. this.el().parentNode.appendChild(script);
  7789. } else {
  7790. this.ready(this.addWebVttScript_);
  7791. }
  7792. }
  7793. /**
  7794. * Emulate texttracks
  7795. *
  7796. */
  7797. emulateTextTracks() {
  7798. const tracks = this.textTracks();
  7799. const remoteTracks = this.remoteTextTracks();
  7800. const handleAddTrack = e => tracks.addTrack(e.track);
  7801. const handleRemoveTrack = e => tracks.removeTrack(e.track);
  7802. remoteTracks.on('addtrack', handleAddTrack);
  7803. remoteTracks.on('removetrack', handleRemoveTrack);
  7804. this.addWebVttScript_();
  7805. const updateDisplay = () => this.trigger('texttrackchange');
  7806. const textTracksChanges = () => {
  7807. updateDisplay();
  7808. for (let i = 0; i < tracks.length; i++) {
  7809. const track = tracks[i];
  7810. track.removeEventListener('cuechange', updateDisplay);
  7811. if (track.mode === 'showing') {
  7812. track.addEventListener('cuechange', updateDisplay);
  7813. }
  7814. }
  7815. };
  7816. textTracksChanges();
  7817. tracks.addEventListener('change', textTracksChanges);
  7818. tracks.addEventListener('addtrack', textTracksChanges);
  7819. tracks.addEventListener('removetrack', textTracksChanges);
  7820. this.on('dispose', function () {
  7821. remoteTracks.off('addtrack', handleAddTrack);
  7822. remoteTracks.off('removetrack', handleRemoveTrack);
  7823. tracks.removeEventListener('change', textTracksChanges);
  7824. tracks.removeEventListener('addtrack', textTracksChanges);
  7825. tracks.removeEventListener('removetrack', textTracksChanges);
  7826. for (let i = 0; i < tracks.length; i++) {
  7827. const track = tracks[i];
  7828. track.removeEventListener('cuechange', updateDisplay);
  7829. }
  7830. });
  7831. }
  7832. /**
  7833. * Create and returns a remote {@link TextTrack} object.
  7834. *
  7835. * @param {string} kind
  7836. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
  7837. *
  7838. * @param {string} [label]
  7839. * Label to identify the text track
  7840. *
  7841. * @param {string} [language]
  7842. * Two letter language abbreviation
  7843. *
  7844. * @return {TextTrack}
  7845. * The TextTrack that gets created.
  7846. */
  7847. addTextTrack(kind, label, language) {
  7848. if (!kind) {
  7849. throw new Error('TextTrack kind is required but was not provided');
  7850. }
  7851. return createTrackHelper(this, kind, label, language);
  7852. }
  7853. /**
  7854. * Create an emulated TextTrack for use by addRemoteTextTrack
  7855. *
  7856. * This is intended to be overridden by classes that inherit from
  7857. * Tech in order to create native or custom TextTracks.
  7858. *
  7859. * @param {Object} options
  7860. * The object should contain the options to initialize the TextTrack with.
  7861. *
  7862. * @param {string} [options.kind]
  7863. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
  7864. *
  7865. * @param {string} [options.label].
  7866. * Label to identify the text track
  7867. *
  7868. * @param {string} [options.language]
  7869. * Two letter language abbreviation.
  7870. *
  7871. * @return {HTMLTrackElement}
  7872. * The track element that gets created.
  7873. */
  7874. createRemoteTextTrack(options) {
  7875. const track = merge(options, {
  7876. tech: this
  7877. });
  7878. return new REMOTE.remoteTextEl.TrackClass(track);
  7879. }
  7880. /**
  7881. * Creates a remote text track object and returns an html track element.
  7882. *
  7883. * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
  7884. *
  7885. * @param {Object} options
  7886. * See {@link Tech#createRemoteTextTrack} for more detailed properties.
  7887. *
  7888. * @param {boolean} [manualCleanup=false]
  7889. * - When false: the TextTrack will be automatically removed from the video
  7890. * element whenever the source changes
  7891. * - When True: The TextTrack will have to be cleaned up manually
  7892. *
  7893. * @return {HTMLTrackElement}
  7894. * An Html Track Element.
  7895. *
  7896. */
  7897. addRemoteTextTrack(options = {}, manualCleanup) {
  7898. const htmlTrackElement = this.createRemoteTextTrack(options);
  7899. if (typeof manualCleanup !== 'boolean') {
  7900. manualCleanup = false;
  7901. }
  7902. // store HTMLTrackElement and TextTrack to remote list
  7903. this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
  7904. this.remoteTextTracks().addTrack(htmlTrackElement.track);
  7905. if (manualCleanup === false) {
  7906. // create the TextTrackList if it doesn't exist
  7907. this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
  7908. }
  7909. return htmlTrackElement;
  7910. }
  7911. /**
  7912. * Remove a remote text track from the remote `TextTrackList`.
  7913. *
  7914. * @param {TextTrack} track
  7915. * `TextTrack` to remove from the `TextTrackList`
  7916. */
  7917. removeRemoteTextTrack(track) {
  7918. const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
  7919. // remove HTMLTrackElement and TextTrack from remote list
  7920. this.remoteTextTrackEls().removeTrackElement_(trackElement);
  7921. this.remoteTextTracks().removeTrack(track);
  7922. this.autoRemoteTextTracks_.removeTrack(track);
  7923. }
  7924. /**
  7925. * Gets available media playback quality metrics as specified by the W3C's Media
  7926. * Playback Quality API.
  7927. *
  7928. * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
  7929. *
  7930. * @return {Object}
  7931. * An object with supported media playback quality metrics
  7932. *
  7933. * @abstract
  7934. */
  7935. getVideoPlaybackQuality() {
  7936. return {};
  7937. }
  7938. /**
  7939. * Attempt to create a floating video window always on top of other windows
  7940. * so that users may continue consuming media while they interact with other
  7941. * content sites, or applications on their device.
  7942. *
  7943. * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
  7944. *
  7945. * @return {Promise|undefined}
  7946. * A promise with a Picture-in-Picture window if the browser supports
  7947. * Promises (or one was passed in as an option). It returns undefined
  7948. * otherwise.
  7949. *
  7950. * @abstract
  7951. */
  7952. requestPictureInPicture() {
  7953. return Promise.reject();
  7954. }
  7955. /**
  7956. * A method to check for the value of the 'disablePictureInPicture' <video> property.
  7957. * Defaults to true, as it should be considered disabled if the tech does not support pip
  7958. *
  7959. * @abstract
  7960. */
  7961. disablePictureInPicture() {
  7962. return true;
  7963. }
  7964. /**
  7965. * A method to set or unset the 'disablePictureInPicture' <video> property.
  7966. *
  7967. * @abstract
  7968. */
  7969. setDisablePictureInPicture() {}
  7970. /**
  7971. * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
  7972. *
  7973. * @param {function} cb
  7974. * @return {number} request id
  7975. */
  7976. requestVideoFrameCallback(cb) {
  7977. const id = newGUID();
  7978. if (!this.isReady_ || this.paused()) {
  7979. this.queuedHanders_.add(id);
  7980. this.one('playing', () => {
  7981. if (this.queuedHanders_.has(id)) {
  7982. this.queuedHanders_.delete(id);
  7983. cb();
  7984. }
  7985. });
  7986. } else {
  7987. this.requestNamedAnimationFrame(id, cb);
  7988. }
  7989. return id;
  7990. }
  7991. /**
  7992. * A fallback implementation of cancelVideoFrameCallback
  7993. *
  7994. * @param {number} id id of callback to be cancelled
  7995. */
  7996. cancelVideoFrameCallback(id) {
  7997. if (this.queuedHanders_.has(id)) {
  7998. this.queuedHanders_.delete(id);
  7999. } else {
  8000. this.cancelNamedAnimationFrame(id);
  8001. }
  8002. }
  8003. /**
  8004. * A method to set a poster from a `Tech`.
  8005. *
  8006. * @abstract
  8007. */
  8008. setPoster() {}
  8009. /**
  8010. * A method to check for the presence of the 'playsinline' <video> attribute.
  8011. *
  8012. * @abstract
  8013. */
  8014. playsinline() {}
  8015. /**
  8016. * A method to set or unset the 'playsinline' <video> attribute.
  8017. *
  8018. * @abstract
  8019. */
  8020. setPlaysinline() {}
  8021. /**
  8022. * Attempt to force override of native audio tracks.
  8023. *
  8024. * @param {boolean} override - If set to true native audio will be overridden,
  8025. * otherwise native audio will potentially be used.
  8026. *
  8027. * @abstract
  8028. */
  8029. overrideNativeAudioTracks(override) {}
  8030. /**
  8031. * Attempt to force override of native video tracks.
  8032. *
  8033. * @param {boolean} override - If set to true native video will be overridden,
  8034. * otherwise native video will potentially be used.
  8035. *
  8036. * @abstract
  8037. */
  8038. overrideNativeVideoTracks(override) {}
  8039. /**
  8040. * Check if the tech can support the given mime-type.
  8041. *
  8042. * The base tech does not support any type, but source handlers might
  8043. * overwrite this.
  8044. *
  8045. * @param {string} _type
  8046. * The mimetype to check for support
  8047. *
  8048. * @return {string}
  8049. * 'probably', 'maybe', or empty string
  8050. *
  8051. * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
  8052. *
  8053. * @abstract
  8054. */
  8055. canPlayType(_type) {
  8056. return '';
  8057. }
  8058. /**
  8059. * Check if the type is supported by this tech.
  8060. *
  8061. * The base tech does not support any type, but source handlers might
  8062. * overwrite this.
  8063. *
  8064. * @param {string} _type
  8065. * The media type to check
  8066. * @return {string} Returns the native video element's response
  8067. */
  8068. static canPlayType(_type) {
  8069. return '';
  8070. }
  8071. /**
  8072. * Check if the tech can support the given source
  8073. *
  8074. * @param {Object} srcObj
  8075. * The source object
  8076. * @param {Object} options
  8077. * The options passed to the tech
  8078. * @return {string} 'probably', 'maybe', or '' (empty string)
  8079. */
  8080. static canPlaySource(srcObj, options) {
  8081. return Tech.canPlayType(srcObj.type);
  8082. }
  8083. /*
  8084. * Return whether the argument is a Tech or not.
  8085. * Can be passed either a Class like `Html5` or a instance like `player.tech_`
  8086. *
  8087. * @param {Object} component
  8088. * The item to check
  8089. *
  8090. * @return {boolean}
  8091. * Whether it is a tech or not
  8092. * - True if it is a tech
  8093. * - False if it is not
  8094. */
  8095. static isTech(component) {
  8096. return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
  8097. }
  8098. /**
  8099. * Registers a `Tech` into a shared list for videojs.
  8100. *
  8101. * @param {string} name
  8102. * Name of the `Tech` to register.
  8103. *
  8104. * @param {Object} tech
  8105. * The `Tech` class to register.
  8106. */
  8107. static registerTech(name, tech) {
  8108. if (!Tech.techs_) {
  8109. Tech.techs_ = {};
  8110. }
  8111. if (!Tech.isTech(tech)) {
  8112. throw new Error(`Tech ${name} must be a Tech`);
  8113. }
  8114. if (!Tech.canPlayType) {
  8115. throw new Error('Techs must have a static canPlayType method on them');
  8116. }
  8117. if (!Tech.canPlaySource) {
  8118. throw new Error('Techs must have a static canPlaySource method on them');
  8119. }
  8120. name = toTitleCase(name);
  8121. Tech.techs_[name] = tech;
  8122. Tech.techs_[toLowerCase(name)] = tech;
  8123. if (name !== 'Tech') {
  8124. // camel case the techName for use in techOrder
  8125. Tech.defaultTechOrder_.push(name);
  8126. }
  8127. return tech;
  8128. }
  8129. /**
  8130. * Get a `Tech` from the shared list by name.
  8131. *
  8132. * @param {string} name
  8133. * `camelCase` or `TitleCase` name of the Tech to get
  8134. *
  8135. * @return {Tech|undefined}
  8136. * The `Tech` or undefined if there was no tech with the name requested.
  8137. */
  8138. static getTech(name) {
  8139. if (!name) {
  8140. return;
  8141. }
  8142. if (Tech.techs_ && Tech.techs_[name]) {
  8143. return Tech.techs_[name];
  8144. }
  8145. name = toTitleCase(name);
  8146. if (window__default["default"] && window__default["default"].videojs && window__default["default"].videojs[name]) {
  8147. log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
  8148. return window__default["default"].videojs[name];
  8149. }
  8150. }
  8151. }
  8152. /**
  8153. * Get the {@link VideoTrackList}
  8154. *
  8155. * @returns {VideoTrackList}
  8156. * @method Tech.prototype.videoTracks
  8157. */
  8158. /**
  8159. * Get the {@link AudioTrackList}
  8160. *
  8161. * @returns {AudioTrackList}
  8162. * @method Tech.prototype.audioTracks
  8163. */
  8164. /**
  8165. * Get the {@link TextTrackList}
  8166. *
  8167. * @returns {TextTrackList}
  8168. * @method Tech.prototype.textTracks
  8169. */
  8170. /**
  8171. * Get the remote element {@link TextTrackList}
  8172. *
  8173. * @returns {TextTrackList}
  8174. * @method Tech.prototype.remoteTextTracks
  8175. */
  8176. /**
  8177. * Get the remote element {@link HtmlTrackElementList}
  8178. *
  8179. * @returns {HtmlTrackElementList}
  8180. * @method Tech.prototype.remoteTextTrackEls
  8181. */
  8182. ALL.names.forEach(function (name) {
  8183. const props = ALL[name];
  8184. Tech.prototype[props.getterName] = function () {
  8185. this[props.privateName] = this[props.privateName] || new props.ListClass();
  8186. return this[props.privateName];
  8187. };
  8188. });
  8189. /**
  8190. * List of associated text tracks
  8191. *
  8192. * @type {TextTrackList}
  8193. * @private
  8194. * @property Tech#textTracks_
  8195. */
  8196. /**
  8197. * List of associated audio tracks.
  8198. *
  8199. * @type {AudioTrackList}
  8200. * @private
  8201. * @property Tech#audioTracks_
  8202. */
  8203. /**
  8204. * List of associated video tracks.
  8205. *
  8206. * @type {VideoTrackList}
  8207. * @private
  8208. * @property Tech#videoTracks_
  8209. */
  8210. /**
  8211. * Boolean indicating whether the `Tech` supports volume control.
  8212. *
  8213. * @type {boolean}
  8214. * @default
  8215. */
  8216. Tech.prototype.featuresVolumeControl = true;
  8217. /**
  8218. * Boolean indicating whether the `Tech` supports muting volume.
  8219. *
  8220. * @type {boolean}
  8221. * @default
  8222. */
  8223. Tech.prototype.featuresMuteControl = true;
  8224. /**
  8225. * Boolean indicating whether the `Tech` supports fullscreen resize control.
  8226. * Resizing plugins using request fullscreen reloads the plugin
  8227. *
  8228. * @type {boolean}
  8229. * @default
  8230. */
  8231. Tech.prototype.featuresFullscreenResize = false;
  8232. /**
  8233. * Boolean indicating whether the `Tech` supports changing the speed at which the video
  8234. * plays. Examples:
  8235. * - Set player to play 2x (twice) as fast
  8236. * - Set player to play 0.5x (half) as fast
  8237. *
  8238. * @type {boolean}
  8239. * @default
  8240. */
  8241. Tech.prototype.featuresPlaybackRate = false;
  8242. /**
  8243. * Boolean indicating whether the `Tech` supports the `progress` event.
  8244. * This will be used to determine if {@link Tech#manualProgressOn} should be called.
  8245. *
  8246. * @type {boolean}
  8247. * @default
  8248. */
  8249. Tech.prototype.featuresProgressEvents = false;
  8250. /**
  8251. * Boolean indicating whether the `Tech` supports the `sourceset` event.
  8252. *
  8253. * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
  8254. * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
  8255. * a new source.
  8256. *
  8257. * @type {boolean}
  8258. * @default
  8259. */
  8260. Tech.prototype.featuresSourceset = false;
  8261. /**
  8262. * Boolean indicating whether the `Tech` supports the `timeupdate` event.
  8263. * This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
  8264. *
  8265. * @type {boolean}
  8266. * @default
  8267. */
  8268. Tech.prototype.featuresTimeupdateEvents = false;
  8269. /**
  8270. * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
  8271. * This will help us integrate with native `TextTrack`s if the browser supports them.
  8272. *
  8273. * @type {boolean}
  8274. * @default
  8275. */
  8276. Tech.prototype.featuresNativeTextTracks = false;
  8277. /**
  8278. * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
  8279. *
  8280. * @type {boolean}
  8281. * @default
  8282. */
  8283. Tech.prototype.featuresVideoFrameCallback = false;
  8284. /**
  8285. * A functional mixin for techs that want to use the Source Handler pattern.
  8286. * Source handlers are scripts for handling specific formats.
  8287. * The source handler pattern is used for adaptive formats (HLS, DASH) that
  8288. * manually load video data and feed it into a Source Buffer (Media Source Extensions)
  8289. * Example: `Tech.withSourceHandlers.call(MyTech);`
  8290. *
  8291. * @param {Tech} _Tech
  8292. * The tech to add source handler functions to.
  8293. *
  8294. * @mixes Tech~SourceHandlerAdditions
  8295. */
  8296. Tech.withSourceHandlers = function (_Tech) {
  8297. /**
  8298. * Register a source handler
  8299. *
  8300. * @param {Function} handler
  8301. * The source handler class
  8302. *
  8303. * @param {number} [index]
  8304. * Register it at the following index
  8305. */
  8306. _Tech.registerSourceHandler = function (handler, index) {
  8307. let handlers = _Tech.sourceHandlers;
  8308. if (!handlers) {
  8309. handlers = _Tech.sourceHandlers = [];
  8310. }
  8311. if (index === undefined) {
  8312. // add to the end of the list
  8313. index = handlers.length;
  8314. }
  8315. handlers.splice(index, 0, handler);
  8316. };
  8317. /**
  8318. * Check if the tech can support the given type. Also checks the
  8319. * Techs sourceHandlers.
  8320. *
  8321. * @param {string} type
  8322. * The mimetype to check.
  8323. *
  8324. * @return {string}
  8325. * 'probably', 'maybe', or '' (empty string)
  8326. */
  8327. _Tech.canPlayType = function (type) {
  8328. const handlers = _Tech.sourceHandlers || [];
  8329. let can;
  8330. for (let i = 0; i < handlers.length; i++) {
  8331. can = handlers[i].canPlayType(type);
  8332. if (can) {
  8333. return can;
  8334. }
  8335. }
  8336. return '';
  8337. };
  8338. /**
  8339. * Returns the first source handler that supports the source.
  8340. *
  8341. * TODO: Answer question: should 'probably' be prioritized over 'maybe'
  8342. *
  8343. * @param {Tech~SourceObject} source
  8344. * The source object
  8345. *
  8346. * @param {Object} options
  8347. * The options passed to the tech
  8348. *
  8349. * @return {SourceHandler|null}
  8350. * The first source handler that supports the source or null if
  8351. * no SourceHandler supports the source
  8352. */
  8353. _Tech.selectSourceHandler = function (source, options) {
  8354. const handlers = _Tech.sourceHandlers || [];
  8355. let can;
  8356. for (let i = 0; i < handlers.length; i++) {
  8357. can = handlers[i].canHandleSource(source, options);
  8358. if (can) {
  8359. return handlers[i];
  8360. }
  8361. }
  8362. return null;
  8363. };
  8364. /**
  8365. * Check if the tech can support the given source.
  8366. *
  8367. * @param {Tech~SourceObject} srcObj
  8368. * The source object
  8369. *
  8370. * @param {Object} options
  8371. * The options passed to the tech
  8372. *
  8373. * @return {string}
  8374. * 'probably', 'maybe', or '' (empty string)
  8375. */
  8376. _Tech.canPlaySource = function (srcObj, options) {
  8377. const sh = _Tech.selectSourceHandler(srcObj, options);
  8378. if (sh) {
  8379. return sh.canHandleSource(srcObj, options);
  8380. }
  8381. return '';
  8382. };
  8383. /**
  8384. * When using a source handler, prefer its implementation of
  8385. * any function normally provided by the tech.
  8386. */
  8387. const deferrable = ['seekable', 'seeking', 'duration'];
  8388. /**
  8389. * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
  8390. * function if it exists, with a fallback to the Techs seekable function.
  8391. *
  8392. * @method _Tech.seekable
  8393. */
  8394. /**
  8395. * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
  8396. * function if it exists, otherwise it will fallback to the techs duration function.
  8397. *
  8398. * @method _Tech.duration
  8399. */
  8400. deferrable.forEach(function (fnName) {
  8401. const originalFn = this[fnName];
  8402. if (typeof originalFn !== 'function') {
  8403. return;
  8404. }
  8405. this[fnName] = function () {
  8406. if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
  8407. return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
  8408. }
  8409. return originalFn.apply(this, arguments);
  8410. };
  8411. }, _Tech.prototype);
  8412. /**
  8413. * Create a function for setting the source using a source object
  8414. * and source handlers.
  8415. * Should never be called unless a source handler was found.
  8416. *
  8417. * @param {Tech~SourceObject} source
  8418. * A source object with src and type keys
  8419. */
  8420. _Tech.prototype.setSource = function (source) {
  8421. let sh = _Tech.selectSourceHandler(source, this.options_);
  8422. if (!sh) {
  8423. // Fall back to a native source handler when unsupported sources are
  8424. // deliberately set
  8425. if (_Tech.nativeSourceHandler) {
  8426. sh = _Tech.nativeSourceHandler;
  8427. } else {
  8428. log.error('No source handler found for the current source.');
  8429. }
  8430. }
  8431. // Dispose any existing source handler
  8432. this.disposeSourceHandler();
  8433. this.off('dispose', this.disposeSourceHandler_);
  8434. if (sh !== _Tech.nativeSourceHandler) {
  8435. this.currentSource_ = source;
  8436. }
  8437. this.sourceHandler_ = sh.handleSource(source, this, this.options_);
  8438. this.one('dispose', this.disposeSourceHandler_);
  8439. };
  8440. /**
  8441. * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
  8442. *
  8443. * @listens Tech#dispose
  8444. */
  8445. _Tech.prototype.disposeSourceHandler = function () {
  8446. // if we have a source and get another one
  8447. // then we are loading something new
  8448. // than clear all of our current tracks
  8449. if (this.currentSource_) {
  8450. this.clearTracks(['audio', 'video']);
  8451. this.currentSource_ = null;
  8452. }
  8453. // always clean up auto-text tracks
  8454. this.cleanupAutoTextTracks();
  8455. if (this.sourceHandler_) {
  8456. if (this.sourceHandler_.dispose) {
  8457. this.sourceHandler_.dispose();
  8458. }
  8459. this.sourceHandler_ = null;
  8460. }
  8461. };
  8462. };
  8463. // The base Tech class needs to be registered as a Component. It is the only
  8464. // Tech that can be registered as a Component.
  8465. Component.registerComponent('Tech', Tech);
  8466. Tech.registerTech('Tech', Tech);
  8467. /**
  8468. * A list of techs that should be added to techOrder on Players
  8469. *
  8470. * @private
  8471. */
  8472. Tech.defaultTechOrder_ = [];
  8473. /**
  8474. * @file middleware.js
  8475. * @module middleware
  8476. */
  8477. const middlewares = {};
  8478. const middlewareInstances = {};
  8479. const TERMINATOR = {};
  8480. /**
  8481. * A middleware object is a plain JavaScript object that has methods that
  8482. * match the {@link Tech} methods found in the lists of allowed
  8483. * {@link module:middleware.allowedGetters|getters},
  8484. * {@link module:middleware.allowedSetters|setters}, and
  8485. * {@link module:middleware.allowedMediators|mediators}.
  8486. *
  8487. * @typedef {Object} MiddlewareObject
  8488. */
  8489. /**
  8490. * A middleware factory function that should return a
  8491. * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
  8492. *
  8493. * This factory will be called for each player when needed, with the player
  8494. * passed in as an argument.
  8495. *
  8496. * @callback MiddlewareFactory
  8497. * @param { import('../player').default } player
  8498. * A Video.js player.
  8499. */
  8500. /**
  8501. * Define a middleware that the player should use by way of a factory function
  8502. * that returns a middleware object.
  8503. *
  8504. * @param {string} type
  8505. * The MIME type to match or `"*"` for all MIME types.
  8506. *
  8507. * @param {MiddlewareFactory} middleware
  8508. * A middleware factory function that will be executed for
  8509. * matching types.
  8510. */
  8511. function use(type, middleware) {
  8512. middlewares[type] = middlewares[type] || [];
  8513. middlewares[type].push(middleware);
  8514. }
  8515. /**
  8516. * Asynchronously sets a source using middleware by recursing through any
  8517. * matching middlewares and calling `setSource` on each, passing along the
  8518. * previous returned value each time.
  8519. *
  8520. * @param { import('../player').default } player
  8521. * A {@link Player} instance.
  8522. *
  8523. * @param {Tech~SourceObject} src
  8524. * A source object.
  8525. *
  8526. * @param {Function}
  8527. * The next middleware to run.
  8528. */
  8529. function setSource(player, src, next) {
  8530. player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
  8531. }
  8532. /**
  8533. * When the tech is set, passes the tech to each middleware's `setTech` method.
  8534. *
  8535. * @param {Object[]} middleware
  8536. * An array of middleware instances.
  8537. *
  8538. * @param { import('../tech/tech').default } tech
  8539. * A Video.js tech.
  8540. */
  8541. function setTech(middleware, tech) {
  8542. middleware.forEach(mw => mw.setTech && mw.setTech(tech));
  8543. }
  8544. /**
  8545. * Calls a getter on the tech first, through each middleware
  8546. * from right to left to the player.
  8547. *
  8548. * @param {Object[]} middleware
  8549. * An array of middleware instances.
  8550. *
  8551. * @param { import('../tech/tech').default } tech
  8552. * The current tech.
  8553. *
  8554. * @param {string} method
  8555. * A method name.
  8556. *
  8557. * @return {*}
  8558. * The final value from the tech after middleware has intercepted it.
  8559. */
  8560. function get(middleware, tech, method) {
  8561. return middleware.reduceRight(middlewareIterator(method), tech[method]());
  8562. }
  8563. /**
  8564. * Takes the argument given to the player and calls the setter method on each
  8565. * middleware from left to right to the tech.
  8566. *
  8567. * @param {Object[]} middleware
  8568. * An array of middleware instances.
  8569. *
  8570. * @param { import('../tech/tech').default } tech
  8571. * The current tech.
  8572. *
  8573. * @param {string} method
  8574. * A method name.
  8575. *
  8576. * @param {*} arg
  8577. * The value to set on the tech.
  8578. *
  8579. * @return {*}
  8580. * The return value of the `method` of the `tech`.
  8581. */
  8582. function set(middleware, tech, method, arg) {
  8583. return tech[method](middleware.reduce(middlewareIterator(method), arg));
  8584. }
  8585. /**
  8586. * Takes the argument given to the player and calls the `call` version of the
  8587. * method on each middleware from left to right.
  8588. *
  8589. * Then, call the passed in method on the tech and return the result unchanged
  8590. * back to the player, through middleware, this time from right to left.
  8591. *
  8592. * @param {Object[]} middleware
  8593. * An array of middleware instances.
  8594. *
  8595. * @param { import('../tech/tech').default } tech
  8596. * The current tech.
  8597. *
  8598. * @param {string} method
  8599. * A method name.
  8600. *
  8601. * @param {*} arg
  8602. * The value to set on the tech.
  8603. *
  8604. * @return {*}
  8605. * The return value of the `method` of the `tech`, regardless of the
  8606. * return values of middlewares.
  8607. */
  8608. function mediate(middleware, tech, method, arg = null) {
  8609. const callMethod = 'call' + toTitleCase(method);
  8610. const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
  8611. const terminated = middlewareValue === TERMINATOR;
  8612. // deprecated. The `null` return value should instead return TERMINATOR to
  8613. // prevent confusion if a techs method actually returns null.
  8614. const returnValue = terminated ? null : tech[method](middlewareValue);
  8615. executeRight(middleware, method, returnValue, terminated);
  8616. return returnValue;
  8617. }
  8618. /**
  8619. * Enumeration of allowed getters where the keys are method names.
  8620. *
  8621. * @type {Object}
  8622. */
  8623. const allowedGetters = {
  8624. buffered: 1,
  8625. currentTime: 1,
  8626. duration: 1,
  8627. muted: 1,
  8628. played: 1,
  8629. paused: 1,
  8630. seekable: 1,
  8631. volume: 1,
  8632. ended: 1
  8633. };
  8634. /**
  8635. * Enumeration of allowed setters where the keys are method names.
  8636. *
  8637. * @type {Object}
  8638. */
  8639. const allowedSetters = {
  8640. setCurrentTime: 1,
  8641. setMuted: 1,
  8642. setVolume: 1
  8643. };
  8644. /**
  8645. * Enumeration of allowed mediators where the keys are method names.
  8646. *
  8647. * @type {Object}
  8648. */
  8649. const allowedMediators = {
  8650. play: 1,
  8651. pause: 1
  8652. };
  8653. function middlewareIterator(method) {
  8654. return (value, mw) => {
  8655. // if the previous middleware terminated, pass along the termination
  8656. if (value === TERMINATOR) {
  8657. return TERMINATOR;
  8658. }
  8659. if (mw[method]) {
  8660. return mw[method](value);
  8661. }
  8662. return value;
  8663. };
  8664. }
  8665. function executeRight(mws, method, value, terminated) {
  8666. for (let i = mws.length - 1; i >= 0; i--) {
  8667. const mw = mws[i];
  8668. if (mw[method]) {
  8669. mw[method](terminated, value);
  8670. }
  8671. }
  8672. }
  8673. /**
  8674. * Clear the middleware cache for a player.
  8675. *
  8676. * @param { import('../player').default } player
  8677. * A {@link Player} instance.
  8678. */
  8679. function clearCacheForPlayer(player) {
  8680. middlewareInstances[player.id()] = null;
  8681. }
  8682. /**
  8683. * {
  8684. * [playerId]: [[mwFactory, mwInstance], ...]
  8685. * }
  8686. *
  8687. * @private
  8688. */
  8689. function getOrCreateFactory(player, mwFactory) {
  8690. const mws = middlewareInstances[player.id()];
  8691. let mw = null;
  8692. if (mws === undefined || mws === null) {
  8693. mw = mwFactory(player);
  8694. middlewareInstances[player.id()] = [[mwFactory, mw]];
  8695. return mw;
  8696. }
  8697. for (let i = 0; i < mws.length; i++) {
  8698. const [mwf, mwi] = mws[i];
  8699. if (mwf !== mwFactory) {
  8700. continue;
  8701. }
  8702. mw = mwi;
  8703. }
  8704. if (mw === null) {
  8705. mw = mwFactory(player);
  8706. mws.push([mwFactory, mw]);
  8707. }
  8708. return mw;
  8709. }
  8710. function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
  8711. const [mwFactory, ...mwrest] = middleware;
  8712. // if mwFactory is a string, then we're at a fork in the road
  8713. if (typeof mwFactory === 'string') {
  8714. setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
  8715. // if we have an mwFactory, call it with the player to get the mw,
  8716. // then call the mw's setSource method
  8717. } else if (mwFactory) {
  8718. const mw = getOrCreateFactory(player, mwFactory);
  8719. // if setSource isn't present, implicitly select this middleware
  8720. if (!mw.setSource) {
  8721. acc.push(mw);
  8722. return setSourceHelper(src, mwrest, next, player, acc, lastRun);
  8723. }
  8724. mw.setSource(Object.assign({}, src), function (err, _src) {
  8725. // something happened, try the next middleware on the current level
  8726. // make sure to use the old src
  8727. if (err) {
  8728. return setSourceHelper(src, mwrest, next, player, acc, lastRun);
  8729. }
  8730. // we've succeeded, now we need to go deeper
  8731. acc.push(mw);
  8732. // if it's the same type, continue down the current chain
  8733. // otherwise, we want to go down the new chain
  8734. setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
  8735. });
  8736. } else if (mwrest.length) {
  8737. setSourceHelper(src, mwrest, next, player, acc, lastRun);
  8738. } else if (lastRun) {
  8739. next(src, acc);
  8740. } else {
  8741. setSourceHelper(src, middlewares['*'], next, player, acc, true);
  8742. }
  8743. }
  8744. /**
  8745. * Mimetypes
  8746. *
  8747. * @see https://www.iana.org/assignments/media-types/media-types.xhtml
  8748. * @typedef Mimetypes~Kind
  8749. * @enum
  8750. */
  8751. const MimetypesKind = {
  8752. opus: 'video/ogg',
  8753. ogv: 'video/ogg',
  8754. mp4: 'video/mp4',
  8755. mov: 'video/mp4',
  8756. m4v: 'video/mp4',
  8757. mkv: 'video/x-matroska',
  8758. m4a: 'audio/mp4',
  8759. mp3: 'audio/mpeg',
  8760. aac: 'audio/aac',
  8761. caf: 'audio/x-caf',
  8762. flac: 'audio/flac',
  8763. oga: 'audio/ogg',
  8764. wav: 'audio/wav',
  8765. m3u8: 'application/x-mpegURL',
  8766. mpd: 'application/dash+xml',
  8767. jpg: 'image/jpeg',
  8768. jpeg: 'image/jpeg',
  8769. gif: 'image/gif',
  8770. png: 'image/png',
  8771. svg: 'image/svg+xml',
  8772. webp: 'image/webp'
  8773. };
  8774. /**
  8775. * Get the mimetype of a given src url if possible
  8776. *
  8777. * @param {string} src
  8778. * The url to the src
  8779. *
  8780. * @return {string}
  8781. * return the mimetype if it was known or empty string otherwise
  8782. */
  8783. const getMimetype = function (src = '') {
  8784. const ext = getFileExtension(src);
  8785. const mimetype = MimetypesKind[ext.toLowerCase()];
  8786. return mimetype || '';
  8787. };
  8788. /**
  8789. * Find the mime type of a given source string if possible. Uses the player
  8790. * source cache.
  8791. *
  8792. * @param { import('../player').default } player
  8793. * The player object
  8794. *
  8795. * @param {string} src
  8796. * The source string
  8797. *
  8798. * @return {string}
  8799. * The type that was found
  8800. */
  8801. const findMimetype = (player, src) => {
  8802. if (!src) {
  8803. return '';
  8804. }
  8805. // 1. check for the type in the `source` cache
  8806. if (player.cache_.source.src === src && player.cache_.source.type) {
  8807. return player.cache_.source.type;
  8808. }
  8809. // 2. see if we have this source in our `currentSources` cache
  8810. const matchingSources = player.cache_.sources.filter(s => s.src === src);
  8811. if (matchingSources.length) {
  8812. return matchingSources[0].type;
  8813. }
  8814. // 3. look for the src url in source elements and use the type there
  8815. const sources = player.$$('source');
  8816. for (let i = 0; i < sources.length; i++) {
  8817. const s = sources[i];
  8818. if (s.type && s.src && s.src === src) {
  8819. return s.type;
  8820. }
  8821. }
  8822. // 4. finally fallback to our list of mime types based on src url extension
  8823. return getMimetype(src);
  8824. };
  8825. /**
  8826. * @module filter-source
  8827. */
  8828. /**
  8829. * Filter out single bad source objects or multiple source objects in an
  8830. * array. Also flattens nested source object arrays into a 1 dimensional
  8831. * array of source objects.
  8832. *
  8833. * @param {Tech~SourceObject|Tech~SourceObject[]} src
  8834. * The src object to filter
  8835. *
  8836. * @return {Tech~SourceObject[]}
  8837. * An array of sourceobjects containing only valid sources
  8838. *
  8839. * @private
  8840. */
  8841. const filterSource = function (src) {
  8842. // traverse array
  8843. if (Array.isArray(src)) {
  8844. let newsrc = [];
  8845. src.forEach(function (srcobj) {
  8846. srcobj = filterSource(srcobj);
  8847. if (Array.isArray(srcobj)) {
  8848. newsrc = newsrc.concat(srcobj);
  8849. } else if (isObject(srcobj)) {
  8850. newsrc.push(srcobj);
  8851. }
  8852. });
  8853. src = newsrc;
  8854. } else if (typeof src === 'string' && src.trim()) {
  8855. // convert string into object
  8856. src = [fixSource({
  8857. src
  8858. })];
  8859. } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
  8860. // src is already valid
  8861. src = [fixSource(src)];
  8862. } else {
  8863. // invalid source, turn it into an empty array
  8864. src = [];
  8865. }
  8866. return src;
  8867. };
  8868. /**
  8869. * Checks src mimetype, adding it when possible
  8870. *
  8871. * @param {Tech~SourceObject} src
  8872. * The src object to check
  8873. * @return {Tech~SourceObject}
  8874. * src Object with known type
  8875. */
  8876. function fixSource(src) {
  8877. if (!src.type) {
  8878. const mimetype = getMimetype(src.src);
  8879. if (mimetype) {
  8880. src.type = mimetype;
  8881. }
  8882. }
  8883. return src;
  8884. }
  8885. /**
  8886. * @file loader.js
  8887. */
  8888. /**
  8889. * The `MediaLoader` is the `Component` that decides which playback technology to load
  8890. * when a player is initialized.
  8891. *
  8892. * @extends Component
  8893. */
  8894. class MediaLoader extends Component {
  8895. /**
  8896. * Create an instance of this class.
  8897. *
  8898. * @param { import('../player').default } player
  8899. * The `Player` that this class should attach to.
  8900. *
  8901. * @param {Object} [options]
  8902. * The key/value store of player options.
  8903. *
  8904. * @param {Function} [ready]
  8905. * The function that is run when this component is ready.
  8906. */
  8907. constructor(player, options, ready) {
  8908. // MediaLoader has no element
  8909. const options_ = merge({
  8910. createEl: false
  8911. }, options);
  8912. super(player, options_, ready);
  8913. // If there are no sources when the player is initialized,
  8914. // load the first supported playback technology.
  8915. if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
  8916. for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
  8917. const techName = toTitleCase(j[i]);
  8918. let tech = Tech.getTech(techName);
  8919. // Support old behavior of techs being registered as components.
  8920. // Remove once that deprecated behavior is removed.
  8921. if (!techName) {
  8922. tech = Component.getComponent(techName);
  8923. }
  8924. // Check if the browser supports this technology
  8925. if (tech && tech.isSupported()) {
  8926. player.loadTech_(techName);
  8927. break;
  8928. }
  8929. }
  8930. } else {
  8931. // Loop through playback technologies (e.g. HTML5) and check for support.
  8932. // Then load the best source.
  8933. // A few assumptions here:
  8934. // All playback technologies respect preload false.
  8935. player.src(options.playerOptions.sources);
  8936. }
  8937. }
  8938. }
  8939. Component.registerComponent('MediaLoader', MediaLoader);
  8940. /**
  8941. * @file clickable-component.js
  8942. */
  8943. /**
  8944. * Component which is clickable or keyboard actionable, but is not a
  8945. * native HTML button.
  8946. *
  8947. * @extends Component
  8948. */
  8949. class ClickableComponent extends Component {
  8950. /**
  8951. * Creates an instance of this class.
  8952. *
  8953. * @param { import('./player').default } player
  8954. * The `Player` that this class should be attached to.
  8955. *
  8956. * @param {Object} [options]
  8957. * The key/value store of component options.
  8958. *
  8959. * @param {function} [options.clickHandler]
  8960. * The function to call when the button is clicked / activated
  8961. *
  8962. * @param {string} [options.controlText]
  8963. * The text to set on the button
  8964. *
  8965. * @param {string} [options.className]
  8966. * A class or space separated list of classes to add the component
  8967. *
  8968. */
  8969. constructor(player, options) {
  8970. super(player, options);
  8971. if (this.options_.controlText) {
  8972. this.controlText(this.options_.controlText);
  8973. }
  8974. this.handleMouseOver_ = e => this.handleMouseOver(e);
  8975. this.handleMouseOut_ = e => this.handleMouseOut(e);
  8976. this.handleClick_ = e => this.handleClick(e);
  8977. this.handleKeyDown_ = e => this.handleKeyDown(e);
  8978. this.emitTapEvents();
  8979. this.enable();
  8980. }
  8981. /**
  8982. * Create the `ClickableComponent`s DOM element.
  8983. *
  8984. * @param {string} [tag=div]
  8985. * The element's node type.
  8986. *
  8987. * @param {Object} [props={}]
  8988. * An object of properties that should be set on the element.
  8989. *
  8990. * @param {Object} [attributes={}]
  8991. * An object of attributes that should be set on the element.
  8992. *
  8993. * @return {Element}
  8994. * The element that gets created.
  8995. */
  8996. createEl(tag = 'div', props = {}, attributes = {}) {
  8997. props = Object.assign({
  8998. className: this.buildCSSClass(),
  8999. tabIndex: 0
  9000. }, props);
  9001. if (tag === 'button') {
  9002. log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
  9003. }
  9004. // Add ARIA attributes for clickable element which is not a native HTML button
  9005. attributes = Object.assign({
  9006. role: 'button'
  9007. }, attributes);
  9008. this.tabIndex_ = props.tabIndex;
  9009. const el = createEl(tag, props, attributes);
  9010. el.appendChild(createEl('span', {
  9011. className: 'vjs-icon-placeholder'
  9012. }, {
  9013. 'aria-hidden': true
  9014. }));
  9015. this.createControlTextEl(el);
  9016. return el;
  9017. }
  9018. dispose() {
  9019. // remove controlTextEl_ on dispose
  9020. this.controlTextEl_ = null;
  9021. super.dispose();
  9022. }
  9023. /**
  9024. * Create a control text element on this `ClickableComponent`
  9025. *
  9026. * @param {Element} [el]
  9027. * Parent element for the control text.
  9028. *
  9029. * @return {Element}
  9030. * The control text element that gets created.
  9031. */
  9032. createControlTextEl(el) {
  9033. this.controlTextEl_ = createEl('span', {
  9034. className: 'vjs-control-text'
  9035. }, {
  9036. // let the screen reader user know that the text of the element may change
  9037. 'aria-live': 'polite'
  9038. });
  9039. if (el) {
  9040. el.appendChild(this.controlTextEl_);
  9041. }
  9042. this.controlText(this.controlText_, el);
  9043. return this.controlTextEl_;
  9044. }
  9045. /**
  9046. * Get or set the localize text to use for the controls on the `ClickableComponent`.
  9047. *
  9048. * @param {string} [text]
  9049. * Control text for element.
  9050. *
  9051. * @param {Element} [el=this.el()]
  9052. * Element to set the title on.
  9053. *
  9054. * @return {string}
  9055. * - The control text when getting
  9056. */
  9057. controlText(text, el = this.el()) {
  9058. if (text === undefined) {
  9059. return this.controlText_ || 'Need Text';
  9060. }
  9061. const localizedText = this.localize(text);
  9062. /** @protected */
  9063. this.controlText_ = text;
  9064. textContent(this.controlTextEl_, localizedText);
  9065. if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
  9066. // Set title attribute if only an icon is shown
  9067. el.setAttribute('title', localizedText);
  9068. }
  9069. }
  9070. /**
  9071. * Builds the default DOM `className`.
  9072. *
  9073. * @return {string}
  9074. * The DOM `className` for this object.
  9075. */
  9076. buildCSSClass() {
  9077. return `vjs-control vjs-button ${super.buildCSSClass()}`;
  9078. }
  9079. /**
  9080. * Enable this `ClickableComponent`
  9081. */
  9082. enable() {
  9083. if (!this.enabled_) {
  9084. this.enabled_ = true;
  9085. this.removeClass('vjs-disabled');
  9086. this.el_.setAttribute('aria-disabled', 'false');
  9087. if (typeof this.tabIndex_ !== 'undefined') {
  9088. this.el_.setAttribute('tabIndex', this.tabIndex_);
  9089. }
  9090. this.on(['tap', 'click'], this.handleClick_);
  9091. this.on('keydown', this.handleKeyDown_);
  9092. }
  9093. }
  9094. /**
  9095. * Disable this `ClickableComponent`
  9096. */
  9097. disable() {
  9098. this.enabled_ = false;
  9099. this.addClass('vjs-disabled');
  9100. this.el_.setAttribute('aria-disabled', 'true');
  9101. if (typeof this.tabIndex_ !== 'undefined') {
  9102. this.el_.removeAttribute('tabIndex');
  9103. }
  9104. this.off('mouseover', this.handleMouseOver_);
  9105. this.off('mouseout', this.handleMouseOut_);
  9106. this.off(['tap', 'click'], this.handleClick_);
  9107. this.off('keydown', this.handleKeyDown_);
  9108. }
  9109. /**
  9110. * Handles language change in ClickableComponent for the player in components
  9111. *
  9112. *
  9113. */
  9114. handleLanguagechange() {
  9115. this.controlText(this.controlText_);
  9116. }
  9117. /**
  9118. * Event handler that is called when a `ClickableComponent` receives a
  9119. * `click` or `tap` event.
  9120. *
  9121. * @param {Event} event
  9122. * The `tap` or `click` event that caused this function to be called.
  9123. *
  9124. * @listens tap
  9125. * @listens click
  9126. * @abstract
  9127. */
  9128. handleClick(event) {
  9129. if (this.options_.clickHandler) {
  9130. this.options_.clickHandler.call(this, arguments);
  9131. }
  9132. }
  9133. /**
  9134. * Event handler that is called when a `ClickableComponent` receives a
  9135. * `keydown` event.
  9136. *
  9137. * By default, if the key is Space or Enter, it will trigger a `click` event.
  9138. *
  9139. * @param {Event} event
  9140. * The `keydown` event that caused this function to be called.
  9141. *
  9142. * @listens keydown
  9143. */
  9144. handleKeyDown(event) {
  9145. // Support Space or Enter key operation to fire a click event. Also,
  9146. // prevent the event from propagating through the DOM and triggering
  9147. // Player hotkeys.
  9148. if (keycode__default["default"].isEventKey(event, 'Space') || keycode__default["default"].isEventKey(event, 'Enter')) {
  9149. event.preventDefault();
  9150. event.stopPropagation();
  9151. this.trigger('click');
  9152. } else {
  9153. // Pass keypress handling up for unsupported keys
  9154. super.handleKeyDown(event);
  9155. }
  9156. }
  9157. }
  9158. Component.registerComponent('ClickableComponent', ClickableComponent);
  9159. /**
  9160. * @file poster-image.js
  9161. */
  9162. /**
  9163. * A `ClickableComponent` that handles showing the poster image for the player.
  9164. *
  9165. * @extends ClickableComponent
  9166. */
  9167. class PosterImage extends ClickableComponent {
  9168. /**
  9169. * Create an instance of this class.
  9170. *
  9171. * @param { import('./player').default } player
  9172. * The `Player` that this class should attach to.
  9173. *
  9174. * @param {Object} [options]
  9175. * The key/value store of player options.
  9176. */
  9177. constructor(player, options) {
  9178. super(player, options);
  9179. this.update();
  9180. this.update_ = e => this.update(e);
  9181. player.on('posterchange', this.update_);
  9182. }
  9183. /**
  9184. * Clean up and dispose of the `PosterImage`.
  9185. */
  9186. dispose() {
  9187. this.player().off('posterchange', this.update_);
  9188. super.dispose();
  9189. }
  9190. /**
  9191. * Create the `PosterImage`s DOM element.
  9192. *
  9193. * @return {Element}
  9194. * The element that gets created.
  9195. */
  9196. createEl() {
  9197. // The el is an empty div to keep position in the DOM
  9198. // A picture and img el will be inserted when a source is set
  9199. return createEl('div', {
  9200. className: 'vjs-poster'
  9201. });
  9202. }
  9203. /**
  9204. * Get or set the `PosterImage`'s crossOrigin option.
  9205. *
  9206. * @param {string|null} [value]
  9207. * The value to set the crossOrigin to. If an argument is
  9208. * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
  9209. *
  9210. * @return {string|null}
  9211. * - The current crossOrigin value of the `Player` when getting.
  9212. * - undefined when setting
  9213. */
  9214. crossOrigin(value) {
  9215. // `null` can be set to unset a value
  9216. if (typeof value === 'undefined') {
  9217. if (this.$('img')) {
  9218. // If the poster's element exists, give its value
  9219. return this.$('img').crossOrigin;
  9220. } else if (this.player_.tech_ && this.player_.tech_.isReady_) {
  9221. // If not but the tech is ready, query the tech
  9222. return this.player_.crossOrigin();
  9223. }
  9224. // Otherwise check options as the poster is usually set before the state of crossorigin
  9225. // can be retrieved by the getter
  9226. return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
  9227. }
  9228. if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
  9229. this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
  9230. return;
  9231. }
  9232. if (this.$('img')) {
  9233. this.$('img').crossOrigin = value;
  9234. }
  9235. return;
  9236. }
  9237. /**
  9238. * An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
  9239. *
  9240. * @listens Player#posterchange
  9241. *
  9242. * @param {Event} [event]
  9243. * The `Player#posterchange` event that triggered this function.
  9244. */
  9245. update(event) {
  9246. const url = this.player().poster();
  9247. this.setSrc(url);
  9248. // If there's no poster source we should display:none on this component
  9249. // so it's not still clickable or right-clickable
  9250. if (url) {
  9251. this.show();
  9252. } else {
  9253. this.hide();
  9254. }
  9255. }
  9256. /**
  9257. * Set the source of the `PosterImage` depending on the display method. (Re)creates
  9258. * the inner picture and img elementss when needed.
  9259. *
  9260. * @param {string} [url]
  9261. * The URL to the source for the `PosterImage`. If not specified or falsy,
  9262. * any source and ant inner picture/img are removed.
  9263. */
  9264. setSrc(url) {
  9265. if (!url) {
  9266. this.el_.textContent = '';
  9267. return;
  9268. }
  9269. if (!this.$('img')) {
  9270. this.el_.appendChild(createEl('picture', {
  9271. className: 'vjs-poster',
  9272. // Don't want poster to be tabbable.
  9273. tabIndex: -1
  9274. }, {}, createEl('img', {
  9275. loading: 'lazy',
  9276. crossOrigin: this.crossOrigin()
  9277. }, {
  9278. alt: ''
  9279. })));
  9280. }
  9281. this.$('img').src = url;
  9282. }
  9283. /**
  9284. * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
  9285. * {@link ClickableComponent#handleClick} for instances where this will be triggered.
  9286. *
  9287. * @listens tap
  9288. * @listens click
  9289. * @listens keydown
  9290. *
  9291. * @param {Event} event
  9292. + The `click`, `tap` or `keydown` event that caused this function to be called.
  9293. */
  9294. handleClick(event) {
  9295. // We don't want a click to trigger playback when controls are disabled
  9296. if (!this.player_.controls()) {
  9297. return;
  9298. }
  9299. if (this.player_.tech(true)) {
  9300. this.player_.tech(true).focus();
  9301. }
  9302. if (this.player_.paused()) {
  9303. silencePromise(this.player_.play());
  9304. } else {
  9305. this.player_.pause();
  9306. }
  9307. }
  9308. }
  9309. /**
  9310. * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
  9311. * sets the `crossOrigin` property on the `<img>` tag to control the CORS
  9312. * behavior.
  9313. *
  9314. * @param {string|null} [value]
  9315. * The value to set the `PosterImages`'s crossorigin to. If an argument is
  9316. * given, must be one of `anonymous` or `use-credentials`.
  9317. *
  9318. * @return {string|null|undefined}
  9319. * - The current crossorigin value of the `Player` when getting.
  9320. * - undefined when setting
  9321. */
  9322. PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
  9323. Component.registerComponent('PosterImage', PosterImage);
  9324. /**
  9325. * @file text-track-display.js
  9326. */
  9327. const darkGray = '#222';
  9328. const lightGray = '#ccc';
  9329. const fontMap = {
  9330. monospace: 'monospace',
  9331. sansSerif: 'sans-serif',
  9332. serif: 'serif',
  9333. monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
  9334. monospaceSerif: '"Courier New", monospace',
  9335. proportionalSansSerif: 'sans-serif',
  9336. proportionalSerif: 'serif',
  9337. casual: '"Comic Sans MS", Impact, fantasy',
  9338. script: '"Monotype Corsiva", cursive',
  9339. smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
  9340. };
  9341. /**
  9342. * Construct an rgba color from a given hex color code.
  9343. *
  9344. * @param {number} color
  9345. * Hex number for color, like #f0e or #f604e2.
  9346. *
  9347. * @param {number} opacity
  9348. * Value for opacity, 0.0 - 1.0.
  9349. *
  9350. * @return {string}
  9351. * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
  9352. */
  9353. function constructColor(color, opacity) {
  9354. let hex;
  9355. if (color.length === 4) {
  9356. // color looks like "#f0e"
  9357. hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
  9358. } else if (color.length === 7) {
  9359. // color looks like "#f604e2"
  9360. hex = color.slice(1);
  9361. } else {
  9362. throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
  9363. }
  9364. return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
  9365. }
  9366. /**
  9367. * Try to update the style of a DOM element. Some style changes will throw an error,
  9368. * particularly in IE8. Those should be noops.
  9369. *
  9370. * @param {Element} el
  9371. * The DOM element to be styled.
  9372. *
  9373. * @param {string} style
  9374. * The CSS property on the element that should be styled.
  9375. *
  9376. * @param {string} rule
  9377. * The style rule that should be applied to the property.
  9378. *
  9379. * @private
  9380. */
  9381. function tryUpdateStyle(el, style, rule) {
  9382. try {
  9383. el.style[style] = rule;
  9384. } catch (e) {
  9385. // Satisfies linter.
  9386. return;
  9387. }
  9388. }
  9389. /**
  9390. * The component for displaying text track cues.
  9391. *
  9392. * @extends Component
  9393. */
  9394. class TextTrackDisplay extends Component {
  9395. /**
  9396. * Creates an instance of this class.
  9397. *
  9398. * @param { import('../player').default } player
  9399. * The `Player` that this class should be attached to.
  9400. *
  9401. * @param {Object} [options]
  9402. * The key/value store of player options.
  9403. *
  9404. * @param {Function} [ready]
  9405. * The function to call when `TextTrackDisplay` is ready.
  9406. */
  9407. constructor(player, options, ready) {
  9408. super(player, options, ready);
  9409. const updateDisplayHandler = e => this.updateDisplay(e);
  9410. player.on('loadstart', e => this.toggleDisplay(e));
  9411. player.on('texttrackchange', updateDisplayHandler);
  9412. player.on('loadedmetadata', e => this.preselectTrack(e));
  9413. // This used to be called during player init, but was causing an error
  9414. // if a track should show by default and the display hadn't loaded yet.
  9415. // Should probably be moved to an external track loader when we support
  9416. // tracks that don't need a display.
  9417. player.ready(bind_(this, function () {
  9418. if (player.tech_ && player.tech_.featuresNativeTextTracks) {
  9419. this.hide();
  9420. return;
  9421. }
  9422. player.on('fullscreenchange', updateDisplayHandler);
  9423. player.on('playerresize', updateDisplayHandler);
  9424. const screenOrientation = window__default["default"].screen.orientation || window__default["default"];
  9425. const changeOrientationEvent = window__default["default"].screen.orientation ? 'change' : 'orientationchange';
  9426. screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
  9427. player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
  9428. const tracks = this.options_.playerOptions.tracks || [];
  9429. for (let i = 0; i < tracks.length; i++) {
  9430. this.player_.addRemoteTextTrack(tracks[i], true);
  9431. }
  9432. this.preselectTrack();
  9433. }));
  9434. }
  9435. /**
  9436. * Preselect a track following this precedence:
  9437. * - matches the previously selected {@link TextTrack}'s language and kind
  9438. * - matches the previously selected {@link TextTrack}'s language only
  9439. * - is the first default captions track
  9440. * - is the first default descriptions track
  9441. *
  9442. * @listens Player#loadstart
  9443. */
  9444. preselectTrack() {
  9445. const modes = {
  9446. captions: 1,
  9447. subtitles: 1
  9448. };
  9449. const trackList = this.player_.textTracks();
  9450. const userPref = this.player_.cache_.selectedLanguage;
  9451. let firstDesc;
  9452. let firstCaptions;
  9453. let preferredTrack;
  9454. for (let i = 0; i < trackList.length; i++) {
  9455. const track = trackList[i];
  9456. if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
  9457. // Always choose the track that matches both language and kind
  9458. if (track.kind === userPref.kind) {
  9459. preferredTrack = track;
  9460. // or choose the first track that matches language
  9461. } else if (!preferredTrack) {
  9462. preferredTrack = track;
  9463. }
  9464. // clear everything if offTextTrackMenuItem was clicked
  9465. } else if (userPref && !userPref.enabled) {
  9466. preferredTrack = null;
  9467. firstDesc = null;
  9468. firstCaptions = null;
  9469. } else if (track.default) {
  9470. if (track.kind === 'descriptions' && !firstDesc) {
  9471. firstDesc = track;
  9472. } else if (track.kind in modes && !firstCaptions) {
  9473. firstCaptions = track;
  9474. }
  9475. }
  9476. }
  9477. // The preferredTrack matches the user preference and takes
  9478. // precedence over all the other tracks.
  9479. // So, display the preferredTrack before the first default track
  9480. // and the subtitles/captions track before the descriptions track
  9481. if (preferredTrack) {
  9482. preferredTrack.mode = 'showing';
  9483. } else if (firstCaptions) {
  9484. firstCaptions.mode = 'showing';
  9485. } else if (firstDesc) {
  9486. firstDesc.mode = 'showing';
  9487. }
  9488. }
  9489. /**
  9490. * Turn display of {@link TextTrack}'s from the current state into the other state.
  9491. * There are only two states:
  9492. * - 'shown'
  9493. * - 'hidden'
  9494. *
  9495. * @listens Player#loadstart
  9496. */
  9497. toggleDisplay() {
  9498. if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
  9499. this.hide();
  9500. } else {
  9501. this.show();
  9502. }
  9503. }
  9504. /**
  9505. * Create the {@link Component}'s DOM element.
  9506. *
  9507. * @return {Element}
  9508. * The element that was created.
  9509. */
  9510. createEl() {
  9511. return super.createEl('div', {
  9512. className: 'vjs-text-track-display'
  9513. }, {
  9514. 'translate': 'yes',
  9515. 'aria-live': 'off',
  9516. 'aria-atomic': 'true'
  9517. });
  9518. }
  9519. /**
  9520. * Clear all displayed {@link TextTrack}s.
  9521. */
  9522. clearDisplay() {
  9523. if (typeof window__default["default"].WebVTT === 'function') {
  9524. window__default["default"].WebVTT.processCues(window__default["default"], [], this.el_);
  9525. }
  9526. }
  9527. /**
  9528. * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
  9529. * a {@link Player#fullscreenchange} is fired.
  9530. *
  9531. * @listens Player#texttrackchange
  9532. * @listens Player#fullscreenchange
  9533. */
  9534. updateDisplay() {
  9535. const tracks = this.player_.textTracks();
  9536. const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
  9537. this.clearDisplay();
  9538. if (allowMultipleShowingTracks) {
  9539. const showingTracks = [];
  9540. for (let i = 0; i < tracks.length; ++i) {
  9541. const track = tracks[i];
  9542. if (track.mode !== 'showing') {
  9543. continue;
  9544. }
  9545. showingTracks.push(track);
  9546. }
  9547. this.updateForTrack(showingTracks);
  9548. return;
  9549. }
  9550. // Track display prioritization model: if multiple tracks are 'showing',
  9551. // display the first 'subtitles' or 'captions' track which is 'showing',
  9552. // otherwise display the first 'descriptions' track which is 'showing'
  9553. let descriptionsTrack = null;
  9554. let captionsSubtitlesTrack = null;
  9555. let i = tracks.length;
  9556. while (i--) {
  9557. const track = tracks[i];
  9558. if (track.mode === 'showing') {
  9559. if (track.kind === 'descriptions') {
  9560. descriptionsTrack = track;
  9561. } else {
  9562. captionsSubtitlesTrack = track;
  9563. }
  9564. }
  9565. }
  9566. if (captionsSubtitlesTrack) {
  9567. if (this.getAttribute('aria-live') !== 'off') {
  9568. this.setAttribute('aria-live', 'off');
  9569. }
  9570. this.updateForTrack(captionsSubtitlesTrack);
  9571. } else if (descriptionsTrack) {
  9572. if (this.getAttribute('aria-live') !== 'assertive') {
  9573. this.setAttribute('aria-live', 'assertive');
  9574. }
  9575. this.updateForTrack(descriptionsTrack);
  9576. }
  9577. }
  9578. /**
  9579. * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
  9580. *
  9581. * @param {TextTrack} track
  9582. * Text track object containing active cues to style.
  9583. */
  9584. updateDisplayState(track) {
  9585. const overrides = this.player_.textTrackSettings.getValues();
  9586. const cues = track.activeCues;
  9587. let i = cues.length;
  9588. while (i--) {
  9589. const cue = cues[i];
  9590. if (!cue) {
  9591. continue;
  9592. }
  9593. const cueDiv = cue.displayState;
  9594. if (overrides.color) {
  9595. cueDiv.firstChild.style.color = overrides.color;
  9596. }
  9597. if (overrides.textOpacity) {
  9598. tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
  9599. }
  9600. if (overrides.backgroundColor) {
  9601. cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
  9602. }
  9603. if (overrides.backgroundOpacity) {
  9604. tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
  9605. }
  9606. if (overrides.windowColor) {
  9607. if (overrides.windowOpacity) {
  9608. tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
  9609. } else {
  9610. cueDiv.style.backgroundColor = overrides.windowColor;
  9611. }
  9612. }
  9613. if (overrides.edgeStyle) {
  9614. if (overrides.edgeStyle === 'dropshadow') {
  9615. cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
  9616. } else if (overrides.edgeStyle === 'raised') {
  9617. cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
  9618. } else if (overrides.edgeStyle === 'depressed') {
  9619. cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
  9620. } else if (overrides.edgeStyle === 'uniform') {
  9621. cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
  9622. }
  9623. }
  9624. if (overrides.fontPercent && overrides.fontPercent !== 1) {
  9625. const fontSize = window__default["default"].parseFloat(cueDiv.style.fontSize);
  9626. cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
  9627. cueDiv.style.height = 'auto';
  9628. cueDiv.style.top = 'auto';
  9629. }
  9630. if (overrides.fontFamily && overrides.fontFamily !== 'default') {
  9631. if (overrides.fontFamily === 'small-caps') {
  9632. cueDiv.firstChild.style.fontVariant = 'small-caps';
  9633. } else {
  9634. cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
  9635. }
  9636. }
  9637. }
  9638. }
  9639. /**
  9640. * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
  9641. *
  9642. * @param {TextTrack|TextTrack[]} tracks
  9643. * Text track object or text track array to be added to the list.
  9644. */
  9645. updateForTrack(tracks) {
  9646. if (!Array.isArray(tracks)) {
  9647. tracks = [tracks];
  9648. }
  9649. if (typeof window__default["default"].WebVTT !== 'function' || tracks.every(track => {
  9650. return !track.activeCues;
  9651. })) {
  9652. return;
  9653. }
  9654. const cues = [];
  9655. // push all active track cues
  9656. for (let i = 0; i < tracks.length; ++i) {
  9657. const track = tracks[i];
  9658. for (let j = 0; j < track.activeCues.length; ++j) {
  9659. cues.push(track.activeCues[j]);
  9660. }
  9661. }
  9662. // removes all cues before it processes new ones
  9663. window__default["default"].WebVTT.processCues(window__default["default"], cues, this.el_);
  9664. // add unique class to each language text track & add settings styling if necessary
  9665. for (let i = 0; i < tracks.length; ++i) {
  9666. const track = tracks[i];
  9667. for (let j = 0; j < track.activeCues.length; ++j) {
  9668. const cueEl = track.activeCues[j].displayState;
  9669. addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
  9670. if (track.language) {
  9671. setAttribute(cueEl, 'lang', track.language);
  9672. }
  9673. }
  9674. if (this.player_.textTrackSettings) {
  9675. this.updateDisplayState(track);
  9676. }
  9677. }
  9678. }
  9679. }
  9680. Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
  9681. /**
  9682. * @file loading-spinner.js
  9683. */
  9684. /**
  9685. * A loading spinner for use during waiting/loading events.
  9686. *
  9687. * @extends Component
  9688. */
  9689. class LoadingSpinner extends Component {
  9690. /**
  9691. * Create the `LoadingSpinner`s DOM element.
  9692. *
  9693. * @return {Element}
  9694. * The dom element that gets created.
  9695. */
  9696. createEl() {
  9697. const isAudio = this.player_.isAudio();
  9698. const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
  9699. const controlText = createEl('span', {
  9700. className: 'vjs-control-text',
  9701. textContent: this.localize('{1} is loading.', [playerType])
  9702. });
  9703. const el = super.createEl('div', {
  9704. className: 'vjs-loading-spinner',
  9705. dir: 'ltr'
  9706. });
  9707. el.appendChild(controlText);
  9708. return el;
  9709. }
  9710. /**
  9711. * Update control text on languagechange
  9712. */
  9713. handleLanguagechange() {
  9714. this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
  9715. }
  9716. }
  9717. Component.registerComponent('LoadingSpinner', LoadingSpinner);
  9718. /**
  9719. * @file button.js
  9720. */
  9721. /**
  9722. * Base class for all buttons.
  9723. *
  9724. * @extends ClickableComponent
  9725. */
  9726. class Button extends ClickableComponent {
  9727. /**
  9728. * Create the `Button`s DOM element.
  9729. *
  9730. * @param {string} [tag="button"]
  9731. * The element's node type. This argument is IGNORED: no matter what
  9732. * is passed, it will always create a `button` element.
  9733. *
  9734. * @param {Object} [props={}]
  9735. * An object of properties that should be set on the element.
  9736. *
  9737. * @param {Object} [attributes={}]
  9738. * An object of attributes that should be set on the element.
  9739. *
  9740. * @return {Element}
  9741. * The element that gets created.
  9742. */
  9743. createEl(tag, props = {}, attributes = {}) {
  9744. tag = 'button';
  9745. props = Object.assign({
  9746. className: this.buildCSSClass()
  9747. }, props);
  9748. // Add attributes for button element
  9749. attributes = Object.assign({
  9750. // Necessary since the default button type is "submit"
  9751. type: 'button'
  9752. }, attributes);
  9753. const el = createEl(tag, props, attributes);
  9754. el.appendChild(createEl('span', {
  9755. className: 'vjs-icon-placeholder'
  9756. }, {
  9757. 'aria-hidden': true
  9758. }));
  9759. this.createControlTextEl(el);
  9760. return el;
  9761. }
  9762. /**
  9763. * Add a child `Component` inside of this `Button`.
  9764. *
  9765. * @param {string|Component} child
  9766. * The name or instance of a child to add.
  9767. *
  9768. * @param {Object} [options={}]
  9769. * The key/value store of options that will get passed to children of
  9770. * the child.
  9771. *
  9772. * @return {Component}
  9773. * The `Component` that gets added as a child. When using a string the
  9774. * `Component` will get created by this process.
  9775. *
  9776. * @deprecated since version 5
  9777. */
  9778. addChild(child, options = {}) {
  9779. const className = this.constructor.name;
  9780. log.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
  9781. // Avoid the error message generated by ClickableComponent's addChild method
  9782. return Component.prototype.addChild.call(this, child, options);
  9783. }
  9784. /**
  9785. * Enable the `Button` element so that it can be activated or clicked. Use this with
  9786. * {@link Button#disable}.
  9787. */
  9788. enable() {
  9789. super.enable();
  9790. this.el_.removeAttribute('disabled');
  9791. }
  9792. /**
  9793. * Disable the `Button` element so that it cannot be activated or clicked. Use this with
  9794. * {@link Button#enable}.
  9795. */
  9796. disable() {
  9797. super.disable();
  9798. this.el_.setAttribute('disabled', 'disabled');
  9799. }
  9800. /**
  9801. * This gets called when a `Button` has focus and `keydown` is triggered via a key
  9802. * press.
  9803. *
  9804. * @param {Event} event
  9805. * The event that caused this function to get called.
  9806. *
  9807. * @listens keydown
  9808. */
  9809. handleKeyDown(event) {
  9810. // Ignore Space or Enter key operation, which is handled by the browser for
  9811. // a button - though not for its super class, ClickableComponent. Also,
  9812. // prevent the event from propagating through the DOM and triggering Player
  9813. // hotkeys. We do not preventDefault here because we _want_ the browser to
  9814. // handle it.
  9815. if (keycode__default["default"].isEventKey(event, 'Space') || keycode__default["default"].isEventKey(event, 'Enter')) {
  9816. event.stopPropagation();
  9817. return;
  9818. }
  9819. // Pass keypress handling up for unsupported keys
  9820. super.handleKeyDown(event);
  9821. }
  9822. }
  9823. Component.registerComponent('Button', Button);
  9824. /**
  9825. * @file big-play-button.js
  9826. */
  9827. /**
  9828. * The initial play button that shows before the video has played. The hiding of the
  9829. * `BigPlayButton` get done via CSS and `Player` states.
  9830. *
  9831. * @extends Button
  9832. */
  9833. class BigPlayButton extends Button {
  9834. constructor(player, options) {
  9835. super(player, options);
  9836. this.mouseused_ = false;
  9837. this.on('mousedown', e => this.handleMouseDown(e));
  9838. }
  9839. /**
  9840. * Builds the default DOM `className`.
  9841. *
  9842. * @return {string}
  9843. * The DOM `className` for this object. Always returns 'vjs-big-play-button'.
  9844. */
  9845. buildCSSClass() {
  9846. return 'vjs-big-play-button';
  9847. }
  9848. /**
  9849. * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
  9850. * for more detailed information on what a click can be.
  9851. *
  9852. * @param {KeyboardEvent} event
  9853. * The `keydown`, `tap`, or `click` event that caused this function to be
  9854. * called.
  9855. *
  9856. * @listens tap
  9857. * @listens click
  9858. */
  9859. handleClick(event) {
  9860. const playPromise = this.player_.play();
  9861. // exit early if clicked via the mouse
  9862. if (this.mouseused_ && event.clientX && event.clientY) {
  9863. silencePromise(playPromise);
  9864. if (this.player_.tech(true)) {
  9865. this.player_.tech(true).focus();
  9866. }
  9867. return;
  9868. }
  9869. const cb = this.player_.getChild('controlBar');
  9870. const playToggle = cb && cb.getChild('playToggle');
  9871. if (!playToggle) {
  9872. this.player_.tech(true).focus();
  9873. return;
  9874. }
  9875. const playFocus = () => playToggle.focus();
  9876. if (isPromise(playPromise)) {
  9877. playPromise.then(playFocus, () => {});
  9878. } else {
  9879. this.setTimeout(playFocus, 1);
  9880. }
  9881. }
  9882. handleKeyDown(event) {
  9883. this.mouseused_ = false;
  9884. super.handleKeyDown(event);
  9885. }
  9886. handleMouseDown(event) {
  9887. this.mouseused_ = true;
  9888. }
  9889. }
  9890. /**
  9891. * The text that should display over the `BigPlayButton`s controls. Added to for localization.
  9892. *
  9893. * @type {string}
  9894. * @protected
  9895. */
  9896. BigPlayButton.prototype.controlText_ = 'Play Video';
  9897. Component.registerComponent('BigPlayButton', BigPlayButton);
  9898. /**
  9899. * @file close-button.js
  9900. */
  9901. /**
  9902. * The `CloseButton` is a `{@link Button}` that fires a `close` event when
  9903. * it gets clicked.
  9904. *
  9905. * @extends Button
  9906. */
  9907. class CloseButton extends Button {
  9908. /**
  9909. * Creates an instance of the this class.
  9910. *
  9911. * @param { import('./player').default } player
  9912. * The `Player` that this class should be attached to.
  9913. *
  9914. * @param {Object} [options]
  9915. * The key/value store of player options.
  9916. */
  9917. constructor(player, options) {
  9918. super(player, options);
  9919. this.controlText(options && options.controlText || this.localize('Close'));
  9920. }
  9921. /**
  9922. * Builds the default DOM `className`.
  9923. *
  9924. * @return {string}
  9925. * The DOM `className` for this object.
  9926. */
  9927. buildCSSClass() {
  9928. return `vjs-close-button ${super.buildCSSClass()}`;
  9929. }
  9930. /**
  9931. * This gets called when a `CloseButton` gets clicked. See
  9932. * {@link ClickableComponent#handleClick} for more information on when
  9933. * this will be triggered
  9934. *
  9935. * @param {Event} event
  9936. * The `keydown`, `tap`, or `click` event that caused this function to be
  9937. * called.
  9938. *
  9939. * @listens tap
  9940. * @listens click
  9941. * @fires CloseButton#close
  9942. */
  9943. handleClick(event) {
  9944. /**
  9945. * Triggered when the a `CloseButton` is clicked.
  9946. *
  9947. * @event CloseButton#close
  9948. * @type {Event}
  9949. *
  9950. * @property {boolean} [bubbles=false]
  9951. * set to false so that the close event does not
  9952. * bubble up to parents if there is no listener
  9953. */
  9954. this.trigger({
  9955. type: 'close',
  9956. bubbles: false
  9957. });
  9958. }
  9959. /**
  9960. * Event handler that is called when a `CloseButton` receives a
  9961. * `keydown` event.
  9962. *
  9963. * By default, if the key is Esc, it will trigger a `click` event.
  9964. *
  9965. * @param {Event} event
  9966. * The `keydown` event that caused this function to be called.
  9967. *
  9968. * @listens keydown
  9969. */
  9970. handleKeyDown(event) {
  9971. // Esc button will trigger `click` event
  9972. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  9973. event.preventDefault();
  9974. event.stopPropagation();
  9975. this.trigger('click');
  9976. } else {
  9977. // Pass keypress handling up for unsupported keys
  9978. super.handleKeyDown(event);
  9979. }
  9980. }
  9981. }
  9982. Component.registerComponent('CloseButton', CloseButton);
  9983. /**
  9984. * @file play-toggle.js
  9985. */
  9986. /**
  9987. * Button to toggle between play and pause.
  9988. *
  9989. * @extends Button
  9990. */
  9991. class PlayToggle extends Button {
  9992. /**
  9993. * Creates an instance of this class.
  9994. *
  9995. * @param { import('./player').default } player
  9996. * The `Player` that this class should be attached to.
  9997. *
  9998. * @param {Object} [options={}]
  9999. * The key/value store of player options.
  10000. */
  10001. constructor(player, options = {}) {
  10002. super(player, options);
  10003. // show or hide replay icon
  10004. options.replay = options.replay === undefined || options.replay;
  10005. this.on(player, 'play', e => this.handlePlay(e));
  10006. this.on(player, 'pause', e => this.handlePause(e));
  10007. if (options.replay) {
  10008. this.on(player, 'ended', e => this.handleEnded(e));
  10009. }
  10010. }
  10011. /**
  10012. * Builds the default DOM `className`.
  10013. *
  10014. * @return {string}
  10015. * The DOM `className` for this object.
  10016. */
  10017. buildCSSClass() {
  10018. return `vjs-play-control ${super.buildCSSClass()}`;
  10019. }
  10020. /**
  10021. * This gets called when an `PlayToggle` is "clicked". See
  10022. * {@link ClickableComponent} for more detailed information on what a click can be.
  10023. *
  10024. * @param {Event} [event]
  10025. * The `keydown`, `tap`, or `click` event that caused this function to be
  10026. * called.
  10027. *
  10028. * @listens tap
  10029. * @listens click
  10030. */
  10031. handleClick(event) {
  10032. if (this.player_.paused()) {
  10033. silencePromise(this.player_.play());
  10034. } else {
  10035. this.player_.pause();
  10036. }
  10037. }
  10038. /**
  10039. * This gets called once after the video has ended and the user seeks so that
  10040. * we can change the replay button back to a play button.
  10041. *
  10042. * @param {Event} [event]
  10043. * The event that caused this function to run.
  10044. *
  10045. * @listens Player#seeked
  10046. */
  10047. handleSeeked(event) {
  10048. this.removeClass('vjs-ended');
  10049. if (this.player_.paused()) {
  10050. this.handlePause(event);
  10051. } else {
  10052. this.handlePlay(event);
  10053. }
  10054. }
  10055. /**
  10056. * Add the vjs-playing class to the element so it can change appearance.
  10057. *
  10058. * @param {Event} [event]
  10059. * The event that caused this function to run.
  10060. *
  10061. * @listens Player#play
  10062. */
  10063. handlePlay(event) {
  10064. this.removeClass('vjs-ended', 'vjs-paused');
  10065. this.addClass('vjs-playing');
  10066. // change the button text to "Pause"
  10067. this.controlText('Pause');
  10068. }
  10069. /**
  10070. * Add the vjs-paused class to the element so it can change appearance.
  10071. *
  10072. * @param {Event} [event]
  10073. * The event that caused this function to run.
  10074. *
  10075. * @listens Player#pause
  10076. */
  10077. handlePause(event) {
  10078. this.removeClass('vjs-playing');
  10079. this.addClass('vjs-paused');
  10080. // change the button text to "Play"
  10081. this.controlText('Play');
  10082. }
  10083. /**
  10084. * Add the vjs-ended class to the element so it can change appearance
  10085. *
  10086. * @param {Event} [event]
  10087. * The event that caused this function to run.
  10088. *
  10089. * @listens Player#ended
  10090. */
  10091. handleEnded(event) {
  10092. this.removeClass('vjs-playing');
  10093. this.addClass('vjs-ended');
  10094. // change the button text to "Replay"
  10095. this.controlText('Replay');
  10096. // on the next seek remove the replay button
  10097. this.one(this.player_, 'seeked', e => this.handleSeeked(e));
  10098. }
  10099. }
  10100. /**
  10101. * The text that should display over the `PlayToggle`s controls. Added for localization.
  10102. *
  10103. * @type {string}
  10104. * @protected
  10105. */
  10106. PlayToggle.prototype.controlText_ = 'Play';
  10107. Component.registerComponent('PlayToggle', PlayToggle);
  10108. /**
  10109. * @file time-display.js
  10110. */
  10111. /**
  10112. * Displays time information about the video
  10113. *
  10114. * @extends Component
  10115. */
  10116. class TimeDisplay extends Component {
  10117. /**
  10118. * Creates an instance of this class.
  10119. *
  10120. * @param { import('../../player').default } player
  10121. * The `Player` that this class should be attached to.
  10122. *
  10123. * @param {Object} [options]
  10124. * The key/value store of player options.
  10125. */
  10126. constructor(player, options) {
  10127. super(player, options);
  10128. this.on(player, ['timeupdate', 'ended'], e => this.updateContent(e));
  10129. this.updateTextNode_();
  10130. }
  10131. /**
  10132. * Create the `Component`'s DOM element
  10133. *
  10134. * @return {Element}
  10135. * The element that was created.
  10136. */
  10137. createEl() {
  10138. const className = this.buildCSSClass();
  10139. const el = super.createEl('div', {
  10140. className: `${className} vjs-time-control vjs-control`
  10141. });
  10142. const span = createEl('span', {
  10143. className: 'vjs-control-text',
  10144. textContent: `${this.localize(this.labelText_)}\u00a0`
  10145. }, {
  10146. role: 'presentation'
  10147. });
  10148. el.appendChild(span);
  10149. this.contentEl_ = createEl('span', {
  10150. className: `${className}-display`
  10151. }, {
  10152. // span elements have no implicit role, but some screen readers (notably VoiceOver)
  10153. // treat them as a break between items in the DOM when using arrow keys
  10154. // (or left-to-right swipes on iOS) to read contents of a page. Using
  10155. // role='presentation' causes VoiceOver to NOT treat this span as a break.
  10156. role: 'presentation'
  10157. });
  10158. el.appendChild(this.contentEl_);
  10159. return el;
  10160. }
  10161. dispose() {
  10162. this.contentEl_ = null;
  10163. this.textNode_ = null;
  10164. super.dispose();
  10165. }
  10166. /**
  10167. * Updates the time display text node with a new time
  10168. *
  10169. * @param {number} [time=0] the time to update to
  10170. *
  10171. * @private
  10172. */
  10173. updateTextNode_(time = 0) {
  10174. time = formatTime(time);
  10175. if (this.formattedTime_ === time) {
  10176. return;
  10177. }
  10178. this.formattedTime_ = time;
  10179. this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
  10180. if (!this.contentEl_) {
  10181. return;
  10182. }
  10183. let oldNode = this.textNode_;
  10184. if (oldNode && this.contentEl_.firstChild !== oldNode) {
  10185. oldNode = null;
  10186. log.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.');
  10187. }
  10188. this.textNode_ = document__default["default"].createTextNode(this.formattedTime_);
  10189. if (!this.textNode_) {
  10190. return;
  10191. }
  10192. if (oldNode) {
  10193. this.contentEl_.replaceChild(this.textNode_, oldNode);
  10194. } else {
  10195. this.contentEl_.appendChild(this.textNode_);
  10196. }
  10197. });
  10198. }
  10199. /**
  10200. * To be filled out in the child class, should update the displayed time
  10201. * in accordance with the fact that the current time has changed.
  10202. *
  10203. * @param {Event} [event]
  10204. * The `timeupdate` event that caused this to run.
  10205. *
  10206. * @listens Player#timeupdate
  10207. */
  10208. updateContent(event) {}
  10209. }
  10210. /**
  10211. * The text that is added to the `TimeDisplay` for screen reader users.
  10212. *
  10213. * @type {string}
  10214. * @private
  10215. */
  10216. TimeDisplay.prototype.labelText_ = 'Time';
  10217. /**
  10218. * The text that should display over the `TimeDisplay`s controls. Added to for localization.
  10219. *
  10220. * @type {string}
  10221. * @protected
  10222. *
  10223. * @deprecated in v7; controlText_ is not used in non-active display Components
  10224. */
  10225. TimeDisplay.prototype.controlText_ = 'Time';
  10226. Component.registerComponent('TimeDisplay', TimeDisplay);
  10227. /**
  10228. * @file current-time-display.js
  10229. */
  10230. /**
  10231. * Displays the current time
  10232. *
  10233. * @extends Component
  10234. */
  10235. class CurrentTimeDisplay extends TimeDisplay {
  10236. /**
  10237. * Builds the default DOM `className`.
  10238. *
  10239. * @return {string}
  10240. * The DOM `className` for this object.
  10241. */
  10242. buildCSSClass() {
  10243. return 'vjs-current-time';
  10244. }
  10245. /**
  10246. * Update current time display
  10247. *
  10248. * @param {Event} [event]
  10249. * The `timeupdate` event that caused this function to run.
  10250. *
  10251. * @listens Player#timeupdate
  10252. */
  10253. updateContent(event) {
  10254. // Allows for smooth scrubbing, when player can't keep up.
  10255. let time;
  10256. if (this.player_.ended()) {
  10257. time = this.player_.duration();
  10258. } else {
  10259. time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
  10260. }
  10261. this.updateTextNode_(time);
  10262. }
  10263. }
  10264. /**
  10265. * The text that is added to the `CurrentTimeDisplay` for screen reader users.
  10266. *
  10267. * @type {string}
  10268. * @private
  10269. */
  10270. CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
  10271. /**
  10272. * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
  10273. *
  10274. * @type {string}
  10275. * @protected
  10276. *
  10277. * @deprecated in v7; controlText_ is not used in non-active display Components
  10278. */
  10279. CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
  10280. Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
  10281. /**
  10282. * @file duration-display.js
  10283. */
  10284. /**
  10285. * Displays the duration
  10286. *
  10287. * @extends Component
  10288. */
  10289. class DurationDisplay extends TimeDisplay {
  10290. /**
  10291. * Creates an instance of this class.
  10292. *
  10293. * @param { import('../../player').default } player
  10294. * The `Player` that this class should be attached to.
  10295. *
  10296. * @param {Object} [options]
  10297. * The key/value store of player options.
  10298. */
  10299. constructor(player, options) {
  10300. super(player, options);
  10301. const updateContent = e => this.updateContent(e);
  10302. // we do not want to/need to throttle duration changes,
  10303. // as they should always display the changed duration as
  10304. // it has changed
  10305. this.on(player, 'durationchange', updateContent);
  10306. // Listen to loadstart because the player duration is reset when a new media element is loaded,
  10307. // but the durationchange on the user agent will not fire.
  10308. // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
  10309. this.on(player, 'loadstart', updateContent);
  10310. // Also listen for timeupdate (in the parent) and loadedmetadata because removing those
  10311. // listeners could have broken dependent applications/libraries. These
  10312. // can likely be removed for 7.0.
  10313. this.on(player, 'loadedmetadata', updateContent);
  10314. }
  10315. /**
  10316. * Builds the default DOM `className`.
  10317. *
  10318. * @return {string}
  10319. * The DOM `className` for this object.
  10320. */
  10321. buildCSSClass() {
  10322. return 'vjs-duration';
  10323. }
  10324. /**
  10325. * Update duration time display.
  10326. *
  10327. * @param {Event} [event]
  10328. * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
  10329. * this function to be called.
  10330. *
  10331. * @listens Player#durationchange
  10332. * @listens Player#timeupdate
  10333. * @listens Player#loadedmetadata
  10334. */
  10335. updateContent(event) {
  10336. const duration = this.player_.duration();
  10337. this.updateTextNode_(duration);
  10338. }
  10339. }
  10340. /**
  10341. * The text that is added to the `DurationDisplay` for screen reader users.
  10342. *
  10343. * @type {string}
  10344. * @private
  10345. */
  10346. DurationDisplay.prototype.labelText_ = 'Duration';
  10347. /**
  10348. * The text that should display over the `DurationDisplay`s controls. Added to for localization.
  10349. *
  10350. * @type {string}
  10351. * @protected
  10352. *
  10353. * @deprecated in v7; controlText_ is not used in non-active display Components
  10354. */
  10355. DurationDisplay.prototype.controlText_ = 'Duration';
  10356. Component.registerComponent('DurationDisplay', DurationDisplay);
  10357. /**
  10358. * @file time-divider.js
  10359. */
  10360. /**
  10361. * The separator between the current time and duration.
  10362. * Can be hidden if it's not needed in the design.
  10363. *
  10364. * @extends Component
  10365. */
  10366. class TimeDivider extends Component {
  10367. /**
  10368. * Create the component's DOM element
  10369. *
  10370. * @return {Element}
  10371. * The element that was created.
  10372. */
  10373. createEl() {
  10374. const el = super.createEl('div', {
  10375. className: 'vjs-time-control vjs-time-divider'
  10376. }, {
  10377. // this element and its contents can be hidden from assistive techs since
  10378. // it is made extraneous by the announcement of the control text
  10379. // for the current time and duration displays
  10380. 'aria-hidden': true
  10381. });
  10382. const div = super.createEl('div');
  10383. const span = super.createEl('span', {
  10384. textContent: '/'
  10385. });
  10386. div.appendChild(span);
  10387. el.appendChild(div);
  10388. return el;
  10389. }
  10390. }
  10391. Component.registerComponent('TimeDivider', TimeDivider);
  10392. /**
  10393. * @file remaining-time-display.js
  10394. */
  10395. /**
  10396. * Displays the time left in the video
  10397. *
  10398. * @extends Component
  10399. */
  10400. class RemainingTimeDisplay extends TimeDisplay {
  10401. /**
  10402. * Creates an instance of this class.
  10403. *
  10404. * @param { import('../../player').default } player
  10405. * The `Player` that this class should be attached to.
  10406. *
  10407. * @param {Object} [options]
  10408. * The key/value store of player options.
  10409. */
  10410. constructor(player, options) {
  10411. super(player, options);
  10412. this.on(player, 'durationchange', e => this.updateContent(e));
  10413. }
  10414. /**
  10415. * Builds the default DOM `className`.
  10416. *
  10417. * @return {string}
  10418. * The DOM `className` for this object.
  10419. */
  10420. buildCSSClass() {
  10421. return 'vjs-remaining-time';
  10422. }
  10423. /**
  10424. * Create the `Component`'s DOM element with the "minus" character prepend to the time
  10425. *
  10426. * @return {Element}
  10427. * The element that was created.
  10428. */
  10429. createEl() {
  10430. const el = super.createEl();
  10431. if (this.options_.displayNegative !== false) {
  10432. el.insertBefore(createEl('span', {}, {
  10433. 'aria-hidden': true
  10434. }, '-'), this.contentEl_);
  10435. }
  10436. return el;
  10437. }
  10438. /**
  10439. * Update remaining time display.
  10440. *
  10441. * @param {Event} [event]
  10442. * The `timeupdate` or `durationchange` event that caused this to run.
  10443. *
  10444. * @listens Player#timeupdate
  10445. * @listens Player#durationchange
  10446. */
  10447. updateContent(event) {
  10448. if (typeof this.player_.duration() !== 'number') {
  10449. return;
  10450. }
  10451. let time;
  10452. // @deprecated We should only use remainingTimeDisplay
  10453. // as of video.js 7
  10454. if (this.player_.ended()) {
  10455. time = 0;
  10456. } else if (this.player_.remainingTimeDisplay) {
  10457. time = this.player_.remainingTimeDisplay();
  10458. } else {
  10459. time = this.player_.remainingTime();
  10460. }
  10461. this.updateTextNode_(time);
  10462. }
  10463. }
  10464. /**
  10465. * The text that is added to the `RemainingTimeDisplay` for screen reader users.
  10466. *
  10467. * @type {string}
  10468. * @private
  10469. */
  10470. RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
  10471. /**
  10472. * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
  10473. *
  10474. * @type {string}
  10475. * @protected
  10476. *
  10477. * @deprecated in v7; controlText_ is not used in non-active display Components
  10478. */
  10479. RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
  10480. Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
  10481. /**
  10482. * @file live-display.js
  10483. */
  10484. // TODO - Future make it click to snap to live
  10485. /**
  10486. * Displays the live indicator when duration is Infinity.
  10487. *
  10488. * @extends Component
  10489. */
  10490. class LiveDisplay extends Component {
  10491. /**
  10492. * Creates an instance of this class.
  10493. *
  10494. * @param { import('./player').default } player
  10495. * The `Player` that this class should be attached to.
  10496. *
  10497. * @param {Object} [options]
  10498. * The key/value store of player options.
  10499. */
  10500. constructor(player, options) {
  10501. super(player, options);
  10502. this.updateShowing();
  10503. this.on(this.player(), 'durationchange', e => this.updateShowing(e));
  10504. }
  10505. /**
  10506. * Create the `Component`'s DOM element
  10507. *
  10508. * @return {Element}
  10509. * The element that was created.
  10510. */
  10511. createEl() {
  10512. const el = super.createEl('div', {
  10513. className: 'vjs-live-control vjs-control'
  10514. });
  10515. this.contentEl_ = createEl('div', {
  10516. className: 'vjs-live-display'
  10517. }, {
  10518. 'aria-live': 'off'
  10519. });
  10520. this.contentEl_.appendChild(createEl('span', {
  10521. className: 'vjs-control-text',
  10522. textContent: `${this.localize('Stream Type')}\u00a0`
  10523. }));
  10524. this.contentEl_.appendChild(document__default["default"].createTextNode(this.localize('LIVE')));
  10525. el.appendChild(this.contentEl_);
  10526. return el;
  10527. }
  10528. dispose() {
  10529. this.contentEl_ = null;
  10530. super.dispose();
  10531. }
  10532. /**
  10533. * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
  10534. * it accordingly
  10535. *
  10536. * @param {Event} [event]
  10537. * The {@link Player#durationchange} event that caused this function to run.
  10538. *
  10539. * @listens Player#durationchange
  10540. */
  10541. updateShowing(event) {
  10542. if (this.player().duration() === Infinity) {
  10543. this.show();
  10544. } else {
  10545. this.hide();
  10546. }
  10547. }
  10548. }
  10549. Component.registerComponent('LiveDisplay', LiveDisplay);
  10550. /**
  10551. * @file seek-to-live.js
  10552. */
  10553. /**
  10554. * Displays the live indicator when duration is Infinity.
  10555. *
  10556. * @extends Component
  10557. */
  10558. class SeekToLive extends Button {
  10559. /**
  10560. * Creates an instance of this class.
  10561. *
  10562. * @param { import('./player').default } player
  10563. * The `Player` that this class should be attached to.
  10564. *
  10565. * @param {Object} [options]
  10566. * The key/value store of player options.
  10567. */
  10568. constructor(player, options) {
  10569. super(player, options);
  10570. this.updateLiveEdgeStatus();
  10571. if (this.player_.liveTracker) {
  10572. this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
  10573. this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
  10574. }
  10575. }
  10576. /**
  10577. * Create the `Component`'s DOM element
  10578. *
  10579. * @return {Element}
  10580. * The element that was created.
  10581. */
  10582. createEl() {
  10583. const el = super.createEl('button', {
  10584. className: 'vjs-seek-to-live-control vjs-control'
  10585. });
  10586. this.textEl_ = createEl('span', {
  10587. className: 'vjs-seek-to-live-text',
  10588. textContent: this.localize('LIVE')
  10589. }, {
  10590. 'aria-hidden': 'true'
  10591. });
  10592. el.appendChild(this.textEl_);
  10593. return el;
  10594. }
  10595. /**
  10596. * Update the state of this button if we are at the live edge
  10597. * or not
  10598. */
  10599. updateLiveEdgeStatus() {
  10600. // default to live edge
  10601. if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
  10602. this.setAttribute('aria-disabled', true);
  10603. this.addClass('vjs-at-live-edge');
  10604. this.controlText('Seek to live, currently playing live');
  10605. } else {
  10606. this.setAttribute('aria-disabled', false);
  10607. this.removeClass('vjs-at-live-edge');
  10608. this.controlText('Seek to live, currently behind live');
  10609. }
  10610. }
  10611. /**
  10612. * On click bring us as near to the live point as possible.
  10613. * This requires that we wait for the next `live-seekable-change`
  10614. * event which will happen every segment length seconds.
  10615. */
  10616. handleClick() {
  10617. this.player_.liveTracker.seekToLiveEdge();
  10618. }
  10619. /**
  10620. * Dispose of the element and stop tracking
  10621. */
  10622. dispose() {
  10623. if (this.player_.liveTracker) {
  10624. this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
  10625. }
  10626. this.textEl_ = null;
  10627. super.dispose();
  10628. }
  10629. }
  10630. /**
  10631. * The text that should display over the `SeekToLive`s control. Added for localization.
  10632. *
  10633. * @type {string}
  10634. * @protected
  10635. */
  10636. SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
  10637. Component.registerComponent('SeekToLive', SeekToLive);
  10638. /**
  10639. * @file num.js
  10640. * @module num
  10641. */
  10642. /**
  10643. * Keep a number between a min and a max value
  10644. *
  10645. * @param {number} number
  10646. * The number to clamp
  10647. *
  10648. * @param {number} min
  10649. * The minimum value
  10650. * @param {number} max
  10651. * The maximum value
  10652. *
  10653. * @return {number}
  10654. * the clamped number
  10655. */
  10656. function clamp(number, min, max) {
  10657. number = Number(number);
  10658. return Math.min(max, Math.max(min, isNaN(number) ? min : number));
  10659. }
  10660. var Num = /*#__PURE__*/Object.freeze({
  10661. __proto__: null,
  10662. clamp: clamp
  10663. });
  10664. /**
  10665. * @file slider.js
  10666. */
  10667. /**
  10668. * The base functionality for a slider. Can be vertical or horizontal.
  10669. * For instance the volume bar or the seek bar on a video is a slider.
  10670. *
  10671. * @extends Component
  10672. */
  10673. class Slider extends Component {
  10674. /**
  10675. * Create an instance of this class
  10676. *
  10677. * @param { import('../player').default } player
  10678. * The `Player` that this class should be attached to.
  10679. *
  10680. * @param {Object} [options]
  10681. * The key/value store of player options.
  10682. */
  10683. constructor(player, options) {
  10684. super(player, options);
  10685. this.handleMouseDown_ = e => this.handleMouseDown(e);
  10686. this.handleMouseUp_ = e => this.handleMouseUp(e);
  10687. this.handleKeyDown_ = e => this.handleKeyDown(e);
  10688. this.handleClick_ = e => this.handleClick(e);
  10689. this.handleMouseMove_ = e => this.handleMouseMove(e);
  10690. this.update_ = e => this.update(e);
  10691. // Set property names to bar to match with the child Slider class is looking for
  10692. this.bar = this.getChild(this.options_.barName);
  10693. // Set a horizontal or vertical class on the slider depending on the slider type
  10694. this.vertical(!!this.options_.vertical);
  10695. this.enable();
  10696. }
  10697. /**
  10698. * Are controls are currently enabled for this slider or not.
  10699. *
  10700. * @return {boolean}
  10701. * true if controls are enabled, false otherwise
  10702. */
  10703. enabled() {
  10704. return this.enabled_;
  10705. }
  10706. /**
  10707. * Enable controls for this slider if they are disabled
  10708. */
  10709. enable() {
  10710. if (this.enabled()) {
  10711. return;
  10712. }
  10713. this.on('mousedown', this.handleMouseDown_);
  10714. this.on('touchstart', this.handleMouseDown_);
  10715. this.on('keydown', this.handleKeyDown_);
  10716. this.on('click', this.handleClick_);
  10717. // TODO: deprecated, controlsvisible does not seem to be fired
  10718. this.on(this.player_, 'controlsvisible', this.update);
  10719. if (this.playerEvent) {
  10720. this.on(this.player_, this.playerEvent, this.update);
  10721. }
  10722. this.removeClass('disabled');
  10723. this.setAttribute('tabindex', 0);
  10724. this.enabled_ = true;
  10725. }
  10726. /**
  10727. * Disable controls for this slider if they are enabled
  10728. */
  10729. disable() {
  10730. if (!this.enabled()) {
  10731. return;
  10732. }
  10733. const doc = this.bar.el_.ownerDocument;
  10734. this.off('mousedown', this.handleMouseDown_);
  10735. this.off('touchstart', this.handleMouseDown_);
  10736. this.off('keydown', this.handleKeyDown_);
  10737. this.off('click', this.handleClick_);
  10738. this.off(this.player_, 'controlsvisible', this.update_);
  10739. this.off(doc, 'mousemove', this.handleMouseMove_);
  10740. this.off(doc, 'mouseup', this.handleMouseUp_);
  10741. this.off(doc, 'touchmove', this.handleMouseMove_);
  10742. this.off(doc, 'touchend', this.handleMouseUp_);
  10743. this.removeAttribute('tabindex');
  10744. this.addClass('disabled');
  10745. if (this.playerEvent) {
  10746. this.off(this.player_, this.playerEvent, this.update);
  10747. }
  10748. this.enabled_ = false;
  10749. }
  10750. /**
  10751. * Create the `Slider`s DOM element.
  10752. *
  10753. * @param {string} type
  10754. * Type of element to create.
  10755. *
  10756. * @param {Object} [props={}]
  10757. * List of properties in Object form.
  10758. *
  10759. * @param {Object} [attributes={}]
  10760. * list of attributes in Object form.
  10761. *
  10762. * @return {Element}
  10763. * The element that gets created.
  10764. */
  10765. createEl(type, props = {}, attributes = {}) {
  10766. // Add the slider element class to all sub classes
  10767. props.className = props.className + ' vjs-slider';
  10768. props = Object.assign({
  10769. tabIndex: 0
  10770. }, props);
  10771. attributes = Object.assign({
  10772. 'role': 'slider',
  10773. 'aria-valuenow': 0,
  10774. 'aria-valuemin': 0,
  10775. 'aria-valuemax': 100
  10776. }, attributes);
  10777. return super.createEl(type, props, attributes);
  10778. }
  10779. /**
  10780. * Handle `mousedown` or `touchstart` events on the `Slider`.
  10781. *
  10782. * @param {MouseEvent} event
  10783. * `mousedown` or `touchstart` event that triggered this function
  10784. *
  10785. * @listens mousedown
  10786. * @listens touchstart
  10787. * @fires Slider#slideractive
  10788. */
  10789. handleMouseDown(event) {
  10790. const doc = this.bar.el_.ownerDocument;
  10791. if (event.type === 'mousedown') {
  10792. event.preventDefault();
  10793. }
  10794. // Do not call preventDefault() on touchstart in Chrome
  10795. // to avoid console warnings. Use a 'touch-action: none' style
  10796. // instead to prevent unintended scrolling.
  10797. // https://developers.google.com/web/updates/2017/01/scrolling-intervention
  10798. if (event.type === 'touchstart' && !IS_CHROME) {
  10799. event.preventDefault();
  10800. }
  10801. blockTextSelection();
  10802. this.addClass('vjs-sliding');
  10803. /**
  10804. * Triggered when the slider is in an active state
  10805. *
  10806. * @event Slider#slideractive
  10807. * @type {MouseEvent}
  10808. */
  10809. this.trigger('slideractive');
  10810. this.on(doc, 'mousemove', this.handleMouseMove_);
  10811. this.on(doc, 'mouseup', this.handleMouseUp_);
  10812. this.on(doc, 'touchmove', this.handleMouseMove_);
  10813. this.on(doc, 'touchend', this.handleMouseUp_);
  10814. this.handleMouseMove(event, true);
  10815. }
  10816. /**
  10817. * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
  10818. * The `mousemove` and `touchmove` events will only only trigger this function during
  10819. * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
  10820. * {@link Slider#handleMouseUp}.
  10821. *
  10822. * @param {MouseEvent} event
  10823. * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
  10824. * this function
  10825. * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
  10826. *
  10827. * @listens mousemove
  10828. * @listens touchmove
  10829. */
  10830. handleMouseMove(event) {}
  10831. /**
  10832. * Handle `mouseup` or `touchend` events on the `Slider`.
  10833. *
  10834. * @param {MouseEvent} event
  10835. * `mouseup` or `touchend` event that triggered this function.
  10836. *
  10837. * @listens touchend
  10838. * @listens mouseup
  10839. * @fires Slider#sliderinactive
  10840. */
  10841. handleMouseUp(event) {
  10842. const doc = this.bar.el_.ownerDocument;
  10843. unblockTextSelection();
  10844. this.removeClass('vjs-sliding');
  10845. /**
  10846. * Triggered when the slider is no longer in an active state.
  10847. *
  10848. * @event Slider#sliderinactive
  10849. * @type {Event}
  10850. */
  10851. this.trigger('sliderinactive');
  10852. this.off(doc, 'mousemove', this.handleMouseMove_);
  10853. this.off(doc, 'mouseup', this.handleMouseUp_);
  10854. this.off(doc, 'touchmove', this.handleMouseMove_);
  10855. this.off(doc, 'touchend', this.handleMouseUp_);
  10856. this.update();
  10857. }
  10858. /**
  10859. * Update the progress bar of the `Slider`.
  10860. *
  10861. * @return {number}
  10862. * The percentage of progress the progress bar represents as a
  10863. * number from 0 to 1.
  10864. */
  10865. update() {
  10866. // In VolumeBar init we have a setTimeout for update that pops and update
  10867. // to the end of the execution stack. The player is destroyed before then
  10868. // update will cause an error
  10869. // If there's no bar...
  10870. if (!this.el_ || !this.bar) {
  10871. return;
  10872. }
  10873. // clamp progress between 0 and 1
  10874. // and only round to four decimal places, as we round to two below
  10875. const progress = this.getProgress();
  10876. if (progress === this.progress_) {
  10877. return progress;
  10878. }
  10879. this.progress_ = progress;
  10880. this.requestNamedAnimationFrame('Slider#update', () => {
  10881. // Set the new bar width or height
  10882. const sizeKey = this.vertical() ? 'height' : 'width';
  10883. // Convert to a percentage for css value
  10884. this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
  10885. });
  10886. return progress;
  10887. }
  10888. /**
  10889. * Get the percentage of the bar that should be filled
  10890. * but clamped and rounded.
  10891. *
  10892. * @return {number}
  10893. * percentage filled that the slider is
  10894. */
  10895. getProgress() {
  10896. return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
  10897. }
  10898. /**
  10899. * Calculate distance for slider
  10900. *
  10901. * @param {Event} event
  10902. * The event that caused this function to run.
  10903. *
  10904. * @return {number}
  10905. * The current position of the Slider.
  10906. * - position.x for vertical `Slider`s
  10907. * - position.y for horizontal `Slider`s
  10908. */
  10909. calculateDistance(event) {
  10910. const position = getPointerPosition(this.el_, event);
  10911. if (this.vertical()) {
  10912. return position.y;
  10913. }
  10914. return position.x;
  10915. }
  10916. /**
  10917. * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
  10918. * arrow keys. This function will only be called when the slider has focus. See
  10919. * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
  10920. *
  10921. * @param {KeyboardEvent} event
  10922. * the `keydown` event that caused this function to run.
  10923. *
  10924. * @listens keydown
  10925. */
  10926. handleKeyDown(event) {
  10927. // Left and Down Arrows
  10928. if (keycode__default["default"].isEventKey(event, 'Left') || keycode__default["default"].isEventKey(event, 'Down')) {
  10929. event.preventDefault();
  10930. event.stopPropagation();
  10931. this.stepBack();
  10932. // Up and Right Arrows
  10933. } else if (keycode__default["default"].isEventKey(event, 'Right') || keycode__default["default"].isEventKey(event, 'Up')) {
  10934. event.preventDefault();
  10935. event.stopPropagation();
  10936. this.stepForward();
  10937. } else {
  10938. // Pass keydown handling up for unsupported keys
  10939. super.handleKeyDown(event);
  10940. }
  10941. }
  10942. /**
  10943. * Listener for click events on slider, used to prevent clicks
  10944. * from bubbling up to parent elements like button menus.
  10945. *
  10946. * @param {Object} event
  10947. * Event that caused this object to run
  10948. */
  10949. handleClick(event) {
  10950. event.stopPropagation();
  10951. event.preventDefault();
  10952. }
  10953. /**
  10954. * Get/set if slider is horizontal for vertical
  10955. *
  10956. * @param {boolean} [bool]
  10957. * - true if slider is vertical,
  10958. * - false is horizontal
  10959. *
  10960. * @return {boolean}
  10961. * - true if slider is vertical, and getting
  10962. * - false if the slider is horizontal, and getting
  10963. */
  10964. vertical(bool) {
  10965. if (bool === undefined) {
  10966. return this.vertical_ || false;
  10967. }
  10968. this.vertical_ = !!bool;
  10969. if (this.vertical_) {
  10970. this.addClass('vjs-slider-vertical');
  10971. } else {
  10972. this.addClass('vjs-slider-horizontal');
  10973. }
  10974. }
  10975. }
  10976. Component.registerComponent('Slider', Slider);
  10977. /**
  10978. * @file load-progress-bar.js
  10979. */
  10980. // get the percent width of a time compared to the total end
  10981. const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
  10982. /**
  10983. * Shows loading progress
  10984. *
  10985. * @extends Component
  10986. */
  10987. class LoadProgressBar extends Component {
  10988. /**
  10989. * Creates an instance of this class.
  10990. *
  10991. * @param { import('../../player').default } player
  10992. * The `Player` that this class should be attached to.
  10993. *
  10994. * @param {Object} [options]
  10995. * The key/value store of player options.
  10996. */
  10997. constructor(player, options) {
  10998. super(player, options);
  10999. this.partEls_ = [];
  11000. this.on(player, 'progress', e => this.update(e));
  11001. }
  11002. /**
  11003. * Create the `Component`'s DOM element
  11004. *
  11005. * @return {Element}
  11006. * The element that was created.
  11007. */
  11008. createEl() {
  11009. const el = super.createEl('div', {
  11010. className: 'vjs-load-progress'
  11011. });
  11012. const wrapper = createEl('span', {
  11013. className: 'vjs-control-text'
  11014. });
  11015. const loadedText = createEl('span', {
  11016. textContent: this.localize('Loaded')
  11017. });
  11018. const separator = document__default["default"].createTextNode(': ');
  11019. this.percentageEl_ = createEl('span', {
  11020. className: 'vjs-control-text-loaded-percentage',
  11021. textContent: '0%'
  11022. });
  11023. el.appendChild(wrapper);
  11024. wrapper.appendChild(loadedText);
  11025. wrapper.appendChild(separator);
  11026. wrapper.appendChild(this.percentageEl_);
  11027. return el;
  11028. }
  11029. dispose() {
  11030. this.partEls_ = null;
  11031. this.percentageEl_ = null;
  11032. super.dispose();
  11033. }
  11034. /**
  11035. * Update progress bar
  11036. *
  11037. * @param {Event} [event]
  11038. * The `progress` event that caused this function to run.
  11039. *
  11040. * @listens Player#progress
  11041. */
  11042. update(event) {
  11043. this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
  11044. const liveTracker = this.player_.liveTracker;
  11045. const buffered = this.player_.buffered();
  11046. const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
  11047. const bufferedEnd = this.player_.bufferedEnd();
  11048. const children = this.partEls_;
  11049. const percent = percentify(bufferedEnd, duration);
  11050. if (this.percent_ !== percent) {
  11051. // update the width of the progress bar
  11052. this.el_.style.width = percent;
  11053. // update the control-text
  11054. textContent(this.percentageEl_, percent);
  11055. this.percent_ = percent;
  11056. }
  11057. // add child elements to represent the individual buffered time ranges
  11058. for (let i = 0; i < buffered.length; i++) {
  11059. const start = buffered.start(i);
  11060. const end = buffered.end(i);
  11061. let part = children[i];
  11062. if (!part) {
  11063. part = this.el_.appendChild(createEl());
  11064. children[i] = part;
  11065. }
  11066. // only update if changed
  11067. if (part.dataset.start === start && part.dataset.end === end) {
  11068. continue;
  11069. }
  11070. part.dataset.start = start;
  11071. part.dataset.end = end;
  11072. // set the percent based on the width of the progress bar (bufferedEnd)
  11073. part.style.left = percentify(start, bufferedEnd);
  11074. part.style.width = percentify(end - start, bufferedEnd);
  11075. }
  11076. // remove unused buffered range elements
  11077. for (let i = children.length; i > buffered.length; i--) {
  11078. this.el_.removeChild(children[i - 1]);
  11079. }
  11080. children.length = buffered.length;
  11081. });
  11082. }
  11083. }
  11084. Component.registerComponent('LoadProgressBar', LoadProgressBar);
  11085. /**
  11086. * @file time-tooltip.js
  11087. */
  11088. /**
  11089. * Time tooltips display a time above the progress bar.
  11090. *
  11091. * @extends Component
  11092. */
  11093. class TimeTooltip extends Component {
  11094. /**
  11095. * Creates an instance of this class.
  11096. *
  11097. * @param { import('../../player').default } player
  11098. * The {@link Player} that this class should be attached to.
  11099. *
  11100. * @param {Object} [options]
  11101. * The key/value store of player options.
  11102. */
  11103. constructor(player, options) {
  11104. super(player, options);
  11105. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  11106. }
  11107. /**
  11108. * Create the time tooltip DOM element
  11109. *
  11110. * @return {Element}
  11111. * The element that was created.
  11112. */
  11113. createEl() {
  11114. return super.createEl('div', {
  11115. className: 'vjs-time-tooltip'
  11116. }, {
  11117. 'aria-hidden': 'true'
  11118. });
  11119. }
  11120. /**
  11121. * Updates the position of the time tooltip relative to the `SeekBar`.
  11122. *
  11123. * @param {Object} seekBarRect
  11124. * The `ClientRect` for the {@link SeekBar} element.
  11125. *
  11126. * @param {number} seekBarPoint
  11127. * A number from 0 to 1, representing a horizontal reference point
  11128. * from the left edge of the {@link SeekBar}
  11129. */
  11130. update(seekBarRect, seekBarPoint, content) {
  11131. const tooltipRect = findPosition(this.el_);
  11132. const playerRect = getBoundingClientRect(this.player_.el());
  11133. const seekBarPointPx = seekBarRect.width * seekBarPoint;
  11134. // do nothing if either rect isn't available
  11135. // for example, if the player isn't in the DOM for testing
  11136. if (!playerRect || !tooltipRect) {
  11137. return;
  11138. }
  11139. // This is the space left of the `seekBarPoint` available within the bounds
  11140. // of the player. We calculate any gap between the left edge of the player
  11141. // and the left edge of the `SeekBar` and add the number of pixels in the
  11142. // `SeekBar` before hitting the `seekBarPoint`
  11143. const spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
  11144. // This is the space right of the `seekBarPoint` available within the bounds
  11145. // of the player. We calculate the number of pixels from the `seekBarPoint`
  11146. // to the right edge of the `SeekBar` and add to that any gap between the
  11147. // right edge of the `SeekBar` and the player.
  11148. const spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
  11149. // This is the number of pixels by which the tooltip will need to be pulled
  11150. // further to the right to center it over the `seekBarPoint`.
  11151. let pullTooltipBy = tooltipRect.width / 2;
  11152. // Adjust the `pullTooltipBy` distance to the left or right depending on
  11153. // the results of the space calculations above.
  11154. if (spaceLeftOfPoint < pullTooltipBy) {
  11155. pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
  11156. } else if (spaceRightOfPoint < pullTooltipBy) {
  11157. pullTooltipBy = spaceRightOfPoint;
  11158. }
  11159. // Due to the imprecision of decimal/ratio based calculations and varying
  11160. // rounding behaviors, there are cases where the spacing adjustment is off
  11161. // by a pixel or two. This adds insurance to these calculations.
  11162. if (pullTooltipBy < 0) {
  11163. pullTooltipBy = 0;
  11164. } else if (pullTooltipBy > tooltipRect.width) {
  11165. pullTooltipBy = tooltipRect.width;
  11166. }
  11167. // prevent small width fluctuations within 0.4px from
  11168. // changing the value below.
  11169. // This really helps for live to prevent the play
  11170. // progress time tooltip from jittering
  11171. pullTooltipBy = Math.round(pullTooltipBy);
  11172. this.el_.style.right = `-${pullTooltipBy}px`;
  11173. this.write(content);
  11174. }
  11175. /**
  11176. * Write the time to the tooltip DOM element.
  11177. *
  11178. * @param {string} content
  11179. * The formatted time for the tooltip.
  11180. */
  11181. write(content) {
  11182. textContent(this.el_, content);
  11183. }
  11184. /**
  11185. * Updates the position of the time tooltip relative to the `SeekBar`.
  11186. *
  11187. * @param {Object} seekBarRect
  11188. * The `ClientRect` for the {@link SeekBar} element.
  11189. *
  11190. * @param {number} seekBarPoint
  11191. * A number from 0 to 1, representing a horizontal reference point
  11192. * from the left edge of the {@link SeekBar}
  11193. *
  11194. * @param {number} time
  11195. * The time to update the tooltip to, not used during live playback
  11196. *
  11197. * @param {Function} cb
  11198. * A function that will be called during the request animation frame
  11199. * for tooltips that need to do additional animations from the default
  11200. */
  11201. updateTime(seekBarRect, seekBarPoint, time, cb) {
  11202. this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
  11203. let content;
  11204. const duration = this.player_.duration();
  11205. if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
  11206. const liveWindow = this.player_.liveTracker.liveWindow();
  11207. const secondsBehind = liveWindow - seekBarPoint * liveWindow;
  11208. content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
  11209. } else {
  11210. content = formatTime(time, duration);
  11211. }
  11212. this.update(seekBarRect, seekBarPoint, content);
  11213. if (cb) {
  11214. cb();
  11215. }
  11216. });
  11217. }
  11218. }
  11219. Component.registerComponent('TimeTooltip', TimeTooltip);
  11220. /**
  11221. * @file play-progress-bar.js
  11222. */
  11223. /**
  11224. * Used by {@link SeekBar} to display media playback progress as part of the
  11225. * {@link ProgressControl}.
  11226. *
  11227. * @extends Component
  11228. */
  11229. class PlayProgressBar extends Component {
  11230. /**
  11231. * Creates an instance of this class.
  11232. *
  11233. * @param { import('../../player').default } player
  11234. * The {@link Player} that this class should be attached to.
  11235. *
  11236. * @param {Object} [options]
  11237. * The key/value store of player options.
  11238. */
  11239. constructor(player, options) {
  11240. super(player, options);
  11241. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  11242. }
  11243. /**
  11244. * Create the the DOM element for this class.
  11245. *
  11246. * @return {Element}
  11247. * The element that was created.
  11248. */
  11249. createEl() {
  11250. return super.createEl('div', {
  11251. className: 'vjs-play-progress vjs-slider-bar'
  11252. }, {
  11253. 'aria-hidden': 'true'
  11254. });
  11255. }
  11256. /**
  11257. * Enqueues updates to its own DOM as well as the DOM of its
  11258. * {@link TimeTooltip} child.
  11259. *
  11260. * @param {Object} seekBarRect
  11261. * The `ClientRect` for the {@link SeekBar} element.
  11262. *
  11263. * @param {number} seekBarPoint
  11264. * A number from 0 to 1, representing a horizontal reference point
  11265. * from the left edge of the {@link SeekBar}
  11266. */
  11267. update(seekBarRect, seekBarPoint) {
  11268. const timeTooltip = this.getChild('timeTooltip');
  11269. if (!timeTooltip) {
  11270. return;
  11271. }
  11272. const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
  11273. timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
  11274. }
  11275. }
  11276. /**
  11277. * Default options for {@link PlayProgressBar}.
  11278. *
  11279. * @type {Object}
  11280. * @private
  11281. */
  11282. PlayProgressBar.prototype.options_ = {
  11283. children: []
  11284. };
  11285. // Time tooltips should not be added to a player on mobile devices
  11286. if (!IS_IOS && !IS_ANDROID) {
  11287. PlayProgressBar.prototype.options_.children.push('timeTooltip');
  11288. }
  11289. Component.registerComponent('PlayProgressBar', PlayProgressBar);
  11290. /**
  11291. * @file mouse-time-display.js
  11292. */
  11293. /**
  11294. * The {@link MouseTimeDisplay} component tracks mouse movement over the
  11295. * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
  11296. * indicating the time which is represented by a given point in the
  11297. * {@link ProgressControl}.
  11298. *
  11299. * @extends Component
  11300. */
  11301. class MouseTimeDisplay extends Component {
  11302. /**
  11303. * Creates an instance of this class.
  11304. *
  11305. * @param { import('../../player').default } player
  11306. * The {@link Player} that this class should be attached to.
  11307. *
  11308. * @param {Object} [options]
  11309. * The key/value store of player options.
  11310. */
  11311. constructor(player, options) {
  11312. super(player, options);
  11313. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  11314. }
  11315. /**
  11316. * Create the DOM element for this class.
  11317. *
  11318. * @return {Element}
  11319. * The element that was created.
  11320. */
  11321. createEl() {
  11322. return super.createEl('div', {
  11323. className: 'vjs-mouse-display'
  11324. });
  11325. }
  11326. /**
  11327. * Enqueues updates to its own DOM as well as the DOM of its
  11328. * {@link TimeTooltip} child.
  11329. *
  11330. * @param {Object} seekBarRect
  11331. * The `ClientRect` for the {@link SeekBar} element.
  11332. *
  11333. * @param {number} seekBarPoint
  11334. * A number from 0 to 1, representing a horizontal reference point
  11335. * from the left edge of the {@link SeekBar}
  11336. */
  11337. update(seekBarRect, seekBarPoint) {
  11338. const time = seekBarPoint * this.player_.duration();
  11339. this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
  11340. this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
  11341. });
  11342. }
  11343. }
  11344. /**
  11345. * Default options for `MouseTimeDisplay`
  11346. *
  11347. * @type {Object}
  11348. * @private
  11349. */
  11350. MouseTimeDisplay.prototype.options_ = {
  11351. children: ['timeTooltip']
  11352. };
  11353. Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
  11354. /**
  11355. * @file seek-bar.js
  11356. */
  11357. // The number of seconds the `step*` functions move the timeline.
  11358. const STEP_SECONDS = 5;
  11359. // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
  11360. const PAGE_KEY_MULTIPLIER = 12;
  11361. /**
  11362. * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
  11363. * as its `bar`.
  11364. *
  11365. * @extends Slider
  11366. */
  11367. class SeekBar extends Slider {
  11368. /**
  11369. * Creates an instance of this class.
  11370. *
  11371. * @param { import('../../player').default } player
  11372. * The `Player` that this class should be attached to.
  11373. *
  11374. * @param {Object} [options]
  11375. * The key/value store of player options.
  11376. */
  11377. constructor(player, options) {
  11378. super(player, options);
  11379. this.setEventHandlers_();
  11380. }
  11381. /**
  11382. * Sets the event handlers
  11383. *
  11384. * @private
  11385. */
  11386. setEventHandlers_() {
  11387. this.update_ = bind_(this, this.update);
  11388. this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
  11389. this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
  11390. if (this.player_.liveTracker) {
  11391. this.on(this.player_.liveTracker, 'liveedgechange', this.update);
  11392. }
  11393. // when playing, let's ensure we smoothly update the play progress bar
  11394. // via an interval
  11395. this.updateInterval = null;
  11396. this.enableIntervalHandler_ = e => this.enableInterval_(e);
  11397. this.disableIntervalHandler_ = e => this.disableInterval_(e);
  11398. this.on(this.player_, ['playing'], this.enableIntervalHandler_);
  11399. this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
  11400. // we don't need to update the play progress if the document is hidden,
  11401. // also, this causes the CPU to spike and eventually crash the page on IE11.
  11402. if ('hidden' in document__default["default"] && 'visibilityState' in document__default["default"]) {
  11403. this.on(document__default["default"], 'visibilitychange', this.toggleVisibility_);
  11404. }
  11405. }
  11406. toggleVisibility_(e) {
  11407. if (document__default["default"].visibilityState === 'hidden') {
  11408. this.cancelNamedAnimationFrame('SeekBar#update');
  11409. this.cancelNamedAnimationFrame('Slider#update');
  11410. this.disableInterval_(e);
  11411. } else {
  11412. if (!this.player_.ended() && !this.player_.paused()) {
  11413. this.enableInterval_();
  11414. }
  11415. // we just switched back to the page and someone may be looking, so, update ASAP
  11416. this.update();
  11417. }
  11418. }
  11419. enableInterval_() {
  11420. if (this.updateInterval) {
  11421. return;
  11422. }
  11423. this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
  11424. }
  11425. disableInterval_(e) {
  11426. if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
  11427. return;
  11428. }
  11429. if (!this.updateInterval) {
  11430. return;
  11431. }
  11432. this.clearInterval(this.updateInterval);
  11433. this.updateInterval = null;
  11434. }
  11435. /**
  11436. * Create the `Component`'s DOM element
  11437. *
  11438. * @return {Element}
  11439. * The element that was created.
  11440. */
  11441. createEl() {
  11442. return super.createEl('div', {
  11443. className: 'vjs-progress-holder'
  11444. }, {
  11445. 'aria-label': this.localize('Progress Bar')
  11446. });
  11447. }
  11448. /**
  11449. * This function updates the play progress bar and accessibility
  11450. * attributes to whatever is passed in.
  11451. *
  11452. * @param {Event} [event]
  11453. * The `timeupdate` or `ended` event that caused this to run.
  11454. *
  11455. * @listens Player#timeupdate
  11456. *
  11457. * @return {number}
  11458. * The current percent at a number from 0-1
  11459. */
  11460. update(event) {
  11461. // ignore updates while the tab is hidden
  11462. if (document__default["default"].visibilityState === 'hidden') {
  11463. return;
  11464. }
  11465. const percent = super.update();
  11466. this.requestNamedAnimationFrame('SeekBar#update', () => {
  11467. const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
  11468. const liveTracker = this.player_.liveTracker;
  11469. let duration = this.player_.duration();
  11470. if (liveTracker && liveTracker.isLive()) {
  11471. duration = this.player_.liveTracker.liveCurrentTime();
  11472. }
  11473. if (this.percent_ !== percent) {
  11474. // machine readable value of progress bar (percentage complete)
  11475. this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
  11476. this.percent_ = percent;
  11477. }
  11478. if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
  11479. // human readable value of progress bar (time complete)
  11480. this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
  11481. this.currentTime_ = currentTime;
  11482. this.duration_ = duration;
  11483. }
  11484. // update the progress bar time tooltip with the current time
  11485. if (this.bar) {
  11486. this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
  11487. }
  11488. });
  11489. return percent;
  11490. }
  11491. /**
  11492. * Prevent liveThreshold from causing seeks to seem like they
  11493. * are not happening from a user perspective.
  11494. *
  11495. * @param {number} ct
  11496. * current time to seek to
  11497. */
  11498. userSeek_(ct) {
  11499. if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
  11500. this.player_.liveTracker.nextSeekedFromUser();
  11501. }
  11502. this.player_.currentTime(ct);
  11503. }
  11504. /**
  11505. * Get the value of current time but allows for smooth scrubbing,
  11506. * when player can't keep up.
  11507. *
  11508. * @return {number}
  11509. * The current time value to display
  11510. *
  11511. * @private
  11512. */
  11513. getCurrentTime_() {
  11514. return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
  11515. }
  11516. /**
  11517. * Get the percentage of media played so far.
  11518. *
  11519. * @return {number}
  11520. * The percentage of media played so far (0 to 1).
  11521. */
  11522. getPercent() {
  11523. const currentTime = this.getCurrentTime_();
  11524. let percent;
  11525. const liveTracker = this.player_.liveTracker;
  11526. if (liveTracker && liveTracker.isLive()) {
  11527. percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
  11528. // prevent the percent from changing at the live edge
  11529. if (liveTracker.atLiveEdge()) {
  11530. percent = 1;
  11531. }
  11532. } else {
  11533. percent = currentTime / this.player_.duration();
  11534. }
  11535. return percent;
  11536. }
  11537. /**
  11538. * Handle mouse down on seek bar
  11539. *
  11540. * @param {MouseEvent} event
  11541. * The `mousedown` event that caused this to run.
  11542. *
  11543. * @listens mousedown
  11544. */
  11545. handleMouseDown(event) {
  11546. if (!isSingleLeftClick(event)) {
  11547. return;
  11548. }
  11549. // Stop event propagation to prevent double fire in progress-control.js
  11550. event.stopPropagation();
  11551. this.videoWasPlaying = !this.player_.paused();
  11552. this.player_.pause();
  11553. super.handleMouseDown(event);
  11554. }
  11555. /**
  11556. * Handle mouse move on seek bar
  11557. *
  11558. * @param {MouseEvent} event
  11559. * The `mousemove` event that caused this to run.
  11560. * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
  11561. *
  11562. * @listens mousemove
  11563. */
  11564. handleMouseMove(event, mouseDown = false) {
  11565. if (!isSingleLeftClick(event)) {
  11566. return;
  11567. }
  11568. if (!mouseDown && !this.player_.scrubbing()) {
  11569. this.player_.scrubbing(true);
  11570. }
  11571. let newTime;
  11572. const distance = this.calculateDistance(event);
  11573. const liveTracker = this.player_.liveTracker;
  11574. if (!liveTracker || !liveTracker.isLive()) {
  11575. newTime = distance * this.player_.duration();
  11576. // Don't let video end while scrubbing.
  11577. if (newTime === this.player_.duration()) {
  11578. newTime = newTime - 0.1;
  11579. }
  11580. } else {
  11581. if (distance >= 0.99) {
  11582. liveTracker.seekToLiveEdge();
  11583. return;
  11584. }
  11585. const seekableStart = liveTracker.seekableStart();
  11586. const seekableEnd = liveTracker.liveCurrentTime();
  11587. newTime = seekableStart + distance * liveTracker.liveWindow();
  11588. // Don't let video end while scrubbing.
  11589. if (newTime >= seekableEnd) {
  11590. newTime = seekableEnd;
  11591. }
  11592. // Compensate for precision differences so that currentTime is not less
  11593. // than seekable start
  11594. if (newTime <= seekableStart) {
  11595. newTime = seekableStart + 0.1;
  11596. }
  11597. // On android seekableEnd can be Infinity sometimes,
  11598. // this will cause newTime to be Infinity, which is
  11599. // not a valid currentTime.
  11600. if (newTime === Infinity) {
  11601. return;
  11602. }
  11603. }
  11604. // Set new time (tell player to seek to new time)
  11605. this.userSeek_(newTime);
  11606. }
  11607. enable() {
  11608. super.enable();
  11609. const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
  11610. if (!mouseTimeDisplay) {
  11611. return;
  11612. }
  11613. mouseTimeDisplay.show();
  11614. }
  11615. disable() {
  11616. super.disable();
  11617. const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
  11618. if (!mouseTimeDisplay) {
  11619. return;
  11620. }
  11621. mouseTimeDisplay.hide();
  11622. }
  11623. /**
  11624. * Handle mouse up on seek bar
  11625. *
  11626. * @param {MouseEvent} event
  11627. * The `mouseup` event that caused this to run.
  11628. *
  11629. * @listens mouseup
  11630. */
  11631. handleMouseUp(event) {
  11632. super.handleMouseUp(event);
  11633. // Stop event propagation to prevent double fire in progress-control.js
  11634. if (event) {
  11635. event.stopPropagation();
  11636. }
  11637. this.player_.scrubbing(false);
  11638. /**
  11639. * Trigger timeupdate because we're done seeking and the time has changed.
  11640. * This is particularly useful for if the player is paused to time the time displays.
  11641. *
  11642. * @event Tech#timeupdate
  11643. * @type {Event}
  11644. */
  11645. this.player_.trigger({
  11646. type: 'timeupdate',
  11647. target: this,
  11648. manuallyTriggered: true
  11649. });
  11650. if (this.videoWasPlaying) {
  11651. silencePromise(this.player_.play());
  11652. } else {
  11653. // We're done seeking and the time has changed.
  11654. // If the player is paused, make sure we display the correct time on the seek bar.
  11655. this.update_();
  11656. }
  11657. }
  11658. /**
  11659. * Move more quickly fast forward for keyboard-only users
  11660. */
  11661. stepForward() {
  11662. this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
  11663. }
  11664. /**
  11665. * Move more quickly rewind for keyboard-only users
  11666. */
  11667. stepBack() {
  11668. this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
  11669. }
  11670. /**
  11671. * Toggles the playback state of the player
  11672. * This gets called when enter or space is used on the seekbar
  11673. *
  11674. * @param {KeyboardEvent} event
  11675. * The `keydown` event that caused this function to be called
  11676. *
  11677. */
  11678. handleAction(event) {
  11679. if (this.player_.paused()) {
  11680. this.player_.play();
  11681. } else {
  11682. this.player_.pause();
  11683. }
  11684. }
  11685. /**
  11686. * Called when this SeekBar has focus and a key gets pressed down.
  11687. * Supports the following keys:
  11688. *
  11689. * Space or Enter key fire a click event
  11690. * Home key moves to start of the timeline
  11691. * End key moves to end of the timeline
  11692. * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
  11693. * PageDown key moves back a larger step than ArrowDown
  11694. * PageUp key moves forward a large step
  11695. *
  11696. * @param {KeyboardEvent} event
  11697. * The `keydown` event that caused this function to be called.
  11698. *
  11699. * @listens keydown
  11700. */
  11701. handleKeyDown(event) {
  11702. const liveTracker = this.player_.liveTracker;
  11703. if (keycode__default["default"].isEventKey(event, 'Space') || keycode__default["default"].isEventKey(event, 'Enter')) {
  11704. event.preventDefault();
  11705. event.stopPropagation();
  11706. this.handleAction(event);
  11707. } else if (keycode__default["default"].isEventKey(event, 'Home')) {
  11708. event.preventDefault();
  11709. event.stopPropagation();
  11710. this.userSeek_(0);
  11711. } else if (keycode__default["default"].isEventKey(event, 'End')) {
  11712. event.preventDefault();
  11713. event.stopPropagation();
  11714. if (liveTracker && liveTracker.isLive()) {
  11715. this.userSeek_(liveTracker.liveCurrentTime());
  11716. } else {
  11717. this.userSeek_(this.player_.duration());
  11718. }
  11719. } else if (/^[0-9]$/.test(keycode__default["default"](event))) {
  11720. event.preventDefault();
  11721. event.stopPropagation();
  11722. const gotoFraction = (keycode__default["default"].codes[keycode__default["default"](event)] - keycode__default["default"].codes['0']) * 10.0 / 100.0;
  11723. if (liveTracker && liveTracker.isLive()) {
  11724. this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
  11725. } else {
  11726. this.userSeek_(this.player_.duration() * gotoFraction);
  11727. }
  11728. } else if (keycode__default["default"].isEventKey(event, 'PgDn')) {
  11729. event.preventDefault();
  11730. event.stopPropagation();
  11731. this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
  11732. } else if (keycode__default["default"].isEventKey(event, 'PgUp')) {
  11733. event.preventDefault();
  11734. event.stopPropagation();
  11735. this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
  11736. } else {
  11737. // Pass keydown handling up for unsupported keys
  11738. super.handleKeyDown(event);
  11739. }
  11740. }
  11741. dispose() {
  11742. this.disableInterval_();
  11743. this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
  11744. if (this.player_.liveTracker) {
  11745. this.off(this.player_.liveTracker, 'liveedgechange', this.update);
  11746. }
  11747. this.off(this.player_, ['playing'], this.enableIntervalHandler_);
  11748. this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
  11749. // we don't need to update the play progress if the document is hidden,
  11750. // also, this causes the CPU to spike and eventually crash the page on IE11.
  11751. if ('hidden' in document__default["default"] && 'visibilityState' in document__default["default"]) {
  11752. this.off(document__default["default"], 'visibilitychange', this.toggleVisibility_);
  11753. }
  11754. super.dispose();
  11755. }
  11756. }
  11757. /**
  11758. * Default options for the `SeekBar`
  11759. *
  11760. * @type {Object}
  11761. * @private
  11762. */
  11763. SeekBar.prototype.options_ = {
  11764. children: ['loadProgressBar', 'playProgressBar'],
  11765. barName: 'playProgressBar'
  11766. };
  11767. // MouseTimeDisplay tooltips should not be added to a player on mobile devices
  11768. if (!IS_IOS && !IS_ANDROID) {
  11769. SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
  11770. }
  11771. Component.registerComponent('SeekBar', SeekBar);
  11772. /**
  11773. * @file progress-control.js
  11774. */
  11775. /**
  11776. * The Progress Control component contains the seek bar, load progress,
  11777. * and play progress.
  11778. *
  11779. * @extends Component
  11780. */
  11781. class ProgressControl extends Component {
  11782. /**
  11783. * Creates an instance of this class.
  11784. *
  11785. * @param { import('../../player').default } player
  11786. * The `Player` that this class should be attached to.
  11787. *
  11788. * @param {Object} [options]
  11789. * The key/value store of player options.
  11790. */
  11791. constructor(player, options) {
  11792. super(player, options);
  11793. this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
  11794. this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
  11795. this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
  11796. this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
  11797. this.enable();
  11798. }
  11799. /**
  11800. * Create the `Component`'s DOM element
  11801. *
  11802. * @return {Element}
  11803. * The element that was created.
  11804. */
  11805. createEl() {
  11806. return super.createEl('div', {
  11807. className: 'vjs-progress-control vjs-control'
  11808. });
  11809. }
  11810. /**
  11811. * When the mouse moves over the `ProgressControl`, the pointer position
  11812. * gets passed down to the `MouseTimeDisplay` component.
  11813. *
  11814. * @param {Event} event
  11815. * The `mousemove` event that caused this function to run.
  11816. *
  11817. * @listen mousemove
  11818. */
  11819. handleMouseMove(event) {
  11820. const seekBar = this.getChild('seekBar');
  11821. if (!seekBar) {
  11822. return;
  11823. }
  11824. const playProgressBar = seekBar.getChild('playProgressBar');
  11825. const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
  11826. if (!playProgressBar && !mouseTimeDisplay) {
  11827. return;
  11828. }
  11829. const seekBarEl = seekBar.el();
  11830. const seekBarRect = findPosition(seekBarEl);
  11831. let seekBarPoint = getPointerPosition(seekBarEl, event).x;
  11832. // The default skin has a gap on either side of the `SeekBar`. This means
  11833. // that it's possible to trigger this behavior outside the boundaries of
  11834. // the `SeekBar`. This ensures we stay within it at all times.
  11835. seekBarPoint = clamp(seekBarPoint, 0, 1);
  11836. if (mouseTimeDisplay) {
  11837. mouseTimeDisplay.update(seekBarRect, seekBarPoint);
  11838. }
  11839. if (playProgressBar) {
  11840. playProgressBar.update(seekBarRect, seekBar.getProgress());
  11841. }
  11842. }
  11843. /**
  11844. * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
  11845. *
  11846. * @method ProgressControl#throttledHandleMouseSeek
  11847. * @param {Event} event
  11848. * The `mousemove` event that caused this function to run.
  11849. *
  11850. * @listen mousemove
  11851. * @listen touchmove
  11852. */
  11853. /**
  11854. * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
  11855. *
  11856. * @param {Event} event
  11857. * `mousedown` or `touchstart` event that triggered this function
  11858. *
  11859. * @listens mousemove
  11860. * @listens touchmove
  11861. */
  11862. handleMouseSeek(event) {
  11863. const seekBar = this.getChild('seekBar');
  11864. if (seekBar) {
  11865. seekBar.handleMouseMove(event);
  11866. }
  11867. }
  11868. /**
  11869. * Are controls are currently enabled for this progress control.
  11870. *
  11871. * @return {boolean}
  11872. * true if controls are enabled, false otherwise
  11873. */
  11874. enabled() {
  11875. return this.enabled_;
  11876. }
  11877. /**
  11878. * Disable all controls on the progress control and its children
  11879. */
  11880. disable() {
  11881. this.children().forEach(child => child.disable && child.disable());
  11882. if (!this.enabled()) {
  11883. return;
  11884. }
  11885. this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
  11886. this.off(this.el_, 'mousemove', this.handleMouseMove);
  11887. this.removeListenersAddedOnMousedownAndTouchstart();
  11888. this.addClass('disabled');
  11889. this.enabled_ = false;
  11890. // Restore normal playback state if controls are disabled while scrubbing
  11891. if (this.player_.scrubbing()) {
  11892. const seekBar = this.getChild('seekBar');
  11893. this.player_.scrubbing(false);
  11894. if (seekBar.videoWasPlaying) {
  11895. silencePromise(this.player_.play());
  11896. }
  11897. }
  11898. }
  11899. /**
  11900. * Enable all controls on the progress control and its children
  11901. */
  11902. enable() {
  11903. this.children().forEach(child => child.enable && child.enable());
  11904. if (this.enabled()) {
  11905. return;
  11906. }
  11907. this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
  11908. this.on(this.el_, 'mousemove', this.handleMouseMove);
  11909. this.removeClass('disabled');
  11910. this.enabled_ = true;
  11911. }
  11912. /**
  11913. * Cleanup listeners after the user finishes interacting with the progress controls
  11914. */
  11915. removeListenersAddedOnMousedownAndTouchstart() {
  11916. const doc = this.el_.ownerDocument;
  11917. this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
  11918. this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
  11919. this.off(doc, 'mouseup', this.handleMouseUpHandler_);
  11920. this.off(doc, 'touchend', this.handleMouseUpHandler_);
  11921. }
  11922. /**
  11923. * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
  11924. *
  11925. * @param {Event} event
  11926. * `mousedown` or `touchstart` event that triggered this function
  11927. *
  11928. * @listens mousedown
  11929. * @listens touchstart
  11930. */
  11931. handleMouseDown(event) {
  11932. const doc = this.el_.ownerDocument;
  11933. const seekBar = this.getChild('seekBar');
  11934. if (seekBar) {
  11935. seekBar.handleMouseDown(event);
  11936. }
  11937. this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
  11938. this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
  11939. this.on(doc, 'mouseup', this.handleMouseUpHandler_);
  11940. this.on(doc, 'touchend', this.handleMouseUpHandler_);
  11941. }
  11942. /**
  11943. * Handle `mouseup` or `touchend` events on the `ProgressControl`.
  11944. *
  11945. * @param {Event} event
  11946. * `mouseup` or `touchend` event that triggered this function.
  11947. *
  11948. * @listens touchend
  11949. * @listens mouseup
  11950. */
  11951. handleMouseUp(event) {
  11952. const seekBar = this.getChild('seekBar');
  11953. if (seekBar) {
  11954. seekBar.handleMouseUp(event);
  11955. }
  11956. this.removeListenersAddedOnMousedownAndTouchstart();
  11957. }
  11958. }
  11959. /**
  11960. * Default options for `ProgressControl`
  11961. *
  11962. * @type {Object}
  11963. * @private
  11964. */
  11965. ProgressControl.prototype.options_ = {
  11966. children: ['seekBar']
  11967. };
  11968. Component.registerComponent('ProgressControl', ProgressControl);
  11969. /**
  11970. * @file picture-in-picture-toggle.js
  11971. */
  11972. /**
  11973. * Toggle Picture-in-Picture mode
  11974. *
  11975. * @extends Button
  11976. */
  11977. class PictureInPictureToggle extends Button {
  11978. /**
  11979. * Creates an instance of this class.
  11980. *
  11981. * @param { import('./player').default } player
  11982. * The `Player` that this class should be attached to.
  11983. *
  11984. * @param {Object} [options]
  11985. * The key/value store of player options.
  11986. *
  11987. * @listens Player#enterpictureinpicture
  11988. * @listens Player#leavepictureinpicture
  11989. */
  11990. constructor(player, options) {
  11991. super(player, options);
  11992. this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
  11993. this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
  11994. this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => {
  11995. // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
  11996. const isSourceAudio = player.currentType().substring(0, 5) === 'audio';
  11997. if (isSourceAudio || player.audioPosterMode() || player.audioOnlyMode()) {
  11998. if (player.isInPictureInPicture()) {
  11999. player.exitPictureInPicture();
  12000. }
  12001. this.hide();
  12002. } else {
  12003. this.show();
  12004. }
  12005. });
  12006. // TODO: Deactivate button on player emptied event.
  12007. this.disable();
  12008. }
  12009. /**
  12010. * Builds the default DOM `className`.
  12011. *
  12012. * @return {string}
  12013. * The DOM `className` for this object.
  12014. */
  12015. buildCSSClass() {
  12016. return `vjs-picture-in-picture-control ${super.buildCSSClass()}`;
  12017. }
  12018. /**
  12019. * Enables or disables button based on availability of a Picture-In-Picture mode.
  12020. *
  12021. * Enabled if
  12022. * - `player.options().enableDocumentPictureInPicture` is true and
  12023. * window.documentPictureInPicture is available; or
  12024. * - `player.disablePictureInPicture()` is false and
  12025. * element.requestPictureInPicture is available
  12026. */
  12027. handlePictureInPictureEnabledChange() {
  12028. if (document__default["default"].pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window__default["default"]) {
  12029. this.enable();
  12030. } else {
  12031. this.disable();
  12032. }
  12033. }
  12034. /**
  12035. * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
  12036. *
  12037. * @param {Event} [event]
  12038. * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
  12039. * called.
  12040. *
  12041. * @listens Player#enterpictureinpicture
  12042. * @listens Player#leavepictureinpicture
  12043. */
  12044. handlePictureInPictureChange(event) {
  12045. if (this.player_.isInPictureInPicture()) {
  12046. this.controlText('Exit Picture-in-Picture');
  12047. } else {
  12048. this.controlText('Picture-in-Picture');
  12049. }
  12050. this.handlePictureInPictureEnabledChange();
  12051. }
  12052. /**
  12053. * This gets called when an `PictureInPictureToggle` is "clicked". See
  12054. * {@link ClickableComponent} for more detailed information on what a click can be.
  12055. *
  12056. * @param {Event} [event]
  12057. * The `keydown`, `tap`, or `click` event that caused this function to be
  12058. * called.
  12059. *
  12060. * @listens tap
  12061. * @listens click
  12062. */
  12063. handleClick(event) {
  12064. if (!this.player_.isInPictureInPicture()) {
  12065. this.player_.requestPictureInPicture();
  12066. } else {
  12067. this.player_.exitPictureInPicture();
  12068. }
  12069. }
  12070. }
  12071. /**
  12072. * The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
  12073. *
  12074. * @type {string}
  12075. * @protected
  12076. */
  12077. PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
  12078. Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
  12079. /**
  12080. * @file fullscreen-toggle.js
  12081. */
  12082. /**
  12083. * Toggle fullscreen video
  12084. *
  12085. * @extends Button
  12086. */
  12087. class FullscreenToggle extends Button {
  12088. /**
  12089. * Creates an instance of this class.
  12090. *
  12091. * @param { import('./player').default } player
  12092. * The `Player` that this class should be attached to.
  12093. *
  12094. * @param {Object} [options]
  12095. * The key/value store of player options.
  12096. */
  12097. constructor(player, options) {
  12098. super(player, options);
  12099. this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
  12100. if (document__default["default"][player.fsApi_.fullscreenEnabled] === false) {
  12101. this.disable();
  12102. }
  12103. }
  12104. /**
  12105. * Builds the default DOM `className`.
  12106. *
  12107. * @return {string}
  12108. * The DOM `className` for this object.
  12109. */
  12110. buildCSSClass() {
  12111. return `vjs-fullscreen-control ${super.buildCSSClass()}`;
  12112. }
  12113. /**
  12114. * Handles fullscreenchange on the player and change control text accordingly.
  12115. *
  12116. * @param {Event} [event]
  12117. * The {@link Player#fullscreenchange} event that caused this function to be
  12118. * called.
  12119. *
  12120. * @listens Player#fullscreenchange
  12121. */
  12122. handleFullscreenChange(event) {
  12123. if (this.player_.isFullscreen()) {
  12124. this.controlText('Exit Fullscreen');
  12125. } else {
  12126. this.controlText('Fullscreen');
  12127. }
  12128. }
  12129. /**
  12130. * This gets called when an `FullscreenToggle` is "clicked". See
  12131. * {@link ClickableComponent} for more detailed information on what a click can be.
  12132. *
  12133. * @param {Event} [event]
  12134. * The `keydown`, `tap`, or `click` event that caused this function to be
  12135. * called.
  12136. *
  12137. * @listens tap
  12138. * @listens click
  12139. */
  12140. handleClick(event) {
  12141. if (!this.player_.isFullscreen()) {
  12142. this.player_.requestFullscreen();
  12143. } else {
  12144. this.player_.exitFullscreen();
  12145. }
  12146. }
  12147. }
  12148. /**
  12149. * The text that should display over the `FullscreenToggle`s controls. Added for localization.
  12150. *
  12151. * @type {string}
  12152. * @protected
  12153. */
  12154. FullscreenToggle.prototype.controlText_ = 'Fullscreen';
  12155. Component.registerComponent('FullscreenToggle', FullscreenToggle);
  12156. /**
  12157. * Check if volume control is supported and if it isn't hide the
  12158. * `Component` that was passed using the `vjs-hidden` class.
  12159. *
  12160. * @param { import('../../component').default } self
  12161. * The component that should be hidden if volume is unsupported
  12162. *
  12163. * @param { import('../../player').default } player
  12164. * A reference to the player
  12165. *
  12166. * @private
  12167. */
  12168. const checkVolumeSupport = function (self, player) {
  12169. // hide volume controls when they're not supported by the current tech
  12170. if (player.tech_ && !player.tech_.featuresVolumeControl) {
  12171. self.addClass('vjs-hidden');
  12172. }
  12173. self.on(player, 'loadstart', function () {
  12174. if (!player.tech_.featuresVolumeControl) {
  12175. self.addClass('vjs-hidden');
  12176. } else {
  12177. self.removeClass('vjs-hidden');
  12178. }
  12179. });
  12180. };
  12181. /**
  12182. * @file volume-level.js
  12183. */
  12184. /**
  12185. * Shows volume level
  12186. *
  12187. * @extends Component
  12188. */
  12189. class VolumeLevel extends Component {
  12190. /**
  12191. * Create the `Component`'s DOM element
  12192. *
  12193. * @return {Element}
  12194. * The element that was created.
  12195. */
  12196. createEl() {
  12197. const el = super.createEl('div', {
  12198. className: 'vjs-volume-level'
  12199. });
  12200. el.appendChild(super.createEl('span', {
  12201. className: 'vjs-control-text'
  12202. }));
  12203. return el;
  12204. }
  12205. }
  12206. Component.registerComponent('VolumeLevel', VolumeLevel);
  12207. /**
  12208. * @file volume-level-tooltip.js
  12209. */
  12210. /**
  12211. * Volume level tooltips display a volume above or side by side the volume bar.
  12212. *
  12213. * @extends Component
  12214. */
  12215. class VolumeLevelTooltip extends Component {
  12216. /**
  12217. * Creates an instance of this class.
  12218. *
  12219. * @param { import('../../player').default } player
  12220. * The {@link Player} that this class should be attached to.
  12221. *
  12222. * @param {Object} [options]
  12223. * The key/value store of player options.
  12224. */
  12225. constructor(player, options) {
  12226. super(player, options);
  12227. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  12228. }
  12229. /**
  12230. * Create the volume tooltip DOM element
  12231. *
  12232. * @return {Element}
  12233. * The element that was created.
  12234. */
  12235. createEl() {
  12236. return super.createEl('div', {
  12237. className: 'vjs-volume-tooltip'
  12238. }, {
  12239. 'aria-hidden': 'true'
  12240. });
  12241. }
  12242. /**
  12243. * Updates the position of the tooltip relative to the `VolumeBar` and
  12244. * its content text.
  12245. *
  12246. * @param {Object} rangeBarRect
  12247. * The `ClientRect` for the {@link VolumeBar} element.
  12248. *
  12249. * @param {number} rangeBarPoint
  12250. * A number from 0 to 1, representing a horizontal/vertical reference point
  12251. * from the left edge of the {@link VolumeBar}
  12252. *
  12253. * @param {boolean} vertical
  12254. * Referees to the Volume control position
  12255. * in the control bar{@link VolumeControl}
  12256. *
  12257. */
  12258. update(rangeBarRect, rangeBarPoint, vertical, content) {
  12259. if (!vertical) {
  12260. const tooltipRect = getBoundingClientRect(this.el_);
  12261. const playerRect = getBoundingClientRect(this.player_.el());
  12262. const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
  12263. if (!playerRect || !tooltipRect) {
  12264. return;
  12265. }
  12266. const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
  12267. const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
  12268. let pullTooltipBy = tooltipRect.width / 2;
  12269. if (spaceLeftOfPoint < pullTooltipBy) {
  12270. pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
  12271. } else if (spaceRightOfPoint < pullTooltipBy) {
  12272. pullTooltipBy = spaceRightOfPoint;
  12273. }
  12274. if (pullTooltipBy < 0) {
  12275. pullTooltipBy = 0;
  12276. } else if (pullTooltipBy > tooltipRect.width) {
  12277. pullTooltipBy = tooltipRect.width;
  12278. }
  12279. this.el_.style.right = `-${pullTooltipBy}px`;
  12280. }
  12281. this.write(`${content}%`);
  12282. }
  12283. /**
  12284. * Write the volume to the tooltip DOM element.
  12285. *
  12286. * @param {string} content
  12287. * The formatted volume for the tooltip.
  12288. */
  12289. write(content) {
  12290. textContent(this.el_, content);
  12291. }
  12292. /**
  12293. * Updates the position of the volume tooltip relative to the `VolumeBar`.
  12294. *
  12295. * @param {Object} rangeBarRect
  12296. * The `ClientRect` for the {@link VolumeBar} element.
  12297. *
  12298. * @param {number} rangeBarPoint
  12299. * A number from 0 to 1, representing a horizontal/vertical reference point
  12300. * from the left edge of the {@link VolumeBar}
  12301. *
  12302. * @param {boolean} vertical
  12303. * Referees to the Volume control position
  12304. * in the control bar{@link VolumeControl}
  12305. *
  12306. * @param {number} volume
  12307. * The volume level to update the tooltip to
  12308. *
  12309. * @param {Function} cb
  12310. * A function that will be called during the request animation frame
  12311. * for tooltips that need to do additional animations from the default
  12312. */
  12313. updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
  12314. this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
  12315. this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
  12316. if (cb) {
  12317. cb();
  12318. }
  12319. });
  12320. }
  12321. }
  12322. Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
  12323. /**
  12324. * @file mouse-volume-level-display.js
  12325. */
  12326. /**
  12327. * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
  12328. * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
  12329. * indicating the volume level which is represented by a given point in the
  12330. * {@link VolumeBar}.
  12331. *
  12332. * @extends Component
  12333. */
  12334. class MouseVolumeLevelDisplay extends Component {
  12335. /**
  12336. * Creates an instance of this class.
  12337. *
  12338. * @param { import('../../player').default } player
  12339. * The {@link Player} that this class should be attached to.
  12340. *
  12341. * @param {Object} [options]
  12342. * The key/value store of player options.
  12343. */
  12344. constructor(player, options) {
  12345. super(player, options);
  12346. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  12347. }
  12348. /**
  12349. * Create the DOM element for this class.
  12350. *
  12351. * @return {Element}
  12352. * The element that was created.
  12353. */
  12354. createEl() {
  12355. return super.createEl('div', {
  12356. className: 'vjs-mouse-display'
  12357. });
  12358. }
  12359. /**
  12360. * Enquires updates to its own DOM as well as the DOM of its
  12361. * {@link VolumeLevelTooltip} child.
  12362. *
  12363. * @param {Object} rangeBarRect
  12364. * The `ClientRect` for the {@link VolumeBar} element.
  12365. *
  12366. * @param {number} rangeBarPoint
  12367. * A number from 0 to 1, representing a horizontal/vertical reference point
  12368. * from the left edge of the {@link VolumeBar}
  12369. *
  12370. * @param {boolean} vertical
  12371. * Referees to the Volume control position
  12372. * in the control bar{@link VolumeControl}
  12373. *
  12374. */
  12375. update(rangeBarRect, rangeBarPoint, vertical) {
  12376. const volume = 100 * rangeBarPoint;
  12377. this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
  12378. if (vertical) {
  12379. this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
  12380. } else {
  12381. this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
  12382. }
  12383. });
  12384. }
  12385. }
  12386. /**
  12387. * Default options for `MouseVolumeLevelDisplay`
  12388. *
  12389. * @type {Object}
  12390. * @private
  12391. */
  12392. MouseVolumeLevelDisplay.prototype.options_ = {
  12393. children: ['volumeLevelTooltip']
  12394. };
  12395. Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
  12396. /**
  12397. * @file volume-bar.js
  12398. */
  12399. /**
  12400. * The bar that contains the volume level and can be clicked on to adjust the level
  12401. *
  12402. * @extends Slider
  12403. */
  12404. class VolumeBar extends Slider {
  12405. /**
  12406. * Creates an instance of this class.
  12407. *
  12408. * @param { import('../../player').default } player
  12409. * The `Player` that this class should be attached to.
  12410. *
  12411. * @param {Object} [options]
  12412. * The key/value store of player options.
  12413. */
  12414. constructor(player, options) {
  12415. super(player, options);
  12416. this.on('slideractive', e => this.updateLastVolume_(e));
  12417. this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
  12418. player.ready(() => this.updateARIAAttributes());
  12419. }
  12420. /**
  12421. * Create the `Component`'s DOM element
  12422. *
  12423. * @return {Element}
  12424. * The element that was created.
  12425. */
  12426. createEl() {
  12427. return super.createEl('div', {
  12428. className: 'vjs-volume-bar vjs-slider-bar'
  12429. }, {
  12430. 'aria-label': this.localize('Volume Level'),
  12431. 'aria-live': 'polite'
  12432. });
  12433. }
  12434. /**
  12435. * Handle mouse down on volume bar
  12436. *
  12437. * @param {Event} event
  12438. * The `mousedown` event that caused this to run.
  12439. *
  12440. * @listens mousedown
  12441. */
  12442. handleMouseDown(event) {
  12443. if (!isSingleLeftClick(event)) {
  12444. return;
  12445. }
  12446. super.handleMouseDown(event);
  12447. }
  12448. /**
  12449. * Handle movement events on the {@link VolumeMenuButton}.
  12450. *
  12451. * @param {Event} event
  12452. * The event that caused this function to run.
  12453. *
  12454. * @listens mousemove
  12455. */
  12456. handleMouseMove(event) {
  12457. const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
  12458. if (mouseVolumeLevelDisplay) {
  12459. const volumeBarEl = this.el();
  12460. const volumeBarRect = getBoundingClientRect(volumeBarEl);
  12461. const vertical = this.vertical();
  12462. let volumeBarPoint = getPointerPosition(volumeBarEl, event);
  12463. volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
  12464. // The default skin has a gap on either side of the `VolumeBar`. This means
  12465. // that it's possible to trigger this behavior outside the boundaries of
  12466. // the `VolumeBar`. This ensures we stay within it at all times.
  12467. volumeBarPoint = clamp(volumeBarPoint, 0, 1);
  12468. mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
  12469. }
  12470. if (!isSingleLeftClick(event)) {
  12471. return;
  12472. }
  12473. this.checkMuted();
  12474. this.player_.volume(this.calculateDistance(event));
  12475. }
  12476. /**
  12477. * If the player is muted unmute it.
  12478. */
  12479. checkMuted() {
  12480. if (this.player_.muted()) {
  12481. this.player_.muted(false);
  12482. }
  12483. }
  12484. /**
  12485. * Get percent of volume level
  12486. *
  12487. * @return {number}
  12488. * Volume level percent as a decimal number.
  12489. */
  12490. getPercent() {
  12491. if (this.player_.muted()) {
  12492. return 0;
  12493. }
  12494. return this.player_.volume();
  12495. }
  12496. /**
  12497. * Increase volume level for keyboard users
  12498. */
  12499. stepForward() {
  12500. this.checkMuted();
  12501. this.player_.volume(this.player_.volume() + 0.1);
  12502. }
  12503. /**
  12504. * Decrease volume level for keyboard users
  12505. */
  12506. stepBack() {
  12507. this.checkMuted();
  12508. this.player_.volume(this.player_.volume() - 0.1);
  12509. }
  12510. /**
  12511. * Update ARIA accessibility attributes
  12512. *
  12513. * @param {Event} [event]
  12514. * The `volumechange` event that caused this function to run.
  12515. *
  12516. * @listens Player#volumechange
  12517. */
  12518. updateARIAAttributes(event) {
  12519. const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
  12520. this.el_.setAttribute('aria-valuenow', ariaValue);
  12521. this.el_.setAttribute('aria-valuetext', ariaValue + '%');
  12522. }
  12523. /**
  12524. * Returns the current value of the player volume as a percentage
  12525. *
  12526. * @private
  12527. */
  12528. volumeAsPercentage_() {
  12529. return Math.round(this.player_.volume() * 100);
  12530. }
  12531. /**
  12532. * When user starts dragging the VolumeBar, store the volume and listen for
  12533. * the end of the drag. When the drag ends, if the volume was set to zero,
  12534. * set lastVolume to the stored volume.
  12535. *
  12536. * @listens slideractive
  12537. * @private
  12538. */
  12539. updateLastVolume_() {
  12540. const volumeBeforeDrag = this.player_.volume();
  12541. this.one('sliderinactive', () => {
  12542. if (this.player_.volume() === 0) {
  12543. this.player_.lastVolume_(volumeBeforeDrag);
  12544. }
  12545. });
  12546. }
  12547. }
  12548. /**
  12549. * Default options for the `VolumeBar`
  12550. *
  12551. * @type {Object}
  12552. * @private
  12553. */
  12554. VolumeBar.prototype.options_ = {
  12555. children: ['volumeLevel'],
  12556. barName: 'volumeLevel'
  12557. };
  12558. // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
  12559. if (!IS_IOS && !IS_ANDROID) {
  12560. VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
  12561. }
  12562. /**
  12563. * Call the update event for this Slider when this event happens on the player.
  12564. *
  12565. * @type {string}
  12566. */
  12567. VolumeBar.prototype.playerEvent = 'volumechange';
  12568. Component.registerComponent('VolumeBar', VolumeBar);
  12569. /**
  12570. * @file volume-control.js
  12571. */
  12572. /**
  12573. * The component for controlling the volume level
  12574. *
  12575. * @extends Component
  12576. */
  12577. class VolumeControl extends Component {
  12578. /**
  12579. * Creates an instance of this class.
  12580. *
  12581. * @param { import('../../player').default } player
  12582. * The `Player` that this class should be attached to.
  12583. *
  12584. * @param {Object} [options={}]
  12585. * The key/value store of player options.
  12586. */
  12587. constructor(player, options = {}) {
  12588. options.vertical = options.vertical || false;
  12589. // Pass the vertical option down to the VolumeBar if
  12590. // the VolumeBar is turned on.
  12591. if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
  12592. options.volumeBar = options.volumeBar || {};
  12593. options.volumeBar.vertical = options.vertical;
  12594. }
  12595. super(player, options);
  12596. // hide this control if volume support is missing
  12597. checkVolumeSupport(this, player);
  12598. this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
  12599. this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
  12600. this.on('mousedown', e => this.handleMouseDown(e));
  12601. this.on('touchstart', e => this.handleMouseDown(e));
  12602. this.on('mousemove', e => this.handleMouseMove(e));
  12603. // while the slider is active (the mouse has been pressed down and
  12604. // is dragging) or in focus we do not want to hide the VolumeBar
  12605. this.on(this.volumeBar, ['focus', 'slideractive'], () => {
  12606. this.volumeBar.addClass('vjs-slider-active');
  12607. this.addClass('vjs-slider-active');
  12608. this.trigger('slideractive');
  12609. });
  12610. this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
  12611. this.volumeBar.removeClass('vjs-slider-active');
  12612. this.removeClass('vjs-slider-active');
  12613. this.trigger('sliderinactive');
  12614. });
  12615. }
  12616. /**
  12617. * Create the `Component`'s DOM element
  12618. *
  12619. * @return {Element}
  12620. * The element that was created.
  12621. */
  12622. createEl() {
  12623. let orientationClass = 'vjs-volume-horizontal';
  12624. if (this.options_.vertical) {
  12625. orientationClass = 'vjs-volume-vertical';
  12626. }
  12627. return super.createEl('div', {
  12628. className: `vjs-volume-control vjs-control ${orientationClass}`
  12629. });
  12630. }
  12631. /**
  12632. * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
  12633. *
  12634. * @param {Event} event
  12635. * `mousedown` or `touchstart` event that triggered this function
  12636. *
  12637. * @listens mousedown
  12638. * @listens touchstart
  12639. */
  12640. handleMouseDown(event) {
  12641. const doc = this.el_.ownerDocument;
  12642. this.on(doc, 'mousemove', this.throttledHandleMouseMove);
  12643. this.on(doc, 'touchmove', this.throttledHandleMouseMove);
  12644. this.on(doc, 'mouseup', this.handleMouseUpHandler_);
  12645. this.on(doc, 'touchend', this.handleMouseUpHandler_);
  12646. }
  12647. /**
  12648. * Handle `mouseup` or `touchend` events on the `VolumeControl`.
  12649. *
  12650. * @param {Event} event
  12651. * `mouseup` or `touchend` event that triggered this function.
  12652. *
  12653. * @listens touchend
  12654. * @listens mouseup
  12655. */
  12656. handleMouseUp(event) {
  12657. const doc = this.el_.ownerDocument;
  12658. this.off(doc, 'mousemove', this.throttledHandleMouseMove);
  12659. this.off(doc, 'touchmove', this.throttledHandleMouseMove);
  12660. this.off(doc, 'mouseup', this.handleMouseUpHandler_);
  12661. this.off(doc, 'touchend', this.handleMouseUpHandler_);
  12662. }
  12663. /**
  12664. * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
  12665. *
  12666. * @param {Event} event
  12667. * `mousedown` or `touchstart` event that triggered this function
  12668. *
  12669. * @listens mousedown
  12670. * @listens touchstart
  12671. */
  12672. handleMouseMove(event) {
  12673. this.volumeBar.handleMouseMove(event);
  12674. }
  12675. }
  12676. /**
  12677. * Default options for the `VolumeControl`
  12678. *
  12679. * @type {Object}
  12680. * @private
  12681. */
  12682. VolumeControl.prototype.options_ = {
  12683. children: ['volumeBar']
  12684. };
  12685. Component.registerComponent('VolumeControl', VolumeControl);
  12686. /**
  12687. * Check if muting volume is supported and if it isn't hide the mute toggle
  12688. * button.
  12689. *
  12690. * @param { import('../../component').default } self
  12691. * A reference to the mute toggle button
  12692. *
  12693. * @param { import('../../player').default } player
  12694. * A reference to the player
  12695. *
  12696. * @private
  12697. */
  12698. const checkMuteSupport = function (self, player) {
  12699. // hide mute toggle button if it's not supported by the current tech
  12700. if (player.tech_ && !player.tech_.featuresMuteControl) {
  12701. self.addClass('vjs-hidden');
  12702. }
  12703. self.on(player, 'loadstart', function () {
  12704. if (!player.tech_.featuresMuteControl) {
  12705. self.addClass('vjs-hidden');
  12706. } else {
  12707. self.removeClass('vjs-hidden');
  12708. }
  12709. });
  12710. };
  12711. /**
  12712. * @file mute-toggle.js
  12713. */
  12714. /**
  12715. * A button component for muting the audio.
  12716. *
  12717. * @extends Button
  12718. */
  12719. class MuteToggle extends Button {
  12720. /**
  12721. * Creates an instance of this class.
  12722. *
  12723. * @param { import('./player').default } player
  12724. * The `Player` that this class should be attached to.
  12725. *
  12726. * @param {Object} [options]
  12727. * The key/value store of player options.
  12728. */
  12729. constructor(player, options) {
  12730. super(player, options);
  12731. // hide this control if volume support is missing
  12732. checkMuteSupport(this, player);
  12733. this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
  12734. }
  12735. /**
  12736. * Builds the default DOM `className`.
  12737. *
  12738. * @return {string}
  12739. * The DOM `className` for this object.
  12740. */
  12741. buildCSSClass() {
  12742. return `vjs-mute-control ${super.buildCSSClass()}`;
  12743. }
  12744. /**
  12745. * This gets called when an `MuteToggle` is "clicked". See
  12746. * {@link ClickableComponent} for more detailed information on what a click can be.
  12747. *
  12748. * @param {Event} [event]
  12749. * The `keydown`, `tap`, or `click` event that caused this function to be
  12750. * called.
  12751. *
  12752. * @listens tap
  12753. * @listens click
  12754. */
  12755. handleClick(event) {
  12756. const vol = this.player_.volume();
  12757. const lastVolume = this.player_.lastVolume_();
  12758. if (vol === 0) {
  12759. const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
  12760. this.player_.volume(volumeToSet);
  12761. this.player_.muted(false);
  12762. } else {
  12763. this.player_.muted(this.player_.muted() ? false : true);
  12764. }
  12765. }
  12766. /**
  12767. * Update the `MuteToggle` button based on the state of `volume` and `muted`
  12768. * on the player.
  12769. *
  12770. * @param {Event} [event]
  12771. * The {@link Player#loadstart} event if this function was called
  12772. * through an event.
  12773. *
  12774. * @listens Player#loadstart
  12775. * @listens Player#volumechange
  12776. */
  12777. update(event) {
  12778. this.updateIcon_();
  12779. this.updateControlText_();
  12780. }
  12781. /**
  12782. * Update the appearance of the `MuteToggle` icon.
  12783. *
  12784. * Possible states (given `level` variable below):
  12785. * - 0: crossed out
  12786. * - 1: zero bars of volume
  12787. * - 2: one bar of volume
  12788. * - 3: two bars of volume
  12789. *
  12790. * @private
  12791. */
  12792. updateIcon_() {
  12793. const vol = this.player_.volume();
  12794. let level = 3;
  12795. // in iOS when a player is loaded with muted attribute
  12796. // and volume is changed with a native mute button
  12797. // we want to make sure muted state is updated
  12798. if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
  12799. this.player_.muted(this.player_.tech_.el_.muted);
  12800. }
  12801. if (vol === 0 || this.player_.muted()) {
  12802. level = 0;
  12803. } else if (vol < 0.33) {
  12804. level = 1;
  12805. } else if (vol < 0.67) {
  12806. level = 2;
  12807. }
  12808. removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
  12809. addClass(this.el_, `vjs-vol-${level}`);
  12810. }
  12811. /**
  12812. * If `muted` has changed on the player, update the control text
  12813. * (`title` attribute on `vjs-mute-control` element and content of
  12814. * `vjs-control-text` element).
  12815. *
  12816. * @private
  12817. */
  12818. updateControlText_() {
  12819. const soundOff = this.player_.muted() || this.player_.volume() === 0;
  12820. const text = soundOff ? 'Unmute' : 'Mute';
  12821. if (this.controlText() !== text) {
  12822. this.controlText(text);
  12823. }
  12824. }
  12825. }
  12826. /**
  12827. * The text that should display over the `MuteToggle`s controls. Added for localization.
  12828. *
  12829. * @type {string}
  12830. * @protected
  12831. */
  12832. MuteToggle.prototype.controlText_ = 'Mute';
  12833. Component.registerComponent('MuteToggle', MuteToggle);
  12834. /**
  12835. * @file volume-control.js
  12836. */
  12837. /**
  12838. * A Component to contain the MuteToggle and VolumeControl so that
  12839. * they can work together.
  12840. *
  12841. * @extends Component
  12842. */
  12843. class VolumePanel extends Component {
  12844. /**
  12845. * Creates an instance of this class.
  12846. *
  12847. * @param { import('./player').default } player
  12848. * The `Player` that this class should be attached to.
  12849. *
  12850. * @param {Object} [options={}]
  12851. * The key/value store of player options.
  12852. */
  12853. constructor(player, options = {}) {
  12854. if (typeof options.inline !== 'undefined') {
  12855. options.inline = options.inline;
  12856. } else {
  12857. options.inline = true;
  12858. }
  12859. // pass the inline option down to the VolumeControl as vertical if
  12860. // the VolumeControl is on.
  12861. if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
  12862. options.volumeControl = options.volumeControl || {};
  12863. options.volumeControl.vertical = !options.inline;
  12864. }
  12865. super(player, options);
  12866. // this handler is used by mouse handler methods below
  12867. this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
  12868. this.on(player, ['loadstart'], e => this.volumePanelState_(e));
  12869. this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
  12870. this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
  12871. this.on('keydown', e => this.handleKeyPress(e));
  12872. this.on('mouseover', e => this.handleMouseOver(e));
  12873. this.on('mouseout', e => this.handleMouseOut(e));
  12874. // while the slider is active (the mouse has been pressed down and
  12875. // is dragging) we do not want to hide the VolumeBar
  12876. this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
  12877. this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
  12878. }
  12879. /**
  12880. * Add vjs-slider-active class to the VolumePanel
  12881. *
  12882. * @listens VolumeControl#slideractive
  12883. * @private
  12884. */
  12885. sliderActive_() {
  12886. this.addClass('vjs-slider-active');
  12887. }
  12888. /**
  12889. * Removes vjs-slider-active class to the VolumePanel
  12890. *
  12891. * @listens VolumeControl#sliderinactive
  12892. * @private
  12893. */
  12894. sliderInactive_() {
  12895. this.removeClass('vjs-slider-active');
  12896. }
  12897. /**
  12898. * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
  12899. * depending on MuteToggle and VolumeControl state
  12900. *
  12901. * @listens Player#loadstart
  12902. * @private
  12903. */
  12904. volumePanelState_() {
  12905. // hide volume panel if neither volume control or mute toggle
  12906. // are displayed
  12907. if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
  12908. this.addClass('vjs-hidden');
  12909. }
  12910. // if only mute toggle is visible we don't want
  12911. // volume panel expanding when hovered or active
  12912. if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
  12913. this.addClass('vjs-mute-toggle-only');
  12914. }
  12915. }
  12916. /**
  12917. * Create the `Component`'s DOM element
  12918. *
  12919. * @return {Element}
  12920. * The element that was created.
  12921. */
  12922. createEl() {
  12923. let orientationClass = 'vjs-volume-panel-horizontal';
  12924. if (!this.options_.inline) {
  12925. orientationClass = 'vjs-volume-panel-vertical';
  12926. }
  12927. return super.createEl('div', {
  12928. className: `vjs-volume-panel vjs-control ${orientationClass}`
  12929. });
  12930. }
  12931. /**
  12932. * Dispose of the `volume-panel` and all child components.
  12933. */
  12934. dispose() {
  12935. this.handleMouseOut();
  12936. super.dispose();
  12937. }
  12938. /**
  12939. * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
  12940. * the volume panel and sets focus on `MuteToggle`.
  12941. *
  12942. * @param {Event} event
  12943. * The `keyup` event that caused this function to be called.
  12944. *
  12945. * @listens keyup
  12946. */
  12947. handleVolumeControlKeyUp(event) {
  12948. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  12949. this.muteToggle.focus();
  12950. }
  12951. }
  12952. /**
  12953. * This gets called when a `VolumePanel` gains hover via a `mouseover` event.
  12954. * Turns on listening for `mouseover` event. When they happen it
  12955. * calls `this.handleMouseOver`.
  12956. *
  12957. * @param {Event} event
  12958. * The `mouseover` event that caused this function to be called.
  12959. *
  12960. * @listens mouseover
  12961. */
  12962. handleMouseOver(event) {
  12963. this.addClass('vjs-hover');
  12964. on(document__default["default"], 'keyup', this.handleKeyPressHandler_);
  12965. }
  12966. /**
  12967. * This gets called when a `VolumePanel` gains hover via a `mouseout` event.
  12968. * Turns on listening for `mouseout` event. When they happen it
  12969. * calls `this.handleMouseOut`.
  12970. *
  12971. * @param {Event} event
  12972. * The `mouseout` event that caused this function to be called.
  12973. *
  12974. * @listens mouseout
  12975. */
  12976. handleMouseOut(event) {
  12977. this.removeClass('vjs-hover');
  12978. off(document__default["default"], 'keyup', this.handleKeyPressHandler_);
  12979. }
  12980. /**
  12981. * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
  12982. * looking for ESC, which hides the `VolumeControl`.
  12983. *
  12984. * @param {Event} event
  12985. * The keypress that triggered this event.
  12986. *
  12987. * @listens keydown | keyup
  12988. */
  12989. handleKeyPress(event) {
  12990. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  12991. this.handleMouseOut();
  12992. }
  12993. }
  12994. }
  12995. /**
  12996. * Default options for the `VolumeControl`
  12997. *
  12998. * @type {Object}
  12999. * @private
  13000. */
  13001. VolumePanel.prototype.options_ = {
  13002. children: ['muteToggle', 'volumeControl']
  13003. };
  13004. Component.registerComponent('VolumePanel', VolumePanel);
  13005. /**
  13006. * Button to skip forward a configurable amount of time
  13007. * through a video. Renders in the control bar.
  13008. *
  13009. * e.g. options: {controlBar: {skipButtons: forward: 5}}
  13010. *
  13011. * @extends Button
  13012. */
  13013. class SkipForward extends Button {
  13014. constructor(player, options) {
  13015. super(player, options);
  13016. this.validOptions = [5, 10, 30];
  13017. this.skipTime = this.getSkipForwardTime();
  13018. if (this.skipTime && this.validOptions.includes(this.skipTime)) {
  13019. this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
  13020. this.show();
  13021. } else {
  13022. this.hide();
  13023. }
  13024. }
  13025. getSkipForwardTime() {
  13026. const playerOptions = this.options_.playerOptions;
  13027. return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
  13028. }
  13029. buildCSSClass() {
  13030. return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
  13031. }
  13032. /**
  13033. * On click, skips forward in the duration/seekable range by a configurable amount of seconds.
  13034. * If the time left in the duration/seekable range is less than the configured 'skip forward' time,
  13035. * skips to end of duration/seekable range.
  13036. *
  13037. * Handle a click on a `SkipForward` button
  13038. *
  13039. * @param {EventTarget~Event} event
  13040. * The `click` event that caused this function
  13041. * to be called
  13042. */
  13043. handleClick(event) {
  13044. const currentVideoTime = this.player_.currentTime();
  13045. const liveTracker = this.player_.liveTracker;
  13046. const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
  13047. let newTime;
  13048. if (currentVideoTime + this.skipTime <= duration) {
  13049. newTime = currentVideoTime + this.skipTime;
  13050. } else {
  13051. newTime = duration;
  13052. }
  13053. this.player_.currentTime(newTime);
  13054. }
  13055. /**
  13056. * Update control text on languagechange
  13057. */
  13058. handleLanguagechange() {
  13059. this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
  13060. }
  13061. }
  13062. Component.registerComponent('SkipForward', SkipForward);
  13063. /**
  13064. * Button to skip backward a configurable amount of time
  13065. * through a video. Renders in the control bar.
  13066. *
  13067. * * e.g. options: {controlBar: {skipButtons: backward: 5}}
  13068. *
  13069. * @extends Button
  13070. */
  13071. class SkipBackward extends Button {
  13072. constructor(player, options) {
  13073. super(player, options);
  13074. this.validOptions = [5, 10, 30];
  13075. this.skipTime = this.getSkipBackwardTime();
  13076. if (this.skipTime && this.validOptions.includes(this.skipTime)) {
  13077. this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
  13078. this.show();
  13079. } else {
  13080. this.hide();
  13081. }
  13082. }
  13083. getSkipBackwardTime() {
  13084. const playerOptions = this.options_.playerOptions;
  13085. return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
  13086. }
  13087. buildCSSClass() {
  13088. return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
  13089. }
  13090. /**
  13091. * On click, skips backward in the video by a configurable amount of seconds.
  13092. * If the current time in the video is less than the configured 'skip backward' time,
  13093. * skips to beginning of video or seekable range.
  13094. *
  13095. * Handle a click on a `SkipBackward` button
  13096. *
  13097. * @param {EventTarget~Event} event
  13098. * The `click` event that caused this function
  13099. * to be called
  13100. */
  13101. handleClick(event) {
  13102. const currentVideoTime = this.player_.currentTime();
  13103. const liveTracker = this.player_.liveTracker;
  13104. const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
  13105. let newTime;
  13106. if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
  13107. newTime = seekableStart;
  13108. } else if (currentVideoTime >= this.skipTime) {
  13109. newTime = currentVideoTime - this.skipTime;
  13110. } else {
  13111. newTime = 0;
  13112. }
  13113. this.player_.currentTime(newTime);
  13114. }
  13115. /**
  13116. * Update control text on languagechange
  13117. */
  13118. handleLanguagechange() {
  13119. this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
  13120. }
  13121. }
  13122. SkipBackward.prototype.controlText_ = 'Skip Backward';
  13123. Component.registerComponent('SkipBackward', SkipBackward);
  13124. /**
  13125. * @file menu.js
  13126. */
  13127. /**
  13128. * The Menu component is used to build popup menus, including subtitle and
  13129. * captions selection menus.
  13130. *
  13131. * @extends Component
  13132. */
  13133. class Menu extends Component {
  13134. /**
  13135. * Create an instance of this class.
  13136. *
  13137. * @param { import('../player').default } player
  13138. * the player that this component should attach to
  13139. *
  13140. * @param {Object} [options]
  13141. * Object of option names and values
  13142. *
  13143. */
  13144. constructor(player, options) {
  13145. super(player, options);
  13146. if (options) {
  13147. this.menuButton_ = options.menuButton;
  13148. }
  13149. this.focusedChild_ = -1;
  13150. this.on('keydown', e => this.handleKeyDown(e));
  13151. // All the menu item instances share the same blur handler provided by the menu container.
  13152. this.boundHandleBlur_ = e => this.handleBlur(e);
  13153. this.boundHandleTapClick_ = e => this.handleTapClick(e);
  13154. }
  13155. /**
  13156. * Add event listeners to the {@link MenuItem}.
  13157. *
  13158. * @param {Object} component
  13159. * The instance of the `MenuItem` to add listeners to.
  13160. *
  13161. */
  13162. addEventListenerForItem(component) {
  13163. if (!(component instanceof Component)) {
  13164. return;
  13165. }
  13166. this.on(component, 'blur', this.boundHandleBlur_);
  13167. this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
  13168. }
  13169. /**
  13170. * Remove event listeners from the {@link MenuItem}.
  13171. *
  13172. * @param {Object} component
  13173. * The instance of the `MenuItem` to remove listeners.
  13174. *
  13175. */
  13176. removeEventListenerForItem(component) {
  13177. if (!(component instanceof Component)) {
  13178. return;
  13179. }
  13180. this.off(component, 'blur', this.boundHandleBlur_);
  13181. this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
  13182. }
  13183. /**
  13184. * This method will be called indirectly when the component has been added
  13185. * before the component adds to the new menu instance by `addItem`.
  13186. * In this case, the original menu instance will remove the component
  13187. * by calling `removeChild`.
  13188. *
  13189. * @param {Object} component
  13190. * The instance of the `MenuItem`
  13191. */
  13192. removeChild(component) {
  13193. if (typeof component === 'string') {
  13194. component = this.getChild(component);
  13195. }
  13196. this.removeEventListenerForItem(component);
  13197. super.removeChild(component);
  13198. }
  13199. /**
  13200. * Add a {@link MenuItem} to the menu.
  13201. *
  13202. * @param {Object|string} component
  13203. * The name or instance of the `MenuItem` to add.
  13204. *
  13205. */
  13206. addItem(component) {
  13207. const childComponent = this.addChild(component);
  13208. if (childComponent) {
  13209. this.addEventListenerForItem(childComponent);
  13210. }
  13211. }
  13212. /**
  13213. * Create the `Menu`s DOM element.
  13214. *
  13215. * @return {Element}
  13216. * the element that was created
  13217. */
  13218. createEl() {
  13219. const contentElType = this.options_.contentElType || 'ul';
  13220. this.contentEl_ = createEl(contentElType, {
  13221. className: 'vjs-menu-content'
  13222. });
  13223. this.contentEl_.setAttribute('role', 'menu');
  13224. const el = super.createEl('div', {
  13225. append: this.contentEl_,
  13226. className: 'vjs-menu'
  13227. });
  13228. el.appendChild(this.contentEl_);
  13229. // Prevent clicks from bubbling up. Needed for Menu Buttons,
  13230. // where a click on the parent is significant
  13231. on(el, 'click', function (event) {
  13232. event.preventDefault();
  13233. event.stopImmediatePropagation();
  13234. });
  13235. return el;
  13236. }
  13237. dispose() {
  13238. this.contentEl_ = null;
  13239. this.boundHandleBlur_ = null;
  13240. this.boundHandleTapClick_ = null;
  13241. super.dispose();
  13242. }
  13243. /**
  13244. * Called when a `MenuItem` loses focus.
  13245. *
  13246. * @param {Event} event
  13247. * The `blur` event that caused this function to be called.
  13248. *
  13249. * @listens blur
  13250. */
  13251. handleBlur(event) {
  13252. const relatedTarget = event.relatedTarget || document__default["default"].activeElement;
  13253. // Close menu popup when a user clicks outside the menu
  13254. if (!this.children().some(element => {
  13255. return element.el() === relatedTarget;
  13256. })) {
  13257. const btn = this.menuButton_;
  13258. if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
  13259. btn.unpressButton();
  13260. }
  13261. }
  13262. }
  13263. /**
  13264. * Called when a `MenuItem` gets clicked or tapped.
  13265. *
  13266. * @param {Event} event
  13267. * The `click` or `tap` event that caused this function to be called.
  13268. *
  13269. * @listens click,tap
  13270. */
  13271. handleTapClick(event) {
  13272. // Unpress the associated MenuButton, and move focus back to it
  13273. if (this.menuButton_) {
  13274. this.menuButton_.unpressButton();
  13275. const childComponents = this.children();
  13276. if (!Array.isArray(childComponents)) {
  13277. return;
  13278. }
  13279. const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
  13280. if (!foundComponent) {
  13281. return;
  13282. }
  13283. // don't focus menu button if item is a caption settings item
  13284. // because focus will move elsewhere
  13285. if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
  13286. this.menuButton_.focus();
  13287. }
  13288. }
  13289. }
  13290. /**
  13291. * Handle a `keydown` event on this menu. This listener is added in the constructor.
  13292. *
  13293. * @param {Event} event
  13294. * A `keydown` event that happened on the menu.
  13295. *
  13296. * @listens keydown
  13297. */
  13298. handleKeyDown(event) {
  13299. // Left and Down Arrows
  13300. if (keycode__default["default"].isEventKey(event, 'Left') || keycode__default["default"].isEventKey(event, 'Down')) {
  13301. event.preventDefault();
  13302. event.stopPropagation();
  13303. this.stepForward();
  13304. // Up and Right Arrows
  13305. } else if (keycode__default["default"].isEventKey(event, 'Right') || keycode__default["default"].isEventKey(event, 'Up')) {
  13306. event.preventDefault();
  13307. event.stopPropagation();
  13308. this.stepBack();
  13309. }
  13310. }
  13311. /**
  13312. * Move to next (lower) menu item for keyboard users.
  13313. */
  13314. stepForward() {
  13315. let stepChild = 0;
  13316. if (this.focusedChild_ !== undefined) {
  13317. stepChild = this.focusedChild_ + 1;
  13318. }
  13319. this.focus(stepChild);
  13320. }
  13321. /**
  13322. * Move to previous (higher) menu item for keyboard users.
  13323. */
  13324. stepBack() {
  13325. let stepChild = 0;
  13326. if (this.focusedChild_ !== undefined) {
  13327. stepChild = this.focusedChild_ - 1;
  13328. }
  13329. this.focus(stepChild);
  13330. }
  13331. /**
  13332. * Set focus on a {@link MenuItem} in the `Menu`.
  13333. *
  13334. * @param {Object|string} [item=0]
  13335. * Index of child item set focus on.
  13336. */
  13337. focus(item = 0) {
  13338. const children = this.children().slice();
  13339. const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
  13340. if (haveTitle) {
  13341. children.shift();
  13342. }
  13343. if (children.length > 0) {
  13344. if (item < 0) {
  13345. item = 0;
  13346. } else if (item >= children.length) {
  13347. item = children.length - 1;
  13348. }
  13349. this.focusedChild_ = item;
  13350. children[item].el_.focus();
  13351. }
  13352. }
  13353. }
  13354. Component.registerComponent('Menu', Menu);
  13355. /**
  13356. * @file menu-button.js
  13357. */
  13358. /**
  13359. * A `MenuButton` class for any popup {@link Menu}.
  13360. *
  13361. * @extends Component
  13362. */
  13363. class MenuButton extends Component {
  13364. /**
  13365. * Creates an instance of this class.
  13366. *
  13367. * @param { import('../player').default } player
  13368. * The `Player` that this class should be attached to.
  13369. *
  13370. * @param {Object} [options={}]
  13371. * The key/value store of player options.
  13372. */
  13373. constructor(player, options = {}) {
  13374. super(player, options);
  13375. this.menuButton_ = new Button(player, options);
  13376. this.menuButton_.controlText(this.controlText_);
  13377. this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
  13378. // Add buildCSSClass values to the button, not the wrapper
  13379. const buttonClass = Button.prototype.buildCSSClass();
  13380. this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
  13381. this.menuButton_.removeClass('vjs-control');
  13382. this.addChild(this.menuButton_);
  13383. this.update();
  13384. this.enabled_ = true;
  13385. const handleClick = e => this.handleClick(e);
  13386. this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
  13387. this.on(this.menuButton_, 'tap', handleClick);
  13388. this.on(this.menuButton_, 'click', handleClick);
  13389. this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
  13390. this.on(this.menuButton_, 'mouseenter', () => {
  13391. this.addClass('vjs-hover');
  13392. this.menu.show();
  13393. on(document__default["default"], 'keyup', this.handleMenuKeyUp_);
  13394. });
  13395. this.on('mouseleave', e => this.handleMouseLeave(e));
  13396. this.on('keydown', e => this.handleSubmenuKeyDown(e));
  13397. }
  13398. /**
  13399. * Update the menu based on the current state of its items.
  13400. */
  13401. update() {
  13402. const menu = this.createMenu();
  13403. if (this.menu) {
  13404. this.menu.dispose();
  13405. this.removeChild(this.menu);
  13406. }
  13407. this.menu = menu;
  13408. this.addChild(menu);
  13409. /**
  13410. * Track the state of the menu button
  13411. *
  13412. * @type {Boolean}
  13413. * @private
  13414. */
  13415. this.buttonPressed_ = false;
  13416. this.menuButton_.el_.setAttribute('aria-expanded', 'false');
  13417. if (this.items && this.items.length <= this.hideThreshold_) {
  13418. this.hide();
  13419. this.menu.contentEl_.removeAttribute('role');
  13420. } else {
  13421. this.show();
  13422. this.menu.contentEl_.setAttribute('role', 'menu');
  13423. }
  13424. }
  13425. /**
  13426. * Create the menu and add all items to it.
  13427. *
  13428. * @return {Menu}
  13429. * The constructed menu
  13430. */
  13431. createMenu() {
  13432. const menu = new Menu(this.player_, {
  13433. menuButton: this
  13434. });
  13435. /**
  13436. * Hide the menu if the number of items is less than or equal to this threshold. This defaults
  13437. * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
  13438. * it here because every time we run `createMenu` we need to reset the value.
  13439. *
  13440. * @protected
  13441. * @type {Number}
  13442. */
  13443. this.hideThreshold_ = 0;
  13444. // Add a title list item to the top
  13445. if (this.options_.title) {
  13446. const titleEl = createEl('li', {
  13447. className: 'vjs-menu-title',
  13448. textContent: toTitleCase(this.options_.title),
  13449. tabIndex: -1
  13450. });
  13451. const titleComponent = new Component(this.player_, {
  13452. el: titleEl
  13453. });
  13454. menu.addItem(titleComponent);
  13455. }
  13456. this.items = this.createItems();
  13457. if (this.items) {
  13458. // Add menu items to the menu
  13459. for (let i = 0; i < this.items.length; i++) {
  13460. menu.addItem(this.items[i]);
  13461. }
  13462. }
  13463. return menu;
  13464. }
  13465. /**
  13466. * Create the list of menu items. Specific to each subclass.
  13467. *
  13468. * @abstract
  13469. */
  13470. createItems() {}
  13471. /**
  13472. * Create the `MenuButtons`s DOM element.
  13473. *
  13474. * @return {Element}
  13475. * The element that gets created.
  13476. */
  13477. createEl() {
  13478. return super.createEl('div', {
  13479. className: this.buildWrapperCSSClass()
  13480. }, {});
  13481. }
  13482. /**
  13483. * Allow sub components to stack CSS class names for the wrapper element
  13484. *
  13485. * @return {string}
  13486. * The constructed wrapper DOM `className`
  13487. */
  13488. buildWrapperCSSClass() {
  13489. let menuButtonClass = 'vjs-menu-button';
  13490. // If the inline option is passed, we want to use different styles altogether.
  13491. if (this.options_.inline === true) {
  13492. menuButtonClass += '-inline';
  13493. } else {
  13494. menuButtonClass += '-popup';
  13495. }
  13496. // TODO: Fix the CSS so that this isn't necessary
  13497. const buttonClass = Button.prototype.buildCSSClass();
  13498. return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
  13499. }
  13500. /**
  13501. * Builds the default DOM `className`.
  13502. *
  13503. * @return {string}
  13504. * The DOM `className` for this object.
  13505. */
  13506. buildCSSClass() {
  13507. let menuButtonClass = 'vjs-menu-button';
  13508. // If the inline option is passed, we want to use different styles altogether.
  13509. if (this.options_.inline === true) {
  13510. menuButtonClass += '-inline';
  13511. } else {
  13512. menuButtonClass += '-popup';
  13513. }
  13514. return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
  13515. }
  13516. /**
  13517. * Get or set the localized control text that will be used for accessibility.
  13518. *
  13519. * > NOTE: This will come from the internal `menuButton_` element.
  13520. *
  13521. * @param {string} [text]
  13522. * Control text for element.
  13523. *
  13524. * @param {Element} [el=this.menuButton_.el()]
  13525. * Element to set the title on.
  13526. *
  13527. * @return {string}
  13528. * - The control text when getting
  13529. */
  13530. controlText(text, el = this.menuButton_.el()) {
  13531. return this.menuButton_.controlText(text, el);
  13532. }
  13533. /**
  13534. * Dispose of the `menu-button` and all child components.
  13535. */
  13536. dispose() {
  13537. this.handleMouseLeave();
  13538. super.dispose();
  13539. }
  13540. /**
  13541. * Handle a click on a `MenuButton`.
  13542. * See {@link ClickableComponent#handleClick} for instances where this is called.
  13543. *
  13544. * @param {Event} event
  13545. * The `keydown`, `tap`, or `click` event that caused this function to be
  13546. * called.
  13547. *
  13548. * @listens tap
  13549. * @listens click
  13550. */
  13551. handleClick(event) {
  13552. if (this.buttonPressed_) {
  13553. this.unpressButton();
  13554. } else {
  13555. this.pressButton();
  13556. }
  13557. }
  13558. /**
  13559. * Handle `mouseleave` for `MenuButton`.
  13560. *
  13561. * @param {Event} event
  13562. * The `mouseleave` event that caused this function to be called.
  13563. *
  13564. * @listens mouseleave
  13565. */
  13566. handleMouseLeave(event) {
  13567. this.removeClass('vjs-hover');
  13568. off(document__default["default"], 'keyup', this.handleMenuKeyUp_);
  13569. }
  13570. /**
  13571. * Set the focus to the actual button, not to this element
  13572. */
  13573. focus() {
  13574. this.menuButton_.focus();
  13575. }
  13576. /**
  13577. * Remove the focus from the actual button, not this element
  13578. */
  13579. blur() {
  13580. this.menuButton_.blur();
  13581. }
  13582. /**
  13583. * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
  13584. * {@link ClickableComponent#handleKeyDown} for instances where this is called.
  13585. *
  13586. * @param {Event} event
  13587. * The `keydown` event that caused this function to be called.
  13588. *
  13589. * @listens keydown
  13590. */
  13591. handleKeyDown(event) {
  13592. // Escape or Tab unpress the 'button'
  13593. if (keycode__default["default"].isEventKey(event, 'Esc') || keycode__default["default"].isEventKey(event, 'Tab')) {
  13594. if (this.buttonPressed_) {
  13595. this.unpressButton();
  13596. }
  13597. // Don't preventDefault for Tab key - we still want to lose focus
  13598. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  13599. event.preventDefault();
  13600. // Set focus back to the menu button's button
  13601. this.menuButton_.focus();
  13602. }
  13603. // Up Arrow or Down Arrow also 'press' the button to open the menu
  13604. } else if (keycode__default["default"].isEventKey(event, 'Up') || keycode__default["default"].isEventKey(event, 'Down')) {
  13605. if (!this.buttonPressed_) {
  13606. event.preventDefault();
  13607. this.pressButton();
  13608. }
  13609. }
  13610. }
  13611. /**
  13612. * Handle a `keyup` event on a `MenuButton`. The listener for this is added in
  13613. * the constructor.
  13614. *
  13615. * @param {Event} event
  13616. * Key press event
  13617. *
  13618. * @listens keyup
  13619. */
  13620. handleMenuKeyUp(event) {
  13621. // Escape hides popup menu
  13622. if (keycode__default["default"].isEventKey(event, 'Esc') || keycode__default["default"].isEventKey(event, 'Tab')) {
  13623. this.removeClass('vjs-hover');
  13624. }
  13625. }
  13626. /**
  13627. * This method name now delegates to `handleSubmenuKeyDown`. This means
  13628. * anyone calling `handleSubmenuKeyPress` will not see their method calls
  13629. * stop working.
  13630. *
  13631. * @param {Event} event
  13632. * The event that caused this function to be called.
  13633. */
  13634. handleSubmenuKeyPress(event) {
  13635. this.handleSubmenuKeyDown(event);
  13636. }
  13637. /**
  13638. * Handle a `keydown` event on a sub-menu. The listener for this is added in
  13639. * the constructor.
  13640. *
  13641. * @param {Event} event
  13642. * Key press event
  13643. *
  13644. * @listens keydown
  13645. */
  13646. handleSubmenuKeyDown(event) {
  13647. // Escape or Tab unpress the 'button'
  13648. if (keycode__default["default"].isEventKey(event, 'Esc') || keycode__default["default"].isEventKey(event, 'Tab')) {
  13649. if (this.buttonPressed_) {
  13650. this.unpressButton();
  13651. }
  13652. // Don't preventDefault for Tab key - we still want to lose focus
  13653. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  13654. event.preventDefault();
  13655. // Set focus back to the menu button's button
  13656. this.menuButton_.focus();
  13657. }
  13658. }
  13659. }
  13660. /**
  13661. * Put the current `MenuButton` into a pressed state.
  13662. */
  13663. pressButton() {
  13664. if (this.enabled_) {
  13665. this.buttonPressed_ = true;
  13666. this.menu.show();
  13667. this.menu.lockShowing();
  13668. this.menuButton_.el_.setAttribute('aria-expanded', 'true');
  13669. // set the focus into the submenu, except on iOS where it is resulting in
  13670. // undesired scrolling behavior when the player is in an iframe
  13671. if (IS_IOS && isInFrame()) {
  13672. // Return early so that the menu isn't focused
  13673. return;
  13674. }
  13675. this.menu.focus();
  13676. }
  13677. }
  13678. /**
  13679. * Take the current `MenuButton` out of a pressed state.
  13680. */
  13681. unpressButton() {
  13682. if (this.enabled_) {
  13683. this.buttonPressed_ = false;
  13684. this.menu.unlockShowing();
  13685. this.menu.hide();
  13686. this.menuButton_.el_.setAttribute('aria-expanded', 'false');
  13687. }
  13688. }
  13689. /**
  13690. * Disable the `MenuButton`. Don't allow it to be clicked.
  13691. */
  13692. disable() {
  13693. this.unpressButton();
  13694. this.enabled_ = false;
  13695. this.addClass('vjs-disabled');
  13696. this.menuButton_.disable();
  13697. }
  13698. /**
  13699. * Enable the `MenuButton`. Allow it to be clicked.
  13700. */
  13701. enable() {
  13702. this.enabled_ = true;
  13703. this.removeClass('vjs-disabled');
  13704. this.menuButton_.enable();
  13705. }
  13706. }
  13707. Component.registerComponent('MenuButton', MenuButton);
  13708. /**
  13709. * @file track-button.js
  13710. */
  13711. /**
  13712. * The base class for buttons that toggle specific track types (e.g. subtitles).
  13713. *
  13714. * @extends MenuButton
  13715. */
  13716. class TrackButton extends MenuButton {
  13717. /**
  13718. * Creates an instance of this class.
  13719. *
  13720. * @param { import('./player').default } player
  13721. * The `Player` that this class should be attached to.
  13722. *
  13723. * @param {Object} [options]
  13724. * The key/value store of player options.
  13725. */
  13726. constructor(player, options) {
  13727. const tracks = options.tracks;
  13728. super(player, options);
  13729. if (this.items.length <= 1) {
  13730. this.hide();
  13731. }
  13732. if (!tracks) {
  13733. return;
  13734. }
  13735. const updateHandler = bind_(this, this.update);
  13736. tracks.addEventListener('removetrack', updateHandler);
  13737. tracks.addEventListener('addtrack', updateHandler);
  13738. tracks.addEventListener('labelchange', updateHandler);
  13739. this.player_.on('ready', updateHandler);
  13740. this.player_.on('dispose', function () {
  13741. tracks.removeEventListener('removetrack', updateHandler);
  13742. tracks.removeEventListener('addtrack', updateHandler);
  13743. tracks.removeEventListener('labelchange', updateHandler);
  13744. });
  13745. }
  13746. }
  13747. Component.registerComponent('TrackButton', TrackButton);
  13748. /**
  13749. * @file menu-keys.js
  13750. */
  13751. /**
  13752. * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`)
  13753. * Note that 'Enter' and 'Space' are not included here (otherwise they would
  13754. * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable)
  13755. *
  13756. * @typedef MenuKeys
  13757. * @array
  13758. */
  13759. const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left'];
  13760. /**
  13761. * @file menu-item.js
  13762. */
  13763. /**
  13764. * The component for a menu item. `<li>`
  13765. *
  13766. * @extends ClickableComponent
  13767. */
  13768. class MenuItem extends ClickableComponent {
  13769. /**
  13770. * Creates an instance of the this class.
  13771. *
  13772. * @param { import('../player').default } player
  13773. * The `Player` that this class should be attached to.
  13774. *
  13775. * @param {Object} [options={}]
  13776. * The key/value store of player options.
  13777. *
  13778. */
  13779. constructor(player, options) {
  13780. super(player, options);
  13781. this.selectable = options.selectable;
  13782. this.isSelected_ = options.selected || false;
  13783. this.multiSelectable = options.multiSelectable;
  13784. this.selected(this.isSelected_);
  13785. if (this.selectable) {
  13786. if (this.multiSelectable) {
  13787. this.el_.setAttribute('role', 'menuitemcheckbox');
  13788. } else {
  13789. this.el_.setAttribute('role', 'menuitemradio');
  13790. }
  13791. } else {
  13792. this.el_.setAttribute('role', 'menuitem');
  13793. }
  13794. }
  13795. /**
  13796. * Create the `MenuItem's DOM element
  13797. *
  13798. * @param {string} [type=li]
  13799. * Element's node type, not actually used, always set to `li`.
  13800. *
  13801. * @param {Object} [props={}]
  13802. * An object of properties that should be set on the element
  13803. *
  13804. * @param {Object} [attrs={}]
  13805. * An object of attributes that should be set on the element
  13806. *
  13807. * @return {Element}
  13808. * The element that gets created.
  13809. */
  13810. createEl(type, props, attrs) {
  13811. // The control is textual, not just an icon
  13812. this.nonIconControl = true;
  13813. const el = super.createEl('li', Object.assign({
  13814. className: 'vjs-menu-item',
  13815. tabIndex: -1
  13816. }, props), attrs);
  13817. // swap icon with menu item text.
  13818. el.replaceChild(createEl('span', {
  13819. className: 'vjs-menu-item-text',
  13820. textContent: this.localize(this.options_.label)
  13821. }), el.querySelector('.vjs-icon-placeholder'));
  13822. return el;
  13823. }
  13824. /**
  13825. * Ignore keys which are used by the menu, but pass any other ones up. See
  13826. * {@link ClickableComponent#handleKeyDown} for instances where this is called.
  13827. *
  13828. * @param {Event} event
  13829. * The `keydown` event that caused this function to be called.
  13830. *
  13831. * @listens keydown
  13832. */
  13833. handleKeyDown(event) {
  13834. if (!MenuKeys.some(key => keycode__default["default"].isEventKey(event, key))) {
  13835. // Pass keydown handling up for unused keys
  13836. super.handleKeyDown(event);
  13837. }
  13838. }
  13839. /**
  13840. * Any click on a `MenuItem` puts it into the selected state.
  13841. * See {@link ClickableComponent#handleClick} for instances where this is called.
  13842. *
  13843. * @param {Event} event
  13844. * The `keydown`, `tap`, or `click` event that caused this function to be
  13845. * called.
  13846. *
  13847. * @listens tap
  13848. * @listens click
  13849. */
  13850. handleClick(event) {
  13851. this.selected(true);
  13852. }
  13853. /**
  13854. * Set the state for this menu item as selected or not.
  13855. *
  13856. * @param {boolean} selected
  13857. * if the menu item is selected or not
  13858. */
  13859. selected(selected) {
  13860. if (this.selectable) {
  13861. if (selected) {
  13862. this.addClass('vjs-selected');
  13863. this.el_.setAttribute('aria-checked', 'true');
  13864. // aria-checked isn't fully supported by browsers/screen readers,
  13865. // so indicate selected state to screen reader in the control text.
  13866. this.controlText(', selected');
  13867. this.isSelected_ = true;
  13868. } else {
  13869. this.removeClass('vjs-selected');
  13870. this.el_.setAttribute('aria-checked', 'false');
  13871. // Indicate un-selected state to screen reader
  13872. this.controlText('');
  13873. this.isSelected_ = false;
  13874. }
  13875. }
  13876. }
  13877. }
  13878. Component.registerComponent('MenuItem', MenuItem);
  13879. /**
  13880. * @file text-track-menu-item.js
  13881. */
  13882. /**
  13883. * The specific menu item type for selecting a language within a text track kind
  13884. *
  13885. * @extends MenuItem
  13886. */
  13887. class TextTrackMenuItem extends MenuItem {
  13888. /**
  13889. * Creates an instance of this class.
  13890. *
  13891. * @param { import('../../player').default } player
  13892. * The `Player` that this class should be attached to.
  13893. *
  13894. * @param {Object} [options]
  13895. * The key/value store of player options.
  13896. */
  13897. constructor(player, options) {
  13898. const track = options.track;
  13899. const tracks = player.textTracks();
  13900. // Modify options for parent MenuItem class's init.
  13901. options.label = track.label || track.language || 'Unknown';
  13902. options.selected = track.mode === 'showing';
  13903. super(player, options);
  13904. this.track = track;
  13905. // Determine the relevant kind(s) of tracks for this component and filter
  13906. // out empty kinds.
  13907. this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
  13908. const changeHandler = (...args) => {
  13909. this.handleTracksChange.apply(this, args);
  13910. };
  13911. const selectedLanguageChangeHandler = (...args) => {
  13912. this.handleSelectedLanguageChange.apply(this, args);
  13913. };
  13914. player.on(['loadstart', 'texttrackchange'], changeHandler);
  13915. tracks.addEventListener('change', changeHandler);
  13916. tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
  13917. this.on('dispose', function () {
  13918. player.off(['loadstart', 'texttrackchange'], changeHandler);
  13919. tracks.removeEventListener('change', changeHandler);
  13920. tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
  13921. });
  13922. // iOS7 doesn't dispatch change events to TextTrackLists when an
  13923. // associated track's mode changes. Without something like
  13924. // Object.observe() (also not present on iOS7), it's not
  13925. // possible to detect changes to the mode attribute and polyfill
  13926. // the change event. As a poor substitute, we manually dispatch
  13927. // change events whenever the controls modify the mode.
  13928. if (tracks.onchange === undefined) {
  13929. let event;
  13930. this.on(['tap', 'click'], function () {
  13931. if (typeof window__default["default"].Event !== 'object') {
  13932. // Android 2.3 throws an Illegal Constructor error for window.Event
  13933. try {
  13934. event = new window__default["default"].Event('change');
  13935. } catch (err) {
  13936. // continue regardless of error
  13937. }
  13938. }
  13939. if (!event) {
  13940. event = document__default["default"].createEvent('Event');
  13941. event.initEvent('change', true, true);
  13942. }
  13943. tracks.dispatchEvent(event);
  13944. });
  13945. }
  13946. // set the default state based on current tracks
  13947. this.handleTracksChange();
  13948. }
  13949. /**
  13950. * This gets called when an `TextTrackMenuItem` is "clicked". See
  13951. * {@link ClickableComponent} for more detailed information on what a click can be.
  13952. *
  13953. * @param {Event} event
  13954. * The `keydown`, `tap`, or `click` event that caused this function to be
  13955. * called.
  13956. *
  13957. * @listens tap
  13958. * @listens click
  13959. */
  13960. handleClick(event) {
  13961. const referenceTrack = this.track;
  13962. const tracks = this.player_.textTracks();
  13963. super.handleClick(event);
  13964. if (!tracks) {
  13965. return;
  13966. }
  13967. for (let i = 0; i < tracks.length; i++) {
  13968. const track = tracks[i];
  13969. // If the track from the text tracks list is not of the right kind,
  13970. // skip it. We do not want to affect tracks of incompatible kind(s).
  13971. if (this.kinds.indexOf(track.kind) === -1) {
  13972. continue;
  13973. }
  13974. // If this text track is the component's track and it is not showing,
  13975. // set it to showing.
  13976. if (track === referenceTrack) {
  13977. if (track.mode !== 'showing') {
  13978. track.mode = 'showing';
  13979. }
  13980. // If this text track is not the component's track and it is not
  13981. // disabled, set it to disabled.
  13982. } else if (track.mode !== 'disabled') {
  13983. track.mode = 'disabled';
  13984. }
  13985. }
  13986. }
  13987. /**
  13988. * Handle text track list change
  13989. *
  13990. * @param {Event} event
  13991. * The `change` event that caused this function to be called.
  13992. *
  13993. * @listens TextTrackList#change
  13994. */
  13995. handleTracksChange(event) {
  13996. const shouldBeSelected = this.track.mode === 'showing';
  13997. // Prevent redundant selected() calls because they may cause
  13998. // screen readers to read the appended control text unnecessarily
  13999. if (shouldBeSelected !== this.isSelected_) {
  14000. this.selected(shouldBeSelected);
  14001. }
  14002. }
  14003. handleSelectedLanguageChange(event) {
  14004. if (this.track.mode === 'showing') {
  14005. const selectedLanguage = this.player_.cache_.selectedLanguage;
  14006. // Don't replace the kind of track across the same language
  14007. if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
  14008. return;
  14009. }
  14010. this.player_.cache_.selectedLanguage = {
  14011. enabled: true,
  14012. language: this.track.language,
  14013. kind: this.track.kind
  14014. };
  14015. }
  14016. }
  14017. dispose() {
  14018. // remove reference to track object on dispose
  14019. this.track = null;
  14020. super.dispose();
  14021. }
  14022. }
  14023. Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
  14024. /**
  14025. * @file off-text-track-menu-item.js
  14026. */
  14027. /**
  14028. * A special menu item for turning of a specific type of text track
  14029. *
  14030. * @extends TextTrackMenuItem
  14031. */
  14032. class OffTextTrackMenuItem extends TextTrackMenuItem {
  14033. /**
  14034. * Creates an instance of this class.
  14035. *
  14036. * @param { import('../../player').default } player
  14037. * The `Player` that this class should be attached to.
  14038. *
  14039. * @param {Object} [options]
  14040. * The key/value store of player options.
  14041. */
  14042. constructor(player, options) {
  14043. // Create pseudo track info
  14044. // Requires options['kind']
  14045. options.track = {
  14046. player,
  14047. // it is no longer necessary to store `kind` or `kinds` on the track itself
  14048. // since they are now stored in the `kinds` property of all instances of
  14049. // TextTrackMenuItem, but this will remain for backwards compatibility
  14050. kind: options.kind,
  14051. kinds: options.kinds,
  14052. default: false,
  14053. mode: 'disabled'
  14054. };
  14055. if (!options.kinds) {
  14056. options.kinds = [options.kind];
  14057. }
  14058. if (options.label) {
  14059. options.track.label = options.label;
  14060. } else {
  14061. options.track.label = options.kinds.join(' and ') + ' off';
  14062. }
  14063. // MenuItem is selectable
  14064. options.selectable = true;
  14065. // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
  14066. options.multiSelectable = false;
  14067. super(player, options);
  14068. }
  14069. /**
  14070. * Handle text track change
  14071. *
  14072. * @param {Event} event
  14073. * The event that caused this function to run
  14074. */
  14075. handleTracksChange(event) {
  14076. const tracks = this.player().textTracks();
  14077. let shouldBeSelected = true;
  14078. for (let i = 0, l = tracks.length; i < l; i++) {
  14079. const track = tracks[i];
  14080. if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
  14081. shouldBeSelected = false;
  14082. break;
  14083. }
  14084. }
  14085. // Prevent redundant selected() calls because they may cause
  14086. // screen readers to read the appended control text unnecessarily
  14087. if (shouldBeSelected !== this.isSelected_) {
  14088. this.selected(shouldBeSelected);
  14089. }
  14090. }
  14091. handleSelectedLanguageChange(event) {
  14092. const tracks = this.player().textTracks();
  14093. let allHidden = true;
  14094. for (let i = 0, l = tracks.length; i < l; i++) {
  14095. const track = tracks[i];
  14096. if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
  14097. allHidden = false;
  14098. break;
  14099. }
  14100. }
  14101. if (allHidden) {
  14102. this.player_.cache_.selectedLanguage = {
  14103. enabled: false
  14104. };
  14105. }
  14106. }
  14107. /**
  14108. * Update control text and label on languagechange
  14109. */
  14110. handleLanguagechange() {
  14111. this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
  14112. super.handleLanguagechange();
  14113. }
  14114. }
  14115. Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
  14116. /**
  14117. * @file text-track-button.js
  14118. */
  14119. /**
  14120. * The base class for buttons that toggle specific text track types (e.g. subtitles)
  14121. *
  14122. * @extends MenuButton
  14123. */
  14124. class TextTrackButton extends TrackButton {
  14125. /**
  14126. * Creates an instance of this class.
  14127. *
  14128. * @param { import('../../player').default } player
  14129. * The `Player` that this class should be attached to.
  14130. *
  14131. * @param {Object} [options={}]
  14132. * The key/value store of player options.
  14133. */
  14134. constructor(player, options = {}) {
  14135. options.tracks = player.textTracks();
  14136. super(player, options);
  14137. }
  14138. /**
  14139. * Create a menu item for each text track
  14140. *
  14141. * @param {TextTrackMenuItem[]} [items=[]]
  14142. * Existing array of items to use during creation
  14143. *
  14144. * @return {TextTrackMenuItem[]}
  14145. * Array of menu items that were created
  14146. */
  14147. createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
  14148. // Label is an override for the [track] off label
  14149. // USed to localise captions/subtitles
  14150. let label;
  14151. if (this.label_) {
  14152. label = `${this.label_} off`;
  14153. }
  14154. // Add an OFF menu item to turn all tracks off
  14155. items.push(new OffTextTrackMenuItem(this.player_, {
  14156. kinds: this.kinds_,
  14157. kind: this.kind_,
  14158. label
  14159. }));
  14160. this.hideThreshold_ += 1;
  14161. const tracks = this.player_.textTracks();
  14162. if (!Array.isArray(this.kinds_)) {
  14163. this.kinds_ = [this.kind_];
  14164. }
  14165. for (let i = 0; i < tracks.length; i++) {
  14166. const track = tracks[i];
  14167. // only add tracks that are of an appropriate kind and have a label
  14168. if (this.kinds_.indexOf(track.kind) > -1) {
  14169. const item = new TrackMenuItem(this.player_, {
  14170. track,
  14171. kinds: this.kinds_,
  14172. kind: this.kind_,
  14173. // MenuItem is selectable
  14174. selectable: true,
  14175. // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
  14176. multiSelectable: false
  14177. });
  14178. item.addClass(`vjs-${track.kind}-menu-item`);
  14179. items.push(item);
  14180. }
  14181. }
  14182. return items;
  14183. }
  14184. }
  14185. Component.registerComponent('TextTrackButton', TextTrackButton);
  14186. /**
  14187. * @file chapters-track-menu-item.js
  14188. */
  14189. /**
  14190. * The chapter track menu item
  14191. *
  14192. * @extends MenuItem
  14193. */
  14194. class ChaptersTrackMenuItem extends MenuItem {
  14195. /**
  14196. * Creates an instance of this class.
  14197. *
  14198. * @param { import('../../player').default } player
  14199. * The `Player` that this class should be attached to.
  14200. *
  14201. * @param {Object} [options]
  14202. * The key/value store of player options.
  14203. */
  14204. constructor(player, options) {
  14205. const track = options.track;
  14206. const cue = options.cue;
  14207. const currentTime = player.currentTime();
  14208. // Modify options for parent MenuItem class's init.
  14209. options.selectable = true;
  14210. options.multiSelectable = false;
  14211. options.label = cue.text;
  14212. options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
  14213. super(player, options);
  14214. this.track = track;
  14215. this.cue = cue;
  14216. }
  14217. /**
  14218. * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
  14219. * {@link ClickableComponent} for more detailed information on what a click can be.
  14220. *
  14221. * @param {Event} [event]
  14222. * The `keydown`, `tap`, or `click` event that caused this function to be
  14223. * called.
  14224. *
  14225. * @listens tap
  14226. * @listens click
  14227. */
  14228. handleClick(event) {
  14229. super.handleClick();
  14230. this.player_.currentTime(this.cue.startTime);
  14231. }
  14232. }
  14233. Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
  14234. /**
  14235. * @file chapters-button.js
  14236. */
  14237. /**
  14238. * The button component for toggling and selecting chapters
  14239. * Chapters act much differently than other text tracks
  14240. * Cues are navigation vs. other tracks of alternative languages
  14241. *
  14242. * @extends TextTrackButton
  14243. */
  14244. class ChaptersButton extends TextTrackButton {
  14245. /**
  14246. * Creates an instance of this class.
  14247. *
  14248. * @param { import('../../player').default } player
  14249. * The `Player` that this class should be attached to.
  14250. *
  14251. * @param {Object} [options]
  14252. * The key/value store of player options.
  14253. *
  14254. * @param {Function} [ready]
  14255. * The function to call when this function is ready.
  14256. */
  14257. constructor(player, options, ready) {
  14258. super(player, options, ready);
  14259. this.selectCurrentItem_ = () => {
  14260. this.items.forEach(item => {
  14261. item.selected(this.track_.activeCues[0] === item.cue);
  14262. });
  14263. };
  14264. }
  14265. /**
  14266. * Builds the default DOM `className`.
  14267. *
  14268. * @return {string}
  14269. * The DOM `className` for this object.
  14270. */
  14271. buildCSSClass() {
  14272. return `vjs-chapters-button ${super.buildCSSClass()}`;
  14273. }
  14274. buildWrapperCSSClass() {
  14275. return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
  14276. }
  14277. /**
  14278. * Update the menu based on the current state of its items.
  14279. *
  14280. * @param {Event} [event]
  14281. * An event that triggered this function to run.
  14282. *
  14283. * @listens TextTrackList#addtrack
  14284. * @listens TextTrackList#removetrack
  14285. * @listens TextTrackList#change
  14286. */
  14287. update(event) {
  14288. if (event && event.track && event.track.kind !== 'chapters') {
  14289. return;
  14290. }
  14291. const track = this.findChaptersTrack();
  14292. if (track !== this.track_) {
  14293. this.setTrack(track);
  14294. super.update();
  14295. } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
  14296. // Update the menu initially or if the number of cues has changed since set
  14297. super.update();
  14298. }
  14299. }
  14300. /**
  14301. * Set the currently selected track for the chapters button.
  14302. *
  14303. * @param {TextTrack} track
  14304. * The new track to select. Nothing will change if this is the currently selected
  14305. * track.
  14306. */
  14307. setTrack(track) {
  14308. if (this.track_ === track) {
  14309. return;
  14310. }
  14311. if (!this.updateHandler_) {
  14312. this.updateHandler_ = this.update.bind(this);
  14313. }
  14314. // here this.track_ refers to the old track instance
  14315. if (this.track_) {
  14316. const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
  14317. if (remoteTextTrackEl) {
  14318. remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
  14319. }
  14320. this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
  14321. this.track_ = null;
  14322. }
  14323. this.track_ = track;
  14324. // here this.track_ refers to the new track instance
  14325. if (this.track_) {
  14326. this.track_.mode = 'hidden';
  14327. const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
  14328. if (remoteTextTrackEl) {
  14329. remoteTextTrackEl.addEventListener('load', this.updateHandler_);
  14330. }
  14331. this.track_.addEventListener('cuechange', this.selectCurrentItem_);
  14332. }
  14333. }
  14334. /**
  14335. * Find the track object that is currently in use by this ChaptersButton
  14336. *
  14337. * @return {TextTrack|undefined}
  14338. * The current track or undefined if none was found.
  14339. */
  14340. findChaptersTrack() {
  14341. const tracks = this.player_.textTracks() || [];
  14342. for (let i = tracks.length - 1; i >= 0; i--) {
  14343. // We will always choose the last track as our chaptersTrack
  14344. const track = tracks[i];
  14345. if (track.kind === this.kind_) {
  14346. return track;
  14347. }
  14348. }
  14349. }
  14350. /**
  14351. * Get the caption for the ChaptersButton based on the track label. This will also
  14352. * use the current tracks localized kind as a fallback if a label does not exist.
  14353. *
  14354. * @return {string}
  14355. * The tracks current label or the localized track kind.
  14356. */
  14357. getMenuCaption() {
  14358. if (this.track_ && this.track_.label) {
  14359. return this.track_.label;
  14360. }
  14361. return this.localize(toTitleCase(this.kind_));
  14362. }
  14363. /**
  14364. * Create menu from chapter track
  14365. *
  14366. * @return { import('../../menu/menu').default }
  14367. * New menu for the chapter buttons
  14368. */
  14369. createMenu() {
  14370. this.options_.title = this.getMenuCaption();
  14371. return super.createMenu();
  14372. }
  14373. /**
  14374. * Create a menu item for each text track
  14375. *
  14376. * @return { import('./text-track-menu-item').default[] }
  14377. * Array of menu items
  14378. */
  14379. createItems() {
  14380. const items = [];
  14381. if (!this.track_) {
  14382. return items;
  14383. }
  14384. const cues = this.track_.cues;
  14385. if (!cues) {
  14386. return items;
  14387. }
  14388. for (let i = 0, l = cues.length; i < l; i++) {
  14389. const cue = cues[i];
  14390. const mi = new ChaptersTrackMenuItem(this.player_, {
  14391. track: this.track_,
  14392. cue
  14393. });
  14394. items.push(mi);
  14395. }
  14396. return items;
  14397. }
  14398. }
  14399. /**
  14400. * `kind` of TextTrack to look for to associate it with this menu.
  14401. *
  14402. * @type {string}
  14403. * @private
  14404. */
  14405. ChaptersButton.prototype.kind_ = 'chapters';
  14406. /**
  14407. * The text that should display over the `ChaptersButton`s controls. Added for localization.
  14408. *
  14409. * @type {string}
  14410. * @protected
  14411. */
  14412. ChaptersButton.prototype.controlText_ = 'Chapters';
  14413. Component.registerComponent('ChaptersButton', ChaptersButton);
  14414. /**
  14415. * @file descriptions-button.js
  14416. */
  14417. /**
  14418. * The button component for toggling and selecting descriptions
  14419. *
  14420. * @extends TextTrackButton
  14421. */
  14422. class DescriptionsButton extends TextTrackButton {
  14423. /**
  14424. * Creates an instance of this class.
  14425. *
  14426. * @param { import('../../player').default } player
  14427. * The `Player` that this class should be attached to.
  14428. *
  14429. * @param {Object} [options]
  14430. * The key/value store of player options.
  14431. *
  14432. * @param {Function} [ready]
  14433. * The function to call when this component is ready.
  14434. */
  14435. constructor(player, options, ready) {
  14436. super(player, options, ready);
  14437. const tracks = player.textTracks();
  14438. const changeHandler = bind_(this, this.handleTracksChange);
  14439. tracks.addEventListener('change', changeHandler);
  14440. this.on('dispose', function () {
  14441. tracks.removeEventListener('change', changeHandler);
  14442. });
  14443. }
  14444. /**
  14445. * Handle text track change
  14446. *
  14447. * @param {Event} event
  14448. * The event that caused this function to run
  14449. *
  14450. * @listens TextTrackList#change
  14451. */
  14452. handleTracksChange(event) {
  14453. const tracks = this.player().textTracks();
  14454. let disabled = false;
  14455. // Check whether a track of a different kind is showing
  14456. for (let i = 0, l = tracks.length; i < l; i++) {
  14457. const track = tracks[i];
  14458. if (track.kind !== this.kind_ && track.mode === 'showing') {
  14459. disabled = true;
  14460. break;
  14461. }
  14462. }
  14463. // If another track is showing, disable this menu button
  14464. if (disabled) {
  14465. this.disable();
  14466. } else {
  14467. this.enable();
  14468. }
  14469. }
  14470. /**
  14471. * Builds the default DOM `className`.
  14472. *
  14473. * @return {string}
  14474. * The DOM `className` for this object.
  14475. */
  14476. buildCSSClass() {
  14477. return `vjs-descriptions-button ${super.buildCSSClass()}`;
  14478. }
  14479. buildWrapperCSSClass() {
  14480. return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
  14481. }
  14482. }
  14483. /**
  14484. * `kind` of TextTrack to look for to associate it with this menu.
  14485. *
  14486. * @type {string}
  14487. * @private
  14488. */
  14489. DescriptionsButton.prototype.kind_ = 'descriptions';
  14490. /**
  14491. * The text that should display over the `DescriptionsButton`s controls. Added for localization.
  14492. *
  14493. * @type {string}
  14494. * @protected
  14495. */
  14496. DescriptionsButton.prototype.controlText_ = 'Descriptions';
  14497. Component.registerComponent('DescriptionsButton', DescriptionsButton);
  14498. /**
  14499. * @file subtitles-button.js
  14500. */
  14501. /**
  14502. * The button component for toggling and selecting subtitles
  14503. *
  14504. * @extends TextTrackButton
  14505. */
  14506. class SubtitlesButton extends TextTrackButton {
  14507. /**
  14508. * Creates an instance of this class.
  14509. *
  14510. * @param { import('../../player').default } player
  14511. * The `Player` that this class should be attached to.
  14512. *
  14513. * @param {Object} [options]
  14514. * The key/value store of player options.
  14515. *
  14516. * @param {Function} [ready]
  14517. * The function to call when this component is ready.
  14518. */
  14519. constructor(player, options, ready) {
  14520. super(player, options, ready);
  14521. }
  14522. /**
  14523. * Builds the default DOM `className`.
  14524. *
  14525. * @return {string}
  14526. * The DOM `className` for this object.
  14527. */
  14528. buildCSSClass() {
  14529. return `vjs-subtitles-button ${super.buildCSSClass()}`;
  14530. }
  14531. buildWrapperCSSClass() {
  14532. return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
  14533. }
  14534. }
  14535. /**
  14536. * `kind` of TextTrack to look for to associate it with this menu.
  14537. *
  14538. * @type {string}
  14539. * @private
  14540. */
  14541. SubtitlesButton.prototype.kind_ = 'subtitles';
  14542. /**
  14543. * The text that should display over the `SubtitlesButton`s controls. Added for localization.
  14544. *
  14545. * @type {string}
  14546. * @protected
  14547. */
  14548. SubtitlesButton.prototype.controlText_ = 'Subtitles';
  14549. Component.registerComponent('SubtitlesButton', SubtitlesButton);
  14550. /**
  14551. * @file caption-settings-menu-item.js
  14552. */
  14553. /**
  14554. * The menu item for caption track settings menu
  14555. *
  14556. * @extends TextTrackMenuItem
  14557. */
  14558. class CaptionSettingsMenuItem extends TextTrackMenuItem {
  14559. /**
  14560. * Creates an instance of this class.
  14561. *
  14562. * @param { import('../../player').default } player
  14563. * The `Player` that this class should be attached to.
  14564. *
  14565. * @param {Object} [options]
  14566. * The key/value store of player options.
  14567. */
  14568. constructor(player, options) {
  14569. options.track = {
  14570. player,
  14571. kind: options.kind,
  14572. label: options.kind + ' settings',
  14573. selectable: false,
  14574. default: false,
  14575. mode: 'disabled'
  14576. };
  14577. // CaptionSettingsMenuItem has no concept of 'selected'
  14578. options.selectable = false;
  14579. options.name = 'CaptionSettingsMenuItem';
  14580. super(player, options);
  14581. this.addClass('vjs-texttrack-settings');
  14582. this.controlText(', opens ' + options.kind + ' settings dialog');
  14583. }
  14584. /**
  14585. * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
  14586. * {@link ClickableComponent} for more detailed information on what a click can be.
  14587. *
  14588. * @param {Event} [event]
  14589. * The `keydown`, `tap`, or `click` event that caused this function to be
  14590. * called.
  14591. *
  14592. * @listens tap
  14593. * @listens click
  14594. */
  14595. handleClick(event) {
  14596. this.player().getChild('textTrackSettings').open();
  14597. }
  14598. /**
  14599. * Update control text and label on languagechange
  14600. */
  14601. handleLanguagechange() {
  14602. this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
  14603. super.handleLanguagechange();
  14604. }
  14605. }
  14606. Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
  14607. /**
  14608. * @file captions-button.js
  14609. */
  14610. /**
  14611. * The button component for toggling and selecting captions
  14612. *
  14613. * @extends TextTrackButton
  14614. */
  14615. class CaptionsButton extends TextTrackButton {
  14616. /**
  14617. * Creates an instance of this class.
  14618. *
  14619. * @param { import('../../player').default } player
  14620. * The `Player` that this class should be attached to.
  14621. *
  14622. * @param {Object} [options]
  14623. * The key/value store of player options.
  14624. *
  14625. * @param {Function} [ready]
  14626. * The function to call when this component is ready.
  14627. */
  14628. constructor(player, options, ready) {
  14629. super(player, options, ready);
  14630. }
  14631. /**
  14632. * Builds the default DOM `className`.
  14633. *
  14634. * @return {string}
  14635. * The DOM `className` for this object.
  14636. */
  14637. buildCSSClass() {
  14638. return `vjs-captions-button ${super.buildCSSClass()}`;
  14639. }
  14640. buildWrapperCSSClass() {
  14641. return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
  14642. }
  14643. /**
  14644. * Create caption menu items
  14645. *
  14646. * @return {CaptionSettingsMenuItem[]}
  14647. * The array of current menu items.
  14648. */
  14649. createItems() {
  14650. const items = [];
  14651. if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
  14652. items.push(new CaptionSettingsMenuItem(this.player_, {
  14653. kind: this.kind_
  14654. }));
  14655. this.hideThreshold_ += 1;
  14656. }
  14657. return super.createItems(items);
  14658. }
  14659. }
  14660. /**
  14661. * `kind` of TextTrack to look for to associate it with this menu.
  14662. *
  14663. * @type {string}
  14664. * @private
  14665. */
  14666. CaptionsButton.prototype.kind_ = 'captions';
  14667. /**
  14668. * The text that should display over the `CaptionsButton`s controls. Added for localization.
  14669. *
  14670. * @type {string}
  14671. * @protected
  14672. */
  14673. CaptionsButton.prototype.controlText_ = 'Captions';
  14674. Component.registerComponent('CaptionsButton', CaptionsButton);
  14675. /**
  14676. * @file subs-caps-menu-item.js
  14677. */
  14678. /**
  14679. * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
  14680. * in the SubsCapsMenu.
  14681. *
  14682. * @extends TextTrackMenuItem
  14683. */
  14684. class SubsCapsMenuItem extends TextTrackMenuItem {
  14685. createEl(type, props, attrs) {
  14686. const el = super.createEl(type, props, attrs);
  14687. const parentSpan = el.querySelector('.vjs-menu-item-text');
  14688. if (this.options_.track.kind === 'captions') {
  14689. parentSpan.appendChild(createEl('span', {
  14690. className: 'vjs-icon-placeholder'
  14691. }, {
  14692. 'aria-hidden': true
  14693. }));
  14694. parentSpan.appendChild(createEl('span', {
  14695. className: 'vjs-control-text',
  14696. // space added as the text will visually flow with the
  14697. // label
  14698. textContent: ` ${this.localize('Captions')}`
  14699. }));
  14700. }
  14701. return el;
  14702. }
  14703. }
  14704. Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
  14705. /**
  14706. * @file sub-caps-button.js
  14707. */
  14708. /**
  14709. * The button component for toggling and selecting captions and/or subtitles
  14710. *
  14711. * @extends TextTrackButton
  14712. */
  14713. class SubsCapsButton extends TextTrackButton {
  14714. /**
  14715. * Creates an instance of this class.
  14716. *
  14717. * @param { import('../../player').default } player
  14718. * The `Player` that this class should be attached to.
  14719. *
  14720. * @param {Object} [options]
  14721. * The key/value store of player options.
  14722. *
  14723. * @param {Function} [ready]
  14724. * The function to call when this component is ready.
  14725. */
  14726. constructor(player, options = {}) {
  14727. super(player, options);
  14728. // Although North America uses "captions" in most cases for
  14729. // "captions and subtitles" other locales use "subtitles"
  14730. this.label_ = 'subtitles';
  14731. if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
  14732. this.label_ = 'captions';
  14733. }
  14734. this.menuButton_.controlText(toTitleCase(this.label_));
  14735. }
  14736. /**
  14737. * Builds the default DOM `className`.
  14738. *
  14739. * @return {string}
  14740. * The DOM `className` for this object.
  14741. */
  14742. buildCSSClass() {
  14743. return `vjs-subs-caps-button ${super.buildCSSClass()}`;
  14744. }
  14745. buildWrapperCSSClass() {
  14746. return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
  14747. }
  14748. /**
  14749. * Create caption/subtitles menu items
  14750. *
  14751. * @return {CaptionSettingsMenuItem[]}
  14752. * The array of current menu items.
  14753. */
  14754. createItems() {
  14755. let items = [];
  14756. if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
  14757. items.push(new CaptionSettingsMenuItem(this.player_, {
  14758. kind: this.label_
  14759. }));
  14760. this.hideThreshold_ += 1;
  14761. }
  14762. items = super.createItems(items, SubsCapsMenuItem);
  14763. return items;
  14764. }
  14765. }
  14766. /**
  14767. * `kind`s of TextTrack to look for to associate it with this menu.
  14768. *
  14769. * @type {array}
  14770. * @private
  14771. */
  14772. SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
  14773. /**
  14774. * The text that should display over the `SubsCapsButton`s controls.
  14775. *
  14776. *
  14777. * @type {string}
  14778. * @protected
  14779. */
  14780. SubsCapsButton.prototype.controlText_ = 'Subtitles';
  14781. Component.registerComponent('SubsCapsButton', SubsCapsButton);
  14782. /**
  14783. * @file audio-track-menu-item.js
  14784. */
  14785. /**
  14786. * An {@link AudioTrack} {@link MenuItem}
  14787. *
  14788. * @extends MenuItem
  14789. */
  14790. class AudioTrackMenuItem extends MenuItem {
  14791. /**
  14792. * Creates an instance of this class.
  14793. *
  14794. * @param { import('../../player').default } player
  14795. * The `Player` that this class should be attached to.
  14796. *
  14797. * @param {Object} [options]
  14798. * The key/value store of player options.
  14799. */
  14800. constructor(player, options) {
  14801. const track = options.track;
  14802. const tracks = player.audioTracks();
  14803. // Modify options for parent MenuItem class's init.
  14804. options.label = track.label || track.language || 'Unknown';
  14805. options.selected = track.enabled;
  14806. super(player, options);
  14807. this.track = track;
  14808. this.addClass(`vjs-${track.kind}-menu-item`);
  14809. const changeHandler = (...args) => {
  14810. this.handleTracksChange.apply(this, args);
  14811. };
  14812. tracks.addEventListener('change', changeHandler);
  14813. this.on('dispose', () => {
  14814. tracks.removeEventListener('change', changeHandler);
  14815. });
  14816. }
  14817. createEl(type, props, attrs) {
  14818. const el = super.createEl(type, props, attrs);
  14819. const parentSpan = el.querySelector('.vjs-menu-item-text');
  14820. if (this.options_.track.kind === 'main-desc') {
  14821. parentSpan.appendChild(createEl('span', {
  14822. className: 'vjs-icon-placeholder'
  14823. }, {
  14824. 'aria-hidden': true
  14825. }));
  14826. parentSpan.appendChild(createEl('span', {
  14827. className: 'vjs-control-text',
  14828. textContent: ' ' + this.localize('Descriptions')
  14829. }));
  14830. }
  14831. return el;
  14832. }
  14833. /**
  14834. * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
  14835. * for more detailed information on what a click can be.
  14836. *
  14837. * @param {Event} [event]
  14838. * The `keydown`, `tap`, or `click` event that caused this function to be
  14839. * called.
  14840. *
  14841. * @listens tap
  14842. * @listens click
  14843. */
  14844. handleClick(event) {
  14845. super.handleClick(event);
  14846. // the audio track list will automatically toggle other tracks
  14847. // off for us.
  14848. this.track.enabled = true;
  14849. // when native audio tracks are used, we want to make sure that other tracks are turned off
  14850. if (this.player_.tech_.featuresNativeAudioTracks) {
  14851. const tracks = this.player_.audioTracks();
  14852. for (let i = 0; i < tracks.length; i++) {
  14853. const track = tracks[i];
  14854. // skip the current track since we enabled it above
  14855. if (track === this.track) {
  14856. continue;
  14857. }
  14858. track.enabled = track === this.track;
  14859. }
  14860. }
  14861. }
  14862. /**
  14863. * Handle any {@link AudioTrack} change.
  14864. *
  14865. * @param {Event} [event]
  14866. * The {@link AudioTrackList#change} event that caused this to run.
  14867. *
  14868. * @listens AudioTrackList#change
  14869. */
  14870. handleTracksChange(event) {
  14871. this.selected(this.track.enabled);
  14872. }
  14873. }
  14874. Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
  14875. /**
  14876. * @file audio-track-button.js
  14877. */
  14878. /**
  14879. * The base class for buttons that toggle specific {@link AudioTrack} types.
  14880. *
  14881. * @extends TrackButton
  14882. */
  14883. class AudioTrackButton extends TrackButton {
  14884. /**
  14885. * Creates an instance of this class.
  14886. *
  14887. * @param {Player} player
  14888. * The `Player` that this class should be attached to.
  14889. *
  14890. * @param {Object} [options={}]
  14891. * The key/value store of player options.
  14892. */
  14893. constructor(player, options = {}) {
  14894. options.tracks = player.audioTracks();
  14895. super(player, options);
  14896. }
  14897. /**
  14898. * Builds the default DOM `className`.
  14899. *
  14900. * @return {string}
  14901. * The DOM `className` for this object.
  14902. */
  14903. buildCSSClass() {
  14904. return `vjs-audio-button ${super.buildCSSClass()}`;
  14905. }
  14906. buildWrapperCSSClass() {
  14907. return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
  14908. }
  14909. /**
  14910. * Create a menu item for each audio track
  14911. *
  14912. * @param {AudioTrackMenuItem[]} [items=[]]
  14913. * An array of existing menu items to use.
  14914. *
  14915. * @return {AudioTrackMenuItem[]}
  14916. * An array of menu items
  14917. */
  14918. createItems(items = []) {
  14919. // if there's only one audio track, there no point in showing it
  14920. this.hideThreshold_ = 1;
  14921. const tracks = this.player_.audioTracks();
  14922. for (let i = 0; i < tracks.length; i++) {
  14923. const track = tracks[i];
  14924. items.push(new AudioTrackMenuItem(this.player_, {
  14925. track,
  14926. // MenuItem is selectable
  14927. selectable: true,
  14928. // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
  14929. multiSelectable: false
  14930. }));
  14931. }
  14932. return items;
  14933. }
  14934. }
  14935. /**
  14936. * The text that should display over the `AudioTrackButton`s controls. Added for localization.
  14937. *
  14938. * @type {string}
  14939. * @protected
  14940. */
  14941. AudioTrackButton.prototype.controlText_ = 'Audio Track';
  14942. Component.registerComponent('AudioTrackButton', AudioTrackButton);
  14943. /**
  14944. * @file playback-rate-menu-item.js
  14945. */
  14946. /**
  14947. * The specific menu item type for selecting a playback rate.
  14948. *
  14949. * @extends MenuItem
  14950. */
  14951. class PlaybackRateMenuItem extends MenuItem {
  14952. /**
  14953. * Creates an instance of this class.
  14954. *
  14955. * @param { import('../../player').default } player
  14956. * The `Player` that this class should be attached to.
  14957. *
  14958. * @param {Object} [options]
  14959. * The key/value store of player options.
  14960. */
  14961. constructor(player, options) {
  14962. const label = options.rate;
  14963. const rate = parseFloat(label, 10);
  14964. // Modify options for parent MenuItem class's init.
  14965. options.label = label;
  14966. options.selected = rate === player.playbackRate();
  14967. options.selectable = true;
  14968. options.multiSelectable = false;
  14969. super(player, options);
  14970. this.label = label;
  14971. this.rate = rate;
  14972. this.on(player, 'ratechange', e => this.update(e));
  14973. }
  14974. /**
  14975. * This gets called when an `PlaybackRateMenuItem` is "clicked". See
  14976. * {@link ClickableComponent} for more detailed information on what a click can be.
  14977. *
  14978. * @param {Event} [event]
  14979. * The `keydown`, `tap`, or `click` event that caused this function to be
  14980. * called.
  14981. *
  14982. * @listens tap
  14983. * @listens click
  14984. */
  14985. handleClick(event) {
  14986. super.handleClick();
  14987. this.player().playbackRate(this.rate);
  14988. }
  14989. /**
  14990. * Update the PlaybackRateMenuItem when the playbackrate changes.
  14991. *
  14992. * @param {Event} [event]
  14993. * The `ratechange` event that caused this function to run.
  14994. *
  14995. * @listens Player#ratechange
  14996. */
  14997. update(event) {
  14998. this.selected(this.player().playbackRate() === this.rate);
  14999. }
  15000. }
  15001. /**
  15002. * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
  15003. *
  15004. * @type {string}
  15005. * @private
  15006. */
  15007. PlaybackRateMenuItem.prototype.contentElType = 'button';
  15008. Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
  15009. /**
  15010. * @file playback-rate-menu-button.js
  15011. */
  15012. /**
  15013. * The component for controlling the playback rate.
  15014. *
  15015. * @extends MenuButton
  15016. */
  15017. class PlaybackRateMenuButton extends MenuButton {
  15018. /**
  15019. * Creates an instance of this class.
  15020. *
  15021. * @param { import('../../player').default } player
  15022. * The `Player` that this class should be attached to.
  15023. *
  15024. * @param {Object} [options]
  15025. * The key/value store of player options.
  15026. */
  15027. constructor(player, options) {
  15028. super(player, options);
  15029. this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
  15030. this.updateVisibility();
  15031. this.updateLabel();
  15032. this.on(player, 'loadstart', e => this.updateVisibility(e));
  15033. this.on(player, 'ratechange', e => this.updateLabel(e));
  15034. this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
  15035. }
  15036. /**
  15037. * Create the `Component`'s DOM element
  15038. *
  15039. * @return {Element}
  15040. * The element that was created.
  15041. */
  15042. createEl() {
  15043. const el = super.createEl();
  15044. this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
  15045. this.labelEl_ = createEl('div', {
  15046. className: 'vjs-playback-rate-value',
  15047. id: this.labelElId_,
  15048. textContent: '1x'
  15049. });
  15050. el.appendChild(this.labelEl_);
  15051. return el;
  15052. }
  15053. dispose() {
  15054. this.labelEl_ = null;
  15055. super.dispose();
  15056. }
  15057. /**
  15058. * Builds the default DOM `className`.
  15059. *
  15060. * @return {string}
  15061. * The DOM `className` for this object.
  15062. */
  15063. buildCSSClass() {
  15064. return `vjs-playback-rate ${super.buildCSSClass()}`;
  15065. }
  15066. buildWrapperCSSClass() {
  15067. return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
  15068. }
  15069. /**
  15070. * Create the list of menu items. Specific to each subclass.
  15071. *
  15072. */
  15073. createItems() {
  15074. const rates = this.playbackRates();
  15075. const items = [];
  15076. for (let i = rates.length - 1; i >= 0; i--) {
  15077. items.push(new PlaybackRateMenuItem(this.player(), {
  15078. rate: rates[i] + 'x'
  15079. }));
  15080. }
  15081. return items;
  15082. }
  15083. /**
  15084. * On playbackrateschange, update the menu to account for the new items.
  15085. *
  15086. * @listens Player#playbackrateschange
  15087. */
  15088. handlePlaybackRateschange(event) {
  15089. this.update();
  15090. }
  15091. /**
  15092. * Get possible playback rates
  15093. *
  15094. * @return {Array}
  15095. * All possible playback rates
  15096. */
  15097. playbackRates() {
  15098. const player = this.player();
  15099. return player.playbackRates && player.playbackRates() || [];
  15100. }
  15101. /**
  15102. * Get whether playback rates is supported by the tech
  15103. * and an array of playback rates exists
  15104. *
  15105. * @return {boolean}
  15106. * Whether changing playback rate is supported
  15107. */
  15108. playbackRateSupported() {
  15109. return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
  15110. }
  15111. /**
  15112. * Hide playback rate controls when they're no playback rate options to select
  15113. *
  15114. * @param {Event} [event]
  15115. * The event that caused this function to run.
  15116. *
  15117. * @listens Player#loadstart
  15118. */
  15119. updateVisibility(event) {
  15120. if (this.playbackRateSupported()) {
  15121. this.removeClass('vjs-hidden');
  15122. } else {
  15123. this.addClass('vjs-hidden');
  15124. }
  15125. }
  15126. /**
  15127. * Update button label when rate changed
  15128. *
  15129. * @param {Event} [event]
  15130. * The event that caused this function to run.
  15131. *
  15132. * @listens Player#ratechange
  15133. */
  15134. updateLabel(event) {
  15135. if (this.playbackRateSupported()) {
  15136. this.labelEl_.textContent = this.player().playbackRate() + 'x';
  15137. }
  15138. }
  15139. }
  15140. /**
  15141. * The text that should display over the `PlaybackRateMenuButton`s controls.
  15142. *
  15143. * Added for localization.
  15144. *
  15145. * @type {string}
  15146. * @protected
  15147. */
  15148. PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
  15149. Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
  15150. /**
  15151. * @file spacer.js
  15152. */
  15153. /**
  15154. * Just an empty spacer element that can be used as an append point for plugins, etc.
  15155. * Also can be used to create space between elements when necessary.
  15156. *
  15157. * @extends Component
  15158. */
  15159. class Spacer extends Component {
  15160. /**
  15161. * Builds the default DOM `className`.
  15162. *
  15163. * @return {string}
  15164. * The DOM `className` for this object.
  15165. */
  15166. buildCSSClass() {
  15167. return `vjs-spacer ${super.buildCSSClass()}`;
  15168. }
  15169. /**
  15170. * Create the `Component`'s DOM element
  15171. *
  15172. * @return {Element}
  15173. * The element that was created.
  15174. */
  15175. createEl(tag = 'div', props = {}, attributes = {}) {
  15176. if (!props.className) {
  15177. props.className = this.buildCSSClass();
  15178. }
  15179. return super.createEl(tag, props, attributes);
  15180. }
  15181. }
  15182. Component.registerComponent('Spacer', Spacer);
  15183. /**
  15184. * @file custom-control-spacer.js
  15185. */
  15186. /**
  15187. * Spacer specifically meant to be used as an insertion point for new plugins, etc.
  15188. *
  15189. * @extends Spacer
  15190. */
  15191. class CustomControlSpacer extends Spacer {
  15192. /**
  15193. * Builds the default DOM `className`.
  15194. *
  15195. * @return {string}
  15196. * The DOM `className` for this object.
  15197. */
  15198. buildCSSClass() {
  15199. return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
  15200. }
  15201. /**
  15202. * Create the `Component`'s DOM element
  15203. *
  15204. * @return {Element}
  15205. * The element that was created.
  15206. */
  15207. createEl() {
  15208. return super.createEl('div', {
  15209. className: this.buildCSSClass(),
  15210. // No-flex/table-cell mode requires there be some content
  15211. // in the cell to fill the remaining space of the table.
  15212. textContent: '\u00a0'
  15213. });
  15214. }
  15215. }
  15216. Component.registerComponent('CustomControlSpacer', CustomControlSpacer);
  15217. /**
  15218. * @file control-bar.js
  15219. */
  15220. /**
  15221. * Container of main controls.
  15222. *
  15223. * @extends Component
  15224. */
  15225. class ControlBar extends Component {
  15226. /**
  15227. * Create the `Component`'s DOM element
  15228. *
  15229. * @return {Element}
  15230. * The element that was created.
  15231. */
  15232. createEl() {
  15233. return super.createEl('div', {
  15234. className: 'vjs-control-bar',
  15235. dir: 'ltr'
  15236. });
  15237. }
  15238. }
  15239. /**
  15240. * Default options for `ControlBar`
  15241. *
  15242. * @type {Object}
  15243. * @private
  15244. */
  15245. ControlBar.prototype.options_ = {
  15246. children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'fullscreenToggle']
  15247. };
  15248. if ('exitPictureInPicture' in document__default["default"]) {
  15249. ControlBar.prototype.options_.children.splice(ControlBar.prototype.options_.children.length - 1, 0, 'pictureInPictureToggle');
  15250. }
  15251. Component.registerComponent('ControlBar', ControlBar);
  15252. /**
  15253. * @file error-display.js
  15254. */
  15255. /**
  15256. * A display that indicates an error has occurred. This means that the video
  15257. * is unplayable.
  15258. *
  15259. * @extends ModalDialog
  15260. */
  15261. class ErrorDisplay extends ModalDialog {
  15262. /**
  15263. * Creates an instance of this class.
  15264. *
  15265. * @param { import('./player').default } player
  15266. * The `Player` that this class should be attached to.
  15267. *
  15268. * @param {Object} [options]
  15269. * The key/value store of player options.
  15270. */
  15271. constructor(player, options) {
  15272. super(player, options);
  15273. this.on(player, 'error', e => this.open(e));
  15274. }
  15275. /**
  15276. * Builds the default DOM `className`.
  15277. *
  15278. * @return {string}
  15279. * The DOM `className` for this object.
  15280. *
  15281. * @deprecated Since version 5.
  15282. */
  15283. buildCSSClass() {
  15284. return `vjs-error-display ${super.buildCSSClass()}`;
  15285. }
  15286. /**
  15287. * Gets the localized error message based on the `Player`s error.
  15288. *
  15289. * @return {string}
  15290. * The `Player`s error message localized or an empty string.
  15291. */
  15292. content() {
  15293. const error = this.player().error();
  15294. return error ? this.localize(error.message) : '';
  15295. }
  15296. }
  15297. /**
  15298. * The default options for an `ErrorDisplay`.
  15299. *
  15300. * @private
  15301. */
  15302. ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
  15303. pauseOnOpen: false,
  15304. fillAlways: true,
  15305. temporary: false,
  15306. uncloseable: true
  15307. });
  15308. Component.registerComponent('ErrorDisplay', ErrorDisplay);
  15309. /**
  15310. * @file text-track-settings.js
  15311. */
  15312. const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
  15313. const COLOR_BLACK = ['#000', 'Black'];
  15314. const COLOR_BLUE = ['#00F', 'Blue'];
  15315. const COLOR_CYAN = ['#0FF', 'Cyan'];
  15316. const COLOR_GREEN = ['#0F0', 'Green'];
  15317. const COLOR_MAGENTA = ['#F0F', 'Magenta'];
  15318. const COLOR_RED = ['#F00', 'Red'];
  15319. const COLOR_WHITE = ['#FFF', 'White'];
  15320. const COLOR_YELLOW = ['#FF0', 'Yellow'];
  15321. const OPACITY_OPAQUE = ['1', 'Opaque'];
  15322. const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
  15323. const OPACITY_TRANS = ['0', 'Transparent'];
  15324. // Configuration for the various <select> elements in the DOM of this component.
  15325. //
  15326. // Possible keys include:
  15327. //
  15328. // `default`:
  15329. // The default option index. Only needs to be provided if not zero.
  15330. // `parser`:
  15331. // A function which is used to parse the value from the selected option in
  15332. // a customized way.
  15333. // `selector`:
  15334. // The selector used to find the associated <select> element.
  15335. const selectConfigs = {
  15336. backgroundColor: {
  15337. selector: '.vjs-bg-color > select',
  15338. id: 'captions-background-color-%s',
  15339. label: 'Color',
  15340. options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
  15341. },
  15342. backgroundOpacity: {
  15343. selector: '.vjs-bg-opacity > select',
  15344. id: 'captions-background-opacity-%s',
  15345. label: 'Opacity',
  15346. options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS]
  15347. },
  15348. color: {
  15349. selector: '.vjs-text-color > select',
  15350. id: 'captions-foreground-color-%s',
  15351. label: 'Color',
  15352. options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
  15353. },
  15354. edgeStyle: {
  15355. selector: '.vjs-edge-style > select',
  15356. id: '%s',
  15357. label: 'Text Edge Style',
  15358. options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Dropshadow']]
  15359. },
  15360. fontFamily: {
  15361. selector: '.vjs-font-family > select',
  15362. id: 'captions-font-family-%s',
  15363. label: 'Font Family',
  15364. options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
  15365. },
  15366. fontPercent: {
  15367. selector: '.vjs-font-percent > select',
  15368. id: 'captions-font-size-%s',
  15369. label: 'Font Size',
  15370. options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']],
  15371. default: 2,
  15372. parser: v => v === '1.00' ? null : Number(v)
  15373. },
  15374. textOpacity: {
  15375. selector: '.vjs-text-opacity > select',
  15376. id: 'captions-foreground-opacity-%s',
  15377. label: 'Opacity',
  15378. options: [OPACITY_OPAQUE, OPACITY_SEMI]
  15379. },
  15380. // Options for this object are defined below.
  15381. windowColor: {
  15382. selector: '.vjs-window-color > select',
  15383. id: 'captions-window-color-%s',
  15384. label: 'Color'
  15385. },
  15386. // Options for this object are defined below.
  15387. windowOpacity: {
  15388. selector: '.vjs-window-opacity > select',
  15389. id: 'captions-window-opacity-%s',
  15390. label: 'Opacity',
  15391. options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE]
  15392. }
  15393. };
  15394. selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
  15395. /**
  15396. * Get the actual value of an option.
  15397. *
  15398. * @param {string} value
  15399. * The value to get
  15400. *
  15401. * @param {Function} [parser]
  15402. * Optional function to adjust the value.
  15403. *
  15404. * @return {*}
  15405. * - Will be `undefined` if no value exists
  15406. * - Will be `undefined` if the given value is "none".
  15407. * - Will be the actual value otherwise.
  15408. *
  15409. * @private
  15410. */
  15411. function parseOptionValue(value, parser) {
  15412. if (parser) {
  15413. value = parser(value);
  15414. }
  15415. if (value && value !== 'none') {
  15416. return value;
  15417. }
  15418. }
  15419. /**
  15420. * Gets the value of the selected <option> element within a <select> element.
  15421. *
  15422. * @param {Element} el
  15423. * the element to look in
  15424. *
  15425. * @param {Function} [parser]
  15426. * Optional function to adjust the value.
  15427. *
  15428. * @return {*}
  15429. * - Will be `undefined` if no value exists
  15430. * - Will be `undefined` if the given value is "none".
  15431. * - Will be the actual value otherwise.
  15432. *
  15433. * @private
  15434. */
  15435. function getSelectedOptionValue(el, parser) {
  15436. const value = el.options[el.options.selectedIndex].value;
  15437. return parseOptionValue(value, parser);
  15438. }
  15439. /**
  15440. * Sets the selected <option> element within a <select> element based on a
  15441. * given value.
  15442. *
  15443. * @param {Element} el
  15444. * The element to look in.
  15445. *
  15446. * @param {string} value
  15447. * the property to look on.
  15448. *
  15449. * @param {Function} [parser]
  15450. * Optional function to adjust the value before comparing.
  15451. *
  15452. * @private
  15453. */
  15454. function setSelectedOption(el, value, parser) {
  15455. if (!value) {
  15456. return;
  15457. }
  15458. for (let i = 0; i < el.options.length; i++) {
  15459. if (parseOptionValue(el.options[i].value, parser) === value) {
  15460. el.selectedIndex = i;
  15461. break;
  15462. }
  15463. }
  15464. }
  15465. /**
  15466. * Manipulate Text Tracks settings.
  15467. *
  15468. * @extends ModalDialog
  15469. */
  15470. class TextTrackSettings extends ModalDialog {
  15471. /**
  15472. * Creates an instance of this class.
  15473. *
  15474. * @param { import('../player').default } player
  15475. * The `Player` that this class should be attached to.
  15476. *
  15477. * @param {Object} [options]
  15478. * The key/value store of player options.
  15479. */
  15480. constructor(player, options) {
  15481. options.temporary = false;
  15482. super(player, options);
  15483. this.updateDisplay = this.updateDisplay.bind(this);
  15484. // fill the modal and pretend we have opened it
  15485. this.fill();
  15486. this.hasBeenOpened_ = this.hasBeenFilled_ = true;
  15487. this.endDialog = createEl('p', {
  15488. className: 'vjs-control-text',
  15489. textContent: this.localize('End of dialog window.')
  15490. });
  15491. this.el().appendChild(this.endDialog);
  15492. this.setDefaults();
  15493. // Grab `persistTextTrackSettings` from the player options if not passed in child options
  15494. if (options.persistTextTrackSettings === undefined) {
  15495. this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
  15496. }
  15497. this.on(this.$('.vjs-done-button'), 'click', () => {
  15498. this.saveSettings();
  15499. this.close();
  15500. });
  15501. this.on(this.$('.vjs-default-button'), 'click', () => {
  15502. this.setDefaults();
  15503. this.updateDisplay();
  15504. });
  15505. each(selectConfigs, config => {
  15506. this.on(this.$(config.selector), 'change', this.updateDisplay);
  15507. });
  15508. if (this.options_.persistTextTrackSettings) {
  15509. this.restoreSettings();
  15510. }
  15511. }
  15512. dispose() {
  15513. this.endDialog = null;
  15514. super.dispose();
  15515. }
  15516. /**
  15517. * Create a <select> element with configured options.
  15518. *
  15519. * @param {string} key
  15520. * Configuration key to use during creation.
  15521. *
  15522. * @return {string}
  15523. * An HTML string.
  15524. *
  15525. * @private
  15526. */
  15527. createElSelect_(key, legendId = '', type = 'label') {
  15528. const config = selectConfigs[key];
  15529. const id = config.id.replace('%s', this.id_);
  15530. const selectLabelledbyIds = [legendId, id].join(' ').trim();
  15531. return [`<${type} id="${id}" class="${type === 'label' ? 'vjs-label' : ''}">`, this.localize(config.label), `</${type}>`, `<select aria-labelledby="${selectLabelledbyIds}">`].concat(config.options.map(o => {
  15532. const optionId = id + '-' + o[1].replace(/\W+/g, '');
  15533. return [`<option id="${optionId}" value="${o[0]}" `, `aria-labelledby="${selectLabelledbyIds} ${optionId}">`, this.localize(o[1]), '</option>'].join('');
  15534. })).concat('</select>').join('');
  15535. }
  15536. /**
  15537. * Create foreground color element for the component
  15538. *
  15539. * @return {string}
  15540. * An HTML string.
  15541. *
  15542. * @private
  15543. */
  15544. createElFgColor_() {
  15545. const legendId = `captions-text-legend-${this.id_}`;
  15546. return ['<fieldset class="vjs-fg vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Text'), '</legend>', '<span class="vjs-text-color">', this.createElSelect_('color', legendId), '</span>', '<span class="vjs-text-opacity vjs-opacity">', this.createElSelect_('textOpacity', legendId), '</span>', '</fieldset>'].join('');
  15547. }
  15548. /**
  15549. * Create background color element for the component
  15550. *
  15551. * @return {string}
  15552. * An HTML string.
  15553. *
  15554. * @private
  15555. */
  15556. createElBgColor_() {
  15557. const legendId = `captions-background-${this.id_}`;
  15558. return ['<fieldset class="vjs-bg vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Text Background'), '</legend>', '<span class="vjs-bg-color">', this.createElSelect_('backgroundColor', legendId), '</span>', '<span class="vjs-bg-opacity vjs-opacity">', this.createElSelect_('backgroundOpacity', legendId), '</span>', '</fieldset>'].join('');
  15559. }
  15560. /**
  15561. * Create window color element for the component
  15562. *
  15563. * @return {string}
  15564. * An HTML string.
  15565. *
  15566. * @private
  15567. */
  15568. createElWinColor_() {
  15569. const legendId = `captions-window-${this.id_}`;
  15570. return ['<fieldset class="vjs-window vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Caption Area Background'), '</legend>', '<span class="vjs-window-color">', this.createElSelect_('windowColor', legendId), '</span>', '<span class="vjs-window-opacity vjs-opacity">', this.createElSelect_('windowOpacity', legendId), '</span>', '</fieldset>'].join('');
  15571. }
  15572. /**
  15573. * Create color elements for the component
  15574. *
  15575. * @return {Element}
  15576. * The element that was created
  15577. *
  15578. * @private
  15579. */
  15580. createElColors_() {
  15581. return createEl('div', {
  15582. className: 'vjs-track-settings-colors',
  15583. innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('')
  15584. });
  15585. }
  15586. /**
  15587. * Create font elements for the component
  15588. *
  15589. * @return {Element}
  15590. * The element that was created.
  15591. *
  15592. * @private
  15593. */
  15594. createElFont_() {
  15595. return createEl('div', {
  15596. className: 'vjs-track-settings-font',
  15597. innerHTML: ['<fieldset class="vjs-font-percent vjs-track-setting">', this.createElSelect_('fontPercent', '', 'legend'), '</fieldset>', '<fieldset class="vjs-edge-style vjs-track-setting">', this.createElSelect_('edgeStyle', '', 'legend'), '</fieldset>', '<fieldset class="vjs-font-family vjs-track-setting">', this.createElSelect_('fontFamily', '', 'legend'), '</fieldset>'].join('')
  15598. });
  15599. }
  15600. /**
  15601. * Create controls for the component
  15602. *
  15603. * @return {Element}
  15604. * The element that was created.
  15605. *
  15606. * @private
  15607. */
  15608. createElControls_() {
  15609. const defaultsDescription = this.localize('restore all settings to the default values');
  15610. return createEl('div', {
  15611. className: 'vjs-track-settings-controls',
  15612. innerHTML: [`<button type="button" class="vjs-default-button" title="${defaultsDescription}">`, this.localize('Reset'), `<span class="vjs-control-text"> ${defaultsDescription}</span>`, '</button>', `<button type="button" class="vjs-done-button">${this.localize('Done')}</button>`].join('')
  15613. });
  15614. }
  15615. content() {
  15616. return [this.createElColors_(), this.createElFont_(), this.createElControls_()];
  15617. }
  15618. label() {
  15619. return this.localize('Caption Settings Dialog');
  15620. }
  15621. description() {
  15622. return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
  15623. }
  15624. buildCSSClass() {
  15625. return super.buildCSSClass() + ' vjs-text-track-settings';
  15626. }
  15627. /**
  15628. * Gets an object of text track settings (or null).
  15629. *
  15630. * @return {Object}
  15631. * An object with config values parsed from the DOM or localStorage.
  15632. */
  15633. getValues() {
  15634. return reduce(selectConfigs, (accum, config, key) => {
  15635. const value = getSelectedOptionValue(this.$(config.selector), config.parser);
  15636. if (value !== undefined) {
  15637. accum[key] = value;
  15638. }
  15639. return accum;
  15640. }, {});
  15641. }
  15642. /**
  15643. * Sets text track settings from an object of values.
  15644. *
  15645. * @param {Object} values
  15646. * An object with config values parsed from the DOM or localStorage.
  15647. */
  15648. setValues(values) {
  15649. each(selectConfigs, (config, key) => {
  15650. setSelectedOption(this.$(config.selector), values[key], config.parser);
  15651. });
  15652. }
  15653. /**
  15654. * Sets all `<select>` elements to their default values.
  15655. */
  15656. setDefaults() {
  15657. each(selectConfigs, config => {
  15658. const index = config.hasOwnProperty('default') ? config.default : 0;
  15659. this.$(config.selector).selectedIndex = index;
  15660. });
  15661. }
  15662. /**
  15663. * Restore texttrack settings from localStorage
  15664. */
  15665. restoreSettings() {
  15666. let values;
  15667. try {
  15668. values = JSON.parse(window__default["default"].localStorage.getItem(LOCAL_STORAGE_KEY));
  15669. } catch (err) {
  15670. log.warn(err);
  15671. }
  15672. if (values) {
  15673. this.setValues(values);
  15674. }
  15675. }
  15676. /**
  15677. * Save text track settings to localStorage
  15678. */
  15679. saveSettings() {
  15680. if (!this.options_.persistTextTrackSettings) {
  15681. return;
  15682. }
  15683. const values = this.getValues();
  15684. try {
  15685. if (Object.keys(values).length) {
  15686. window__default["default"].localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values));
  15687. } else {
  15688. window__default["default"].localStorage.removeItem(LOCAL_STORAGE_KEY);
  15689. }
  15690. } catch (err) {
  15691. log.warn(err);
  15692. }
  15693. }
  15694. /**
  15695. * Update display of text track settings
  15696. */
  15697. updateDisplay() {
  15698. const ttDisplay = this.player_.getChild('textTrackDisplay');
  15699. if (ttDisplay) {
  15700. ttDisplay.updateDisplay();
  15701. }
  15702. }
  15703. /**
  15704. * conditionally blur the element and refocus the captions button
  15705. *
  15706. * @private
  15707. */
  15708. conditionalBlur_() {
  15709. this.previouslyActiveEl_ = null;
  15710. const cb = this.player_.controlBar;
  15711. const subsCapsBtn = cb && cb.subsCapsButton;
  15712. const ccBtn = cb && cb.captionsButton;
  15713. if (subsCapsBtn) {
  15714. subsCapsBtn.focus();
  15715. } else if (ccBtn) {
  15716. ccBtn.focus();
  15717. }
  15718. }
  15719. /**
  15720. * Repopulate dialog with new localizations on languagechange
  15721. */
  15722. handleLanguagechange() {
  15723. this.fill();
  15724. }
  15725. }
  15726. Component.registerComponent('TextTrackSettings', TextTrackSettings);
  15727. /**
  15728. * @file resize-manager.js
  15729. */
  15730. /**
  15731. * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
  15732. *
  15733. * It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
  15734. *
  15735. * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
  15736. * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
  15737. *
  15738. * @example <caption>How to disable the resize manager</caption>
  15739. * const player = videojs('#vid', {
  15740. * resizeManager: false
  15741. * });
  15742. *
  15743. * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
  15744. *
  15745. * @extends Component
  15746. */
  15747. class ResizeManager extends Component {
  15748. /**
  15749. * Create the ResizeManager.
  15750. *
  15751. * @param {Object} player
  15752. * The `Player` that this class should be attached to.
  15753. *
  15754. * @param {Object} [options]
  15755. * The key/value store of ResizeManager options.
  15756. *
  15757. * @param {Object} [options.ResizeObserver]
  15758. * A polyfill for ResizeObserver can be passed in here.
  15759. * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
  15760. */
  15761. constructor(player, options) {
  15762. let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window__default["default"].ResizeObserver;
  15763. // if `null` was passed, we want to disable the ResizeObserver
  15764. if (options.ResizeObserver === null) {
  15765. RESIZE_OBSERVER_AVAILABLE = false;
  15766. }
  15767. // Only create an element when ResizeObserver isn't available
  15768. const options_ = merge({
  15769. createEl: !RESIZE_OBSERVER_AVAILABLE,
  15770. reportTouchActivity: false
  15771. }, options);
  15772. super(player, options_);
  15773. this.ResizeObserver = options.ResizeObserver || window__default["default"].ResizeObserver;
  15774. this.loadListener_ = null;
  15775. this.resizeObserver_ = null;
  15776. this.debouncedHandler_ = debounce(() => {
  15777. this.resizeHandler();
  15778. }, 100, false, this);
  15779. if (RESIZE_OBSERVER_AVAILABLE) {
  15780. this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
  15781. this.resizeObserver_.observe(player.el());
  15782. } else {
  15783. this.loadListener_ = () => {
  15784. if (!this.el_ || !this.el_.contentWindow) {
  15785. return;
  15786. }
  15787. const debouncedHandler_ = this.debouncedHandler_;
  15788. let unloadListener_ = this.unloadListener_ = function () {
  15789. off(this, 'resize', debouncedHandler_);
  15790. off(this, 'unload', unloadListener_);
  15791. unloadListener_ = null;
  15792. };
  15793. // safari and edge can unload the iframe before resizemanager dispose
  15794. // we have to dispose of event handlers correctly before that happens
  15795. on(this.el_.contentWindow, 'unload', unloadListener_);
  15796. on(this.el_.contentWindow, 'resize', debouncedHandler_);
  15797. };
  15798. this.one('load', this.loadListener_);
  15799. }
  15800. }
  15801. createEl() {
  15802. return super.createEl('iframe', {
  15803. className: 'vjs-resize-manager',
  15804. tabIndex: -1,
  15805. title: this.localize('No content')
  15806. }, {
  15807. 'aria-hidden': 'true'
  15808. });
  15809. }
  15810. /**
  15811. * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
  15812. *
  15813. * @fires Player#playerresize
  15814. */
  15815. resizeHandler() {
  15816. /**
  15817. * Called when the player size has changed
  15818. *
  15819. * @event Player#playerresize
  15820. * @type {Event}
  15821. */
  15822. // make sure player is still around to trigger
  15823. // prevents this from causing an error after dispose
  15824. if (!this.player_ || !this.player_.trigger) {
  15825. return;
  15826. }
  15827. this.player_.trigger('playerresize');
  15828. }
  15829. dispose() {
  15830. if (this.debouncedHandler_) {
  15831. this.debouncedHandler_.cancel();
  15832. }
  15833. if (this.resizeObserver_) {
  15834. if (this.player_.el()) {
  15835. this.resizeObserver_.unobserve(this.player_.el());
  15836. }
  15837. this.resizeObserver_.disconnect();
  15838. }
  15839. if (this.loadListener_) {
  15840. this.off('load', this.loadListener_);
  15841. }
  15842. if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
  15843. this.unloadListener_.call(this.el_.contentWindow);
  15844. }
  15845. this.ResizeObserver = null;
  15846. this.resizeObserver = null;
  15847. this.debouncedHandler_ = null;
  15848. this.loadListener_ = null;
  15849. super.dispose();
  15850. }
  15851. }
  15852. Component.registerComponent('ResizeManager', ResizeManager);
  15853. const defaults = {
  15854. trackingThreshold: 20,
  15855. liveTolerance: 15
  15856. };
  15857. /*
  15858. track when we are at the live edge, and other helpers for live playback */
  15859. /**
  15860. * A class for checking live current time and determining when the player
  15861. * is at or behind the live edge.
  15862. */
  15863. class LiveTracker extends Component {
  15864. /**
  15865. * Creates an instance of this class.
  15866. *
  15867. * @param { import('./player').default } player
  15868. * The `Player` that this class should be attached to.
  15869. *
  15870. * @param {Object} [options]
  15871. * The key/value store of player options.
  15872. *
  15873. * @param {number} [options.trackingThreshold=20]
  15874. * Number of seconds of live window (seekableEnd - seekableStart) that
  15875. * media needs to have before the liveui will be shown.
  15876. *
  15877. * @param {number} [options.liveTolerance=15]
  15878. * Number of seconds behind live that we have to be
  15879. * before we will be considered non-live. Note that this will only
  15880. * be used when playing at the live edge. This allows large seekable end
  15881. * changes to not effect whether we are live or not.
  15882. */
  15883. constructor(player, options) {
  15884. // LiveTracker does not need an element
  15885. const options_ = merge(defaults, options, {
  15886. createEl: false
  15887. });
  15888. super(player, options_);
  15889. this.trackLiveHandler_ = () => this.trackLive_();
  15890. this.handlePlay_ = e => this.handlePlay(e);
  15891. this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
  15892. this.handleSeeked_ = e => this.handleSeeked(e);
  15893. this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
  15894. this.reset_();
  15895. this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
  15896. // we should try to toggle tracking on canplay as native playback engines, like Safari
  15897. // may not have the proper values for things like seekableEnd until then
  15898. this.on(this.player_, 'canplay', () => this.toggleTracking());
  15899. }
  15900. /**
  15901. * all the functionality for tracking when seek end changes
  15902. * and for tracking how far past seek end we should be
  15903. */
  15904. trackLive_() {
  15905. const seekable = this.player_.seekable();
  15906. // skip undefined seekable
  15907. if (!seekable || !seekable.length) {
  15908. return;
  15909. }
  15910. const newTime = Number(window__default["default"].performance.now().toFixed(4));
  15911. const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
  15912. this.lastTime_ = newTime;
  15913. this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
  15914. const liveCurrentTime = this.liveCurrentTime();
  15915. const currentTime = this.player_.currentTime();
  15916. // we are behind live if any are true
  15917. // 1. the player is paused
  15918. // 2. the user seeked to a location 2 seconds away from live
  15919. // 3. the difference between live and current time is greater
  15920. // liveTolerance which defaults to 15s
  15921. let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
  15922. // we cannot be behind if
  15923. // 1. until we have not seen a timeupdate yet
  15924. // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
  15925. if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
  15926. isBehind = false;
  15927. }
  15928. if (isBehind !== this.behindLiveEdge_) {
  15929. this.behindLiveEdge_ = isBehind;
  15930. this.trigger('liveedgechange');
  15931. }
  15932. }
  15933. /**
  15934. * handle a durationchange event on the player
  15935. * and start/stop tracking accordingly.
  15936. */
  15937. handleDurationchange() {
  15938. this.toggleTracking();
  15939. }
  15940. /**
  15941. * start/stop tracking
  15942. */
  15943. toggleTracking() {
  15944. if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
  15945. if (this.player_.options_.liveui) {
  15946. this.player_.addClass('vjs-liveui');
  15947. }
  15948. this.startTracking();
  15949. } else {
  15950. this.player_.removeClass('vjs-liveui');
  15951. this.stopTracking();
  15952. }
  15953. }
  15954. /**
  15955. * start tracking live playback
  15956. */
  15957. startTracking() {
  15958. if (this.isTracking()) {
  15959. return;
  15960. }
  15961. // If we haven't seen a timeupdate, we need to check whether playback
  15962. // began before this component started tracking. This can happen commonly
  15963. // when using autoplay.
  15964. if (!this.timeupdateSeen_) {
  15965. this.timeupdateSeen_ = this.player_.hasStarted();
  15966. }
  15967. this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
  15968. this.trackLive_();
  15969. this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
  15970. if (!this.timeupdateSeen_) {
  15971. this.one(this.player_, 'play', this.handlePlay_);
  15972. this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
  15973. } else {
  15974. this.on(this.player_, 'seeked', this.handleSeeked_);
  15975. }
  15976. }
  15977. /**
  15978. * handle the first timeupdate on the player if it wasn't already playing
  15979. * when live tracker started tracking.
  15980. */
  15981. handleFirstTimeupdate() {
  15982. this.timeupdateSeen_ = true;
  15983. this.on(this.player_, 'seeked', this.handleSeeked_);
  15984. }
  15985. /**
  15986. * Keep track of what time a seek starts, and listen for seeked
  15987. * to find where a seek ends.
  15988. */
  15989. handleSeeked() {
  15990. const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
  15991. this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
  15992. this.nextSeekedFromUser_ = false;
  15993. this.trackLive_();
  15994. }
  15995. /**
  15996. * handle the first play on the player, and make sure that we seek
  15997. * right to the live edge.
  15998. */
  15999. handlePlay() {
  16000. this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
  16001. }
  16002. /**
  16003. * Stop tracking, and set all internal variables to
  16004. * their initial value.
  16005. */
  16006. reset_() {
  16007. this.lastTime_ = -1;
  16008. this.pastSeekEnd_ = 0;
  16009. this.lastSeekEnd_ = -1;
  16010. this.behindLiveEdge_ = true;
  16011. this.timeupdateSeen_ = false;
  16012. this.seekedBehindLive_ = false;
  16013. this.nextSeekedFromUser_ = false;
  16014. this.clearInterval(this.trackingInterval_);
  16015. this.trackingInterval_ = null;
  16016. this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
  16017. this.off(this.player_, 'seeked', this.handleSeeked_);
  16018. this.off(this.player_, 'play', this.handlePlay_);
  16019. this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
  16020. this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
  16021. }
  16022. /**
  16023. * The next seeked event is from the user. Meaning that any seek
  16024. * > 2s behind live will be considered behind live for real and
  16025. * liveTolerance will be ignored.
  16026. */
  16027. nextSeekedFromUser() {
  16028. this.nextSeekedFromUser_ = true;
  16029. }
  16030. /**
  16031. * stop tracking live playback
  16032. */
  16033. stopTracking() {
  16034. if (!this.isTracking()) {
  16035. return;
  16036. }
  16037. this.reset_();
  16038. this.trigger('liveedgechange');
  16039. }
  16040. /**
  16041. * A helper to get the player seekable end
  16042. * so that we don't have to null check everywhere
  16043. *
  16044. * @return {number}
  16045. * The furthest seekable end or Infinity.
  16046. */
  16047. seekableEnd() {
  16048. const seekable = this.player_.seekable();
  16049. const seekableEnds = [];
  16050. let i = seekable ? seekable.length : 0;
  16051. while (i--) {
  16052. seekableEnds.push(seekable.end(i));
  16053. }
  16054. // grab the furthest seekable end after sorting, or if there are none
  16055. // default to Infinity
  16056. return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
  16057. }
  16058. /**
  16059. * A helper to get the player seekable start
  16060. * so that we don't have to null check everywhere
  16061. *
  16062. * @return {number}
  16063. * The earliest seekable start or 0.
  16064. */
  16065. seekableStart() {
  16066. const seekable = this.player_.seekable();
  16067. const seekableStarts = [];
  16068. let i = seekable ? seekable.length : 0;
  16069. while (i--) {
  16070. seekableStarts.push(seekable.start(i));
  16071. }
  16072. // grab the first seekable start after sorting, or if there are none
  16073. // default to 0
  16074. return seekableStarts.length ? seekableStarts.sort()[0] : 0;
  16075. }
  16076. /**
  16077. * Get the live time window aka
  16078. * the amount of time between seekable start and
  16079. * live current time.
  16080. *
  16081. * @return {number}
  16082. * The amount of seconds that are seekable in
  16083. * the live video.
  16084. */
  16085. liveWindow() {
  16086. const liveCurrentTime = this.liveCurrentTime();
  16087. // if liveCurrenTime is Infinity then we don't have a liveWindow at all
  16088. if (liveCurrentTime === Infinity) {
  16089. return 0;
  16090. }
  16091. return liveCurrentTime - this.seekableStart();
  16092. }
  16093. /**
  16094. * Determines if the player is live, only checks if this component
  16095. * is tracking live playback or not
  16096. *
  16097. * @return {boolean}
  16098. * Whether liveTracker is tracking
  16099. */
  16100. isLive() {
  16101. return this.isTracking();
  16102. }
  16103. /**
  16104. * Determines if currentTime is at the live edge and won't fall behind
  16105. * on each seekableendchange
  16106. *
  16107. * @return {boolean}
  16108. * Whether playback is at the live edge
  16109. */
  16110. atLiveEdge() {
  16111. return !this.behindLiveEdge();
  16112. }
  16113. /**
  16114. * get what we expect the live current time to be
  16115. *
  16116. * @return {number}
  16117. * The expected live current time
  16118. */
  16119. liveCurrentTime() {
  16120. return this.pastSeekEnd() + this.seekableEnd();
  16121. }
  16122. /**
  16123. * The number of seconds that have occurred after seekable end
  16124. * changed. This will be reset to 0 once seekable end changes.
  16125. *
  16126. * @return {number}
  16127. * Seconds past the current seekable end
  16128. */
  16129. pastSeekEnd() {
  16130. const seekableEnd = this.seekableEnd();
  16131. if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
  16132. this.pastSeekEnd_ = 0;
  16133. }
  16134. this.lastSeekEnd_ = seekableEnd;
  16135. return this.pastSeekEnd_;
  16136. }
  16137. /**
  16138. * If we are currently behind the live edge, aka currentTime will be
  16139. * behind on a seekableendchange
  16140. *
  16141. * @return {boolean}
  16142. * If we are behind the live edge
  16143. */
  16144. behindLiveEdge() {
  16145. return this.behindLiveEdge_;
  16146. }
  16147. /**
  16148. * Whether live tracker is currently tracking or not.
  16149. */
  16150. isTracking() {
  16151. return typeof this.trackingInterval_ === 'number';
  16152. }
  16153. /**
  16154. * Seek to the live edge if we are behind the live edge
  16155. */
  16156. seekToLiveEdge() {
  16157. this.seekedBehindLive_ = false;
  16158. if (this.atLiveEdge()) {
  16159. return;
  16160. }
  16161. this.nextSeekedFromUser_ = false;
  16162. this.player_.currentTime(this.liveCurrentTime());
  16163. }
  16164. /**
  16165. * Dispose of liveTracker
  16166. */
  16167. dispose() {
  16168. this.stopTracking();
  16169. super.dispose();
  16170. }
  16171. }
  16172. Component.registerComponent('LiveTracker', LiveTracker);
  16173. /**
  16174. * Displays an element over the player which contains an optional title and
  16175. * description for the current content.
  16176. *
  16177. * Much of the code for this component originated in the now obsolete
  16178. * videojs-dock plugin: https://github.com/brightcove/videojs-dock/
  16179. *
  16180. * @extends Component
  16181. */
  16182. class TitleBar extends Component {
  16183. constructor(player, options) {
  16184. super(player, options);
  16185. this.on('statechanged', e => this.updateDom_());
  16186. this.updateDom_();
  16187. }
  16188. /**
  16189. * Create the `TitleBar`'s DOM element
  16190. *
  16191. * @return {Element}
  16192. * The element that was created.
  16193. */
  16194. createEl() {
  16195. this.els = {
  16196. title: createEl('div', {
  16197. className: 'vjs-title-bar-title',
  16198. id: `vjs-title-bar-title-${newGUID()}`
  16199. }),
  16200. description: createEl('div', {
  16201. className: 'vjs-title-bar-description',
  16202. id: `vjs-title-bar-description-${newGUID()}`
  16203. })
  16204. };
  16205. return createEl('div', {
  16206. className: 'vjs-title-bar'
  16207. }, {}, Object.values(this.els));
  16208. }
  16209. /**
  16210. * Updates the DOM based on the component's state object.
  16211. */
  16212. updateDom_() {
  16213. const tech = this.player_.tech_;
  16214. const techEl = tech && tech.el_;
  16215. const techAriaAttrs = {
  16216. title: 'aria-labelledby',
  16217. description: 'aria-describedby'
  16218. };
  16219. ['title', 'description'].forEach(k => {
  16220. const value = this.state[k];
  16221. const el = this.els[k];
  16222. const techAriaAttr = techAriaAttrs[k];
  16223. emptyEl(el);
  16224. if (value) {
  16225. textContent(el, value);
  16226. }
  16227. // If there is a tech element available, update its ARIA attributes
  16228. // according to whether a title and/or description have been provided.
  16229. if (techEl) {
  16230. techEl.removeAttribute(techAriaAttr);
  16231. if (value) {
  16232. techEl.setAttribute(techAriaAttr, el.id);
  16233. }
  16234. }
  16235. });
  16236. if (this.state.title || this.state.description) {
  16237. this.show();
  16238. } else {
  16239. this.hide();
  16240. }
  16241. }
  16242. /**
  16243. * Update the contents of the title bar component with new title and
  16244. * description text.
  16245. *
  16246. * If both title and description are missing, the title bar will be hidden.
  16247. *
  16248. * If either title or description are present, the title bar will be visible.
  16249. *
  16250. * NOTE: Any previously set value will be preserved. To unset a previously
  16251. * set value, you must pass an empty string or null.
  16252. *
  16253. * For example:
  16254. *
  16255. * ```
  16256. * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
  16257. * update({description: 'bar2'}) // title: 'foo', description: 'bar2'
  16258. * update({title: ''}) // title: '', description: 'bar2'
  16259. * update({title: 'foo', description: null}) // title: 'foo', description: null
  16260. * ```
  16261. *
  16262. * @param {Object} [options={}]
  16263. * An options object. When empty, the title bar will be hidden.
  16264. *
  16265. * @param {string} [options.title]
  16266. * A title to display in the title bar.
  16267. *
  16268. * @param {string} [options.description]
  16269. * A description to display in the title bar.
  16270. */
  16271. update(options) {
  16272. this.setState(options);
  16273. }
  16274. /**
  16275. * Dispose the component.
  16276. */
  16277. dispose() {
  16278. const tech = this.player_.tech_;
  16279. const techEl = tech && tech.el_;
  16280. if (techEl) {
  16281. techEl.removeAttribute('aria-labelledby');
  16282. techEl.removeAttribute('aria-describedby');
  16283. }
  16284. super.dispose();
  16285. this.els = null;
  16286. }
  16287. }
  16288. Component.registerComponent('TitleBar', TitleBar);
  16289. /**
  16290. * This function is used to fire a sourceset when there is something
  16291. * similar to `mediaEl.load()` being called. It will try to find the source via
  16292. * the `src` attribute and then the `<source>` elements. It will then fire `sourceset`
  16293. * with the source that was found or empty string if we cannot know. If it cannot
  16294. * find a source then `sourceset` will not be fired.
  16295. *
  16296. * @param { import('./html5').default } tech
  16297. * The tech object that sourceset was setup on
  16298. *
  16299. * @return {boolean}
  16300. * returns false if the sourceset was not fired and true otherwise.
  16301. */
  16302. const sourcesetLoad = tech => {
  16303. const el = tech.el();
  16304. // if `el.src` is set, that source will be loaded.
  16305. if (el.hasAttribute('src')) {
  16306. tech.triggerSourceset(el.src);
  16307. return true;
  16308. }
  16309. /**
  16310. * Since there isn't a src property on the media element, source elements will be used for
  16311. * implementing the source selection algorithm. This happens asynchronously and
  16312. * for most cases were there is more than one source we cannot tell what source will
  16313. * be loaded, without re-implementing the source selection algorithm. At this time we are not
  16314. * going to do that. There are three special cases that we do handle here though:
  16315. *
  16316. * 1. If there are no sources, do not fire `sourceset`.
  16317. * 2. If there is only one `<source>` with a `src` property/attribute that is our `src`
  16318. * 3. If there is more than one `<source>` but all of them have the same `src` url.
  16319. * That will be our src.
  16320. */
  16321. const sources = tech.$$('source');
  16322. const srcUrls = [];
  16323. let src = '';
  16324. // if there are no sources, do not fire sourceset
  16325. if (!sources.length) {
  16326. return false;
  16327. }
  16328. // only count valid/non-duplicate source elements
  16329. for (let i = 0; i < sources.length; i++) {
  16330. const url = sources[i].src;
  16331. if (url && srcUrls.indexOf(url) === -1) {
  16332. srcUrls.push(url);
  16333. }
  16334. }
  16335. // there were no valid sources
  16336. if (!srcUrls.length) {
  16337. return false;
  16338. }
  16339. // there is only one valid source element url
  16340. // use that
  16341. if (srcUrls.length === 1) {
  16342. src = srcUrls[0];
  16343. }
  16344. tech.triggerSourceset(src);
  16345. return true;
  16346. };
  16347. /**
  16348. * our implementation of an `innerHTML` descriptor for browsers
  16349. * that do not have one.
  16350. */
  16351. const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
  16352. get() {
  16353. return this.cloneNode(true).innerHTML;
  16354. },
  16355. set(v) {
  16356. // make a dummy node to use innerHTML on
  16357. const dummy = document__default["default"].createElement(this.nodeName.toLowerCase());
  16358. // set innerHTML to the value provided
  16359. dummy.innerHTML = v;
  16360. // make a document fragment to hold the nodes from dummy
  16361. const docFrag = document__default["default"].createDocumentFragment();
  16362. // copy all of the nodes created by the innerHTML on dummy
  16363. // to the document fragment
  16364. while (dummy.childNodes.length) {
  16365. docFrag.appendChild(dummy.childNodes[0]);
  16366. }
  16367. // remove content
  16368. this.innerText = '';
  16369. // now we add all of that html in one by appending the
  16370. // document fragment. This is how innerHTML does it.
  16371. window__default["default"].Element.prototype.appendChild.call(this, docFrag);
  16372. // then return the result that innerHTML's setter would
  16373. return this.innerHTML;
  16374. }
  16375. });
  16376. /**
  16377. * Get a property descriptor given a list of priorities and the
  16378. * property to get.
  16379. */
  16380. const getDescriptor = (priority, prop) => {
  16381. let descriptor = {};
  16382. for (let i = 0; i < priority.length; i++) {
  16383. descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
  16384. if (descriptor && descriptor.set && descriptor.get) {
  16385. break;
  16386. }
  16387. }
  16388. descriptor.enumerable = true;
  16389. descriptor.configurable = true;
  16390. return descriptor;
  16391. };
  16392. const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window__default["default"].HTMLMediaElement.prototype, window__default["default"].Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
  16393. /**
  16394. * Patches browser internal functions so that we can tell synchronously
  16395. * if a `<source>` was appended to the media element. For some reason this
  16396. * causes a `sourceset` if the the media element is ready and has no source.
  16397. * This happens when:
  16398. * - The page has just loaded and the media element does not have a source.
  16399. * - The media element was emptied of all sources, then `load()` was called.
  16400. *
  16401. * It does this by patching the following functions/properties when they are supported:
  16402. *
  16403. * - `append()` - can be used to add a `<source>` element to the media element
  16404. * - `appendChild()` - can be used to add a `<source>` element to the media element
  16405. * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element
  16406. * - `innerHTML` - can be used to add a `<source>` element to the media element
  16407. *
  16408. * @param {Html5} tech
  16409. * The tech object that sourceset is being setup on.
  16410. */
  16411. const firstSourceWatch = function (tech) {
  16412. const el = tech.el();
  16413. // make sure firstSourceWatch isn't setup twice.
  16414. if (el.resetSourceWatch_) {
  16415. return;
  16416. }
  16417. const old = {};
  16418. const innerDescriptor = getInnerHTMLDescriptor(tech);
  16419. const appendWrapper = appendFn => (...args) => {
  16420. const retval = appendFn.apply(el, args);
  16421. sourcesetLoad(tech);
  16422. return retval;
  16423. };
  16424. ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
  16425. if (!el[k]) {
  16426. return;
  16427. }
  16428. // store the old function
  16429. old[k] = el[k];
  16430. // call the old function with a sourceset if a source
  16431. // was loaded
  16432. el[k] = appendWrapper(old[k]);
  16433. });
  16434. Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, {
  16435. set: appendWrapper(innerDescriptor.set)
  16436. }));
  16437. el.resetSourceWatch_ = () => {
  16438. el.resetSourceWatch_ = null;
  16439. Object.keys(old).forEach(k => {
  16440. el[k] = old[k];
  16441. });
  16442. Object.defineProperty(el, 'innerHTML', innerDescriptor);
  16443. };
  16444. // on the first sourceset, we need to revert our changes
  16445. tech.one('sourceset', el.resetSourceWatch_);
  16446. };
  16447. /**
  16448. * our implementation of a `src` descriptor for browsers
  16449. * that do not have one
  16450. */
  16451. const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
  16452. get() {
  16453. if (this.hasAttribute('src')) {
  16454. return getAbsoluteURL(window__default["default"].Element.prototype.getAttribute.call(this, 'src'));
  16455. }
  16456. return '';
  16457. },
  16458. set(v) {
  16459. window__default["default"].Element.prototype.setAttribute.call(this, 'src', v);
  16460. return v;
  16461. }
  16462. });
  16463. const getSrcDescriptor = tech => getDescriptor([tech.el(), window__default["default"].HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
  16464. /**
  16465. * setup `sourceset` handling on the `Html5` tech. This function
  16466. * patches the following element properties/functions:
  16467. *
  16468. * - `src` - to determine when `src` is set
  16469. * - `setAttribute()` - to determine when `src` is set
  16470. * - `load()` - this re-triggers the source selection algorithm, and can
  16471. * cause a sourceset.
  16472. *
  16473. * If there is no source when we are adding `sourceset` support or during a `load()`
  16474. * we also patch the functions listed in `firstSourceWatch`.
  16475. *
  16476. * @param {Html5} tech
  16477. * The tech to patch
  16478. */
  16479. const setupSourceset = function (tech) {
  16480. if (!tech.featuresSourceset) {
  16481. return;
  16482. }
  16483. const el = tech.el();
  16484. // make sure sourceset isn't setup twice.
  16485. if (el.resetSourceset_) {
  16486. return;
  16487. }
  16488. const srcDescriptor = getSrcDescriptor(tech);
  16489. const oldSetAttribute = el.setAttribute;
  16490. const oldLoad = el.load;
  16491. Object.defineProperty(el, 'src', merge(srcDescriptor, {
  16492. set: v => {
  16493. const retval = srcDescriptor.set.call(el, v);
  16494. // we use the getter here to get the actual value set on src
  16495. tech.triggerSourceset(el.src);
  16496. return retval;
  16497. }
  16498. }));
  16499. el.setAttribute = (n, v) => {
  16500. const retval = oldSetAttribute.call(el, n, v);
  16501. if (/src/i.test(n)) {
  16502. tech.triggerSourceset(el.src);
  16503. }
  16504. return retval;
  16505. };
  16506. el.load = () => {
  16507. const retval = oldLoad.call(el);
  16508. // if load was called, but there was no source to fire
  16509. // sourceset on. We have to watch for a source append
  16510. // as that can trigger a `sourceset` when the media element
  16511. // has no source
  16512. if (!sourcesetLoad(tech)) {
  16513. tech.triggerSourceset('');
  16514. firstSourceWatch(tech);
  16515. }
  16516. return retval;
  16517. };
  16518. if (el.currentSrc) {
  16519. tech.triggerSourceset(el.currentSrc);
  16520. } else if (!sourcesetLoad(tech)) {
  16521. firstSourceWatch(tech);
  16522. }
  16523. el.resetSourceset_ = () => {
  16524. el.resetSourceset_ = null;
  16525. el.load = oldLoad;
  16526. el.setAttribute = oldSetAttribute;
  16527. Object.defineProperty(el, 'src', srcDescriptor);
  16528. if (el.resetSourceWatch_) {
  16529. el.resetSourceWatch_();
  16530. }
  16531. };
  16532. };
  16533. /**
  16534. * @file html5.js
  16535. */
  16536. /**
  16537. * HTML5 Media Controller - Wrapper for HTML5 Media API
  16538. *
  16539. * @mixes Tech~SourceHandlerAdditions
  16540. * @extends Tech
  16541. */
  16542. class Html5 extends Tech {
  16543. /**
  16544. * Create an instance of this Tech.
  16545. *
  16546. * @param {Object} [options]
  16547. * The key/value store of player options.
  16548. *
  16549. * @param {Function} [ready]
  16550. * Callback function to call when the `HTML5` Tech is ready.
  16551. */
  16552. constructor(options, ready) {
  16553. super(options, ready);
  16554. const source = options.source;
  16555. let crossoriginTracks = false;
  16556. this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
  16557. // Set the source if one is provided
  16558. // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
  16559. // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
  16560. // anyway so the error gets fired.
  16561. if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
  16562. this.setSource(source);
  16563. } else {
  16564. this.handleLateInit_(this.el_);
  16565. }
  16566. // setup sourceset after late sourceset/init
  16567. if (options.enableSourceset) {
  16568. this.setupSourcesetHandling_();
  16569. }
  16570. this.isScrubbing_ = false;
  16571. if (this.el_.hasChildNodes()) {
  16572. const nodes = this.el_.childNodes;
  16573. let nodesLength = nodes.length;
  16574. const removeNodes = [];
  16575. while (nodesLength--) {
  16576. const node = nodes[nodesLength];
  16577. const nodeName = node.nodeName.toLowerCase();
  16578. if (nodeName === 'track') {
  16579. if (!this.featuresNativeTextTracks) {
  16580. // Empty video tag tracks so the built-in player doesn't use them also.
  16581. // This may not be fast enough to stop HTML5 browsers from reading the tags
  16582. // so we'll need to turn off any default tracks if we're manually doing
  16583. // captions and subtitles. videoElement.textTracks
  16584. removeNodes.push(node);
  16585. } else {
  16586. // store HTMLTrackElement and TextTrack to remote list
  16587. this.remoteTextTrackEls().addTrackElement_(node);
  16588. this.remoteTextTracks().addTrack(node.track);
  16589. this.textTracks().addTrack(node.track);
  16590. if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
  16591. crossoriginTracks = true;
  16592. }
  16593. }
  16594. }
  16595. }
  16596. for (let i = 0; i < removeNodes.length; i++) {
  16597. this.el_.removeChild(removeNodes[i]);
  16598. }
  16599. }
  16600. this.proxyNativeTracks_();
  16601. if (this.featuresNativeTextTracks && crossoriginTracks) {
  16602. log.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.');
  16603. }
  16604. // prevent iOS Safari from disabling metadata text tracks during native playback
  16605. this.restoreMetadataTracksInIOSNativePlayer_();
  16606. // Determine if native controls should be used
  16607. // Our goal should be to get the custom controls on mobile solid everywhere
  16608. // so we can remove this all together. Right now this will block custom
  16609. // controls on touch enabled laptops like the Chrome Pixel
  16610. if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
  16611. this.setControls(true);
  16612. }
  16613. // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
  16614. // into a `fullscreenchange` event
  16615. this.proxyWebkitFullscreen_();
  16616. this.triggerReady();
  16617. }
  16618. /**
  16619. * Dispose of `HTML5` media element and remove all tracks.
  16620. */
  16621. dispose() {
  16622. if (this.el_ && this.el_.resetSourceset_) {
  16623. this.el_.resetSourceset_();
  16624. }
  16625. Html5.disposeMediaElement(this.el_);
  16626. this.options_ = null;
  16627. // tech will handle clearing of the emulated track list
  16628. super.dispose();
  16629. }
  16630. /**
  16631. * Modify the media element so that we can detect when
  16632. * the source is changed. Fires `sourceset` just after the source has changed
  16633. */
  16634. setupSourcesetHandling_() {
  16635. setupSourceset(this);
  16636. }
  16637. /**
  16638. * When a captions track is enabled in the iOS Safari native player, all other
  16639. * tracks are disabled (including metadata tracks), which nulls all of their
  16640. * associated cue points. This will restore metadata tracks to their pre-fullscreen
  16641. * state in those cases so that cue points are not needlessly lost.
  16642. *
  16643. * @private
  16644. */
  16645. restoreMetadataTracksInIOSNativePlayer_() {
  16646. const textTracks = this.textTracks();
  16647. let metadataTracksPreFullscreenState;
  16648. // captures a snapshot of every metadata track's current state
  16649. const takeMetadataTrackSnapshot = () => {
  16650. metadataTracksPreFullscreenState = [];
  16651. for (let i = 0; i < textTracks.length; i++) {
  16652. const track = textTracks[i];
  16653. if (track.kind === 'metadata') {
  16654. metadataTracksPreFullscreenState.push({
  16655. track,
  16656. storedMode: track.mode
  16657. });
  16658. }
  16659. }
  16660. };
  16661. // snapshot each metadata track's initial state, and update the snapshot
  16662. // each time there is a track 'change' event
  16663. takeMetadataTrackSnapshot();
  16664. textTracks.addEventListener('change', takeMetadataTrackSnapshot);
  16665. this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
  16666. const restoreTrackMode = () => {
  16667. for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
  16668. const storedTrack = metadataTracksPreFullscreenState[i];
  16669. if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
  16670. storedTrack.track.mode = storedTrack.storedMode;
  16671. }
  16672. }
  16673. // we only want this handler to be executed on the first 'change' event
  16674. textTracks.removeEventListener('change', restoreTrackMode);
  16675. };
  16676. // when we enter fullscreen playback, stop updating the snapshot and
  16677. // restore all track modes to their pre-fullscreen state
  16678. this.on('webkitbeginfullscreen', () => {
  16679. textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
  16680. // remove the listener before adding it just in case it wasn't previously removed
  16681. textTracks.removeEventListener('change', restoreTrackMode);
  16682. textTracks.addEventListener('change', restoreTrackMode);
  16683. });
  16684. // start updating the snapshot again after leaving fullscreen
  16685. this.on('webkitendfullscreen', () => {
  16686. // remove the listener before adding it just in case it wasn't previously removed
  16687. textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
  16688. textTracks.addEventListener('change', takeMetadataTrackSnapshot);
  16689. // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
  16690. textTracks.removeEventListener('change', restoreTrackMode);
  16691. });
  16692. }
  16693. /**
  16694. * Attempt to force override of tracks for the given type
  16695. *
  16696. * @param {string} type - Track type to override, possible values include 'Audio',
  16697. * 'Video', and 'Text'.
  16698. * @param {boolean} override - If set to true native audio/video will be overridden,
  16699. * otherwise native audio/video will potentially be used.
  16700. * @private
  16701. */
  16702. overrideNative_(type, override) {
  16703. // If there is no behavioral change don't add/remove listeners
  16704. if (override !== this[`featuresNative${type}Tracks`]) {
  16705. return;
  16706. }
  16707. const lowerCaseType = type.toLowerCase();
  16708. if (this[`${lowerCaseType}TracksListeners_`]) {
  16709. Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
  16710. const elTracks = this.el()[`${lowerCaseType}Tracks`];
  16711. elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
  16712. });
  16713. }
  16714. this[`featuresNative${type}Tracks`] = !override;
  16715. this[`${lowerCaseType}TracksListeners_`] = null;
  16716. this.proxyNativeTracksForType_(lowerCaseType);
  16717. }
  16718. /**
  16719. * Attempt to force override of native audio tracks.
  16720. *
  16721. * @param {boolean} override - If set to true native audio will be overridden,
  16722. * otherwise native audio will potentially be used.
  16723. */
  16724. overrideNativeAudioTracks(override) {
  16725. this.overrideNative_('Audio', override);
  16726. }
  16727. /**
  16728. * Attempt to force override of native video tracks.
  16729. *
  16730. * @param {boolean} override - If set to true native video will be overridden,
  16731. * otherwise native video will potentially be used.
  16732. */
  16733. overrideNativeVideoTracks(override) {
  16734. this.overrideNative_('Video', override);
  16735. }
  16736. /**
  16737. * Proxy native track list events for the given type to our track
  16738. * lists if the browser we are playing in supports that type of track list.
  16739. *
  16740. * @param {string} name - Track type; values include 'audio', 'video', and 'text'
  16741. * @private
  16742. */
  16743. proxyNativeTracksForType_(name) {
  16744. const props = NORMAL[name];
  16745. const elTracks = this.el()[props.getterName];
  16746. const techTracks = this[props.getterName]();
  16747. if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
  16748. return;
  16749. }
  16750. const listeners = {
  16751. change: e => {
  16752. const event = {
  16753. type: 'change',
  16754. target: techTracks,
  16755. currentTarget: techTracks,
  16756. srcElement: techTracks
  16757. };
  16758. techTracks.trigger(event);
  16759. // if we are a text track change event, we should also notify the
  16760. // remote text track list. This can potentially cause a false positive
  16761. // if we were to get a change event on a non-remote track and
  16762. // we triggered the event on the remote text track list which doesn't
  16763. // contain that track. However, best practices mean looping through the
  16764. // list of tracks and searching for the appropriate mode value, so,
  16765. // this shouldn't pose an issue
  16766. if (name === 'text') {
  16767. this[REMOTE.remoteText.getterName]().trigger(event);
  16768. }
  16769. },
  16770. addtrack(e) {
  16771. techTracks.addTrack(e.track);
  16772. },
  16773. removetrack(e) {
  16774. techTracks.removeTrack(e.track);
  16775. }
  16776. };
  16777. const removeOldTracks = function () {
  16778. const removeTracks = [];
  16779. for (let i = 0; i < techTracks.length; i++) {
  16780. let found = false;
  16781. for (let j = 0; j < elTracks.length; j++) {
  16782. if (elTracks[j] === techTracks[i]) {
  16783. found = true;
  16784. break;
  16785. }
  16786. }
  16787. if (!found) {
  16788. removeTracks.push(techTracks[i]);
  16789. }
  16790. }
  16791. while (removeTracks.length) {
  16792. techTracks.removeTrack(removeTracks.shift());
  16793. }
  16794. };
  16795. this[props.getterName + 'Listeners_'] = listeners;
  16796. Object.keys(listeners).forEach(eventName => {
  16797. const listener = listeners[eventName];
  16798. elTracks.addEventListener(eventName, listener);
  16799. this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
  16800. });
  16801. // Remove (native) tracks that are not used anymore
  16802. this.on('loadstart', removeOldTracks);
  16803. this.on('dispose', e => this.off('loadstart', removeOldTracks));
  16804. }
  16805. /**
  16806. * Proxy all native track list events to our track lists if the browser we are playing
  16807. * in supports that type of track list.
  16808. *
  16809. * @private
  16810. */
  16811. proxyNativeTracks_() {
  16812. NORMAL.names.forEach(name => {
  16813. this.proxyNativeTracksForType_(name);
  16814. });
  16815. }
  16816. /**
  16817. * Create the `Html5` Tech's DOM element.
  16818. *
  16819. * @return {Element}
  16820. * The element that gets created.
  16821. */
  16822. createEl() {
  16823. let el = this.options_.tag;
  16824. // Check if this browser supports moving the element into the box.
  16825. // On the iPhone video will break if you move the element,
  16826. // So we have to create a brand new element.
  16827. // If we ingested the player div, we do not need to move the media element.
  16828. if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
  16829. // If the original tag is still there, clone and remove it.
  16830. if (el) {
  16831. const clone = el.cloneNode(true);
  16832. if (el.parentNode) {
  16833. el.parentNode.insertBefore(clone, el);
  16834. }
  16835. Html5.disposeMediaElement(el);
  16836. el = clone;
  16837. } else {
  16838. el = document__default["default"].createElement('video');
  16839. // determine if native controls should be used
  16840. const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
  16841. const attributes = merge({}, tagAttributes);
  16842. if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
  16843. delete attributes.controls;
  16844. }
  16845. setAttributes(el, Object.assign(attributes, {
  16846. id: this.options_.techId,
  16847. class: 'vjs-tech'
  16848. }));
  16849. }
  16850. el.playerId = this.options_.playerId;
  16851. }
  16852. if (typeof this.options_.preload !== 'undefined') {
  16853. setAttribute(el, 'preload', this.options_.preload);
  16854. }
  16855. if (this.options_.disablePictureInPicture !== undefined) {
  16856. el.disablePictureInPicture = this.options_.disablePictureInPicture;
  16857. }
  16858. // Update specific tag settings, in case they were overridden
  16859. // `autoplay` has to be *last* so that `muted` and `playsinline` are present
  16860. // when iOS/Safari or other browsers attempt to autoplay.
  16861. const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
  16862. for (let i = 0; i < settingsAttrs.length; i++) {
  16863. const attr = settingsAttrs[i];
  16864. const value = this.options_[attr];
  16865. if (typeof value !== 'undefined') {
  16866. if (value) {
  16867. setAttribute(el, attr, attr);
  16868. } else {
  16869. removeAttribute(el, attr);
  16870. }
  16871. el[attr] = value;
  16872. }
  16873. }
  16874. return el;
  16875. }
  16876. /**
  16877. * This will be triggered if the loadstart event has already fired, before videojs was
  16878. * ready. Two known examples of when this can happen are:
  16879. * 1. If we're loading the playback object after it has started loading
  16880. * 2. The media is already playing the (often with autoplay on) then
  16881. *
  16882. * This function will fire another loadstart so that videojs can catchup.
  16883. *
  16884. * @fires Tech#loadstart
  16885. *
  16886. * @return {undefined}
  16887. * returns nothing.
  16888. */
  16889. handleLateInit_(el) {
  16890. if (el.networkState === 0 || el.networkState === 3) {
  16891. // The video element hasn't started loading the source yet
  16892. // or didn't find a source
  16893. return;
  16894. }
  16895. if (el.readyState === 0) {
  16896. // NetworkState is set synchronously BUT loadstart is fired at the
  16897. // end of the current stack, usually before setInterval(fn, 0).
  16898. // So at this point we know loadstart may have already fired or is
  16899. // about to fire, and either way the player hasn't seen it yet.
  16900. // We don't want to fire loadstart prematurely here and cause a
  16901. // double loadstart so we'll wait and see if it happens between now
  16902. // and the next loop, and fire it if not.
  16903. // HOWEVER, we also want to make sure it fires before loadedmetadata
  16904. // which could also happen between now and the next loop, so we'll
  16905. // watch for that also.
  16906. let loadstartFired = false;
  16907. const setLoadstartFired = function () {
  16908. loadstartFired = true;
  16909. };
  16910. this.on('loadstart', setLoadstartFired);
  16911. const triggerLoadstart = function () {
  16912. // We did miss the original loadstart. Make sure the player
  16913. // sees loadstart before loadedmetadata
  16914. if (!loadstartFired) {
  16915. this.trigger('loadstart');
  16916. }
  16917. };
  16918. this.on('loadedmetadata', triggerLoadstart);
  16919. this.ready(function () {
  16920. this.off('loadstart', setLoadstartFired);
  16921. this.off('loadedmetadata', triggerLoadstart);
  16922. if (!loadstartFired) {
  16923. // We did miss the original native loadstart. Fire it now.
  16924. this.trigger('loadstart');
  16925. }
  16926. });
  16927. return;
  16928. }
  16929. // From here on we know that loadstart already fired and we missed it.
  16930. // The other readyState events aren't as much of a problem if we double
  16931. // them, so not going to go to as much trouble as loadstart to prevent
  16932. // that unless we find reason to.
  16933. const eventsToTrigger = ['loadstart'];
  16934. // loadedmetadata: newly equal to HAVE_METADATA (1) or greater
  16935. eventsToTrigger.push('loadedmetadata');
  16936. // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
  16937. if (el.readyState >= 2) {
  16938. eventsToTrigger.push('loadeddata');
  16939. }
  16940. // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
  16941. if (el.readyState >= 3) {
  16942. eventsToTrigger.push('canplay');
  16943. }
  16944. // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
  16945. if (el.readyState >= 4) {
  16946. eventsToTrigger.push('canplaythrough');
  16947. }
  16948. // We still need to give the player time to add event listeners
  16949. this.ready(function () {
  16950. eventsToTrigger.forEach(function (type) {
  16951. this.trigger(type);
  16952. }, this);
  16953. });
  16954. }
  16955. /**
  16956. * Set whether we are scrubbing or not.
  16957. * This is used to decide whether we should use `fastSeek` or not.
  16958. * `fastSeek` is used to provide trick play on Safari browsers.
  16959. *
  16960. * @param {boolean} isScrubbing
  16961. * - true for we are currently scrubbing
  16962. * - false for we are no longer scrubbing
  16963. */
  16964. setScrubbing(isScrubbing) {
  16965. this.isScrubbing_ = isScrubbing;
  16966. }
  16967. /**
  16968. * Get whether we are scrubbing or not.
  16969. *
  16970. * @return {boolean} isScrubbing
  16971. * - true for we are currently scrubbing
  16972. * - false for we are no longer scrubbing
  16973. */
  16974. scrubbing() {
  16975. return this.isScrubbing_;
  16976. }
  16977. /**
  16978. * Set current time for the `HTML5` tech.
  16979. *
  16980. * @param {number} seconds
  16981. * Set the current time of the media to this.
  16982. */
  16983. setCurrentTime(seconds) {
  16984. try {
  16985. if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
  16986. this.el_.fastSeek(seconds);
  16987. } else {
  16988. this.el_.currentTime = seconds;
  16989. }
  16990. } catch (e) {
  16991. log(e, 'Video is not ready. (Video.js)');
  16992. // this.warning(VideoJS.warnings.videoNotReady);
  16993. }
  16994. }
  16995. /**
  16996. * Get the current duration of the HTML5 media element.
  16997. *
  16998. * @return {number}
  16999. * The duration of the media or 0 if there is no duration.
  17000. */
  17001. duration() {
  17002. // Android Chrome will report duration as Infinity for VOD HLS until after
  17003. // playback has started, which triggers the live display erroneously.
  17004. // Return NaN if playback has not started and trigger a durationupdate once
  17005. // the duration can be reliably known.
  17006. if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
  17007. // Wait for the first `timeupdate` with currentTime > 0 - there may be
  17008. // several with 0
  17009. const checkProgress = () => {
  17010. if (this.el_.currentTime > 0) {
  17011. // Trigger durationchange for genuinely live video
  17012. if (this.el_.duration === Infinity) {
  17013. this.trigger('durationchange');
  17014. }
  17015. this.off('timeupdate', checkProgress);
  17016. }
  17017. };
  17018. this.on('timeupdate', checkProgress);
  17019. return NaN;
  17020. }
  17021. return this.el_.duration || NaN;
  17022. }
  17023. /**
  17024. * Get the current width of the HTML5 media element.
  17025. *
  17026. * @return {number}
  17027. * The width of the HTML5 media element.
  17028. */
  17029. width() {
  17030. return this.el_.offsetWidth;
  17031. }
  17032. /**
  17033. * Get the current height of the HTML5 media element.
  17034. *
  17035. * @return {number}
  17036. * The height of the HTML5 media element.
  17037. */
  17038. height() {
  17039. return this.el_.offsetHeight;
  17040. }
  17041. /**
  17042. * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
  17043. * `fullscreenchange` event.
  17044. *
  17045. * @private
  17046. * @fires fullscreenchange
  17047. * @listens webkitendfullscreen
  17048. * @listens webkitbeginfullscreen
  17049. * @listens webkitbeginfullscreen
  17050. */
  17051. proxyWebkitFullscreen_() {
  17052. if (!('webkitDisplayingFullscreen' in this.el_)) {
  17053. return;
  17054. }
  17055. const endFn = function () {
  17056. this.trigger('fullscreenchange', {
  17057. isFullscreen: false
  17058. });
  17059. // Safari will sometimes set controls on the videoelement when existing fullscreen.
  17060. if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
  17061. this.el_.controls = false;
  17062. }
  17063. };
  17064. const beginFn = function () {
  17065. if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
  17066. this.one('webkitendfullscreen', endFn);
  17067. this.trigger('fullscreenchange', {
  17068. isFullscreen: true,
  17069. // set a flag in case another tech triggers fullscreenchange
  17070. nativeIOSFullscreen: true
  17071. });
  17072. }
  17073. };
  17074. this.on('webkitbeginfullscreen', beginFn);
  17075. this.on('dispose', () => {
  17076. this.off('webkitbeginfullscreen', beginFn);
  17077. this.off('webkitendfullscreen', endFn);
  17078. });
  17079. }
  17080. /**
  17081. * Check if fullscreen is supported on the video el.
  17082. *
  17083. * @return {boolean}
  17084. * - True if fullscreen is supported.
  17085. * - False if fullscreen is not supported.
  17086. */
  17087. supportsFullScreen() {
  17088. return typeof this.el_.webkitEnterFullScreen === 'function';
  17089. }
  17090. /**
  17091. * Request that the `HTML5` Tech enter fullscreen.
  17092. */
  17093. enterFullScreen() {
  17094. const video = this.el_;
  17095. if (video.paused && video.networkState <= video.HAVE_METADATA) {
  17096. // attempt to prime the video element for programmatic access
  17097. // this isn't necessary on the desktop but shouldn't hurt
  17098. silencePromise(this.el_.play());
  17099. // playing and pausing synchronously during the transition to fullscreen
  17100. // can get iOS ~6.1 devices into a play/pause loop
  17101. this.setTimeout(function () {
  17102. video.pause();
  17103. try {
  17104. video.webkitEnterFullScreen();
  17105. } catch (e) {
  17106. this.trigger('fullscreenerror', e);
  17107. }
  17108. }, 0);
  17109. } else {
  17110. try {
  17111. video.webkitEnterFullScreen();
  17112. } catch (e) {
  17113. this.trigger('fullscreenerror', e);
  17114. }
  17115. }
  17116. }
  17117. /**
  17118. * Request that the `HTML5` Tech exit fullscreen.
  17119. */
  17120. exitFullScreen() {
  17121. if (!this.el_.webkitDisplayingFullscreen) {
  17122. this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
  17123. return;
  17124. }
  17125. this.el_.webkitExitFullScreen();
  17126. }
  17127. /**
  17128. * Create a floating video window always on top of other windows so that users may
  17129. * continue consuming media while they interact with other content sites, or
  17130. * applications on their device.
  17131. *
  17132. * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
  17133. *
  17134. * @return {Promise}
  17135. * A promise with a Picture-in-Picture window.
  17136. */
  17137. requestPictureInPicture() {
  17138. return this.el_.requestPictureInPicture();
  17139. }
  17140. /**
  17141. * Native requestVideoFrameCallback if supported by browser/tech, or fallback
  17142. * Don't use rVCF on Safari when DRM is playing, as it doesn't fire
  17143. * Needs to be checked later than the constructor
  17144. * This will be a false positive for clear sources loaded after a Fairplay source
  17145. *
  17146. * @param {function} cb function to call
  17147. * @return {number} id of request
  17148. */
  17149. requestVideoFrameCallback(cb) {
  17150. if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
  17151. return this.el_.requestVideoFrameCallback(cb);
  17152. }
  17153. return super.requestVideoFrameCallback(cb);
  17154. }
  17155. /**
  17156. * Native or fallback requestVideoFrameCallback
  17157. *
  17158. * @param {number} id request id to cancel
  17159. */
  17160. cancelVideoFrameCallback(id) {
  17161. if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
  17162. this.el_.cancelVideoFrameCallback(id);
  17163. } else {
  17164. super.cancelVideoFrameCallback(id);
  17165. }
  17166. }
  17167. /**
  17168. * A getter/setter for the `Html5` Tech's source object.
  17169. * > Note: Please use {@link Html5#setSource}
  17170. *
  17171. * @param {Tech~SourceObject} [src]
  17172. * The source object you want to set on the `HTML5` techs element.
  17173. *
  17174. * @return {Tech~SourceObject|undefined}
  17175. * - The current source object when a source is not passed in.
  17176. * - undefined when setting
  17177. *
  17178. * @deprecated Since version 5.
  17179. */
  17180. src(src) {
  17181. if (src === undefined) {
  17182. return this.el_.src;
  17183. }
  17184. // Setting src through `src` instead of `setSrc` will be deprecated
  17185. this.setSrc(src);
  17186. }
  17187. /**
  17188. * Reset the tech by removing all sources and then calling
  17189. * {@link Html5.resetMediaElement}.
  17190. */
  17191. reset() {
  17192. Html5.resetMediaElement(this.el_);
  17193. }
  17194. /**
  17195. * Get the current source on the `HTML5` Tech. Falls back to returning the source from
  17196. * the HTML5 media element.
  17197. *
  17198. * @return {Tech~SourceObject}
  17199. * The current source object from the HTML5 tech. With a fallback to the
  17200. * elements source.
  17201. */
  17202. currentSrc() {
  17203. if (this.currentSource_) {
  17204. return this.currentSource_.src;
  17205. }
  17206. return this.el_.currentSrc;
  17207. }
  17208. /**
  17209. * Set controls attribute for the HTML5 media Element.
  17210. *
  17211. * @param {string} val
  17212. * Value to set the controls attribute to
  17213. */
  17214. setControls(val) {
  17215. this.el_.controls = !!val;
  17216. }
  17217. /**
  17218. * Create and returns a remote {@link TextTrack} object.
  17219. *
  17220. * @param {string} kind
  17221. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
  17222. *
  17223. * @param {string} [label]
  17224. * Label to identify the text track
  17225. *
  17226. * @param {string} [language]
  17227. * Two letter language abbreviation
  17228. *
  17229. * @return {TextTrack}
  17230. * The TextTrack that gets created.
  17231. */
  17232. addTextTrack(kind, label, language) {
  17233. if (!this.featuresNativeTextTracks) {
  17234. return super.addTextTrack(kind, label, language);
  17235. }
  17236. return this.el_.addTextTrack(kind, label, language);
  17237. }
  17238. /**
  17239. * Creates either native TextTrack or an emulated TextTrack depending
  17240. * on the value of `featuresNativeTextTracks`
  17241. *
  17242. * @param {Object} options
  17243. * The object should contain the options to initialize the TextTrack with.
  17244. *
  17245. * @param {string} [options.kind]
  17246. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
  17247. *
  17248. * @param {string} [options.label]
  17249. * Label to identify the text track
  17250. *
  17251. * @param {string} [options.language]
  17252. * Two letter language abbreviation.
  17253. *
  17254. * @param {boolean} [options.default]
  17255. * Default this track to on.
  17256. *
  17257. * @param {string} [options.id]
  17258. * The internal id to assign this track.
  17259. *
  17260. * @param {string} [options.src]
  17261. * A source url for the track.
  17262. *
  17263. * @return {HTMLTrackElement}
  17264. * The track element that gets created.
  17265. */
  17266. createRemoteTextTrack(options) {
  17267. if (!this.featuresNativeTextTracks) {
  17268. return super.createRemoteTextTrack(options);
  17269. }
  17270. const htmlTrackElement = document__default["default"].createElement('track');
  17271. if (options.kind) {
  17272. htmlTrackElement.kind = options.kind;
  17273. }
  17274. if (options.label) {
  17275. htmlTrackElement.label = options.label;
  17276. }
  17277. if (options.language || options.srclang) {
  17278. htmlTrackElement.srclang = options.language || options.srclang;
  17279. }
  17280. if (options.default) {
  17281. htmlTrackElement.default = options.default;
  17282. }
  17283. if (options.id) {
  17284. htmlTrackElement.id = options.id;
  17285. }
  17286. if (options.src) {
  17287. htmlTrackElement.src = options.src;
  17288. }
  17289. return htmlTrackElement;
  17290. }
  17291. /**
  17292. * Creates a remote text track object and returns an html track element.
  17293. *
  17294. * @param {Object} options The object should contain values for
  17295. * kind, language, label, and src (location of the WebVTT file)
  17296. * @param {boolean} [manualCleanup=false] if set to true, the TextTrack
  17297. * will not be removed from the TextTrackList and HtmlTrackElementList
  17298. * after a source change
  17299. * @return {HTMLTrackElement} An Html Track Element.
  17300. * This can be an emulated {@link HTMLTrackElement} or a native one.
  17301. *
  17302. */
  17303. addRemoteTextTrack(options, manualCleanup) {
  17304. const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
  17305. if (this.featuresNativeTextTracks) {
  17306. this.el().appendChild(htmlTrackElement);
  17307. }
  17308. return htmlTrackElement;
  17309. }
  17310. /**
  17311. * Remove remote `TextTrack` from `TextTrackList` object
  17312. *
  17313. * @param {TextTrack} track
  17314. * `TextTrack` object to remove
  17315. */
  17316. removeRemoteTextTrack(track) {
  17317. super.removeRemoteTextTrack(track);
  17318. if (this.featuresNativeTextTracks) {
  17319. const tracks = this.$$('track');
  17320. let i = tracks.length;
  17321. while (i--) {
  17322. if (track === tracks[i] || track === tracks[i].track) {
  17323. this.el().removeChild(tracks[i]);
  17324. }
  17325. }
  17326. }
  17327. }
  17328. /**
  17329. * Gets available media playback quality metrics as specified by the W3C's Media
  17330. * Playback Quality API.
  17331. *
  17332. * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
  17333. *
  17334. * @return {Object}
  17335. * An object with supported media playback quality metrics
  17336. */
  17337. getVideoPlaybackQuality() {
  17338. if (typeof this.el().getVideoPlaybackQuality === 'function') {
  17339. return this.el().getVideoPlaybackQuality();
  17340. }
  17341. const videoPlaybackQuality = {};
  17342. if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
  17343. videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
  17344. videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
  17345. }
  17346. if (window__default["default"].performance) {
  17347. videoPlaybackQuality.creationTime = window__default["default"].performance.now();
  17348. }
  17349. return videoPlaybackQuality;
  17350. }
  17351. }
  17352. /* HTML5 Support Testing ---------------------------------------------------- */
  17353. /**
  17354. * Element for testing browser HTML5 media capabilities
  17355. *
  17356. * @type {Element}
  17357. * @constant
  17358. * @private
  17359. */
  17360. defineLazyProperty(Html5, 'TEST_VID', function () {
  17361. if (!isReal()) {
  17362. return;
  17363. }
  17364. const video = document__default["default"].createElement('video');
  17365. const track = document__default["default"].createElement('track');
  17366. track.kind = 'captions';
  17367. track.srclang = 'en';
  17368. track.label = 'English';
  17369. video.appendChild(track);
  17370. return video;
  17371. });
  17372. /**
  17373. * Check if HTML5 media is supported by this browser/device.
  17374. *
  17375. * @return {boolean}
  17376. * - True if HTML5 media is supported.
  17377. * - False if HTML5 media is not supported.
  17378. */
  17379. Html5.isSupported = function () {
  17380. // IE with no Media Player is a LIAR! (#984)
  17381. try {
  17382. Html5.TEST_VID.volume = 0.5;
  17383. } catch (e) {
  17384. return false;
  17385. }
  17386. return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
  17387. };
  17388. /**
  17389. * Check if the tech can support the given type
  17390. *
  17391. * @param {string} type
  17392. * The mimetype to check
  17393. * @return {string} 'probably', 'maybe', or '' (empty string)
  17394. */
  17395. Html5.canPlayType = function (type) {
  17396. return Html5.TEST_VID.canPlayType(type);
  17397. };
  17398. /**
  17399. * Check if the tech can support the given source
  17400. *
  17401. * @param {Object} srcObj
  17402. * The source object
  17403. * @param {Object} options
  17404. * The options passed to the tech
  17405. * @return {string} 'probably', 'maybe', or '' (empty string)
  17406. */
  17407. Html5.canPlaySource = function (srcObj, options) {
  17408. return Html5.canPlayType(srcObj.type);
  17409. };
  17410. /**
  17411. * Check if the volume can be changed in this browser/device.
  17412. * Volume cannot be changed in a lot of mobile devices.
  17413. * Specifically, it can't be changed from 1 on iOS.
  17414. *
  17415. * @return {boolean}
  17416. * - True if volume can be controlled
  17417. * - False otherwise
  17418. */
  17419. Html5.canControlVolume = function () {
  17420. // IE will error if Windows Media Player not installed #3315
  17421. try {
  17422. const volume = Html5.TEST_VID.volume;
  17423. Html5.TEST_VID.volume = volume / 2 + 0.1;
  17424. const canControl = volume !== Html5.TEST_VID.volume;
  17425. // With the introduction of iOS 15, there are cases where the volume is read as
  17426. // changed but reverts back to its original state at the start of the next tick.
  17427. // To determine whether volume can be controlled on iOS,
  17428. // a timeout is set and the volume is checked asynchronously.
  17429. // Since `features` doesn't currently work asynchronously, the value is manually set.
  17430. if (canControl && IS_IOS) {
  17431. window__default["default"].setTimeout(() => {
  17432. if (Html5 && Html5.prototype) {
  17433. Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
  17434. }
  17435. });
  17436. // default iOS to false, which will be updated in the timeout above.
  17437. return false;
  17438. }
  17439. return canControl;
  17440. } catch (e) {
  17441. return false;
  17442. }
  17443. };
  17444. /**
  17445. * Check if the volume can be muted in this browser/device.
  17446. * Some devices, e.g. iOS, don't allow changing volume
  17447. * but permits muting/unmuting.
  17448. *
  17449. * @return {boolean}
  17450. * - True if volume can be muted
  17451. * - False otherwise
  17452. */
  17453. Html5.canMuteVolume = function () {
  17454. try {
  17455. const muted = Html5.TEST_VID.muted;
  17456. // in some versions of iOS muted property doesn't always
  17457. // work, so we want to set both property and attribute
  17458. Html5.TEST_VID.muted = !muted;
  17459. if (Html5.TEST_VID.muted) {
  17460. setAttribute(Html5.TEST_VID, 'muted', 'muted');
  17461. } else {
  17462. removeAttribute(Html5.TEST_VID, 'muted', 'muted');
  17463. }
  17464. return muted !== Html5.TEST_VID.muted;
  17465. } catch (e) {
  17466. return false;
  17467. }
  17468. };
  17469. /**
  17470. * Check if the playback rate can be changed in this browser/device.
  17471. *
  17472. * @return {boolean}
  17473. * - True if playback rate can be controlled
  17474. * - False otherwise
  17475. */
  17476. Html5.canControlPlaybackRate = function () {
  17477. // Playback rate API is implemented in Android Chrome, but doesn't do anything
  17478. // https://github.com/videojs/video.js/issues/3180
  17479. if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
  17480. return false;
  17481. }
  17482. // IE will error if Windows Media Player not installed #3315
  17483. try {
  17484. const playbackRate = Html5.TEST_VID.playbackRate;
  17485. Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
  17486. return playbackRate !== Html5.TEST_VID.playbackRate;
  17487. } catch (e) {
  17488. return false;
  17489. }
  17490. };
  17491. /**
  17492. * Check if we can override a video/audio elements attributes, with
  17493. * Object.defineProperty.
  17494. *
  17495. * @return {boolean}
  17496. * - True if builtin attributes can be overridden
  17497. * - False otherwise
  17498. */
  17499. Html5.canOverrideAttributes = function () {
  17500. // if we cannot overwrite the src/innerHTML property, there is no support
  17501. // iOS 7 safari for instance cannot do this.
  17502. try {
  17503. const noop = () => {};
  17504. Object.defineProperty(document__default["default"].createElement('video'), 'src', {
  17505. get: noop,
  17506. set: noop
  17507. });
  17508. Object.defineProperty(document__default["default"].createElement('audio'), 'src', {
  17509. get: noop,
  17510. set: noop
  17511. });
  17512. Object.defineProperty(document__default["default"].createElement('video'), 'innerHTML', {
  17513. get: noop,
  17514. set: noop
  17515. });
  17516. Object.defineProperty(document__default["default"].createElement('audio'), 'innerHTML', {
  17517. get: noop,
  17518. set: noop
  17519. });
  17520. } catch (e) {
  17521. return false;
  17522. }
  17523. return true;
  17524. };
  17525. /**
  17526. * Check to see if native `TextTrack`s are supported by this browser/device.
  17527. *
  17528. * @return {boolean}
  17529. * - True if native `TextTrack`s are supported.
  17530. * - False otherwise
  17531. */
  17532. Html5.supportsNativeTextTracks = function () {
  17533. return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
  17534. };
  17535. /**
  17536. * Check to see if native `VideoTrack`s are supported by this browser/device
  17537. *
  17538. * @return {boolean}
  17539. * - True if native `VideoTrack`s are supported.
  17540. * - False otherwise
  17541. */
  17542. Html5.supportsNativeVideoTracks = function () {
  17543. return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
  17544. };
  17545. /**
  17546. * Check to see if native `AudioTrack`s are supported by this browser/device
  17547. *
  17548. * @return {boolean}
  17549. * - True if native `AudioTrack`s are supported.
  17550. * - False otherwise
  17551. */
  17552. Html5.supportsNativeAudioTracks = function () {
  17553. return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
  17554. };
  17555. /**
  17556. * An array of events available on the Html5 tech.
  17557. *
  17558. * @private
  17559. * @type {Array}
  17560. */
  17561. Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange'];
  17562. /**
  17563. * Boolean indicating whether the `Tech` supports volume control.
  17564. *
  17565. * @type {boolean}
  17566. * @default {@link Html5.canControlVolume}
  17567. */
  17568. /**
  17569. * Boolean indicating whether the `Tech` supports muting volume.
  17570. *
  17571. * @type {boolean}
  17572. * @default {@link Html5.canMuteVolume}
  17573. */
  17574. /**
  17575. * Boolean indicating whether the `Tech` supports changing the speed at which the media
  17576. * plays. Examples:
  17577. * - Set player to play 2x (twice) as fast
  17578. * - Set player to play 0.5x (half) as fast
  17579. *
  17580. * @type {boolean}
  17581. * @default {@link Html5.canControlPlaybackRate}
  17582. */
  17583. /**
  17584. * Boolean indicating whether the `Tech` supports the `sourceset` event.
  17585. *
  17586. * @type {boolean}
  17587. * @default
  17588. */
  17589. /**
  17590. * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
  17591. *
  17592. * @type {boolean}
  17593. * @default {@link Html5.supportsNativeTextTracks}
  17594. */
  17595. /**
  17596. * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
  17597. *
  17598. * @type {boolean}
  17599. * @default {@link Html5.supportsNativeVideoTracks}
  17600. */
  17601. /**
  17602. * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
  17603. *
  17604. * @type {boolean}
  17605. * @default {@link Html5.supportsNativeAudioTracks}
  17606. */
  17607. [['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
  17608. defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
  17609. });
  17610. Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
  17611. /**
  17612. * Boolean indicating whether the `HTML5` tech currently supports the media element
  17613. * moving in the DOM. iOS breaks if you move the media element, so this is set this to
  17614. * false there. Everywhere else this should be true.
  17615. *
  17616. * @type {boolean}
  17617. * @default
  17618. */
  17619. Html5.prototype.movingMediaElementInDOM = !IS_IOS;
  17620. // TODO: Previous comment: No longer appears to be used. Can probably be removed.
  17621. // Is this true?
  17622. /**
  17623. * Boolean indicating whether the `HTML5` tech currently supports automatic media resize
  17624. * when going into fullscreen.
  17625. *
  17626. * @type {boolean}
  17627. * @default
  17628. */
  17629. Html5.prototype.featuresFullscreenResize = true;
  17630. /**
  17631. * Boolean indicating whether the `HTML5` tech currently supports the progress event.
  17632. * If this is false, manual `progress` events will be triggered instead.
  17633. *
  17634. * @type {boolean}
  17635. * @default
  17636. */
  17637. Html5.prototype.featuresProgressEvents = true;
  17638. /**
  17639. * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
  17640. * If this is false, manual `timeupdate` events will be triggered instead.
  17641. *
  17642. * @default
  17643. */
  17644. Html5.prototype.featuresTimeupdateEvents = true;
  17645. /**
  17646. * Whether the HTML5 el supports `requestVideoFrameCallback`
  17647. *
  17648. * @type {boolean}
  17649. */
  17650. Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
  17651. Html5.disposeMediaElement = function (el) {
  17652. if (!el) {
  17653. return;
  17654. }
  17655. if (el.parentNode) {
  17656. el.parentNode.removeChild(el);
  17657. }
  17658. // remove any child track or source nodes to prevent their loading
  17659. while (el.hasChildNodes()) {
  17660. el.removeChild(el.firstChild);
  17661. }
  17662. // remove any src reference. not setting `src=''` because that causes a warning
  17663. // in firefox
  17664. el.removeAttribute('src');
  17665. // force the media element to update its loading state by calling load()
  17666. // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
  17667. if (typeof el.load === 'function') {
  17668. // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
  17669. (function () {
  17670. try {
  17671. el.load();
  17672. } catch (e) {
  17673. // not supported
  17674. }
  17675. })();
  17676. }
  17677. };
  17678. Html5.resetMediaElement = function (el) {
  17679. if (!el) {
  17680. return;
  17681. }
  17682. const sources = el.querySelectorAll('source');
  17683. let i = sources.length;
  17684. while (i--) {
  17685. el.removeChild(sources[i]);
  17686. }
  17687. // remove any src reference.
  17688. // not setting `src=''` because that throws an error
  17689. el.removeAttribute('src');
  17690. if (typeof el.load === 'function') {
  17691. // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
  17692. (function () {
  17693. try {
  17694. el.load();
  17695. } catch (e) {
  17696. // satisfy linter
  17697. }
  17698. })();
  17699. }
  17700. };
  17701. /* Native HTML5 element property wrapping ----------------------------------- */
  17702. // Wrap native boolean attributes with getters that check both property and attribute
  17703. // The list is as followed:
  17704. // muted, defaultMuted, autoplay, controls, loop, playsinline
  17705. [
  17706. /**
  17707. * Get the value of `muted` from the media element. `muted` indicates
  17708. * that the volume for the media should be set to silent. This does not actually change
  17709. * the `volume` attribute.
  17710. *
  17711. * @method Html5#muted
  17712. * @return {boolean}
  17713. * - True if the value of `volume` should be ignored and the audio set to silent.
  17714. * - False if the value of `volume` should be used.
  17715. *
  17716. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
  17717. */
  17718. 'muted',
  17719. /**
  17720. * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
  17721. * whether the media should start muted or not. Only changes the default state of the
  17722. * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
  17723. * current state.
  17724. *
  17725. * @method Html5#defaultMuted
  17726. * @return {boolean}
  17727. * - The value of `defaultMuted` from the media element.
  17728. * - True indicates that the media should start muted.
  17729. * - False indicates that the media should not start muted
  17730. *
  17731. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
  17732. */
  17733. 'defaultMuted',
  17734. /**
  17735. * Get the value of `autoplay` from the media element. `autoplay` indicates
  17736. * that the media should start to play as soon as the page is ready.
  17737. *
  17738. * @method Html5#autoplay
  17739. * @return {boolean}
  17740. * - The value of `autoplay` from the media element.
  17741. * - True indicates that the media should start as soon as the page loads.
  17742. * - False indicates that the media should not start as soon as the page loads.
  17743. *
  17744. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
  17745. */
  17746. 'autoplay',
  17747. /**
  17748. * Get the value of `controls` from the media element. `controls` indicates
  17749. * whether the native media controls should be shown or hidden.
  17750. *
  17751. * @method Html5#controls
  17752. * @return {boolean}
  17753. * - The value of `controls` from the media element.
  17754. * - True indicates that native controls should be showing.
  17755. * - False indicates that native controls should be hidden.
  17756. *
  17757. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
  17758. */
  17759. 'controls',
  17760. /**
  17761. * Get the value of `loop` from the media element. `loop` indicates
  17762. * that the media should return to the start of the media and continue playing once
  17763. * it reaches the end.
  17764. *
  17765. * @method Html5#loop
  17766. * @return {boolean}
  17767. * - The value of `loop` from the media element.
  17768. * - True indicates that playback should seek back to start once
  17769. * the end of a media is reached.
  17770. * - False indicates that playback should not loop back to the start when the
  17771. * end of the media is reached.
  17772. *
  17773. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
  17774. */
  17775. 'loop',
  17776. /**
  17777. * Get the value of `playsinline` from the media element. `playsinline` indicates
  17778. * to the browser that non-fullscreen playback is preferred when fullscreen
  17779. * playback is the native default, such as in iOS Safari.
  17780. *
  17781. * @method Html5#playsinline
  17782. * @return {boolean}
  17783. * - The value of `playsinline` from the media element.
  17784. * - True indicates that the media should play inline.
  17785. * - False indicates that the media should not play inline.
  17786. *
  17787. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
  17788. */
  17789. 'playsinline'].forEach(function (prop) {
  17790. Html5.prototype[prop] = function () {
  17791. return this.el_[prop] || this.el_.hasAttribute(prop);
  17792. };
  17793. });
  17794. // Wrap native boolean attributes with setters that set both property and attribute
  17795. // The list is as followed:
  17796. // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
  17797. // setControls is special-cased above
  17798. [
  17799. /**
  17800. * Set the value of `muted` on the media element. `muted` indicates that the current
  17801. * audio level should be silent.
  17802. *
  17803. * @method Html5#setMuted
  17804. * @param {boolean} muted
  17805. * - True if the audio should be set to silent
  17806. * - False otherwise
  17807. *
  17808. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
  17809. */
  17810. 'muted',
  17811. /**
  17812. * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
  17813. * audio level should be silent, but will only effect the muted level on initial playback..
  17814. *
  17815. * @method Html5.prototype.setDefaultMuted
  17816. * @param {boolean} defaultMuted
  17817. * - True if the audio should be set to silent
  17818. * - False otherwise
  17819. *
  17820. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
  17821. */
  17822. 'defaultMuted',
  17823. /**
  17824. * Set the value of `autoplay` on the media element. `autoplay` indicates
  17825. * that the media should start to play as soon as the page is ready.
  17826. *
  17827. * @method Html5#setAutoplay
  17828. * @param {boolean} autoplay
  17829. * - True indicates that the media should start as soon as the page loads.
  17830. * - False indicates that the media should not start as soon as the page loads.
  17831. *
  17832. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
  17833. */
  17834. 'autoplay',
  17835. /**
  17836. * Set the value of `loop` on the media element. `loop` indicates
  17837. * that the media should return to the start of the media and continue playing once
  17838. * it reaches the end.
  17839. *
  17840. * @method Html5#setLoop
  17841. * @param {boolean} loop
  17842. * - True indicates that playback should seek back to start once
  17843. * the end of a media is reached.
  17844. * - False indicates that playback should not loop back to the start when the
  17845. * end of the media is reached.
  17846. *
  17847. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
  17848. */
  17849. 'loop',
  17850. /**
  17851. * Set the value of `playsinline` from the media element. `playsinline` indicates
  17852. * to the browser that non-fullscreen playback is preferred when fullscreen
  17853. * playback is the native default, such as in iOS Safari.
  17854. *
  17855. * @method Html5#setPlaysinline
  17856. * @param {boolean} playsinline
  17857. * - True indicates that the media should play inline.
  17858. * - False indicates that the media should not play inline.
  17859. *
  17860. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
  17861. */
  17862. 'playsinline'].forEach(function (prop) {
  17863. Html5.prototype['set' + toTitleCase(prop)] = function (v) {
  17864. this.el_[prop] = v;
  17865. if (v) {
  17866. this.el_.setAttribute(prop, prop);
  17867. } else {
  17868. this.el_.removeAttribute(prop);
  17869. }
  17870. };
  17871. });
  17872. // Wrap native properties with a getter
  17873. // The list is as followed
  17874. // paused, currentTime, buffered, volume, poster, preload, error, seeking
  17875. // seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
  17876. // played, networkState, readyState, videoWidth, videoHeight, crossOrigin
  17877. [
  17878. /**
  17879. * Get the value of `paused` from the media element. `paused` indicates whether the media element
  17880. * is currently paused or not.
  17881. *
  17882. * @method Html5#paused
  17883. * @return {boolean}
  17884. * The value of `paused` from the media element.
  17885. *
  17886. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
  17887. */
  17888. 'paused',
  17889. /**
  17890. * Get the value of `currentTime` from the media element. `currentTime` indicates
  17891. * the current second that the media is at in playback.
  17892. *
  17893. * @method Html5#currentTime
  17894. * @return {number}
  17895. * The value of `currentTime` from the media element.
  17896. *
  17897. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
  17898. */
  17899. 'currentTime',
  17900. /**
  17901. * Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
  17902. * object that represents the parts of the media that are already downloaded and
  17903. * available for playback.
  17904. *
  17905. * @method Html5#buffered
  17906. * @return {TimeRange}
  17907. * The value of `buffered` from the media element.
  17908. *
  17909. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
  17910. */
  17911. 'buffered',
  17912. /**
  17913. * Get the value of `volume` from the media element. `volume` indicates
  17914. * the current playback volume of audio for a media. `volume` will be a value from 0
  17915. * (silent) to 1 (loudest and default).
  17916. *
  17917. * @method Html5#volume
  17918. * @return {number}
  17919. * The value of `volume` from the media element. Value will be between 0-1.
  17920. *
  17921. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
  17922. */
  17923. 'volume',
  17924. /**
  17925. * Get the value of `poster` from the media element. `poster` indicates
  17926. * that the url of an image file that can/will be shown when no media data is available.
  17927. *
  17928. * @method Html5#poster
  17929. * @return {string}
  17930. * The value of `poster` from the media element. Value will be a url to an
  17931. * image.
  17932. *
  17933. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
  17934. */
  17935. 'poster',
  17936. /**
  17937. * Get the value of `preload` from the media element. `preload` indicates
  17938. * what should download before the media is interacted with. It can have the following
  17939. * values:
  17940. * - none: nothing should be downloaded
  17941. * - metadata: poster and the first few frames of the media may be downloaded to get
  17942. * media dimensions and other metadata
  17943. * - auto: allow the media and metadata for the media to be downloaded before
  17944. * interaction
  17945. *
  17946. * @method Html5#preload
  17947. * @return {string}
  17948. * The value of `preload` from the media element. Will be 'none', 'metadata',
  17949. * or 'auto'.
  17950. *
  17951. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
  17952. */
  17953. 'preload',
  17954. /**
  17955. * Get the value of the `error` from the media element. `error` indicates any
  17956. * MediaError that may have occurred during playback. If error returns null there is no
  17957. * current error.
  17958. *
  17959. * @method Html5#error
  17960. * @return {MediaError|null}
  17961. * The value of `error` from the media element. Will be `MediaError` if there
  17962. * is a current error and null otherwise.
  17963. *
  17964. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
  17965. */
  17966. 'error',
  17967. /**
  17968. * Get the value of `seeking` from the media element. `seeking` indicates whether the
  17969. * media is currently seeking to a new position or not.
  17970. *
  17971. * @method Html5#seeking
  17972. * @return {boolean}
  17973. * - The value of `seeking` from the media element.
  17974. * - True indicates that the media is currently seeking to a new position.
  17975. * - False indicates that the media is not seeking to a new position at this time.
  17976. *
  17977. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
  17978. */
  17979. 'seeking',
  17980. /**
  17981. * Get the value of `seekable` from the media element. `seekable` returns a
  17982. * `TimeRange` object indicating ranges of time that can currently be `seeked` to.
  17983. *
  17984. * @method Html5#seekable
  17985. * @return {TimeRange}
  17986. * The value of `seekable` from the media element. A `TimeRange` object
  17987. * indicating the current ranges of time that can be seeked to.
  17988. *
  17989. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
  17990. */
  17991. 'seekable',
  17992. /**
  17993. * Get the value of `ended` from the media element. `ended` indicates whether
  17994. * the media has reached the end or not.
  17995. *
  17996. * @method Html5#ended
  17997. * @return {boolean}
  17998. * - The value of `ended` from the media element.
  17999. * - True indicates that the media has ended.
  18000. * - False indicates that the media has not ended.
  18001. *
  18002. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
  18003. */
  18004. 'ended',
  18005. /**
  18006. * Get the value of `playbackRate` from the media element. `playbackRate` indicates
  18007. * the rate at which the media is currently playing back. Examples:
  18008. * - if playbackRate is set to 2, media will play twice as fast.
  18009. * - if playbackRate is set to 0.5, media will play half as fast.
  18010. *
  18011. * @method Html5#playbackRate
  18012. * @return {number}
  18013. * The value of `playbackRate` from the media element. A number indicating
  18014. * the current playback speed of the media, where 1 is normal speed.
  18015. *
  18016. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
  18017. */
  18018. 'playbackRate',
  18019. /**
  18020. * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
  18021. * the rate at which the media is currently playing back. This value will not indicate the current
  18022. * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
  18023. *
  18024. * Examples:
  18025. * - if defaultPlaybackRate is set to 2, media will play twice as fast.
  18026. * - if defaultPlaybackRate is set to 0.5, media will play half as fast.
  18027. *
  18028. * @method Html5.prototype.defaultPlaybackRate
  18029. * @return {number}
  18030. * The value of `defaultPlaybackRate` from the media element. A number indicating
  18031. * the current playback speed of the media, where 1 is normal speed.
  18032. *
  18033. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
  18034. */
  18035. 'defaultPlaybackRate',
  18036. /**
  18037. * Get the value of 'disablePictureInPicture' from the video element.
  18038. *
  18039. * @method Html5#disablePictureInPicture
  18040. * @return {boolean} value
  18041. * - The value of `disablePictureInPicture` from the video element.
  18042. * - True indicates that the video can't be played in Picture-In-Picture mode
  18043. * - False indicates that the video can be played in Picture-In-Picture mode
  18044. *
  18045. * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
  18046. */
  18047. 'disablePictureInPicture',
  18048. /**
  18049. * Get the value of `played` from the media element. `played` returns a `TimeRange`
  18050. * object representing points in the media timeline that have been played.
  18051. *
  18052. * @method Html5#played
  18053. * @return {TimeRange}
  18054. * The value of `played` from the media element. A `TimeRange` object indicating
  18055. * the ranges of time that have been played.
  18056. *
  18057. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
  18058. */
  18059. 'played',
  18060. /**
  18061. * Get the value of `networkState` from the media element. `networkState` indicates
  18062. * the current network state. It returns an enumeration from the following list:
  18063. * - 0: NETWORK_EMPTY
  18064. * - 1: NETWORK_IDLE
  18065. * - 2: NETWORK_LOADING
  18066. * - 3: NETWORK_NO_SOURCE
  18067. *
  18068. * @method Html5#networkState
  18069. * @return {number}
  18070. * The value of `networkState` from the media element. This will be a number
  18071. * from the list in the description.
  18072. *
  18073. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
  18074. */
  18075. 'networkState',
  18076. /**
  18077. * Get the value of `readyState` from the media element. `readyState` indicates
  18078. * the current state of the media element. It returns an enumeration from the
  18079. * following list:
  18080. * - 0: HAVE_NOTHING
  18081. * - 1: HAVE_METADATA
  18082. * - 2: HAVE_CURRENT_DATA
  18083. * - 3: HAVE_FUTURE_DATA
  18084. * - 4: HAVE_ENOUGH_DATA
  18085. *
  18086. * @method Html5#readyState
  18087. * @return {number}
  18088. * The value of `readyState` from the media element. This will be a number
  18089. * from the list in the description.
  18090. *
  18091. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
  18092. */
  18093. 'readyState',
  18094. /**
  18095. * Get the value of `videoWidth` from the video element. `videoWidth` indicates
  18096. * the current width of the video in css pixels.
  18097. *
  18098. * @method Html5#videoWidth
  18099. * @return {number}
  18100. * The value of `videoWidth` from the video element. This will be a number
  18101. * in css pixels.
  18102. *
  18103. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
  18104. */
  18105. 'videoWidth',
  18106. /**
  18107. * Get the value of `videoHeight` from the video element. `videoHeight` indicates
  18108. * the current height of the video in css pixels.
  18109. *
  18110. * @method Html5#videoHeight
  18111. * @return {number}
  18112. * The value of `videoHeight` from the video element. This will be a number
  18113. * in css pixels.
  18114. *
  18115. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
  18116. */
  18117. 'videoHeight',
  18118. /**
  18119. * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
  18120. * to the browser that should sent the cookies along with the requests for the
  18121. * different assets/playlists
  18122. *
  18123. * @method Html5#crossOrigin
  18124. * @return {string}
  18125. * - anonymous indicates that the media should not sent cookies.
  18126. * - use-credentials indicates that the media should sent cookies along the requests.
  18127. *
  18128. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
  18129. */
  18130. 'crossOrigin'].forEach(function (prop) {
  18131. Html5.prototype[prop] = function () {
  18132. return this.el_[prop];
  18133. };
  18134. });
  18135. // Wrap native properties with a setter in this format:
  18136. // set + toTitleCase(name)
  18137. // The list is as follows:
  18138. // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
  18139. // setDisablePictureInPicture, setCrossOrigin
  18140. [
  18141. /**
  18142. * Set the value of `volume` on the media element. `volume` indicates the current
  18143. * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
  18144. * so on.
  18145. *
  18146. * @method Html5#setVolume
  18147. * @param {number} percentAsDecimal
  18148. * The volume percent as a decimal. Valid range is from 0-1.
  18149. *
  18150. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
  18151. */
  18152. 'volume',
  18153. /**
  18154. * Set the value of `src` on the media element. `src` indicates the current
  18155. * {@link Tech~SourceObject} for the media.
  18156. *
  18157. * @method Html5#setSrc
  18158. * @param {Tech~SourceObject} src
  18159. * The source object to set as the current source.
  18160. *
  18161. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
  18162. */
  18163. 'src',
  18164. /**
  18165. * Set the value of `poster` on the media element. `poster` is the url to
  18166. * an image file that can/will be shown when no media data is available.
  18167. *
  18168. * @method Html5#setPoster
  18169. * @param {string} poster
  18170. * The url to an image that should be used as the `poster` for the media
  18171. * element.
  18172. *
  18173. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
  18174. */
  18175. 'poster',
  18176. /**
  18177. * Set the value of `preload` on the media element. `preload` indicates
  18178. * what should download before the media is interacted with. It can have the following
  18179. * values:
  18180. * - none: nothing should be downloaded
  18181. * - metadata: poster and the first few frames of the media may be downloaded to get
  18182. * media dimensions and other metadata
  18183. * - auto: allow the media and metadata for the media to be downloaded before
  18184. * interaction
  18185. *
  18186. * @method Html5#setPreload
  18187. * @param {string} preload
  18188. * The value of `preload` to set on the media element. Must be 'none', 'metadata',
  18189. * or 'auto'.
  18190. *
  18191. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
  18192. */
  18193. 'preload',
  18194. /**
  18195. * Set the value of `playbackRate` on the media element. `playbackRate` indicates
  18196. * the rate at which the media should play back. Examples:
  18197. * - if playbackRate is set to 2, media will play twice as fast.
  18198. * - if playbackRate is set to 0.5, media will play half as fast.
  18199. *
  18200. * @method Html5#setPlaybackRate
  18201. * @return {number}
  18202. * The value of `playbackRate` from the media element. A number indicating
  18203. * the current playback speed of the media, where 1 is normal speed.
  18204. *
  18205. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
  18206. */
  18207. 'playbackRate',
  18208. /**
  18209. * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
  18210. * the rate at which the media should play back upon initial startup. Changing this value
  18211. * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
  18212. *
  18213. * Example Values:
  18214. * - if playbackRate is set to 2, media will play twice as fast.
  18215. * - if playbackRate is set to 0.5, media will play half as fast.
  18216. *
  18217. * @method Html5.prototype.setDefaultPlaybackRate
  18218. * @return {number}
  18219. * The value of `defaultPlaybackRate` from the media element. A number indicating
  18220. * the current playback speed of the media, where 1 is normal speed.
  18221. *
  18222. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
  18223. */
  18224. 'defaultPlaybackRate',
  18225. /**
  18226. * Prevents the browser from suggesting a Picture-in-Picture context menu
  18227. * or to request Picture-in-Picture automatically in some cases.
  18228. *
  18229. * @method Html5#setDisablePictureInPicture
  18230. * @param {boolean} value
  18231. * The true value will disable Picture-in-Picture mode.
  18232. *
  18233. * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
  18234. */
  18235. 'disablePictureInPicture',
  18236. /**
  18237. * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
  18238. * to the browser that should sent the cookies along with the requests for the
  18239. * different assets/playlists
  18240. *
  18241. * @method Html5#setCrossOrigin
  18242. * @param {string} crossOrigin
  18243. * - anonymous indicates that the media should not sent cookies.
  18244. * - use-credentials indicates that the media should sent cookies along the requests.
  18245. *
  18246. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
  18247. */
  18248. 'crossOrigin'].forEach(function (prop) {
  18249. Html5.prototype['set' + toTitleCase(prop)] = function (v) {
  18250. this.el_[prop] = v;
  18251. };
  18252. });
  18253. // wrap native functions with a function
  18254. // The list is as follows:
  18255. // pause, load, play
  18256. [
  18257. /**
  18258. * A wrapper around the media elements `pause` function. This will call the `HTML5`
  18259. * media elements `pause` function.
  18260. *
  18261. * @method Html5#pause
  18262. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
  18263. */
  18264. 'pause',
  18265. /**
  18266. * A wrapper around the media elements `load` function. This will call the `HTML5`s
  18267. * media element `load` function.
  18268. *
  18269. * @method Html5#load
  18270. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
  18271. */
  18272. 'load',
  18273. /**
  18274. * A wrapper around the media elements `play` function. This will call the `HTML5`s
  18275. * media element `play` function.
  18276. *
  18277. * @method Html5#play
  18278. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
  18279. */
  18280. 'play'].forEach(function (prop) {
  18281. Html5.prototype[prop] = function () {
  18282. return this.el_[prop]();
  18283. };
  18284. });
  18285. Tech.withSourceHandlers(Html5);
  18286. /**
  18287. * Native source handler for Html5, simply passes the source to the media element.
  18288. *
  18289. * @property {Tech~SourceObject} source
  18290. * The source object
  18291. *
  18292. * @property {Html5} tech
  18293. * The instance of the HTML5 tech.
  18294. */
  18295. Html5.nativeSourceHandler = {};
  18296. /**
  18297. * Check if the media element can play the given mime type.
  18298. *
  18299. * @param {string} type
  18300. * The mimetype to check
  18301. *
  18302. * @return {string}
  18303. * 'probably', 'maybe', or '' (empty string)
  18304. */
  18305. Html5.nativeSourceHandler.canPlayType = function (type) {
  18306. // IE without MediaPlayer throws an error (#519)
  18307. try {
  18308. return Html5.TEST_VID.canPlayType(type);
  18309. } catch (e) {
  18310. return '';
  18311. }
  18312. };
  18313. /**
  18314. * Check if the media element can handle a source natively.
  18315. *
  18316. * @param {Tech~SourceObject} source
  18317. * The source object
  18318. *
  18319. * @param {Object} [options]
  18320. * Options to be passed to the tech.
  18321. *
  18322. * @return {string}
  18323. * 'probably', 'maybe', or '' (empty string).
  18324. */
  18325. Html5.nativeSourceHandler.canHandleSource = function (source, options) {
  18326. // If a type was provided we should rely on that
  18327. if (source.type) {
  18328. return Html5.nativeSourceHandler.canPlayType(source.type);
  18329. // If no type, fall back to checking 'video/[EXTENSION]'
  18330. } else if (source.src) {
  18331. const ext = getFileExtension(source.src);
  18332. return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
  18333. }
  18334. return '';
  18335. };
  18336. /**
  18337. * Pass the source to the native media element.
  18338. *
  18339. * @param {Tech~SourceObject} source
  18340. * The source object
  18341. *
  18342. * @param {Html5} tech
  18343. * The instance of the Html5 tech
  18344. *
  18345. * @param {Object} [options]
  18346. * The options to pass to the source
  18347. */
  18348. Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
  18349. tech.setSrc(source.src);
  18350. };
  18351. /**
  18352. * A noop for the native dispose function, as cleanup is not needed.
  18353. */
  18354. Html5.nativeSourceHandler.dispose = function () {};
  18355. // Register the native source handler
  18356. Html5.registerSourceHandler(Html5.nativeSourceHandler);
  18357. Tech.registerTech('Html5', Html5);
  18358. /**
  18359. * @file player.js
  18360. */
  18361. // The following tech events are simply re-triggered
  18362. // on the player when they happen
  18363. const TECH_EVENTS_RETRIGGER = [
  18364. /**
  18365. * Fired while the user agent is downloading media data.
  18366. *
  18367. * @event Player#progress
  18368. * @type {Event}
  18369. */
  18370. /**
  18371. * Retrigger the `progress` event that was triggered by the {@link Tech}.
  18372. *
  18373. * @private
  18374. * @method Player#handleTechProgress_
  18375. * @fires Player#progress
  18376. * @listens Tech#progress
  18377. */
  18378. 'progress',
  18379. /**
  18380. * Fires when the loading of an audio/video is aborted.
  18381. *
  18382. * @event Player#abort
  18383. * @type {Event}
  18384. */
  18385. /**
  18386. * Retrigger the `abort` event that was triggered by the {@link Tech}.
  18387. *
  18388. * @private
  18389. * @method Player#handleTechAbort_
  18390. * @fires Player#abort
  18391. * @listens Tech#abort
  18392. */
  18393. 'abort',
  18394. /**
  18395. * Fires when the browser is intentionally not getting media data.
  18396. *
  18397. * @event Player#suspend
  18398. * @type {Event}
  18399. */
  18400. /**
  18401. * Retrigger the `suspend` event that was triggered by the {@link Tech}.
  18402. *
  18403. * @private
  18404. * @method Player#handleTechSuspend_
  18405. * @fires Player#suspend
  18406. * @listens Tech#suspend
  18407. */
  18408. 'suspend',
  18409. /**
  18410. * Fires when the current playlist is empty.
  18411. *
  18412. * @event Player#emptied
  18413. * @type {Event}
  18414. */
  18415. /**
  18416. * Retrigger the `emptied` event that was triggered by the {@link Tech}.
  18417. *
  18418. * @private
  18419. * @method Player#handleTechEmptied_
  18420. * @fires Player#emptied
  18421. * @listens Tech#emptied
  18422. */
  18423. 'emptied',
  18424. /**
  18425. * Fires when the browser is trying to get media data, but data is not available.
  18426. *
  18427. * @event Player#stalled
  18428. * @type {Event}
  18429. */
  18430. /**
  18431. * Retrigger the `stalled` event that was triggered by the {@link Tech}.
  18432. *
  18433. * @private
  18434. * @method Player#handleTechStalled_
  18435. * @fires Player#stalled
  18436. * @listens Tech#stalled
  18437. */
  18438. 'stalled',
  18439. /**
  18440. * Fires when the browser has loaded meta data for the audio/video.
  18441. *
  18442. * @event Player#loadedmetadata
  18443. * @type {Event}
  18444. */
  18445. /**
  18446. * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
  18447. *
  18448. * @private
  18449. * @method Player#handleTechLoadedmetadata_
  18450. * @fires Player#loadedmetadata
  18451. * @listens Tech#loadedmetadata
  18452. */
  18453. 'loadedmetadata',
  18454. /**
  18455. * Fires when the browser has loaded the current frame of the audio/video.
  18456. *
  18457. * @event Player#loadeddata
  18458. * @type {event}
  18459. */
  18460. /**
  18461. * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
  18462. *
  18463. * @private
  18464. * @method Player#handleTechLoaddeddata_
  18465. * @fires Player#loadeddata
  18466. * @listens Tech#loadeddata
  18467. */
  18468. 'loadeddata',
  18469. /**
  18470. * Fires when the current playback position has changed.
  18471. *
  18472. * @event Player#timeupdate
  18473. * @type {event}
  18474. */
  18475. /**
  18476. * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
  18477. *
  18478. * @private
  18479. * @method Player#handleTechTimeUpdate_
  18480. * @fires Player#timeupdate
  18481. * @listens Tech#timeupdate
  18482. */
  18483. 'timeupdate',
  18484. /**
  18485. * Fires when the video's intrinsic dimensions change
  18486. *
  18487. * @event Player#resize
  18488. * @type {event}
  18489. */
  18490. /**
  18491. * Retrigger the `resize` event that was triggered by the {@link Tech}.
  18492. *
  18493. * @private
  18494. * @method Player#handleTechResize_
  18495. * @fires Player#resize
  18496. * @listens Tech#resize
  18497. */
  18498. 'resize',
  18499. /**
  18500. * Fires when the volume has been changed
  18501. *
  18502. * @event Player#volumechange
  18503. * @type {event}
  18504. */
  18505. /**
  18506. * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
  18507. *
  18508. * @private
  18509. * @method Player#handleTechVolumechange_
  18510. * @fires Player#volumechange
  18511. * @listens Tech#volumechange
  18512. */
  18513. 'volumechange',
  18514. /**
  18515. * Fires when the text track has been changed
  18516. *
  18517. * @event Player#texttrackchange
  18518. * @type {event}
  18519. */
  18520. /**
  18521. * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
  18522. *
  18523. * @private
  18524. * @method Player#handleTechTexttrackchange_
  18525. * @fires Player#texttrackchange
  18526. * @listens Tech#texttrackchange
  18527. */
  18528. 'texttrackchange'];
  18529. // events to queue when playback rate is zero
  18530. // this is a hash for the sole purpose of mapping non-camel-cased event names
  18531. // to camel-cased function names
  18532. const TECH_EVENTS_QUEUE = {
  18533. canplay: 'CanPlay',
  18534. canplaythrough: 'CanPlayThrough',
  18535. playing: 'Playing',
  18536. seeked: 'Seeked'
  18537. };
  18538. const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
  18539. const BREAKPOINT_CLASSES = {};
  18540. // grep: vjs-layout-tiny
  18541. // grep: vjs-layout-x-small
  18542. // grep: vjs-layout-small
  18543. // grep: vjs-layout-medium
  18544. // grep: vjs-layout-large
  18545. // grep: vjs-layout-x-large
  18546. // grep: vjs-layout-huge
  18547. BREAKPOINT_ORDER.forEach(k => {
  18548. const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
  18549. BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
  18550. });
  18551. const DEFAULT_BREAKPOINTS = {
  18552. tiny: 210,
  18553. xsmall: 320,
  18554. small: 425,
  18555. medium: 768,
  18556. large: 1440,
  18557. xlarge: 2560,
  18558. huge: Infinity
  18559. };
  18560. /**
  18561. * An instance of the `Player` class is created when any of the Video.js setup methods
  18562. * are used to initialize a video.
  18563. *
  18564. * After an instance has been created it can be accessed globally in three ways:
  18565. * 1. By calling `videojs.getPlayer('example_video_1');`
  18566. * 2. By calling `videojs('example_video_1');` (not recomended)
  18567. * 2. By using it directly via `videojs.players.example_video_1;`
  18568. *
  18569. * @extends Component
  18570. * @global
  18571. */
  18572. class Player extends Component {
  18573. /**
  18574. * Create an instance of this class.
  18575. *
  18576. * @param {Element} tag
  18577. * The original video DOM element used for configuring options.
  18578. *
  18579. * @param {Object} [options]
  18580. * Object of option names and values.
  18581. *
  18582. * @param {Function} [ready]
  18583. * Ready callback function.
  18584. */
  18585. constructor(tag, options, ready) {
  18586. // Make sure tag ID exists
  18587. tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
  18588. // Set Options
  18589. // The options argument overrides options set in the video tag
  18590. // which overrides globally set options.
  18591. // This latter part coincides with the load order
  18592. // (tag must exist before Player)
  18593. options = Object.assign(Player.getTagSettings(tag), options);
  18594. // Delay the initialization of children because we need to set up
  18595. // player properties first, and can't use `this` before `super()`
  18596. options.initChildren = false;
  18597. // Same with creating the element
  18598. options.createEl = false;
  18599. // don't auto mixin the evented mixin
  18600. options.evented = false;
  18601. // we don't want the player to report touch activity on itself
  18602. // see enableTouchActivity in Component
  18603. options.reportTouchActivity = false;
  18604. // If language is not set, get the closest lang attribute
  18605. if (!options.language) {
  18606. const closest = tag.closest('[lang]');
  18607. if (closest) {
  18608. options.language = closest.getAttribute('lang');
  18609. }
  18610. }
  18611. // Run base component initializing with new options
  18612. super(null, options, ready);
  18613. // Create bound methods for document listeners.
  18614. this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
  18615. this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
  18616. this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
  18617. this.boundApplyInitTime_ = e => this.applyInitTime_(e);
  18618. this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
  18619. this.boundHandleTechClick_ = e => this.handleTechClick_(e);
  18620. this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
  18621. this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
  18622. this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
  18623. this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
  18624. this.boundHandleTechTap_ = e => this.handleTechTap_(e);
  18625. // default isFullscreen_ to false
  18626. this.isFullscreen_ = false;
  18627. // create logger
  18628. this.log = createLogger(this.id_);
  18629. // Hold our own reference to fullscreen api so it can be mocked in tests
  18630. this.fsApi_ = FullscreenApi;
  18631. // Tracks when a tech changes the poster
  18632. this.isPosterFromTech_ = false;
  18633. // Holds callback info that gets queued when playback rate is zero
  18634. // and a seek is happening
  18635. this.queuedCallbacks_ = [];
  18636. // Turn off API access because we're loading a new tech that might load asynchronously
  18637. this.isReady_ = false;
  18638. // Init state hasStarted_
  18639. this.hasStarted_ = false;
  18640. // Init state userActive_
  18641. this.userActive_ = false;
  18642. // Init debugEnabled_
  18643. this.debugEnabled_ = false;
  18644. // Init state audioOnlyMode_
  18645. this.audioOnlyMode_ = false;
  18646. // Init state audioPosterMode_
  18647. this.audioPosterMode_ = false;
  18648. // Init state audioOnlyCache_
  18649. this.audioOnlyCache_ = {
  18650. playerHeight: null,
  18651. hiddenChildren: []
  18652. };
  18653. // if the global option object was accidentally blown away by
  18654. // someone, bail early with an informative error
  18655. if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
  18656. throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
  18657. }
  18658. // Store the original tag used to set options
  18659. this.tag = tag;
  18660. // Store the tag attributes used to restore html5 element
  18661. this.tagAttributes = tag && getAttributes(tag);
  18662. // Update current language
  18663. this.language(this.options_.language);
  18664. // Update Supported Languages
  18665. if (options.languages) {
  18666. // Normalise player option languages to lowercase
  18667. const languagesToLower = {};
  18668. Object.getOwnPropertyNames(options.languages).forEach(function (name) {
  18669. languagesToLower[name.toLowerCase()] = options.languages[name];
  18670. });
  18671. this.languages_ = languagesToLower;
  18672. } else {
  18673. this.languages_ = Player.prototype.options_.languages;
  18674. }
  18675. this.resetCache_();
  18676. // Set poster
  18677. this.poster_ = options.poster || '';
  18678. // Set controls
  18679. this.controls_ = !!options.controls;
  18680. // Original tag settings stored in options
  18681. // now remove immediately so native controls don't flash.
  18682. // May be turned back on by HTML5 tech if nativeControlsForTouch is true
  18683. tag.controls = false;
  18684. tag.removeAttribute('controls');
  18685. this.changingSrc_ = false;
  18686. this.playCallbacks_ = [];
  18687. this.playTerminatedQueue_ = [];
  18688. // the attribute overrides the option
  18689. if (tag.hasAttribute('autoplay')) {
  18690. this.autoplay(true);
  18691. } else {
  18692. // otherwise use the setter to validate and
  18693. // set the correct value.
  18694. this.autoplay(this.options_.autoplay);
  18695. }
  18696. // check plugins
  18697. if (options.plugins) {
  18698. Object.keys(options.plugins).forEach(name => {
  18699. if (typeof this[name] !== 'function') {
  18700. throw new Error(`plugin "${name}" does not exist`);
  18701. }
  18702. });
  18703. }
  18704. /*
  18705. * Store the internal state of scrubbing
  18706. *
  18707. * @private
  18708. * @return {Boolean} True if the user is scrubbing
  18709. */
  18710. this.scrubbing_ = false;
  18711. this.el_ = this.createEl();
  18712. // Make this an evented object and use `el_` as its event bus.
  18713. evented(this, {
  18714. eventBusKey: 'el_'
  18715. });
  18716. // listen to document and player fullscreenchange handlers so we receive those events
  18717. // before a user can receive them so we can update isFullscreen appropriately.
  18718. // make sure that we listen to fullscreenchange events before everything else to make sure that
  18719. // our isFullscreen method is updated properly for internal components as well as external.
  18720. if (this.fsApi_.requestFullscreen) {
  18721. on(document__default["default"], this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
  18722. this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
  18723. }
  18724. if (this.fluid_) {
  18725. this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
  18726. }
  18727. // We also want to pass the original player options to each component and plugin
  18728. // as well so they don't need to reach back into the player for options later.
  18729. // We also need to do another copy of this.options_ so we don't end up with
  18730. // an infinite loop.
  18731. const playerOptionsCopy = merge(this.options_);
  18732. // Load plugins
  18733. if (options.plugins) {
  18734. Object.keys(options.plugins).forEach(name => {
  18735. this[name](options.plugins[name]);
  18736. });
  18737. }
  18738. // Enable debug mode to fire debugon event for all plugins.
  18739. if (options.debug) {
  18740. this.debug(true);
  18741. }
  18742. this.options_.playerOptions = playerOptionsCopy;
  18743. this.middleware_ = [];
  18744. this.playbackRates(options.playbackRates);
  18745. this.initChildren();
  18746. // Set isAudio based on whether or not an audio tag was used
  18747. this.isAudio(tag.nodeName.toLowerCase() === 'audio');
  18748. // Update controls className. Can't do this when the controls are initially
  18749. // set because the element doesn't exist yet.
  18750. if (this.controls()) {
  18751. this.addClass('vjs-controls-enabled');
  18752. } else {
  18753. this.addClass('vjs-controls-disabled');
  18754. }
  18755. // Set ARIA label and region role depending on player type
  18756. this.el_.setAttribute('role', 'region');
  18757. if (this.isAudio()) {
  18758. this.el_.setAttribute('aria-label', this.localize('Audio Player'));
  18759. } else {
  18760. this.el_.setAttribute('aria-label', this.localize('Video Player'));
  18761. }
  18762. if (this.isAudio()) {
  18763. this.addClass('vjs-audio');
  18764. }
  18765. // TODO: Make this smarter. Toggle user state between touching/mousing
  18766. // using events, since devices can have both touch and mouse events.
  18767. // TODO: Make this check be performed again when the window switches between monitors
  18768. // (See https://github.com/videojs/video.js/issues/5683)
  18769. if (TOUCH_ENABLED) {
  18770. this.addClass('vjs-touch-enabled');
  18771. }
  18772. // iOS Safari has broken hover handling
  18773. if (!IS_IOS) {
  18774. this.addClass('vjs-workinghover');
  18775. }
  18776. // Make player easily findable by ID
  18777. Player.players[this.id_] = this;
  18778. // Add a major version class to aid css in plugins
  18779. const majorVersion = version.split('.')[0];
  18780. this.addClass(`vjs-v${majorVersion}`);
  18781. // When the player is first initialized, trigger activity so components
  18782. // like the control bar show themselves if needed
  18783. this.userActive(true);
  18784. this.reportUserActivity();
  18785. this.one('play', e => this.listenForUserActivity_(e));
  18786. this.on('keydown', e => this.handleKeyDown(e));
  18787. this.on('languagechange', e => this.handleLanguagechange(e));
  18788. this.breakpoints(this.options_.breakpoints);
  18789. this.responsive(this.options_.responsive);
  18790. // Calling both the audio mode methods after the player is fully
  18791. // setup to be able to listen to the events triggered by them
  18792. this.on('ready', () => {
  18793. // Calling the audioPosterMode method first so that
  18794. // the audioOnlyMode can take precedence when both options are set to true
  18795. this.audioPosterMode(this.options_.audioPosterMode);
  18796. this.audioOnlyMode(this.options_.audioOnlyMode);
  18797. });
  18798. }
  18799. /**
  18800. * Destroys the video player and does any necessary cleanup.
  18801. *
  18802. * This is especially helpful if you are dynamically adding and removing videos
  18803. * to/from the DOM.
  18804. *
  18805. * @fires Player#dispose
  18806. */
  18807. dispose() {
  18808. /**
  18809. * Called when the player is being disposed of.
  18810. *
  18811. * @event Player#dispose
  18812. * @type {Event}
  18813. */
  18814. this.trigger('dispose');
  18815. // prevent dispose from being called twice
  18816. this.off('dispose');
  18817. // Make sure all player-specific document listeners are unbound. This is
  18818. off(document__default["default"], this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
  18819. off(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
  18820. if (this.styleEl_ && this.styleEl_.parentNode) {
  18821. this.styleEl_.parentNode.removeChild(this.styleEl_);
  18822. this.styleEl_ = null;
  18823. }
  18824. // Kill reference to this player
  18825. Player.players[this.id_] = null;
  18826. if (this.tag && this.tag.player) {
  18827. this.tag.player = null;
  18828. }
  18829. if (this.el_ && this.el_.player) {
  18830. this.el_.player = null;
  18831. }
  18832. if (this.tech_) {
  18833. this.tech_.dispose();
  18834. this.isPosterFromTech_ = false;
  18835. this.poster_ = '';
  18836. }
  18837. if (this.playerElIngest_) {
  18838. this.playerElIngest_ = null;
  18839. }
  18840. if (this.tag) {
  18841. this.tag = null;
  18842. }
  18843. clearCacheForPlayer(this);
  18844. // remove all event handlers for track lists
  18845. // all tracks and track listeners are removed on
  18846. // tech dispose
  18847. ALL.names.forEach(name => {
  18848. const props = ALL[name];
  18849. const list = this[props.getterName]();
  18850. // if it is not a native list
  18851. // we have to manually remove event listeners
  18852. if (list && list.off) {
  18853. list.off();
  18854. }
  18855. });
  18856. // the actual .el_ is removed here, or replaced if
  18857. super.dispose({
  18858. restoreEl: this.options_.restoreEl
  18859. });
  18860. }
  18861. /**
  18862. * Create the `Player`'s DOM element.
  18863. *
  18864. * @return {Element}
  18865. * The DOM element that gets created.
  18866. */
  18867. createEl() {
  18868. let tag = this.tag;
  18869. let el;
  18870. let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
  18871. const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
  18872. if (playerElIngest) {
  18873. el = this.el_ = tag.parentNode;
  18874. } else if (!divEmbed) {
  18875. el = this.el_ = super.createEl('div');
  18876. }
  18877. // Copy over all the attributes from the tag, including ID and class
  18878. // ID will now reference player box, not the video tag
  18879. const attrs = getAttributes(tag);
  18880. if (divEmbed) {
  18881. el = this.el_ = tag;
  18882. tag = this.tag = document__default["default"].createElement('video');
  18883. while (el.children.length) {
  18884. tag.appendChild(el.firstChild);
  18885. }
  18886. if (!hasClass(el, 'video-js')) {
  18887. addClass(el, 'video-js');
  18888. }
  18889. el.appendChild(tag);
  18890. playerElIngest = this.playerElIngest_ = el;
  18891. // move properties over from our custom `video-js` element
  18892. // to our new `video` element. This will move things like
  18893. // `src` or `controls` that were set via js before the player
  18894. // was initialized.
  18895. Object.keys(el).forEach(k => {
  18896. try {
  18897. tag[k] = el[k];
  18898. } catch (e) {
  18899. // we got a a property like outerHTML which we can't actually copy, ignore it
  18900. }
  18901. });
  18902. }
  18903. // set tabindex to -1 to remove the video element from the focus order
  18904. tag.setAttribute('tabindex', '-1');
  18905. attrs.tabindex = '-1';
  18906. // Workaround for #4583 on Chrome (on Windows) with JAWS.
  18907. // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
  18908. // Note that we can't detect if JAWS is being used, but this ARIA attribute
  18909. // doesn't change behavior of Chrome if JAWS is not being used
  18910. if (IS_CHROME && IS_WINDOWS) {
  18911. tag.setAttribute('role', 'application');
  18912. attrs.role = 'application';
  18913. }
  18914. // Remove width/height attrs from tag so CSS can make it 100% width/height
  18915. tag.removeAttribute('width');
  18916. tag.removeAttribute('height');
  18917. if ('width' in attrs) {
  18918. delete attrs.width;
  18919. }
  18920. if ('height' in attrs) {
  18921. delete attrs.height;
  18922. }
  18923. Object.getOwnPropertyNames(attrs).forEach(function (attr) {
  18924. // don't copy over the class attribute to the player element when we're in a div embed
  18925. // the class is already set up properly in the divEmbed case
  18926. // and we want to make sure that the `video-js` class doesn't get lost
  18927. if (!(divEmbed && attr === 'class')) {
  18928. el.setAttribute(attr, attrs[attr]);
  18929. }
  18930. if (divEmbed) {
  18931. tag.setAttribute(attr, attrs[attr]);
  18932. }
  18933. });
  18934. // Update tag id/class for use as HTML5 playback tech
  18935. // Might think we should do this after embedding in container so .vjs-tech class
  18936. // doesn't flash 100% width/height, but class only applies with .video-js parent
  18937. tag.playerId = tag.id;
  18938. tag.id += '_html5_api';
  18939. tag.className = 'vjs-tech';
  18940. // Make player findable on elements
  18941. tag.player = el.player = this;
  18942. // Default state of video is paused
  18943. this.addClass('vjs-paused');
  18944. // Add a style element in the player that we'll use to set the width/height
  18945. // of the player in a way that's still overridable by CSS, just like the
  18946. // video element
  18947. if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE !== true) {
  18948. this.styleEl_ = createStyleElement('vjs-styles-dimensions');
  18949. const defaultsStyleEl = $('.vjs-styles-defaults');
  18950. const head = $('head');
  18951. head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
  18952. }
  18953. this.fill_ = false;
  18954. this.fluid_ = false;
  18955. // Pass in the width/height/aspectRatio options which will update the style el
  18956. this.width(this.options_.width);
  18957. this.height(this.options_.height);
  18958. this.fill(this.options_.fill);
  18959. this.fluid(this.options_.fluid);
  18960. this.aspectRatio(this.options_.aspectRatio);
  18961. // support both crossOrigin and crossorigin to reduce confusion and issues around the name
  18962. this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
  18963. // Hide any links within the video/audio tag,
  18964. // because IE doesn't hide them completely from screen readers.
  18965. const links = tag.getElementsByTagName('a');
  18966. for (let i = 0; i < links.length; i++) {
  18967. const linkEl = links.item(i);
  18968. addClass(linkEl, 'vjs-hidden');
  18969. linkEl.setAttribute('hidden', 'hidden');
  18970. }
  18971. // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
  18972. // keep track of the original for later so we can know if the source originally failed
  18973. tag.initNetworkState_ = tag.networkState;
  18974. // Wrap video tag in div (el/box) container
  18975. if (tag.parentNode && !playerElIngest) {
  18976. tag.parentNode.insertBefore(el, tag);
  18977. }
  18978. // insert the tag as the first child of the player element
  18979. // then manually add it to the children array so that this.addChild
  18980. // will work properly for other components
  18981. //
  18982. // Breaks iPhone, fixed in HTML5 setup.
  18983. prependTo(tag, el);
  18984. this.children_.unshift(tag);
  18985. // Set lang attr on player to ensure CSS :lang() in consistent with player
  18986. // if it's been set to something different to the doc
  18987. this.el_.setAttribute('lang', this.language_);
  18988. this.el_.setAttribute('translate', 'no');
  18989. this.el_ = el;
  18990. return el;
  18991. }
  18992. /**
  18993. * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
  18994. * sets the `crossOrigin` property on the `<video>` tag to control the CORS
  18995. * behavior.
  18996. *
  18997. * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
  18998. *
  18999. * @param {string|null} [value]
  19000. * The value to set the `Player`'s crossOrigin to. If an argument is
  19001. * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
  19002. *
  19003. * @return {string|null|undefined}
  19004. * - The current crossOrigin value of the `Player` when getting.
  19005. * - undefined when setting
  19006. */
  19007. crossOrigin(value) {
  19008. // `null` can be set to unset a value
  19009. if (typeof value === 'undefined') {
  19010. return this.techGet_('crossOrigin');
  19011. }
  19012. if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
  19013. log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
  19014. return;
  19015. }
  19016. this.techCall_('setCrossOrigin', value);
  19017. if (this.posterImage) {
  19018. this.posterImage.crossOrigin(value);
  19019. }
  19020. return;
  19021. }
  19022. /**
  19023. * A getter/setter for the `Player`'s width. Returns the player's configured value.
  19024. * To get the current width use `currentWidth()`.
  19025. *
  19026. * @param {number} [value]
  19027. * The value to set the `Player`'s width to.
  19028. *
  19029. * @return {number}
  19030. * The current width of the `Player` when getting.
  19031. */
  19032. width(value) {
  19033. return this.dimension('width', value);
  19034. }
  19035. /**
  19036. * A getter/setter for the `Player`'s height. Returns the player's configured value.
  19037. * To get the current height use `currentheight()`.
  19038. *
  19039. * @param {number} [value]
  19040. * The value to set the `Player`'s height to.
  19041. *
  19042. * @return {number}
  19043. * The current height of the `Player` when getting.
  19044. */
  19045. height(value) {
  19046. return this.dimension('height', value);
  19047. }
  19048. /**
  19049. * A getter/setter for the `Player`'s width & height.
  19050. *
  19051. * @param {string} dimension
  19052. * This string can be:
  19053. * - 'width'
  19054. * - 'height'
  19055. *
  19056. * @param {number} [value]
  19057. * Value for dimension specified in the first argument.
  19058. *
  19059. * @return {number}
  19060. * The dimension arguments value when getting (width/height).
  19061. */
  19062. dimension(dimension, value) {
  19063. const privDimension = dimension + '_';
  19064. if (value === undefined) {
  19065. return this[privDimension] || 0;
  19066. }
  19067. if (value === '' || value === 'auto') {
  19068. // If an empty string is given, reset the dimension to be automatic
  19069. this[privDimension] = undefined;
  19070. this.updateStyleEl_();
  19071. return;
  19072. }
  19073. const parsedVal = parseFloat(value);
  19074. if (isNaN(parsedVal)) {
  19075. log.error(`Improper value "${value}" supplied for for ${dimension}`);
  19076. return;
  19077. }
  19078. this[privDimension] = parsedVal;
  19079. this.updateStyleEl_();
  19080. }
  19081. /**
  19082. * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
  19083. *
  19084. * Turning this on will turn off fill mode.
  19085. *
  19086. * @param {boolean} [bool]
  19087. * - A value of true adds the class.
  19088. * - A value of false removes the class.
  19089. * - No value will be a getter.
  19090. *
  19091. * @return {boolean|undefined}
  19092. * - The value of fluid when getting.
  19093. * - `undefined` when setting.
  19094. */
  19095. fluid(bool) {
  19096. if (bool === undefined) {
  19097. return !!this.fluid_;
  19098. }
  19099. this.fluid_ = !!bool;
  19100. if (isEvented(this)) {
  19101. this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
  19102. }
  19103. if (bool) {
  19104. this.addClass('vjs-fluid');
  19105. this.fill(false);
  19106. addEventedCallback(this, () => {
  19107. this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
  19108. });
  19109. } else {
  19110. this.removeClass('vjs-fluid');
  19111. }
  19112. this.updateStyleEl_();
  19113. }
  19114. /**
  19115. * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
  19116. *
  19117. * Turning this on will turn off fluid mode.
  19118. *
  19119. * @param {boolean} [bool]
  19120. * - A value of true adds the class.
  19121. * - A value of false removes the class.
  19122. * - No value will be a getter.
  19123. *
  19124. * @return {boolean|undefined}
  19125. * - The value of fluid when getting.
  19126. * - `undefined` when setting.
  19127. */
  19128. fill(bool) {
  19129. if (bool === undefined) {
  19130. return !!this.fill_;
  19131. }
  19132. this.fill_ = !!bool;
  19133. if (bool) {
  19134. this.addClass('vjs-fill');
  19135. this.fluid(false);
  19136. } else {
  19137. this.removeClass('vjs-fill');
  19138. }
  19139. }
  19140. /**
  19141. * Get/Set the aspect ratio
  19142. *
  19143. * @param {string} [ratio]
  19144. * Aspect ratio for player
  19145. *
  19146. * @return {string|undefined}
  19147. * returns the current aspect ratio when getting
  19148. */
  19149. /**
  19150. * A getter/setter for the `Player`'s aspect ratio.
  19151. *
  19152. * @param {string} [ratio]
  19153. * The value to set the `Player`'s aspect ratio to.
  19154. *
  19155. * @return {string|undefined}
  19156. * - The current aspect ratio of the `Player` when getting.
  19157. * - undefined when setting
  19158. */
  19159. aspectRatio(ratio) {
  19160. if (ratio === undefined) {
  19161. return this.aspectRatio_;
  19162. }
  19163. // Check for width:height format
  19164. if (!/^\d+\:\d+$/.test(ratio)) {
  19165. throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
  19166. }
  19167. this.aspectRatio_ = ratio;
  19168. // We're assuming if you set an aspect ratio you want fluid mode,
  19169. // because in fixed mode you could calculate width and height yourself.
  19170. this.fluid(true);
  19171. this.updateStyleEl_();
  19172. }
  19173. /**
  19174. * Update styles of the `Player` element (height, width and aspect ratio).
  19175. *
  19176. * @private
  19177. * @listens Tech#loadedmetadata
  19178. */
  19179. updateStyleEl_() {
  19180. if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE === true) {
  19181. const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
  19182. const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
  19183. const techEl = this.tech_ && this.tech_.el();
  19184. if (techEl) {
  19185. if (width >= 0) {
  19186. techEl.width = width;
  19187. }
  19188. if (height >= 0) {
  19189. techEl.height = height;
  19190. }
  19191. }
  19192. return;
  19193. }
  19194. let width;
  19195. let height;
  19196. let aspectRatio;
  19197. let idClass;
  19198. // The aspect ratio is either used directly or to calculate width and height.
  19199. if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
  19200. // Use any aspectRatio that's been specifically set
  19201. aspectRatio = this.aspectRatio_;
  19202. } else if (this.videoWidth() > 0) {
  19203. // Otherwise try to get the aspect ratio from the video metadata
  19204. aspectRatio = this.videoWidth() + ':' + this.videoHeight();
  19205. } else {
  19206. // Or use a default. The video element's is 2:1, but 16:9 is more common.
  19207. aspectRatio = '16:9';
  19208. }
  19209. // Get the ratio as a decimal we can use to calculate dimensions
  19210. const ratioParts = aspectRatio.split(':');
  19211. const ratioMultiplier = ratioParts[1] / ratioParts[0];
  19212. if (this.width_ !== undefined) {
  19213. // Use any width that's been specifically set
  19214. width = this.width_;
  19215. } else if (this.height_ !== undefined) {
  19216. // Or calculate the width from the aspect ratio if a height has been set
  19217. width = this.height_ / ratioMultiplier;
  19218. } else {
  19219. // Or use the video's metadata, or use the video el's default of 300
  19220. width = this.videoWidth() || 300;
  19221. }
  19222. if (this.height_ !== undefined) {
  19223. // Use any height that's been specifically set
  19224. height = this.height_;
  19225. } else {
  19226. // Otherwise calculate the height from the ratio and the width
  19227. height = width * ratioMultiplier;
  19228. }
  19229. // Ensure the CSS class is valid by starting with an alpha character
  19230. if (/^[^a-zA-Z]/.test(this.id())) {
  19231. idClass = 'dimensions-' + this.id();
  19232. } else {
  19233. idClass = this.id() + '-dimensions';
  19234. }
  19235. // Ensure the right class is still on the player for the style element
  19236. this.addClass(idClass);
  19237. setTextContent(this.styleEl_, `
  19238. .${idClass} {
  19239. width: ${width}px;
  19240. height: ${height}px;
  19241. }
  19242. .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
  19243. padding-top: ${ratioMultiplier * 100}%;
  19244. }
  19245. `);
  19246. }
  19247. /**
  19248. * Load/Create an instance of playback {@link Tech} including element
  19249. * and API methods. Then append the `Tech` element in `Player` as a child.
  19250. *
  19251. * @param {string} techName
  19252. * name of the playback technology
  19253. *
  19254. * @param {string} source
  19255. * video source
  19256. *
  19257. * @private
  19258. */
  19259. loadTech_(techName, source) {
  19260. // Pause and remove current playback technology
  19261. if (this.tech_) {
  19262. this.unloadTech_();
  19263. }
  19264. const titleTechName = toTitleCase(techName);
  19265. const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
  19266. // get rid of the HTML5 video tag as soon as we are using another tech
  19267. if (titleTechName !== 'Html5' && this.tag) {
  19268. Tech.getTech('Html5').disposeMediaElement(this.tag);
  19269. this.tag.player = null;
  19270. this.tag = null;
  19271. }
  19272. this.techName_ = titleTechName;
  19273. // Turn off API access because we're loading a new tech that might load asynchronously
  19274. this.isReady_ = false;
  19275. let autoplay = this.autoplay();
  19276. // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
  19277. // because the player is going to handle autoplay on `loadstart`
  19278. if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
  19279. autoplay = false;
  19280. }
  19281. // Grab tech-specific options from player options and add source and parent element to use.
  19282. const techOptions = {
  19283. source,
  19284. autoplay,
  19285. 'nativeControlsForTouch': this.options_.nativeControlsForTouch,
  19286. 'playerId': this.id(),
  19287. 'techId': `${this.id()}_${camelTechName}_api`,
  19288. 'playsinline': this.options_.playsinline,
  19289. 'preload': this.options_.preload,
  19290. 'loop': this.options_.loop,
  19291. 'disablePictureInPicture': this.options_.disablePictureInPicture,
  19292. 'muted': this.options_.muted,
  19293. 'poster': this.poster(),
  19294. 'language': this.language(),
  19295. 'playerElIngest': this.playerElIngest_ || false,
  19296. 'vtt.js': this.options_['vtt.js'],
  19297. 'canOverridePoster': !!this.options_.techCanOverridePoster,
  19298. 'enableSourceset': this.options_.enableSourceset
  19299. };
  19300. ALL.names.forEach(name => {
  19301. const props = ALL[name];
  19302. techOptions[props.getterName] = this[props.privateName];
  19303. });
  19304. Object.assign(techOptions, this.options_[titleTechName]);
  19305. Object.assign(techOptions, this.options_[camelTechName]);
  19306. Object.assign(techOptions, this.options_[techName.toLowerCase()]);
  19307. if (this.tag) {
  19308. techOptions.tag = this.tag;
  19309. }
  19310. if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
  19311. techOptions.startTime = this.cache_.currentTime;
  19312. }
  19313. // Initialize tech instance
  19314. const TechClass = Tech.getTech(techName);
  19315. if (!TechClass) {
  19316. throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
  19317. }
  19318. this.tech_ = new TechClass(techOptions);
  19319. // player.triggerReady is always async, so don't need this to be async
  19320. this.tech_.ready(bind_(this, this.handleTechReady_), true);
  19321. textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
  19322. // Listen to all HTML5-defined events and trigger them on the player
  19323. TECH_EVENTS_RETRIGGER.forEach(event => {
  19324. this.on(this.tech_, event, e => this[`handleTech${toTitleCase(event)}_`](e));
  19325. });
  19326. Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
  19327. this.on(this.tech_, event, eventObj => {
  19328. if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
  19329. this.queuedCallbacks_.push({
  19330. callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
  19331. event: eventObj
  19332. });
  19333. return;
  19334. }
  19335. this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
  19336. });
  19337. });
  19338. this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
  19339. this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
  19340. this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
  19341. this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
  19342. this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
  19343. this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
  19344. this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
  19345. this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
  19346. this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
  19347. this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
  19348. this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
  19349. this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
  19350. this.on(this.tech_, 'error', e => this.handleTechError_(e));
  19351. this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
  19352. this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
  19353. this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
  19354. this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
  19355. this.usingNativeControls(this.techGet_('controls'));
  19356. if (this.controls() && !this.usingNativeControls()) {
  19357. this.addTechControlsListeners_();
  19358. }
  19359. // Add the tech element in the DOM if it was not already there
  19360. // Make sure to not insert the original video element if using Html5
  19361. if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
  19362. prependTo(this.tech_.el(), this.el());
  19363. }
  19364. // Get rid of the original video tag reference after the first tech is loaded
  19365. if (this.tag) {
  19366. this.tag.player = null;
  19367. this.tag = null;
  19368. }
  19369. }
  19370. /**
  19371. * Unload and dispose of the current playback {@link Tech}.
  19372. *
  19373. * @private
  19374. */
  19375. unloadTech_() {
  19376. // Save the current text tracks so that we can reuse the same text tracks with the next tech
  19377. ALL.names.forEach(name => {
  19378. const props = ALL[name];
  19379. this[props.privateName] = this[props.getterName]();
  19380. });
  19381. this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
  19382. this.isReady_ = false;
  19383. this.tech_.dispose();
  19384. this.tech_ = false;
  19385. if (this.isPosterFromTech_) {
  19386. this.poster_ = '';
  19387. this.trigger('posterchange');
  19388. }
  19389. this.isPosterFromTech_ = false;
  19390. }
  19391. /**
  19392. * Return a reference to the current {@link Tech}.
  19393. * It will print a warning by default about the danger of using the tech directly
  19394. * but any argument that is passed in will silence the warning.
  19395. *
  19396. * @param {*} [safety]
  19397. * Anything passed in to silence the warning
  19398. *
  19399. * @return {Tech}
  19400. * The Tech
  19401. */
  19402. tech(safety) {
  19403. if (safety === undefined) {
  19404. log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
  19405. }
  19406. return this.tech_;
  19407. }
  19408. /**
  19409. * Set up click and touch listeners for the playback element
  19410. *
  19411. * - On desktops: a click on the video itself will toggle playback
  19412. * - On mobile devices: a click on the video toggles controls
  19413. * which is done by toggling the user state between active and
  19414. * inactive
  19415. * - A tap can signal that a user has become active or has become inactive
  19416. * e.g. a quick tap on an iPhone movie should reveal the controls. Another
  19417. * quick tap should hide them again (signaling the user is in an inactive
  19418. * viewing state)
  19419. * - In addition to this, we still want the user to be considered inactive after
  19420. * a few seconds of inactivity.
  19421. *
  19422. * > Note: the only part of iOS interaction we can't mimic with this setup
  19423. * is a touch and hold on the video element counting as activity in order to
  19424. * keep the controls showing, but that shouldn't be an issue. A touch and hold
  19425. * on any controls will still keep the user active
  19426. *
  19427. * @private
  19428. */
  19429. addTechControlsListeners_() {
  19430. // Make sure to remove all the previous listeners in case we are called multiple times.
  19431. this.removeTechControlsListeners_();
  19432. this.on(this.tech_, 'click', this.boundHandleTechClick_);
  19433. this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  19434. // If the controls were hidden we don't want that to change without a tap event
  19435. // so we'll check if the controls were already showing before reporting user
  19436. // activity
  19437. this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
  19438. this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
  19439. this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
  19440. // The tap listener needs to come after the touchend listener because the tap
  19441. // listener cancels out any reportedUserActivity when setting userActive(false)
  19442. this.on(this.tech_, 'tap', this.boundHandleTechTap_);
  19443. }
  19444. /**
  19445. * Remove the listeners used for click and tap controls. This is needed for
  19446. * toggling to controls disabled, where a tap/touch should do nothing.
  19447. *
  19448. * @private
  19449. */
  19450. removeTechControlsListeners_() {
  19451. // We don't want to just use `this.off()` because there might be other needed
  19452. // listeners added by techs that extend this.
  19453. this.off(this.tech_, 'tap', this.boundHandleTechTap_);
  19454. this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
  19455. this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
  19456. this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
  19457. this.off(this.tech_, 'click', this.boundHandleTechClick_);
  19458. this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  19459. }
  19460. /**
  19461. * Player waits for the tech to be ready
  19462. *
  19463. * @private
  19464. */
  19465. handleTechReady_() {
  19466. this.triggerReady();
  19467. // Keep the same volume as before
  19468. if (this.cache_.volume) {
  19469. this.techCall_('setVolume', this.cache_.volume);
  19470. }
  19471. // Look if the tech found a higher resolution poster while loading
  19472. this.handleTechPosterChange_();
  19473. // Update the duration if available
  19474. this.handleTechDurationChange_();
  19475. }
  19476. /**
  19477. * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
  19478. *
  19479. * @fires Player#loadstart
  19480. * @listens Tech#loadstart
  19481. * @private
  19482. */
  19483. handleTechLoadStart_() {
  19484. // TODO: Update to use `emptied` event instead. See #1277.
  19485. this.removeClass('vjs-ended', 'vjs-seeking');
  19486. // reset the error state
  19487. this.error(null);
  19488. // Update the duration
  19489. this.handleTechDurationChange_();
  19490. if (!this.paused()) {
  19491. /**
  19492. * Fired when the user agent begins looking for media data
  19493. *
  19494. * @event Player#loadstart
  19495. * @type {Event}
  19496. */
  19497. this.trigger('loadstart');
  19498. } else {
  19499. // reset the hasStarted state
  19500. this.hasStarted(false);
  19501. this.trigger('loadstart');
  19502. }
  19503. // autoplay happens after loadstart for the browser,
  19504. // so we mimic that behavior
  19505. this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
  19506. }
  19507. /**
  19508. * Handle autoplay string values, rather than the typical boolean
  19509. * values that should be handled by the tech. Note that this is not
  19510. * part of any specification. Valid values and what they do can be
  19511. * found on the autoplay getter at Player#autoplay()
  19512. */
  19513. manualAutoplay_(type) {
  19514. if (!this.tech_ || typeof type !== 'string') {
  19515. return;
  19516. }
  19517. // Save original muted() value, set muted to true, and attempt to play().
  19518. // On promise rejection, restore muted from saved value
  19519. const resolveMuted = () => {
  19520. const previouslyMuted = this.muted();
  19521. this.muted(true);
  19522. const restoreMuted = () => {
  19523. this.muted(previouslyMuted);
  19524. };
  19525. // restore muted on play terminatation
  19526. this.playTerminatedQueue_.push(restoreMuted);
  19527. const mutedPromise = this.play();
  19528. if (!isPromise(mutedPromise)) {
  19529. return;
  19530. }
  19531. return mutedPromise.catch(err => {
  19532. restoreMuted();
  19533. throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
  19534. });
  19535. };
  19536. let promise;
  19537. // if muted defaults to true
  19538. // the only thing we can do is call play
  19539. if (type === 'any' && !this.muted()) {
  19540. promise = this.play();
  19541. if (isPromise(promise)) {
  19542. promise = promise.catch(resolveMuted);
  19543. }
  19544. } else if (type === 'muted' && !this.muted()) {
  19545. promise = resolveMuted();
  19546. } else {
  19547. promise = this.play();
  19548. }
  19549. if (!isPromise(promise)) {
  19550. return;
  19551. }
  19552. return promise.then(() => {
  19553. this.trigger({
  19554. type: 'autoplay-success',
  19555. autoplay: type
  19556. });
  19557. }).catch(() => {
  19558. this.trigger({
  19559. type: 'autoplay-failure',
  19560. autoplay: type
  19561. });
  19562. });
  19563. }
  19564. /**
  19565. * Update the internal source caches so that we return the correct source from
  19566. * `src()`, `currentSource()`, and `currentSources()`.
  19567. *
  19568. * > Note: `currentSources` will not be updated if the source that is passed in exists
  19569. * in the current `currentSources` cache.
  19570. *
  19571. *
  19572. * @param {Tech~SourceObject} srcObj
  19573. * A string or object source to update our caches to.
  19574. */
  19575. updateSourceCaches_(srcObj = '') {
  19576. let src = srcObj;
  19577. let type = '';
  19578. if (typeof src !== 'string') {
  19579. src = srcObj.src;
  19580. type = srcObj.type;
  19581. }
  19582. // make sure all the caches are set to default values
  19583. // to prevent null checking
  19584. this.cache_.source = this.cache_.source || {};
  19585. this.cache_.sources = this.cache_.sources || [];
  19586. // try to get the type of the src that was passed in
  19587. if (src && !type) {
  19588. type = findMimetype(this, src);
  19589. }
  19590. // update `currentSource` cache always
  19591. this.cache_.source = merge({}, srcObj, {
  19592. src,
  19593. type
  19594. });
  19595. const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
  19596. const sourceElSources = [];
  19597. const sourceEls = this.$$('source');
  19598. const matchingSourceEls = [];
  19599. for (let i = 0; i < sourceEls.length; i++) {
  19600. const sourceObj = getAttributes(sourceEls[i]);
  19601. sourceElSources.push(sourceObj);
  19602. if (sourceObj.src && sourceObj.src === src) {
  19603. matchingSourceEls.push(sourceObj.src);
  19604. }
  19605. }
  19606. // if we have matching source els but not matching sources
  19607. // the current source cache is not up to date
  19608. if (matchingSourceEls.length && !matchingSources.length) {
  19609. this.cache_.sources = sourceElSources;
  19610. // if we don't have matching source or source els set the
  19611. // sources cache to the `currentSource` cache
  19612. } else if (!matchingSources.length) {
  19613. this.cache_.sources = [this.cache_.source];
  19614. }
  19615. // update the tech `src` cache
  19616. this.cache_.src = src;
  19617. }
  19618. /**
  19619. * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
  19620. * causing the media element to reload.
  19621. *
  19622. * It will fire for the initial source and each subsequent source.
  19623. * This event is a custom event from Video.js and is triggered by the {@link Tech}.
  19624. *
  19625. * The event object for this event contains a `src` property that will contain the source
  19626. * that was available when the event was triggered. This is generally only necessary if Video.js
  19627. * is switching techs while the source was being changed.
  19628. *
  19629. * It is also fired when `load` is called on the player (or media element)
  19630. * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
  19631. * says that the resource selection algorithm needs to be aborted and restarted.
  19632. * In this case, it is very likely that the `src` property will be set to the
  19633. * empty string `""` to indicate we do not know what the source will be but
  19634. * that it is changing.
  19635. *
  19636. * *This event is currently still experimental and may change in minor releases.*
  19637. * __To use this, pass `enableSourceset` option to the player.__
  19638. *
  19639. * @event Player#sourceset
  19640. * @type {Event}
  19641. * @prop {string} src
  19642. * The source url available when the `sourceset` was triggered.
  19643. * It will be an empty string if we cannot know what the source is
  19644. * but know that the source will change.
  19645. */
  19646. /**
  19647. * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
  19648. *
  19649. * @fires Player#sourceset
  19650. * @listens Tech#sourceset
  19651. * @private
  19652. */
  19653. handleTechSourceset_(event) {
  19654. // only update the source cache when the source
  19655. // was not updated using the player api
  19656. if (!this.changingSrc_) {
  19657. let updateSourceCaches = src => this.updateSourceCaches_(src);
  19658. const playerSrc = this.currentSource().src;
  19659. const eventSrc = event.src;
  19660. // if we have a playerSrc that is not a blob, and a tech src that is a blob
  19661. if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
  19662. // if both the tech source and the player source were updated we assume
  19663. // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
  19664. if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
  19665. updateSourceCaches = () => {};
  19666. }
  19667. }
  19668. // update the source to the initial source right away
  19669. // in some cases this will be empty string
  19670. updateSourceCaches(eventSrc);
  19671. // if the `sourceset` `src` was an empty string
  19672. // wait for a `loadstart` to update the cache to `currentSrc`.
  19673. // If a sourceset happens before a `loadstart`, we reset the state
  19674. if (!event.src) {
  19675. this.tech_.any(['sourceset', 'loadstart'], e => {
  19676. // if a sourceset happens before a `loadstart` there
  19677. // is nothing to do as this `handleTechSourceset_`
  19678. // will be called again and this will be handled there.
  19679. if (e.type === 'sourceset') {
  19680. return;
  19681. }
  19682. const techSrc = this.techGet('currentSrc');
  19683. this.lastSource_.tech = techSrc;
  19684. this.updateSourceCaches_(techSrc);
  19685. });
  19686. }
  19687. }
  19688. this.lastSource_ = {
  19689. player: this.currentSource().src,
  19690. tech: event.src
  19691. };
  19692. this.trigger({
  19693. src: event.src,
  19694. type: 'sourceset'
  19695. });
  19696. }
  19697. /**
  19698. * Add/remove the vjs-has-started class
  19699. *
  19700. *
  19701. * @param {boolean} request
  19702. * - true: adds the class
  19703. * - false: remove the class
  19704. *
  19705. * @return {boolean}
  19706. * the boolean value of hasStarted_
  19707. */
  19708. hasStarted(request) {
  19709. if (request === undefined) {
  19710. // act as getter, if we have no request to change
  19711. return this.hasStarted_;
  19712. }
  19713. if (request === this.hasStarted_) {
  19714. return;
  19715. }
  19716. this.hasStarted_ = request;
  19717. if (this.hasStarted_) {
  19718. this.addClass('vjs-has-started');
  19719. } else {
  19720. this.removeClass('vjs-has-started');
  19721. }
  19722. }
  19723. /**
  19724. * Fired whenever the media begins or resumes playback
  19725. *
  19726. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
  19727. * @fires Player#play
  19728. * @listens Tech#play
  19729. * @private
  19730. */
  19731. handleTechPlay_() {
  19732. this.removeClass('vjs-ended', 'vjs-paused');
  19733. this.addClass('vjs-playing');
  19734. // hide the poster when the user hits play
  19735. this.hasStarted(true);
  19736. /**
  19737. * Triggered whenever an {@link Tech#play} event happens. Indicates that
  19738. * playback has started or resumed.
  19739. *
  19740. * @event Player#play
  19741. * @type {Event}
  19742. */
  19743. this.trigger('play');
  19744. }
  19745. /**
  19746. * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
  19747. *
  19748. * If there were any events queued while the playback rate was zero, fire
  19749. * those events now.
  19750. *
  19751. * @private
  19752. * @method Player#handleTechRateChange_
  19753. * @fires Player#ratechange
  19754. * @listens Tech#ratechange
  19755. */
  19756. handleTechRateChange_() {
  19757. if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
  19758. this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
  19759. this.queuedCallbacks_ = [];
  19760. }
  19761. this.cache_.lastPlaybackRate = this.tech_.playbackRate();
  19762. /**
  19763. * Fires when the playing speed of the audio/video is changed
  19764. *
  19765. * @event Player#ratechange
  19766. * @type {event}
  19767. */
  19768. this.trigger('ratechange');
  19769. }
  19770. /**
  19771. * Retrigger the `waiting` event that was triggered by the {@link Tech}.
  19772. *
  19773. * @fires Player#waiting
  19774. * @listens Tech#waiting
  19775. * @private
  19776. */
  19777. handleTechWaiting_() {
  19778. this.addClass('vjs-waiting');
  19779. /**
  19780. * A readyState change on the DOM element has caused playback to stop.
  19781. *
  19782. * @event Player#waiting
  19783. * @type {Event}
  19784. */
  19785. this.trigger('waiting');
  19786. // Browsers may emit a timeupdate event after a waiting event. In order to prevent
  19787. // premature removal of the waiting class, wait for the time to change.
  19788. const timeWhenWaiting = this.currentTime();
  19789. const timeUpdateListener = () => {
  19790. if (timeWhenWaiting !== this.currentTime()) {
  19791. this.removeClass('vjs-waiting');
  19792. this.off('timeupdate', timeUpdateListener);
  19793. }
  19794. };
  19795. this.on('timeupdate', timeUpdateListener);
  19796. }
  19797. /**
  19798. * Retrigger the `canplay` event that was triggered by the {@link Tech}.
  19799. * > Note: This is not consistent between browsers. See #1351
  19800. *
  19801. * @fires Player#canplay
  19802. * @listens Tech#canplay
  19803. * @private
  19804. */
  19805. handleTechCanPlay_() {
  19806. this.removeClass('vjs-waiting');
  19807. /**
  19808. * The media has a readyState of HAVE_FUTURE_DATA or greater.
  19809. *
  19810. * @event Player#canplay
  19811. * @type {Event}
  19812. */
  19813. this.trigger('canplay');
  19814. }
  19815. /**
  19816. * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
  19817. *
  19818. * @fires Player#canplaythrough
  19819. * @listens Tech#canplaythrough
  19820. * @private
  19821. */
  19822. handleTechCanPlayThrough_() {
  19823. this.removeClass('vjs-waiting');
  19824. /**
  19825. * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
  19826. * entire media file can be played without buffering.
  19827. *
  19828. * @event Player#canplaythrough
  19829. * @type {Event}
  19830. */
  19831. this.trigger('canplaythrough');
  19832. }
  19833. /**
  19834. * Retrigger the `playing` event that was triggered by the {@link Tech}.
  19835. *
  19836. * @fires Player#playing
  19837. * @listens Tech#playing
  19838. * @private
  19839. */
  19840. handleTechPlaying_() {
  19841. this.removeClass('vjs-waiting');
  19842. /**
  19843. * The media is no longer blocked from playback, and has started playing.
  19844. *
  19845. * @event Player#playing
  19846. * @type {Event}
  19847. */
  19848. this.trigger('playing');
  19849. }
  19850. /**
  19851. * Retrigger the `seeking` event that was triggered by the {@link Tech}.
  19852. *
  19853. * @fires Player#seeking
  19854. * @listens Tech#seeking
  19855. * @private
  19856. */
  19857. handleTechSeeking_() {
  19858. this.addClass('vjs-seeking');
  19859. /**
  19860. * Fired whenever the player is jumping to a new time
  19861. *
  19862. * @event Player#seeking
  19863. * @type {Event}
  19864. */
  19865. this.trigger('seeking');
  19866. }
  19867. /**
  19868. * Retrigger the `seeked` event that was triggered by the {@link Tech}.
  19869. *
  19870. * @fires Player#seeked
  19871. * @listens Tech#seeked
  19872. * @private
  19873. */
  19874. handleTechSeeked_() {
  19875. this.removeClass('vjs-seeking', 'vjs-ended');
  19876. /**
  19877. * Fired when the player has finished jumping to a new time
  19878. *
  19879. * @event Player#seeked
  19880. * @type {Event}
  19881. */
  19882. this.trigger('seeked');
  19883. }
  19884. /**
  19885. * Retrigger the `pause` event that was triggered by the {@link Tech}.
  19886. *
  19887. * @fires Player#pause
  19888. * @listens Tech#pause
  19889. * @private
  19890. */
  19891. handleTechPause_() {
  19892. this.removeClass('vjs-playing');
  19893. this.addClass('vjs-paused');
  19894. /**
  19895. * Fired whenever the media has been paused
  19896. *
  19897. * @event Player#pause
  19898. * @type {Event}
  19899. */
  19900. this.trigger('pause');
  19901. }
  19902. /**
  19903. * Retrigger the `ended` event that was triggered by the {@link Tech}.
  19904. *
  19905. * @fires Player#ended
  19906. * @listens Tech#ended
  19907. * @private
  19908. */
  19909. handleTechEnded_() {
  19910. this.addClass('vjs-ended');
  19911. this.removeClass('vjs-waiting');
  19912. if (this.options_.loop) {
  19913. this.currentTime(0);
  19914. this.play();
  19915. } else if (!this.paused()) {
  19916. this.pause();
  19917. }
  19918. /**
  19919. * Fired when the end of the media resource is reached (currentTime == duration)
  19920. *
  19921. * @event Player#ended
  19922. * @type {Event}
  19923. */
  19924. this.trigger('ended');
  19925. }
  19926. /**
  19927. * Fired when the duration of the media resource is first known or changed
  19928. *
  19929. * @listens Tech#durationchange
  19930. * @private
  19931. */
  19932. handleTechDurationChange_() {
  19933. this.duration(this.techGet_('duration'));
  19934. }
  19935. /**
  19936. * Handle a click on the media element to play/pause
  19937. *
  19938. * @param {Event} event
  19939. * the event that caused this function to trigger
  19940. *
  19941. * @listens Tech#click
  19942. * @private
  19943. */
  19944. handleTechClick_(event) {
  19945. // When controls are disabled a click should not toggle playback because
  19946. // the click is considered a control
  19947. if (!this.controls_) {
  19948. return;
  19949. }
  19950. if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
  19951. if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
  19952. this.options_.userActions.click.call(this, event);
  19953. } else if (this.paused()) {
  19954. silencePromise(this.play());
  19955. } else {
  19956. this.pause();
  19957. }
  19958. }
  19959. }
  19960. /**
  19961. * Handle a double-click on the media element to enter/exit fullscreen
  19962. *
  19963. * @param {Event} event
  19964. * the event that caused this function to trigger
  19965. *
  19966. * @listens Tech#dblclick
  19967. * @private
  19968. */
  19969. handleTechDoubleClick_(event) {
  19970. if (!this.controls_) {
  19971. return;
  19972. }
  19973. // we do not want to toggle fullscreen state
  19974. // when double-clicking inside a control bar or a modal
  19975. const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
  19976. if (!inAllowedEls) {
  19977. /*
  19978. * options.userActions.doubleClick
  19979. *
  19980. * If `undefined` or `true`, double-click toggles fullscreen if controls are present
  19981. * Set to `false` to disable double-click handling
  19982. * Set to a function to substitute an external double-click handler
  19983. */
  19984. if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
  19985. if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
  19986. this.options_.userActions.doubleClick.call(this, event);
  19987. } else if (this.isFullscreen()) {
  19988. this.exitFullscreen();
  19989. } else {
  19990. this.requestFullscreen();
  19991. }
  19992. }
  19993. }
  19994. }
  19995. /**
  19996. * Handle a tap on the media element. It will toggle the user
  19997. * activity state, which hides and shows the controls.
  19998. *
  19999. * @listens Tech#tap
  20000. * @private
  20001. */
  20002. handleTechTap_() {
  20003. this.userActive(!this.userActive());
  20004. }
  20005. /**
  20006. * Handle touch to start
  20007. *
  20008. * @listens Tech#touchstart
  20009. * @private
  20010. */
  20011. handleTechTouchStart_() {
  20012. this.userWasActive = this.userActive();
  20013. }
  20014. /**
  20015. * Handle touch to move
  20016. *
  20017. * @listens Tech#touchmove
  20018. * @private
  20019. */
  20020. handleTechTouchMove_() {
  20021. if (this.userWasActive) {
  20022. this.reportUserActivity();
  20023. }
  20024. }
  20025. /**
  20026. * Handle touch to end
  20027. *
  20028. * @param {Event} event
  20029. * the touchend event that triggered
  20030. * this function
  20031. *
  20032. * @listens Tech#touchend
  20033. * @private
  20034. */
  20035. handleTechTouchEnd_(event) {
  20036. // Stop the mouse events from also happening
  20037. if (event.cancelable) {
  20038. event.preventDefault();
  20039. }
  20040. }
  20041. /**
  20042. * @private
  20043. */
  20044. toggleFullscreenClass_() {
  20045. if (this.isFullscreen()) {
  20046. this.addClass('vjs-fullscreen');
  20047. } else {
  20048. this.removeClass('vjs-fullscreen');
  20049. }
  20050. }
  20051. /**
  20052. * when the document fschange event triggers it calls this
  20053. */
  20054. documentFullscreenChange_(e) {
  20055. const targetPlayer = e.target.player;
  20056. // if another player was fullscreen
  20057. // do a null check for targetPlayer because older firefox's would put document as e.target
  20058. if (targetPlayer && targetPlayer !== this) {
  20059. return;
  20060. }
  20061. const el = this.el();
  20062. let isFs = document__default["default"][this.fsApi_.fullscreenElement] === el;
  20063. if (!isFs && el.matches) {
  20064. isFs = el.matches(':' + this.fsApi_.fullscreen);
  20065. } else if (!isFs && el.msMatchesSelector) {
  20066. isFs = el.msMatchesSelector(':' + this.fsApi_.fullscreen);
  20067. }
  20068. this.isFullscreen(isFs);
  20069. }
  20070. /**
  20071. * Handle Tech Fullscreen Change
  20072. *
  20073. * @param {Event} event
  20074. * the fullscreenchange event that triggered this function
  20075. *
  20076. * @param {Object} data
  20077. * the data that was sent with the event
  20078. *
  20079. * @private
  20080. * @listens Tech#fullscreenchange
  20081. * @fires Player#fullscreenchange
  20082. */
  20083. handleTechFullscreenChange_(event, data) {
  20084. if (data) {
  20085. if (data.nativeIOSFullscreen) {
  20086. this.addClass('vjs-ios-native-fs');
  20087. this.tech_.one('webkitendfullscreen', () => {
  20088. this.removeClass('vjs-ios-native-fs');
  20089. });
  20090. }
  20091. this.isFullscreen(data.isFullscreen);
  20092. }
  20093. }
  20094. handleTechFullscreenError_(event, err) {
  20095. this.trigger('fullscreenerror', err);
  20096. }
  20097. /**
  20098. * @private
  20099. */
  20100. togglePictureInPictureClass_() {
  20101. if (this.isInPictureInPicture()) {
  20102. this.addClass('vjs-picture-in-picture');
  20103. } else {
  20104. this.removeClass('vjs-picture-in-picture');
  20105. }
  20106. }
  20107. /**
  20108. * Handle Tech Enter Picture-in-Picture.
  20109. *
  20110. * @param {Event} event
  20111. * the enterpictureinpicture event that triggered this function
  20112. *
  20113. * @private
  20114. * @listens Tech#enterpictureinpicture
  20115. */
  20116. handleTechEnterPictureInPicture_(event) {
  20117. this.isInPictureInPicture(true);
  20118. }
  20119. /**
  20120. * Handle Tech Leave Picture-in-Picture.
  20121. *
  20122. * @param {Event} event
  20123. * the leavepictureinpicture event that triggered this function
  20124. *
  20125. * @private
  20126. * @listens Tech#leavepictureinpicture
  20127. */
  20128. handleTechLeavePictureInPicture_(event) {
  20129. this.isInPictureInPicture(false);
  20130. }
  20131. /**
  20132. * Fires when an error occurred during the loading of an audio/video.
  20133. *
  20134. * @private
  20135. * @listens Tech#error
  20136. */
  20137. handleTechError_() {
  20138. const error = this.tech_.error();
  20139. this.error(error);
  20140. }
  20141. /**
  20142. * Retrigger the `textdata` event that was triggered by the {@link Tech}.
  20143. *
  20144. * @fires Player#textdata
  20145. * @listens Tech#textdata
  20146. * @private
  20147. */
  20148. handleTechTextData_() {
  20149. let data = null;
  20150. if (arguments.length > 1) {
  20151. data = arguments[1];
  20152. }
  20153. /**
  20154. * Fires when we get a textdata event from tech
  20155. *
  20156. * @event Player#textdata
  20157. * @type {Event}
  20158. */
  20159. this.trigger('textdata', data);
  20160. }
  20161. /**
  20162. * Get object for cached values.
  20163. *
  20164. * @return {Object}
  20165. * get the current object cache
  20166. */
  20167. getCache() {
  20168. return this.cache_;
  20169. }
  20170. /**
  20171. * Resets the internal cache object.
  20172. *
  20173. * Using this function outside the player constructor or reset method may
  20174. * have unintended side-effects.
  20175. *
  20176. * @private
  20177. */
  20178. resetCache_() {
  20179. this.cache_ = {
  20180. // Right now, the currentTime is not _really_ cached because it is always
  20181. // retrieved from the tech (see: currentTime). However, for completeness,
  20182. // we set it to zero here to ensure that if we do start actually caching
  20183. // it, we reset it along with everything else.
  20184. currentTime: 0,
  20185. initTime: 0,
  20186. inactivityTimeout: this.options_.inactivityTimeout,
  20187. duration: NaN,
  20188. lastVolume: 1,
  20189. lastPlaybackRate: this.defaultPlaybackRate(),
  20190. media: null,
  20191. src: '',
  20192. source: {},
  20193. sources: [],
  20194. playbackRates: [],
  20195. volume: 1
  20196. };
  20197. }
  20198. /**
  20199. * Pass values to the playback tech
  20200. *
  20201. * @param {string} [method]
  20202. * the method to call
  20203. *
  20204. * @param {Object} arg
  20205. * the argument to pass
  20206. *
  20207. * @private
  20208. */
  20209. techCall_(method, arg) {
  20210. // If it's not ready yet, call method when it is
  20211. this.ready(function () {
  20212. if (method in allowedSetters) {
  20213. return set(this.middleware_, this.tech_, method, arg);
  20214. } else if (method in allowedMediators) {
  20215. return mediate(this.middleware_, this.tech_, method, arg);
  20216. }
  20217. try {
  20218. if (this.tech_) {
  20219. this.tech_[method](arg);
  20220. }
  20221. } catch (e) {
  20222. log(e);
  20223. throw e;
  20224. }
  20225. }, true);
  20226. }
  20227. /**
  20228. * Mediate attempt to call playback tech method
  20229. * and return the value of the method called.
  20230. *
  20231. * @param {string} method
  20232. * Tech method
  20233. *
  20234. * @return {*}
  20235. * Value returned by the tech method called, undefined if tech
  20236. * is not ready or tech method is not present
  20237. *
  20238. * @private
  20239. */
  20240. techGet_(method) {
  20241. if (!this.tech_ || !this.tech_.isReady_) {
  20242. return;
  20243. }
  20244. if (method in allowedGetters) {
  20245. return get(this.middleware_, this.tech_, method);
  20246. } else if (method in allowedMediators) {
  20247. return mediate(this.middleware_, this.tech_, method);
  20248. }
  20249. // Log error when playback tech object is present but method
  20250. // is undefined or unavailable
  20251. try {
  20252. return this.tech_[method]();
  20253. } catch (e) {
  20254. // When building additional tech libs, an expected method may not be defined yet
  20255. if (this.tech_[method] === undefined) {
  20256. log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
  20257. throw e;
  20258. }
  20259. // When a method isn't available on the object it throws a TypeError
  20260. if (e.name === 'TypeError') {
  20261. log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
  20262. this.tech_.isReady_ = false;
  20263. throw e;
  20264. }
  20265. // If error unknown, just log and throw
  20266. log(e);
  20267. throw e;
  20268. }
  20269. }
  20270. /**
  20271. * Attempt to begin playback at the first opportunity.
  20272. *
  20273. * @return {Promise|undefined}
  20274. * Returns a promise if the browser supports Promises (or one
  20275. * was passed in as an option). This promise will be resolved on
  20276. * the return value of play. If this is undefined it will fulfill the
  20277. * promise chain otherwise the promise chain will be fulfilled when
  20278. * the promise from play is fulfilled.
  20279. */
  20280. play() {
  20281. return new Promise(resolve => {
  20282. this.play_(resolve);
  20283. });
  20284. }
  20285. /**
  20286. * The actual logic for play, takes a callback that will be resolved on the
  20287. * return value of play. This allows us to resolve to the play promise if there
  20288. * is one on modern browsers.
  20289. *
  20290. * @private
  20291. * @param {Function} [callback]
  20292. * The callback that should be called when the techs play is actually called
  20293. */
  20294. play_(callback = silencePromise) {
  20295. this.playCallbacks_.push(callback);
  20296. const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
  20297. const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
  20298. // treat calls to play_ somewhat like the `one` event function
  20299. if (this.waitToPlay_) {
  20300. this.off(['ready', 'loadstart'], this.waitToPlay_);
  20301. this.waitToPlay_ = null;
  20302. }
  20303. // if the player/tech is not ready or the src itself is not ready
  20304. // queue up a call to play on `ready` or `loadstart`
  20305. if (!this.isReady_ || !isSrcReady) {
  20306. this.waitToPlay_ = e => {
  20307. this.play_();
  20308. };
  20309. this.one(['ready', 'loadstart'], this.waitToPlay_);
  20310. // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
  20311. // in that case, we need to prime the video element by calling load so it'll be ready in time
  20312. if (!isSrcReady && isSafariOrIOS) {
  20313. this.load();
  20314. }
  20315. return;
  20316. }
  20317. // If the player/tech is ready and we have a source, we can attempt playback.
  20318. const val = this.techGet_('play');
  20319. // For native playback, reset the progress bar if we get a play call from a replay.
  20320. const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
  20321. if (isNativeReplay) {
  20322. this.resetProgressBar_();
  20323. }
  20324. // play was terminated if the returned value is null
  20325. if (val === null) {
  20326. this.runPlayTerminatedQueue_();
  20327. } else {
  20328. this.runPlayCallbacks_(val);
  20329. }
  20330. }
  20331. /**
  20332. * These functions will be run when if play is terminated. If play
  20333. * runPlayCallbacks_ is run these function will not be run. This allows us
  20334. * to differentiate between a terminated play and an actual call to play.
  20335. */
  20336. runPlayTerminatedQueue_() {
  20337. const queue = this.playTerminatedQueue_.slice(0);
  20338. this.playTerminatedQueue_ = [];
  20339. queue.forEach(function (q) {
  20340. q();
  20341. });
  20342. }
  20343. /**
  20344. * When a callback to play is delayed we have to run these
  20345. * callbacks when play is actually called on the tech. This function
  20346. * runs the callbacks that were delayed and accepts the return value
  20347. * from the tech.
  20348. *
  20349. * @param {undefined|Promise} val
  20350. * The return value from the tech.
  20351. */
  20352. runPlayCallbacks_(val) {
  20353. const callbacks = this.playCallbacks_.slice(0);
  20354. this.playCallbacks_ = [];
  20355. // clear play terminatedQueue since we finished a real play
  20356. this.playTerminatedQueue_ = [];
  20357. callbacks.forEach(function (cb) {
  20358. cb(val);
  20359. });
  20360. }
  20361. /**
  20362. * Pause the video playback
  20363. *
  20364. * @return {Player}
  20365. * A reference to the player object this function was called on
  20366. */
  20367. pause() {
  20368. this.techCall_('pause');
  20369. }
  20370. /**
  20371. * Check if the player is paused or has yet to play
  20372. *
  20373. * @return {boolean}
  20374. * - false: if the media is currently playing
  20375. * - true: if media is not currently playing
  20376. */
  20377. paused() {
  20378. // The initial state of paused should be true (in Safari it's actually false)
  20379. return this.techGet_('paused') === false ? false : true;
  20380. }
  20381. /**
  20382. * Get a TimeRange object representing the current ranges of time that the user
  20383. * has played.
  20384. *
  20385. * @return { import('./utils/time').TimeRange }
  20386. * A time range object that represents all the increments of time that have
  20387. * been played.
  20388. */
  20389. played() {
  20390. return this.techGet_('played') || createTimeRanges(0, 0);
  20391. }
  20392. /**
  20393. * Returns whether or not the user is "scrubbing". Scrubbing is
  20394. * when the user has clicked the progress bar handle and is
  20395. * dragging it along the progress bar.
  20396. *
  20397. * @param {boolean} [isScrubbing]
  20398. * whether the user is or is not scrubbing
  20399. *
  20400. * @return {boolean}
  20401. * The value of scrubbing when getting
  20402. */
  20403. scrubbing(isScrubbing) {
  20404. if (typeof isScrubbing === 'undefined') {
  20405. return this.scrubbing_;
  20406. }
  20407. this.scrubbing_ = !!isScrubbing;
  20408. this.techCall_('setScrubbing', this.scrubbing_);
  20409. if (isScrubbing) {
  20410. this.addClass('vjs-scrubbing');
  20411. } else {
  20412. this.removeClass('vjs-scrubbing');
  20413. }
  20414. }
  20415. /**
  20416. * Get or set the current time (in seconds)
  20417. *
  20418. * @param {number|string} [seconds]
  20419. * The time to seek to in seconds
  20420. *
  20421. * @return {number}
  20422. * - the current time in seconds when getting
  20423. */
  20424. currentTime(seconds) {
  20425. if (typeof seconds !== 'undefined') {
  20426. if (seconds < 0) {
  20427. seconds = 0;
  20428. }
  20429. if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
  20430. this.cache_.initTime = seconds;
  20431. this.off('canplay', this.boundApplyInitTime_);
  20432. this.one('canplay', this.boundApplyInitTime_);
  20433. return;
  20434. }
  20435. this.techCall_('setCurrentTime', seconds);
  20436. this.cache_.initTime = 0;
  20437. return;
  20438. }
  20439. // cache last currentTime and return. default to 0 seconds
  20440. //
  20441. // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
  20442. // currentTime when scrubbing, but may not provide much performance benefit after all.
  20443. // Should be tested. Also something has to read the actual current time or the cache will
  20444. // never get updated.
  20445. this.cache_.currentTime = this.techGet_('currentTime') || 0;
  20446. return this.cache_.currentTime;
  20447. }
  20448. /**
  20449. * Apply the value of initTime stored in cache as currentTime.
  20450. *
  20451. * @private
  20452. */
  20453. applyInitTime_() {
  20454. this.currentTime(this.cache_.initTime);
  20455. }
  20456. /**
  20457. * Normally gets the length in time of the video in seconds;
  20458. * in all but the rarest use cases an argument will NOT be passed to the method
  20459. *
  20460. * > **NOTE**: The video must have started loading before the duration can be
  20461. * known, and depending on preload behaviour may not be known until the video starts
  20462. * playing.
  20463. *
  20464. * @fires Player#durationchange
  20465. *
  20466. * @param {number} [seconds]
  20467. * The duration of the video to set in seconds
  20468. *
  20469. * @return {number}
  20470. * - The duration of the video in seconds when getting
  20471. */
  20472. duration(seconds) {
  20473. if (seconds === undefined) {
  20474. // return NaN if the duration is not known
  20475. return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
  20476. }
  20477. seconds = parseFloat(seconds);
  20478. // Standardize on Infinity for signaling video is live
  20479. if (seconds < 0) {
  20480. seconds = Infinity;
  20481. }
  20482. if (seconds !== this.cache_.duration) {
  20483. // Cache the last set value for optimized scrubbing
  20484. this.cache_.duration = seconds;
  20485. if (seconds === Infinity) {
  20486. this.addClass('vjs-live');
  20487. } else {
  20488. this.removeClass('vjs-live');
  20489. }
  20490. if (!isNaN(seconds)) {
  20491. // Do not fire durationchange unless the duration value is known.
  20492. // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
  20493. /**
  20494. * @event Player#durationchange
  20495. * @type {Event}
  20496. */
  20497. this.trigger('durationchange');
  20498. }
  20499. }
  20500. }
  20501. /**
  20502. * Calculates how much time is left in the video. Not part
  20503. * of the native video API.
  20504. *
  20505. * @return {number}
  20506. * The time remaining in seconds
  20507. */
  20508. remainingTime() {
  20509. return this.duration() - this.currentTime();
  20510. }
  20511. /**
  20512. * A remaining time function that is intended to be used when
  20513. * the time is to be displayed directly to the user.
  20514. *
  20515. * @return {number}
  20516. * The rounded time remaining in seconds
  20517. */
  20518. remainingTimeDisplay() {
  20519. return Math.floor(this.duration()) - Math.floor(this.currentTime());
  20520. }
  20521. //
  20522. // Kind of like an array of portions of the video that have been downloaded.
  20523. /**
  20524. * Get a TimeRange object with an array of the times of the video
  20525. * that have been downloaded. If you just want the percent of the
  20526. * video that's been downloaded, use bufferedPercent.
  20527. *
  20528. * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
  20529. *
  20530. * @return { import('./utils/time').TimeRange }
  20531. * A mock {@link TimeRanges} object (following HTML spec)
  20532. */
  20533. buffered() {
  20534. let buffered = this.techGet_('buffered');
  20535. if (!buffered || !buffered.length) {
  20536. buffered = createTimeRanges(0, 0);
  20537. }
  20538. return buffered;
  20539. }
  20540. /**
  20541. * Get the percent (as a decimal) of the video that's been downloaded.
  20542. * This method is not a part of the native HTML video API.
  20543. *
  20544. * @return {number}
  20545. * A decimal between 0 and 1 representing the percent
  20546. * that is buffered 0 being 0% and 1 being 100%
  20547. */
  20548. bufferedPercent() {
  20549. return bufferedPercent(this.buffered(), this.duration());
  20550. }
  20551. /**
  20552. * Get the ending time of the last buffered time range
  20553. * This is used in the progress bar to encapsulate all time ranges.
  20554. *
  20555. * @return {number}
  20556. * The end of the last buffered time range
  20557. */
  20558. bufferedEnd() {
  20559. const buffered = this.buffered();
  20560. const duration = this.duration();
  20561. let end = buffered.end(buffered.length - 1);
  20562. if (end > duration) {
  20563. end = duration;
  20564. }
  20565. return end;
  20566. }
  20567. /**
  20568. * Get or set the current volume of the media
  20569. *
  20570. * @param {number} [percentAsDecimal]
  20571. * The new volume as a decimal percent:
  20572. * - 0 is muted/0%/off
  20573. * - 1.0 is 100%/full
  20574. * - 0.5 is half volume or 50%
  20575. *
  20576. * @return {number}
  20577. * The current volume as a percent when getting
  20578. */
  20579. volume(percentAsDecimal) {
  20580. let vol;
  20581. if (percentAsDecimal !== undefined) {
  20582. // Force value to between 0 and 1
  20583. vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal)));
  20584. this.cache_.volume = vol;
  20585. this.techCall_('setVolume', vol);
  20586. if (vol > 0) {
  20587. this.lastVolume_(vol);
  20588. }
  20589. return;
  20590. }
  20591. // Default to 1 when returning current volume.
  20592. vol = parseFloat(this.techGet_('volume'));
  20593. return isNaN(vol) ? 1 : vol;
  20594. }
  20595. /**
  20596. * Get the current muted state, or turn mute on or off
  20597. *
  20598. * @param {boolean} [muted]
  20599. * - true to mute
  20600. * - false to unmute
  20601. *
  20602. * @return {boolean}
  20603. * - true if mute is on and getting
  20604. * - false if mute is off and getting
  20605. */
  20606. muted(muted) {
  20607. if (muted !== undefined) {
  20608. this.techCall_('setMuted', muted);
  20609. return;
  20610. }
  20611. return this.techGet_('muted') || false;
  20612. }
  20613. /**
  20614. * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
  20615. * indicates the state of muted on initial playback.
  20616. *
  20617. * ```js
  20618. * var myPlayer = videojs('some-player-id');
  20619. *
  20620. * myPlayer.src("http://www.example.com/path/to/video.mp4");
  20621. *
  20622. * // get, should be false
  20623. * console.log(myPlayer.defaultMuted());
  20624. * // set to true
  20625. * myPlayer.defaultMuted(true);
  20626. * // get should be true
  20627. * console.log(myPlayer.defaultMuted());
  20628. * ```
  20629. *
  20630. * @param {boolean} [defaultMuted]
  20631. * - true to mute
  20632. * - false to unmute
  20633. *
  20634. * @return {boolean|Player}
  20635. * - true if defaultMuted is on and getting
  20636. * - false if defaultMuted is off and getting
  20637. * - A reference to the current player when setting
  20638. */
  20639. defaultMuted(defaultMuted) {
  20640. if (defaultMuted !== undefined) {
  20641. return this.techCall_('setDefaultMuted', defaultMuted);
  20642. }
  20643. return this.techGet_('defaultMuted') || false;
  20644. }
  20645. /**
  20646. * Get the last volume, or set it
  20647. *
  20648. * @param {number} [percentAsDecimal]
  20649. * The new last volume as a decimal percent:
  20650. * - 0 is muted/0%/off
  20651. * - 1.0 is 100%/full
  20652. * - 0.5 is half volume or 50%
  20653. *
  20654. * @return {number}
  20655. * the current value of lastVolume as a percent when getting
  20656. *
  20657. * @private
  20658. */
  20659. lastVolume_(percentAsDecimal) {
  20660. if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
  20661. this.cache_.lastVolume = percentAsDecimal;
  20662. return;
  20663. }
  20664. return this.cache_.lastVolume;
  20665. }
  20666. /**
  20667. * Check if current tech can support native fullscreen
  20668. * (e.g. with built in controls like iOS)
  20669. *
  20670. * @return {boolean}
  20671. * if native fullscreen is supported
  20672. */
  20673. supportsFullScreen() {
  20674. return this.techGet_('supportsFullScreen') || false;
  20675. }
  20676. /**
  20677. * Check if the player is in fullscreen mode or tell the player that it
  20678. * is or is not in fullscreen mode.
  20679. *
  20680. * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
  20681. * property and instead document.fullscreenElement is used. But isFullscreen is
  20682. * still a valuable property for internal player workings.
  20683. *
  20684. * @param {boolean} [isFS]
  20685. * Set the players current fullscreen state
  20686. *
  20687. * @return {boolean}
  20688. * - true if fullscreen is on and getting
  20689. * - false if fullscreen is off and getting
  20690. */
  20691. isFullscreen(isFS) {
  20692. if (isFS !== undefined) {
  20693. const oldValue = this.isFullscreen_;
  20694. this.isFullscreen_ = Boolean(isFS);
  20695. // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
  20696. // this is the only place where we trigger fullscreenchange events for older browsers
  20697. // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
  20698. if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
  20699. /**
  20700. * @event Player#fullscreenchange
  20701. * @type {Event}
  20702. */
  20703. this.trigger('fullscreenchange');
  20704. }
  20705. this.toggleFullscreenClass_();
  20706. return;
  20707. }
  20708. return this.isFullscreen_;
  20709. }
  20710. /**
  20711. * Increase the size of the video to full screen
  20712. * In some browsers, full screen is not supported natively, so it enters
  20713. * "full window mode", where the video fills the browser window.
  20714. * In browsers and devices that support native full screen, sometimes the
  20715. * browser's default controls will be shown, and not the Video.js custom skin.
  20716. * This includes most mobile devices (iOS, Android) and older versions of
  20717. * Safari.
  20718. *
  20719. * @param {Object} [fullscreenOptions]
  20720. * Override the player fullscreen options
  20721. *
  20722. * @fires Player#fullscreenchange
  20723. */
  20724. requestFullscreen(fullscreenOptions) {
  20725. if (this.isInPictureInPicture()) {
  20726. this.exitPictureInPicture();
  20727. }
  20728. const self = this;
  20729. return new Promise((resolve, reject) => {
  20730. function offHandler() {
  20731. self.off('fullscreenerror', errorHandler);
  20732. self.off('fullscreenchange', changeHandler);
  20733. }
  20734. function changeHandler() {
  20735. offHandler();
  20736. resolve();
  20737. }
  20738. function errorHandler(e, err) {
  20739. offHandler();
  20740. reject(err);
  20741. }
  20742. self.one('fullscreenchange', changeHandler);
  20743. self.one('fullscreenerror', errorHandler);
  20744. const promise = self.requestFullscreenHelper_(fullscreenOptions);
  20745. if (promise) {
  20746. promise.then(offHandler, offHandler);
  20747. promise.then(resolve, reject);
  20748. }
  20749. });
  20750. }
  20751. requestFullscreenHelper_(fullscreenOptions) {
  20752. let fsOptions;
  20753. // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
  20754. // Use defaults or player configured option unless passed directly to this method.
  20755. if (!this.fsApi_.prefixed) {
  20756. fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
  20757. if (fullscreenOptions !== undefined) {
  20758. fsOptions = fullscreenOptions;
  20759. }
  20760. }
  20761. // This method works as follows:
  20762. // 1. if a fullscreen api is available, use it
  20763. // 1. call requestFullscreen with potential options
  20764. // 2. if we got a promise from above, use it to update isFullscreen()
  20765. // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
  20766. // This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
  20767. // 3. otherwise, use "fullWindow" mode
  20768. if (this.fsApi_.requestFullscreen) {
  20769. const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
  20770. // Even on browsers with promise support this may not return a promise
  20771. if (promise) {
  20772. promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
  20773. }
  20774. return promise;
  20775. } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
  20776. // we can't take the video.js controls fullscreen but we can go fullscreen
  20777. // with native controls
  20778. this.techCall_('enterFullScreen');
  20779. } else {
  20780. // fullscreen isn't supported so we'll just stretch the video element to
  20781. // fill the viewport
  20782. this.enterFullWindow();
  20783. }
  20784. }
  20785. /**
  20786. * Return the video to its normal size after having been in full screen mode
  20787. *
  20788. * @fires Player#fullscreenchange
  20789. */
  20790. exitFullscreen() {
  20791. const self = this;
  20792. return new Promise((resolve, reject) => {
  20793. function offHandler() {
  20794. self.off('fullscreenerror', errorHandler);
  20795. self.off('fullscreenchange', changeHandler);
  20796. }
  20797. function changeHandler() {
  20798. offHandler();
  20799. resolve();
  20800. }
  20801. function errorHandler(e, err) {
  20802. offHandler();
  20803. reject(err);
  20804. }
  20805. self.one('fullscreenchange', changeHandler);
  20806. self.one('fullscreenerror', errorHandler);
  20807. const promise = self.exitFullscreenHelper_();
  20808. if (promise) {
  20809. promise.then(offHandler, offHandler);
  20810. // map the promise to our resolve/reject methods
  20811. promise.then(resolve, reject);
  20812. }
  20813. });
  20814. }
  20815. exitFullscreenHelper_() {
  20816. if (this.fsApi_.requestFullscreen) {
  20817. const promise = document__default["default"][this.fsApi_.exitFullscreen]();
  20818. // Even on browsers with promise support this may not return a promise
  20819. if (promise) {
  20820. // we're splitting the promise here, so, we want to catch the
  20821. // potential error so that this chain doesn't have unhandled errors
  20822. silencePromise(promise.then(() => this.isFullscreen(false)));
  20823. }
  20824. return promise;
  20825. } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
  20826. this.techCall_('exitFullScreen');
  20827. } else {
  20828. this.exitFullWindow();
  20829. }
  20830. }
  20831. /**
  20832. * When fullscreen isn't supported we can stretch the
  20833. * video container to as wide as the browser will let us.
  20834. *
  20835. * @fires Player#enterFullWindow
  20836. */
  20837. enterFullWindow() {
  20838. this.isFullscreen(true);
  20839. this.isFullWindow = true;
  20840. // Storing original doc overflow value to return to when fullscreen is off
  20841. this.docOrigOverflow = document__default["default"].documentElement.style.overflow;
  20842. // Add listener for esc key to exit fullscreen
  20843. on(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
  20844. // Hide any scroll bars
  20845. document__default["default"].documentElement.style.overflow = 'hidden';
  20846. // Apply fullscreen styles
  20847. addClass(document__default["default"].body, 'vjs-full-window');
  20848. /**
  20849. * @event Player#enterFullWindow
  20850. * @type {Event}
  20851. */
  20852. this.trigger('enterFullWindow');
  20853. }
  20854. /**
  20855. * Check for call to either exit full window or
  20856. * full screen on ESC key
  20857. *
  20858. * @param {string} event
  20859. * Event to check for key press
  20860. */
  20861. fullWindowOnEscKey(event) {
  20862. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  20863. if (this.isFullscreen() === true) {
  20864. if (!this.isFullWindow) {
  20865. this.exitFullscreen();
  20866. } else {
  20867. this.exitFullWindow();
  20868. }
  20869. }
  20870. }
  20871. }
  20872. /**
  20873. * Exit full window
  20874. *
  20875. * @fires Player#exitFullWindow
  20876. */
  20877. exitFullWindow() {
  20878. this.isFullscreen(false);
  20879. this.isFullWindow = false;
  20880. off(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
  20881. // Unhide scroll bars.
  20882. document__default["default"].documentElement.style.overflow = this.docOrigOverflow;
  20883. // Remove fullscreen styles
  20884. removeClass(document__default["default"].body, 'vjs-full-window');
  20885. // Resize the box, controller, and poster to original sizes
  20886. // this.positionAll();
  20887. /**
  20888. * @event Player#exitFullWindow
  20889. * @type {Event}
  20890. */
  20891. this.trigger('exitFullWindow');
  20892. }
  20893. /**
  20894. * Disable Picture-in-Picture mode.
  20895. *
  20896. * @param {boolean} value
  20897. * - true will disable Picture-in-Picture mode
  20898. * - false will enable Picture-in-Picture mode
  20899. */
  20900. disablePictureInPicture(value) {
  20901. if (value === undefined) {
  20902. return this.techGet_('disablePictureInPicture');
  20903. }
  20904. this.techCall_('setDisablePictureInPicture', value);
  20905. this.options_.disablePictureInPicture = value;
  20906. this.trigger('disablepictureinpicturechanged');
  20907. }
  20908. /**
  20909. * Check if the player is in Picture-in-Picture mode or tell the player that it
  20910. * is or is not in Picture-in-Picture mode.
  20911. *
  20912. * @param {boolean} [isPiP]
  20913. * Set the players current Picture-in-Picture state
  20914. *
  20915. * @return {boolean}
  20916. * - true if Picture-in-Picture is on and getting
  20917. * - false if Picture-in-Picture is off and getting
  20918. */
  20919. isInPictureInPicture(isPiP) {
  20920. if (isPiP !== undefined) {
  20921. this.isInPictureInPicture_ = !!isPiP;
  20922. this.togglePictureInPictureClass_();
  20923. return;
  20924. }
  20925. return !!this.isInPictureInPicture_;
  20926. }
  20927. /**
  20928. * Create a floating video window always on top of other windows so that users may
  20929. * continue consuming media while they interact with other content sites, or
  20930. * applications on their device.
  20931. *
  20932. * This can use document picture-in-picture or element picture in picture
  20933. *
  20934. * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
  20935. * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
  20936. *
  20937. *
  20938. * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
  20939. * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
  20940. *
  20941. * @fires Player#enterpictureinpicture
  20942. *
  20943. * @return {Promise}
  20944. * A promise with a Picture-in-Picture window.
  20945. */
  20946. requestPictureInPicture() {
  20947. if (this.options_.enableDocumentPictureInPicture && window__default["default"].documentPictureInPicture) {
  20948. const pipContainer = document__default["default"].createElement(this.el().tagName);
  20949. pipContainer.classList = this.el().classList;
  20950. pipContainer.classList.add('vjs-pip-container');
  20951. if (this.posterImage) {
  20952. pipContainer.appendChild(this.posterImage.el().cloneNode(true));
  20953. }
  20954. if (this.titleBar) {
  20955. pipContainer.appendChild(this.titleBar.el().cloneNode(true));
  20956. }
  20957. pipContainer.appendChild(createEl('p', {
  20958. className: 'vjs-pip-text'
  20959. }, {}, this.localize('Playing in picture-in-picture')));
  20960. return window__default["default"].documentPictureInPicture.requestWindow({
  20961. // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
  20962. initialAspectRatio: this.videoWidth() / this.videoHeight(),
  20963. copyStyleSheets: true
  20964. }).then(pipWindow => {
  20965. this.el_.parentNode.insertBefore(pipContainer, this.el_);
  20966. pipWindow.document.body.append(this.el_);
  20967. pipWindow.document.body.classList.add('vjs-pip-window');
  20968. this.player_.isInPictureInPicture(true);
  20969. this.player_.trigger('enterpictureinpicture');
  20970. // Listen for the PiP closing event to move the video back.
  20971. pipWindow.addEventListener('unload', event => {
  20972. const pipVideo = event.target.querySelector('.video-js');
  20973. pipContainer.replaceWith(pipVideo);
  20974. this.player_.isInPictureInPicture(false);
  20975. this.player_.trigger('leavepictureinpicture');
  20976. });
  20977. return pipWindow;
  20978. });
  20979. }
  20980. if ('pictureInPictureEnabled' in document__default["default"] && this.disablePictureInPicture() === false) {
  20981. /**
  20982. * This event fires when the player enters picture in picture mode
  20983. *
  20984. * @event Player#enterpictureinpicture
  20985. * @type {Event}
  20986. */
  20987. return this.techGet_('requestPictureInPicture');
  20988. }
  20989. return Promise.reject('No PiP mode is available');
  20990. }
  20991. /**
  20992. * Exit Picture-in-Picture mode.
  20993. *
  20994. * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
  20995. *
  20996. * @fires Player#leavepictureinpicture
  20997. *
  20998. * @return {Promise}
  20999. * A promise.
  21000. */
  21001. exitPictureInPicture() {
  21002. if (window__default["default"].documentPictureInPicture && window__default["default"].documentPictureInPicture.window) {
  21003. // With documentPictureInPicture, Player#leavepictureinpicture is fired in the unload handler
  21004. window__default["default"].documentPictureInPicture.window.close();
  21005. return Promise.resolve();
  21006. }
  21007. if ('pictureInPictureEnabled' in document__default["default"]) {
  21008. /**
  21009. * This event fires when the player leaves picture in picture mode
  21010. *
  21011. * @event Player#leavepictureinpicture
  21012. * @type {Event}
  21013. */
  21014. return document__default["default"].exitPictureInPicture();
  21015. }
  21016. }
  21017. /**
  21018. * Called when this Player has focus and a key gets pressed down, or when
  21019. * any Component of this player receives a key press that it doesn't handle.
  21020. * This allows player-wide hotkeys (either as defined below, or optionally
  21021. * by an external function).
  21022. *
  21023. * @param {Event} event
  21024. * The `keydown` event that caused this function to be called.
  21025. *
  21026. * @listens keydown
  21027. */
  21028. handleKeyDown(event) {
  21029. const {
  21030. userActions
  21031. } = this.options_;
  21032. // Bail out if hotkeys are not configured.
  21033. if (!userActions || !userActions.hotkeys) {
  21034. return;
  21035. }
  21036. // Function that determines whether or not to exclude an element from
  21037. // hotkeys handling.
  21038. const excludeElement = el => {
  21039. const tagName = el.tagName.toLowerCase();
  21040. // The first and easiest test is for `contenteditable` elements.
  21041. if (el.isContentEditable) {
  21042. return true;
  21043. }
  21044. // Inputs matching these types will still trigger hotkey handling as
  21045. // they are not text inputs.
  21046. const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
  21047. if (tagName === 'input') {
  21048. return allowedInputTypes.indexOf(el.type) === -1;
  21049. }
  21050. // The final test is by tag name. These tags will be excluded entirely.
  21051. const excludedTags = ['textarea'];
  21052. return excludedTags.indexOf(tagName) !== -1;
  21053. };
  21054. // Bail out if the user is focused on an interactive form element.
  21055. if (excludeElement(this.el_.ownerDocument.activeElement)) {
  21056. return;
  21057. }
  21058. if (typeof userActions.hotkeys === 'function') {
  21059. userActions.hotkeys.call(this, event);
  21060. } else {
  21061. this.handleHotkeys(event);
  21062. }
  21063. }
  21064. /**
  21065. * Called when this Player receives a hotkey keydown event.
  21066. * Supported player-wide hotkeys are:
  21067. *
  21068. * f - toggle fullscreen
  21069. * m - toggle mute
  21070. * k or Space - toggle play/pause
  21071. *
  21072. * @param {Event} event
  21073. * The `keydown` event that caused this function to be called.
  21074. */
  21075. handleHotkeys(event) {
  21076. const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
  21077. // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
  21078. const {
  21079. fullscreenKey = keydownEvent => keycode__default["default"].isEventKey(keydownEvent, 'f'),
  21080. muteKey = keydownEvent => keycode__default["default"].isEventKey(keydownEvent, 'm'),
  21081. playPauseKey = keydownEvent => keycode__default["default"].isEventKey(keydownEvent, 'k') || keycode__default["default"].isEventKey(keydownEvent, 'Space')
  21082. } = hotkeys;
  21083. if (fullscreenKey.call(this, event)) {
  21084. event.preventDefault();
  21085. event.stopPropagation();
  21086. const FSToggle = Component.getComponent('FullscreenToggle');
  21087. if (document__default["default"][this.fsApi_.fullscreenEnabled] !== false) {
  21088. FSToggle.prototype.handleClick.call(this, event);
  21089. }
  21090. } else if (muteKey.call(this, event)) {
  21091. event.preventDefault();
  21092. event.stopPropagation();
  21093. const MuteToggle = Component.getComponent('MuteToggle');
  21094. MuteToggle.prototype.handleClick.call(this, event);
  21095. } else if (playPauseKey.call(this, event)) {
  21096. event.preventDefault();
  21097. event.stopPropagation();
  21098. const PlayToggle = Component.getComponent('PlayToggle');
  21099. PlayToggle.prototype.handleClick.call(this, event);
  21100. }
  21101. }
  21102. /**
  21103. * Check whether the player can play a given mimetype
  21104. *
  21105. * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
  21106. *
  21107. * @param {string} type
  21108. * The mimetype to check
  21109. *
  21110. * @return {string}
  21111. * 'probably', 'maybe', or '' (empty string)
  21112. */
  21113. canPlayType(type) {
  21114. let can;
  21115. // Loop through each playback technology in the options order
  21116. for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
  21117. const techName = j[i];
  21118. let tech = Tech.getTech(techName);
  21119. // Support old behavior of techs being registered as components.
  21120. // Remove once that deprecated behavior is removed.
  21121. if (!tech) {
  21122. tech = Component.getComponent(techName);
  21123. }
  21124. // Check if the current tech is defined before continuing
  21125. if (!tech) {
  21126. log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
  21127. continue;
  21128. }
  21129. // Check if the browser supports this technology
  21130. if (tech.isSupported()) {
  21131. can = tech.canPlayType(type);
  21132. if (can) {
  21133. return can;
  21134. }
  21135. }
  21136. }
  21137. return '';
  21138. }
  21139. /**
  21140. * Select source based on tech-order or source-order
  21141. * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
  21142. * defaults to tech-order selection
  21143. *
  21144. * @param {Array} sources
  21145. * The sources for a media asset
  21146. *
  21147. * @return {Object|boolean}
  21148. * Object of source and tech order or false
  21149. */
  21150. selectSource(sources) {
  21151. // Get only the techs specified in `techOrder` that exist and are supported by the
  21152. // current platform
  21153. const techs = this.options_.techOrder.map(techName => {
  21154. return [techName, Tech.getTech(techName)];
  21155. }).filter(([techName, tech]) => {
  21156. // Check if the current tech is defined before continuing
  21157. if (tech) {
  21158. // Check if the browser supports this technology
  21159. return tech.isSupported();
  21160. }
  21161. log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
  21162. return false;
  21163. });
  21164. // Iterate over each `innerArray` element once per `outerArray` element and execute
  21165. // `tester` with both. If `tester` returns a non-falsy value, exit early and return
  21166. // that value.
  21167. const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
  21168. let found;
  21169. outerArray.some(outerChoice => {
  21170. return innerArray.some(innerChoice => {
  21171. found = tester(outerChoice, innerChoice);
  21172. if (found) {
  21173. return true;
  21174. }
  21175. });
  21176. });
  21177. return found;
  21178. };
  21179. let foundSourceAndTech;
  21180. const flip = fn => (a, b) => fn(b, a);
  21181. const finder = ([techName, tech], source) => {
  21182. if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
  21183. return {
  21184. source,
  21185. tech: techName
  21186. };
  21187. }
  21188. };
  21189. // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
  21190. // to select from them based on their priority.
  21191. if (this.options_.sourceOrder) {
  21192. // Source-first ordering
  21193. foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
  21194. } else {
  21195. // Tech-first ordering
  21196. foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
  21197. }
  21198. return foundSourceAndTech || false;
  21199. }
  21200. /**
  21201. * Executes source setting and getting logic
  21202. *
  21203. * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
  21204. * A SourceObject, an array of SourceObjects, or a string referencing
  21205. * a URL to a media source. It is _highly recommended_ that an object
  21206. * or array of objects is used here, so that source selection
  21207. * algorithms can take the `type` into account.
  21208. *
  21209. * If not provided, this method acts as a getter.
  21210. * @param {boolean} isRetry
  21211. * Indicates whether this is being called internally as a result of a retry
  21212. *
  21213. * @return {string|undefined}
  21214. * If the `source` argument is missing, returns the current source
  21215. * URL. Otherwise, returns nothing/undefined.
  21216. */
  21217. handleSrc_(source, isRetry) {
  21218. // getter usage
  21219. if (typeof source === 'undefined') {
  21220. return this.cache_.src || '';
  21221. }
  21222. // Reset retry behavior for new source
  21223. if (this.resetRetryOnError_) {
  21224. this.resetRetryOnError_();
  21225. }
  21226. // filter out invalid sources and turn our source into
  21227. // an array of source objects
  21228. const sources = filterSource(source);
  21229. // if a source was passed in then it is invalid because
  21230. // it was filtered to a zero length Array. So we have to
  21231. // show an error
  21232. if (!sources.length) {
  21233. this.setTimeout(function () {
  21234. this.error({
  21235. code: 4,
  21236. message: this.options_.notSupportedMessage
  21237. });
  21238. }, 0);
  21239. return;
  21240. }
  21241. // initial sources
  21242. this.changingSrc_ = true;
  21243. // Only update the cached source list if we are not retrying a new source after error,
  21244. // since in that case we want to include the failed source(s) in the cache
  21245. if (!isRetry) {
  21246. this.cache_.sources = sources;
  21247. }
  21248. this.updateSourceCaches_(sources[0]);
  21249. // middlewareSource is the source after it has been changed by middleware
  21250. setSource(this, sources[0], (middlewareSource, mws) => {
  21251. this.middleware_ = mws;
  21252. // since sourceSet is async we have to update the cache again after we select a source since
  21253. // the source that is selected could be out of order from the cache update above this callback.
  21254. if (!isRetry) {
  21255. this.cache_.sources = sources;
  21256. }
  21257. this.updateSourceCaches_(middlewareSource);
  21258. const err = this.src_(middlewareSource);
  21259. if (err) {
  21260. if (sources.length > 1) {
  21261. return this.handleSrc_(sources.slice(1));
  21262. }
  21263. this.changingSrc_ = false;
  21264. // We need to wrap this in a timeout to give folks a chance to add error event handlers
  21265. this.setTimeout(function () {
  21266. this.error({
  21267. code: 4,
  21268. message: this.options_.notSupportedMessage
  21269. });
  21270. }, 0);
  21271. // we could not find an appropriate tech, but let's still notify the delegate that this is it
  21272. // this needs a better comment about why this is needed
  21273. this.triggerReady();
  21274. return;
  21275. }
  21276. setTech(mws, this.tech_);
  21277. });
  21278. // Try another available source if this one fails before playback.
  21279. if (sources.length > 1) {
  21280. const retry = () => {
  21281. // Remove the error modal
  21282. this.error(null);
  21283. this.handleSrc_(sources.slice(1), true);
  21284. };
  21285. const stopListeningForErrors = () => {
  21286. this.off('error', retry);
  21287. };
  21288. this.one('error', retry);
  21289. this.one('playing', stopListeningForErrors);
  21290. this.resetRetryOnError_ = () => {
  21291. this.off('error', retry);
  21292. this.off('playing', stopListeningForErrors);
  21293. };
  21294. }
  21295. }
  21296. /**
  21297. * Get or set the video source.
  21298. *
  21299. * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
  21300. * A SourceObject, an array of SourceObjects, or a string referencing
  21301. * a URL to a media source. It is _highly recommended_ that an object
  21302. * or array of objects is used here, so that source selection
  21303. * algorithms can take the `type` into account.
  21304. *
  21305. * If not provided, this method acts as a getter.
  21306. *
  21307. * @return {string|undefined}
  21308. * If the `source` argument is missing, returns the current source
  21309. * URL. Otherwise, returns nothing/undefined.
  21310. */
  21311. src(source) {
  21312. return this.handleSrc_(source, false);
  21313. }
  21314. /**
  21315. * Set the source object on the tech, returns a boolean that indicates whether
  21316. * there is a tech that can play the source or not
  21317. *
  21318. * @param {Tech~SourceObject} source
  21319. * The source object to set on the Tech
  21320. *
  21321. * @return {boolean}
  21322. * - True if there is no Tech to playback this source
  21323. * - False otherwise
  21324. *
  21325. * @private
  21326. */
  21327. src_(source) {
  21328. const sourceTech = this.selectSource([source]);
  21329. if (!sourceTech) {
  21330. return true;
  21331. }
  21332. if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
  21333. this.changingSrc_ = true;
  21334. // load this technology with the chosen source
  21335. this.loadTech_(sourceTech.tech, sourceTech.source);
  21336. this.tech_.ready(() => {
  21337. this.changingSrc_ = false;
  21338. });
  21339. return false;
  21340. }
  21341. // wait until the tech is ready to set the source
  21342. // and set it synchronously if possible (#2326)
  21343. this.ready(function () {
  21344. // The setSource tech method was added with source handlers
  21345. // so older techs won't support it
  21346. // We need to check the direct prototype for the case where subclasses
  21347. // of the tech do not support source handlers
  21348. if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
  21349. this.techCall_('setSource', source);
  21350. } else {
  21351. this.techCall_('src', source.src);
  21352. }
  21353. this.changingSrc_ = false;
  21354. }, true);
  21355. return false;
  21356. }
  21357. /**
  21358. * Begin loading the src data.
  21359. */
  21360. load() {
  21361. this.techCall_('load');
  21362. }
  21363. /**
  21364. * Reset the player. Loads the first tech in the techOrder,
  21365. * removes all the text tracks in the existing `tech`,
  21366. * and calls `reset` on the `tech`.
  21367. */
  21368. reset() {
  21369. if (this.paused()) {
  21370. this.doReset_();
  21371. } else {
  21372. const playPromise = this.play();
  21373. silencePromise(playPromise.then(() => this.doReset_()));
  21374. }
  21375. }
  21376. doReset_() {
  21377. if (this.tech_) {
  21378. this.tech_.clearTracks('text');
  21379. }
  21380. this.resetCache_();
  21381. this.poster('');
  21382. this.loadTech_(this.options_.techOrder[0], null);
  21383. this.techCall_('reset');
  21384. this.resetControlBarUI_();
  21385. if (isEvented(this)) {
  21386. this.trigger('playerreset');
  21387. }
  21388. }
  21389. /**
  21390. * Reset Control Bar's UI by calling sub-methods that reset
  21391. * all of Control Bar's components
  21392. */
  21393. resetControlBarUI_() {
  21394. this.resetProgressBar_();
  21395. this.resetPlaybackRate_();
  21396. this.resetVolumeBar_();
  21397. }
  21398. /**
  21399. * Reset tech's progress so progress bar is reset in the UI
  21400. */
  21401. resetProgressBar_() {
  21402. this.currentTime(0);
  21403. const {
  21404. currentTimeDisplay,
  21405. durationDisplay,
  21406. progressControl,
  21407. remainingTimeDisplay
  21408. } = this.controlBar || {};
  21409. const {
  21410. seekBar
  21411. } = progressControl || {};
  21412. if (currentTimeDisplay) {
  21413. currentTimeDisplay.updateContent();
  21414. }
  21415. if (durationDisplay) {
  21416. durationDisplay.updateContent();
  21417. }
  21418. if (remainingTimeDisplay) {
  21419. remainingTimeDisplay.updateContent();
  21420. }
  21421. if (seekBar) {
  21422. seekBar.update();
  21423. if (seekBar.loadProgressBar) {
  21424. seekBar.loadProgressBar.update();
  21425. }
  21426. }
  21427. }
  21428. /**
  21429. * Reset Playback ratio
  21430. */
  21431. resetPlaybackRate_() {
  21432. this.playbackRate(this.defaultPlaybackRate());
  21433. this.handleTechRateChange_();
  21434. }
  21435. /**
  21436. * Reset Volume bar
  21437. */
  21438. resetVolumeBar_() {
  21439. this.volume(1.0);
  21440. this.trigger('volumechange');
  21441. }
  21442. /**
  21443. * Returns all of the current source objects.
  21444. *
  21445. * @return {Tech~SourceObject[]}
  21446. * The current source objects
  21447. */
  21448. currentSources() {
  21449. const source = this.currentSource();
  21450. const sources = [];
  21451. // assume `{}` or `{ src }`
  21452. if (Object.keys(source).length !== 0) {
  21453. sources.push(source);
  21454. }
  21455. return this.cache_.sources || sources;
  21456. }
  21457. /**
  21458. * Returns the current source object.
  21459. *
  21460. * @return {Tech~SourceObject}
  21461. * The current source object
  21462. */
  21463. currentSource() {
  21464. return this.cache_.source || {};
  21465. }
  21466. /**
  21467. * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
  21468. * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
  21469. *
  21470. * @return {string}
  21471. * The current source
  21472. */
  21473. currentSrc() {
  21474. return this.currentSource() && this.currentSource().src || '';
  21475. }
  21476. /**
  21477. * Get the current source type e.g. video/mp4
  21478. * This can allow you rebuild the current source object so that you could load the same
  21479. * source and tech later
  21480. *
  21481. * @return {string}
  21482. * The source MIME type
  21483. */
  21484. currentType() {
  21485. return this.currentSource() && this.currentSource().type || '';
  21486. }
  21487. /**
  21488. * Get or set the preload attribute
  21489. *
  21490. * @param {boolean} [value]
  21491. * - true means that we should preload
  21492. * - false means that we should not preload
  21493. *
  21494. * @return {string}
  21495. * The preload attribute value when getting
  21496. */
  21497. preload(value) {
  21498. if (value !== undefined) {
  21499. this.techCall_('setPreload', value);
  21500. this.options_.preload = value;
  21501. return;
  21502. }
  21503. return this.techGet_('preload');
  21504. }
  21505. /**
  21506. * Get or set the autoplay option. When this is a boolean it will
  21507. * modify the attribute on the tech. When this is a string the attribute on
  21508. * the tech will be removed and `Player` will handle autoplay on loadstarts.
  21509. *
  21510. * @param {boolean|string} [value]
  21511. * - true: autoplay using the browser behavior
  21512. * - false: do not autoplay
  21513. * - 'play': call play() on every loadstart
  21514. * - 'muted': call muted() then play() on every loadstart
  21515. * - 'any': call play() on every loadstart. if that fails call muted() then play().
  21516. * - *: values other than those listed here will be set `autoplay` to true
  21517. *
  21518. * @return {boolean|string}
  21519. * The current value of autoplay when getting
  21520. */
  21521. autoplay(value) {
  21522. // getter usage
  21523. if (value === undefined) {
  21524. return this.options_.autoplay || false;
  21525. }
  21526. let techAutoplay;
  21527. // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
  21528. if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
  21529. this.options_.autoplay = value;
  21530. this.manualAutoplay_(typeof value === 'string' ? value : 'play');
  21531. techAutoplay = false;
  21532. // any falsy value sets autoplay to false in the browser,
  21533. // lets do the same
  21534. } else if (!value) {
  21535. this.options_.autoplay = false;
  21536. // any other value (ie truthy) sets autoplay to true
  21537. } else {
  21538. this.options_.autoplay = true;
  21539. }
  21540. techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
  21541. // if we don't have a tech then we do not queue up
  21542. // a setAutoplay call on tech ready. We do this because the
  21543. // autoplay option will be passed in the constructor and we
  21544. // do not need to set it twice
  21545. if (this.tech_) {
  21546. this.techCall_('setAutoplay', techAutoplay);
  21547. }
  21548. }
  21549. /**
  21550. * Set or unset the playsinline attribute.
  21551. * Playsinline tells the browser that non-fullscreen playback is preferred.
  21552. *
  21553. * @param {boolean} [value]
  21554. * - true means that we should try to play inline by default
  21555. * - false means that we should use the browser's default playback mode,
  21556. * which in most cases is inline. iOS Safari is a notable exception
  21557. * and plays fullscreen by default.
  21558. *
  21559. * @return {string|Player}
  21560. * - the current value of playsinline
  21561. * - the player when setting
  21562. *
  21563. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
  21564. */
  21565. playsinline(value) {
  21566. if (value !== undefined) {
  21567. this.techCall_('setPlaysinline', value);
  21568. this.options_.playsinline = value;
  21569. return this;
  21570. }
  21571. return this.techGet_('playsinline');
  21572. }
  21573. /**
  21574. * Get or set the loop attribute on the video element.
  21575. *
  21576. * @param {boolean} [value]
  21577. * - true means that we should loop the video
  21578. * - false means that we should not loop the video
  21579. *
  21580. * @return {boolean}
  21581. * The current value of loop when getting
  21582. */
  21583. loop(value) {
  21584. if (value !== undefined) {
  21585. this.techCall_('setLoop', value);
  21586. this.options_.loop = value;
  21587. return;
  21588. }
  21589. return this.techGet_('loop');
  21590. }
  21591. /**
  21592. * Get or set the poster image source url
  21593. *
  21594. * @fires Player#posterchange
  21595. *
  21596. * @param {string} [src]
  21597. * Poster image source URL
  21598. *
  21599. * @return {string}
  21600. * The current value of poster when getting
  21601. */
  21602. poster(src) {
  21603. if (src === undefined) {
  21604. return this.poster_;
  21605. }
  21606. // The correct way to remove a poster is to set as an empty string
  21607. // other falsey values will throw errors
  21608. if (!src) {
  21609. src = '';
  21610. }
  21611. if (src === this.poster_) {
  21612. return;
  21613. }
  21614. // update the internal poster variable
  21615. this.poster_ = src;
  21616. // update the tech's poster
  21617. this.techCall_('setPoster', src);
  21618. this.isPosterFromTech_ = false;
  21619. // alert components that the poster has been set
  21620. /**
  21621. * This event fires when the poster image is changed on the player.
  21622. *
  21623. * @event Player#posterchange
  21624. * @type {Event}
  21625. */
  21626. this.trigger('posterchange');
  21627. }
  21628. /**
  21629. * Some techs (e.g. YouTube) can provide a poster source in an
  21630. * asynchronous way. We want the poster component to use this
  21631. * poster source so that it covers up the tech's controls.
  21632. * (YouTube's play button). However we only want to use this
  21633. * source if the player user hasn't set a poster through
  21634. * the normal APIs.
  21635. *
  21636. * @fires Player#posterchange
  21637. * @listens Tech#posterchange
  21638. * @private
  21639. */
  21640. handleTechPosterChange_() {
  21641. if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
  21642. const newPoster = this.tech_.poster() || '';
  21643. if (newPoster !== this.poster_) {
  21644. this.poster_ = newPoster;
  21645. this.isPosterFromTech_ = true;
  21646. // Let components know the poster has changed
  21647. this.trigger('posterchange');
  21648. }
  21649. }
  21650. }
  21651. /**
  21652. * Get or set whether or not the controls are showing.
  21653. *
  21654. * @fires Player#controlsenabled
  21655. *
  21656. * @param {boolean} [bool]
  21657. * - true to turn controls on
  21658. * - false to turn controls off
  21659. *
  21660. * @return {boolean}
  21661. * The current value of controls when getting
  21662. */
  21663. controls(bool) {
  21664. if (bool === undefined) {
  21665. return !!this.controls_;
  21666. }
  21667. bool = !!bool;
  21668. // Don't trigger a change event unless it actually changed
  21669. if (this.controls_ === bool) {
  21670. return;
  21671. }
  21672. this.controls_ = bool;
  21673. if (this.usingNativeControls()) {
  21674. this.techCall_('setControls', bool);
  21675. }
  21676. if (this.controls_) {
  21677. this.removeClass('vjs-controls-disabled');
  21678. this.addClass('vjs-controls-enabled');
  21679. /**
  21680. * @event Player#controlsenabled
  21681. * @type {Event}
  21682. */
  21683. this.trigger('controlsenabled');
  21684. if (!this.usingNativeControls()) {
  21685. this.addTechControlsListeners_();
  21686. }
  21687. } else {
  21688. this.removeClass('vjs-controls-enabled');
  21689. this.addClass('vjs-controls-disabled');
  21690. /**
  21691. * @event Player#controlsdisabled
  21692. * @type {Event}
  21693. */
  21694. this.trigger('controlsdisabled');
  21695. if (!this.usingNativeControls()) {
  21696. this.removeTechControlsListeners_();
  21697. }
  21698. }
  21699. }
  21700. /**
  21701. * Toggle native controls on/off. Native controls are the controls built into
  21702. * devices (e.g. default iPhone controls) or other techs
  21703. * (e.g. Vimeo Controls)
  21704. * **This should only be set by the current tech, because only the tech knows
  21705. * if it can support native controls**
  21706. *
  21707. * @fires Player#usingnativecontrols
  21708. * @fires Player#usingcustomcontrols
  21709. *
  21710. * @param {boolean} [bool]
  21711. * - true to turn native controls on
  21712. * - false to turn native controls off
  21713. *
  21714. * @return {boolean}
  21715. * The current value of native controls when getting
  21716. */
  21717. usingNativeControls(bool) {
  21718. if (bool === undefined) {
  21719. return !!this.usingNativeControls_;
  21720. }
  21721. bool = !!bool;
  21722. // Don't trigger a change event unless it actually changed
  21723. if (this.usingNativeControls_ === bool) {
  21724. return;
  21725. }
  21726. this.usingNativeControls_ = bool;
  21727. if (this.usingNativeControls_) {
  21728. this.addClass('vjs-using-native-controls');
  21729. /**
  21730. * player is using the native device controls
  21731. *
  21732. * @event Player#usingnativecontrols
  21733. * @type {Event}
  21734. */
  21735. this.trigger('usingnativecontrols');
  21736. } else {
  21737. this.removeClass('vjs-using-native-controls');
  21738. /**
  21739. * player is using the custom HTML controls
  21740. *
  21741. * @event Player#usingcustomcontrols
  21742. * @type {Event}
  21743. */
  21744. this.trigger('usingcustomcontrols');
  21745. }
  21746. }
  21747. /**
  21748. * Set or get the current MediaError
  21749. *
  21750. * @fires Player#error
  21751. *
  21752. * @param {MediaError|string|number} [err]
  21753. * A MediaError or a string/number to be turned
  21754. * into a MediaError
  21755. *
  21756. * @return {MediaError|null}
  21757. * The current MediaError when getting (or null)
  21758. */
  21759. error(err) {
  21760. if (err === undefined) {
  21761. return this.error_ || null;
  21762. }
  21763. // allow hooks to modify error object
  21764. hooks('beforeerror').forEach(hookFunction => {
  21765. const newErr = hookFunction(this, err);
  21766. if (!(isObject(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
  21767. this.log.error('please return a value that MediaError expects in beforeerror hooks');
  21768. return;
  21769. }
  21770. err = newErr;
  21771. });
  21772. // Suppress the first error message for no compatible source until
  21773. // user interaction
  21774. if (this.options_.suppressNotSupportedError && err && err.code === 4) {
  21775. const triggerSuppressedError = function () {
  21776. this.error(err);
  21777. };
  21778. this.options_.suppressNotSupportedError = false;
  21779. this.any(['click', 'touchstart'], triggerSuppressedError);
  21780. this.one('loadstart', function () {
  21781. this.off(['click', 'touchstart'], triggerSuppressedError);
  21782. });
  21783. return;
  21784. }
  21785. // restoring to default
  21786. if (err === null) {
  21787. this.error_ = err;
  21788. this.removeClass('vjs-error');
  21789. if (this.errorDisplay) {
  21790. this.errorDisplay.close();
  21791. }
  21792. return;
  21793. }
  21794. this.error_ = new MediaError(err);
  21795. // add the vjs-error classname to the player
  21796. this.addClass('vjs-error');
  21797. // log the name of the error type and any message
  21798. // IE11 logs "[object object]" and required you to expand message to see error object
  21799. log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
  21800. /**
  21801. * @event Player#error
  21802. * @type {Event}
  21803. */
  21804. this.trigger('error');
  21805. // notify hooks of the per player error
  21806. hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
  21807. return;
  21808. }
  21809. /**
  21810. * Report user activity
  21811. *
  21812. * @param {Object} event
  21813. * Event object
  21814. */
  21815. reportUserActivity(event) {
  21816. this.userActivity_ = true;
  21817. }
  21818. /**
  21819. * Get/set if user is active
  21820. *
  21821. * @fires Player#useractive
  21822. * @fires Player#userinactive
  21823. *
  21824. * @param {boolean} [bool]
  21825. * - true if the user is active
  21826. * - false if the user is inactive
  21827. *
  21828. * @return {boolean}
  21829. * The current value of userActive when getting
  21830. */
  21831. userActive(bool) {
  21832. if (bool === undefined) {
  21833. return this.userActive_;
  21834. }
  21835. bool = !!bool;
  21836. if (bool === this.userActive_) {
  21837. return;
  21838. }
  21839. this.userActive_ = bool;
  21840. if (this.userActive_) {
  21841. this.userActivity_ = true;
  21842. this.removeClass('vjs-user-inactive');
  21843. this.addClass('vjs-user-active');
  21844. /**
  21845. * @event Player#useractive
  21846. * @type {Event}
  21847. */
  21848. this.trigger('useractive');
  21849. return;
  21850. }
  21851. // Chrome/Safari/IE have bugs where when you change the cursor it can
  21852. // trigger a mousemove event. This causes an issue when you're hiding
  21853. // the cursor when the user is inactive, and a mousemove signals user
  21854. // activity. Making it impossible to go into inactive mode. Specifically
  21855. // this happens in fullscreen when we really need to hide the cursor.
  21856. //
  21857. // When this gets resolved in ALL browsers it can be removed
  21858. // https://code.google.com/p/chromium/issues/detail?id=103041
  21859. if (this.tech_) {
  21860. this.tech_.one('mousemove', function (e) {
  21861. e.stopPropagation();
  21862. e.preventDefault();
  21863. });
  21864. }
  21865. this.userActivity_ = false;
  21866. this.removeClass('vjs-user-active');
  21867. this.addClass('vjs-user-inactive');
  21868. /**
  21869. * @event Player#userinactive
  21870. * @type {Event}
  21871. */
  21872. this.trigger('userinactive');
  21873. }
  21874. /**
  21875. * Listen for user activity based on timeout value
  21876. *
  21877. * @private
  21878. */
  21879. listenForUserActivity_() {
  21880. let mouseInProgress;
  21881. let lastMoveX;
  21882. let lastMoveY;
  21883. const handleActivity = bind_(this, this.reportUserActivity);
  21884. const handleMouseMove = function (e) {
  21885. // #1068 - Prevent mousemove spamming
  21886. // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
  21887. if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
  21888. lastMoveX = e.screenX;
  21889. lastMoveY = e.screenY;
  21890. handleActivity();
  21891. }
  21892. };
  21893. const handleMouseDown = function () {
  21894. handleActivity();
  21895. // For as long as the they are touching the device or have their mouse down,
  21896. // we consider them active even if they're not moving their finger or mouse.
  21897. // So we want to continue to update that they are active
  21898. this.clearInterval(mouseInProgress);
  21899. // Setting userActivity=true now and setting the interval to the same time
  21900. // as the activityCheck interval (250) should ensure we never miss the
  21901. // next activityCheck
  21902. mouseInProgress = this.setInterval(handleActivity, 250);
  21903. };
  21904. const handleMouseUpAndMouseLeave = function (event) {
  21905. handleActivity();
  21906. // Stop the interval that maintains activity if the mouse/touch is down
  21907. this.clearInterval(mouseInProgress);
  21908. };
  21909. // Any mouse movement will be considered user activity
  21910. this.on('mousedown', handleMouseDown);
  21911. this.on('mousemove', handleMouseMove);
  21912. this.on('mouseup', handleMouseUpAndMouseLeave);
  21913. this.on('mouseleave', handleMouseUpAndMouseLeave);
  21914. const controlBar = this.getChild('controlBar');
  21915. // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
  21916. // controlBar would no longer be hidden by default timeout.
  21917. if (controlBar && !IS_IOS && !IS_ANDROID) {
  21918. controlBar.on('mouseenter', function (event) {
  21919. if (this.player().options_.inactivityTimeout !== 0) {
  21920. this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
  21921. }
  21922. this.player().options_.inactivityTimeout = 0;
  21923. });
  21924. controlBar.on('mouseleave', function (event) {
  21925. this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
  21926. });
  21927. }
  21928. // Listen for keyboard navigation
  21929. // Shouldn't need to use inProgress interval because of key repeat
  21930. this.on('keydown', handleActivity);
  21931. this.on('keyup', handleActivity);
  21932. // Run an interval every 250 milliseconds instead of stuffing everything into
  21933. // the mousemove/touchmove function itself, to prevent performance degradation.
  21934. // `this.reportUserActivity` simply sets this.userActivity_ to true, which
  21935. // then gets picked up by this loop
  21936. // http://ejohn.org/blog/learning-from-twitter/
  21937. let inactivityTimeout;
  21938. this.setInterval(function () {
  21939. // Check to see if mouse/touch activity has happened
  21940. if (!this.userActivity_) {
  21941. return;
  21942. }
  21943. // Reset the activity tracker
  21944. this.userActivity_ = false;
  21945. // If the user state was inactive, set the state to active
  21946. this.userActive(true);
  21947. // Clear any existing inactivity timeout to start the timer over
  21948. this.clearTimeout(inactivityTimeout);
  21949. const timeout = this.options_.inactivityTimeout;
  21950. if (timeout <= 0) {
  21951. return;
  21952. }
  21953. // In <timeout> milliseconds, if no more activity has occurred the
  21954. // user will be considered inactive
  21955. inactivityTimeout = this.setTimeout(function () {
  21956. // Protect against the case where the inactivityTimeout can trigger just
  21957. // before the next user activity is picked up by the activity check loop
  21958. // causing a flicker
  21959. if (!this.userActivity_) {
  21960. this.userActive(false);
  21961. }
  21962. }, timeout);
  21963. }, 250);
  21964. }
  21965. /**
  21966. * Gets or sets the current playback rate. A playback rate of
  21967. * 1.0 represents normal speed and 0.5 would indicate half-speed
  21968. * playback, for instance.
  21969. *
  21970. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
  21971. *
  21972. * @param {number} [rate]
  21973. * New playback rate to set.
  21974. *
  21975. * @return {number}
  21976. * The current playback rate when getting or 1.0
  21977. */
  21978. playbackRate(rate) {
  21979. if (rate !== undefined) {
  21980. // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
  21981. // that is registered above
  21982. this.techCall_('setPlaybackRate', rate);
  21983. return;
  21984. }
  21985. if (this.tech_ && this.tech_.featuresPlaybackRate) {
  21986. return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
  21987. }
  21988. return 1.0;
  21989. }
  21990. /**
  21991. * Gets or sets the current default playback rate. A default playback rate of
  21992. * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
  21993. * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
  21994. * not the current playbackRate.
  21995. *
  21996. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
  21997. *
  21998. * @param {number} [rate]
  21999. * New default playback rate to set.
  22000. *
  22001. * @return {number|Player}
  22002. * - The default playback rate when getting or 1.0
  22003. * - the player when setting
  22004. */
  22005. defaultPlaybackRate(rate) {
  22006. if (rate !== undefined) {
  22007. return this.techCall_('setDefaultPlaybackRate', rate);
  22008. }
  22009. if (this.tech_ && this.tech_.featuresPlaybackRate) {
  22010. return this.techGet_('defaultPlaybackRate');
  22011. }
  22012. return 1.0;
  22013. }
  22014. /**
  22015. * Gets or sets the audio flag
  22016. *
  22017. * @param {boolean} bool
  22018. * - true signals that this is an audio player
  22019. * - false signals that this is not an audio player
  22020. *
  22021. * @return {boolean}
  22022. * The current value of isAudio when getting
  22023. */
  22024. isAudio(bool) {
  22025. if (bool !== undefined) {
  22026. this.isAudio_ = !!bool;
  22027. return;
  22028. }
  22029. return !!this.isAudio_;
  22030. }
  22031. enableAudioOnlyUI_() {
  22032. // Update styling immediately to show the control bar so we can get its height
  22033. this.addClass('vjs-audio-only-mode');
  22034. const playerChildren = this.children();
  22035. const controlBar = this.getChild('ControlBar');
  22036. const controlBarHeight = controlBar && controlBar.currentHeight();
  22037. // Hide all player components except the control bar. Control bar components
  22038. // needed only for video are hidden with CSS
  22039. playerChildren.forEach(child => {
  22040. if (child === controlBar) {
  22041. return;
  22042. }
  22043. if (child.el_ && !child.hasClass('vjs-hidden')) {
  22044. child.hide();
  22045. this.audioOnlyCache_.hiddenChildren.push(child);
  22046. }
  22047. });
  22048. this.audioOnlyCache_.playerHeight = this.currentHeight();
  22049. // Set the player height the same as the control bar
  22050. this.height(controlBarHeight);
  22051. this.trigger('audioonlymodechange');
  22052. }
  22053. disableAudioOnlyUI_() {
  22054. this.removeClass('vjs-audio-only-mode');
  22055. // Show player components that were previously hidden
  22056. this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
  22057. // Reset player height
  22058. this.height(this.audioOnlyCache_.playerHeight);
  22059. this.trigger('audioonlymodechange');
  22060. }
  22061. /**
  22062. * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
  22063. *
  22064. * Setting this to `true` will hide all player components except the control bar,
  22065. * as well as control bar components needed only for video.
  22066. *
  22067. * @param {boolean} [value]
  22068. * The value to set audioOnlyMode to.
  22069. *
  22070. * @return {Promise|boolean}
  22071. * A Promise is returned when setting the state, and a boolean when getting
  22072. * the present state
  22073. */
  22074. audioOnlyMode(value) {
  22075. if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
  22076. return this.audioOnlyMode_;
  22077. }
  22078. this.audioOnlyMode_ = value;
  22079. // Enable Audio Only Mode
  22080. if (value) {
  22081. const exitPromises = [];
  22082. // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
  22083. if (this.isInPictureInPicture()) {
  22084. exitPromises.push(this.exitPictureInPicture());
  22085. }
  22086. if (this.isFullscreen()) {
  22087. exitPromises.push(this.exitFullscreen());
  22088. }
  22089. if (this.audioPosterMode()) {
  22090. exitPromises.push(this.audioPosterMode(false));
  22091. }
  22092. return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
  22093. }
  22094. // Disable Audio Only Mode
  22095. return Promise.resolve().then(() => this.disableAudioOnlyUI_());
  22096. }
  22097. enablePosterModeUI_() {
  22098. // Hide the video element and show the poster image to enable posterModeUI
  22099. const tech = this.tech_ && this.tech_;
  22100. tech.hide();
  22101. this.addClass('vjs-audio-poster-mode');
  22102. this.trigger('audiopostermodechange');
  22103. }
  22104. disablePosterModeUI_() {
  22105. // Show the video element and hide the poster image to disable posterModeUI
  22106. const tech = this.tech_ && this.tech_;
  22107. tech.show();
  22108. this.removeClass('vjs-audio-poster-mode');
  22109. this.trigger('audiopostermodechange');
  22110. }
  22111. /**
  22112. * Get the current audioPosterMode state or set audioPosterMode to true or false
  22113. *
  22114. * @param {boolean} [value]
  22115. * The value to set audioPosterMode to.
  22116. *
  22117. * @return {Promise|boolean}
  22118. * A Promise is returned when setting the state, and a boolean when getting
  22119. * the present state
  22120. */
  22121. audioPosterMode(value) {
  22122. if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
  22123. return this.audioPosterMode_;
  22124. }
  22125. this.audioPosterMode_ = value;
  22126. if (value) {
  22127. if (this.audioOnlyMode()) {
  22128. const audioOnlyModePromise = this.audioOnlyMode(false);
  22129. return audioOnlyModePromise.then(() => {
  22130. // enable audio poster mode after audio only mode is disabled
  22131. this.enablePosterModeUI_();
  22132. });
  22133. }
  22134. return Promise.resolve().then(() => {
  22135. // enable audio poster mode
  22136. this.enablePosterModeUI_();
  22137. });
  22138. }
  22139. return Promise.resolve().then(() => {
  22140. // disable audio poster mode
  22141. this.disablePosterModeUI_();
  22142. });
  22143. }
  22144. /**
  22145. * A helper method for adding a {@link TextTrack} to our
  22146. * {@link TextTrackList}.
  22147. *
  22148. * In addition to the W3C settings we allow adding additional info through options.
  22149. *
  22150. * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
  22151. *
  22152. * @param {string} [kind]
  22153. * the kind of TextTrack you are adding
  22154. *
  22155. * @param {string} [label]
  22156. * the label to give the TextTrack label
  22157. *
  22158. * @param {string} [language]
  22159. * the language to set on the TextTrack
  22160. *
  22161. * @return {TextTrack|undefined}
  22162. * the TextTrack that was added or undefined
  22163. * if there is no tech
  22164. */
  22165. addTextTrack(kind, label, language) {
  22166. if (this.tech_) {
  22167. return this.tech_.addTextTrack(kind, label, language);
  22168. }
  22169. }
  22170. /**
  22171. * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
  22172. *
  22173. * @param {Object} options
  22174. * Options to pass to {@link HTMLTrackElement} during creation. See
  22175. * {@link HTMLTrackElement} for object properties that you should use.
  22176. *
  22177. * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
  22178. * from the TextTrackList and HtmlTrackElementList
  22179. * after a source change
  22180. *
  22181. * @return { import('./tracks/html-track-element').default }
  22182. * the HTMLTrackElement that was created and added
  22183. * to the HtmlTrackElementList and the remote
  22184. * TextTrackList
  22185. *
  22186. */
  22187. addRemoteTextTrack(options, manualCleanup) {
  22188. if (this.tech_) {
  22189. return this.tech_.addRemoteTextTrack(options, manualCleanup);
  22190. }
  22191. }
  22192. /**
  22193. * Remove a remote {@link TextTrack} from the respective
  22194. * {@link TextTrackList} and {@link HtmlTrackElementList}.
  22195. *
  22196. * @param {Object} track
  22197. * Remote {@link TextTrack} to remove
  22198. *
  22199. * @return {undefined}
  22200. * does not return anything
  22201. */
  22202. removeRemoteTextTrack(obj = {}) {
  22203. let {
  22204. track
  22205. } = obj;
  22206. if (!track) {
  22207. track = obj;
  22208. }
  22209. // destructure the input into an object with a track argument, defaulting to arguments[0]
  22210. // default the whole argument to an empty object if nothing was passed in
  22211. if (this.tech_) {
  22212. return this.tech_.removeRemoteTextTrack(track);
  22213. }
  22214. }
  22215. /**
  22216. * Gets available media playback quality metrics as specified by the W3C's Media
  22217. * Playback Quality API.
  22218. *
  22219. * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
  22220. *
  22221. * @return {Object|undefined}
  22222. * An object with supported media playback quality metrics or undefined if there
  22223. * is no tech or the tech does not support it.
  22224. */
  22225. getVideoPlaybackQuality() {
  22226. return this.techGet_('getVideoPlaybackQuality');
  22227. }
  22228. /**
  22229. * Get video width
  22230. *
  22231. * @return {number}
  22232. * current video width
  22233. */
  22234. videoWidth() {
  22235. return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
  22236. }
  22237. /**
  22238. * Get video height
  22239. *
  22240. * @return {number}
  22241. * current video height
  22242. */
  22243. videoHeight() {
  22244. return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
  22245. }
  22246. /**
  22247. * The player's language code.
  22248. *
  22249. * Changing the language will trigger
  22250. * [languagechange]{@link Player#event:languagechange}
  22251. * which Components can use to update control text.
  22252. * ClickableComponent will update its control text by default on
  22253. * [languagechange]{@link Player#event:languagechange}.
  22254. *
  22255. * @fires Player#languagechange
  22256. *
  22257. * @param {string} [code]
  22258. * the language code to set the player to
  22259. *
  22260. * @return {string}
  22261. * The current language code when getting
  22262. */
  22263. language(code) {
  22264. if (code === undefined) {
  22265. return this.language_;
  22266. }
  22267. if (this.language_ !== String(code).toLowerCase()) {
  22268. this.language_ = String(code).toLowerCase();
  22269. // during first init, it's possible some things won't be evented
  22270. if (isEvented(this)) {
  22271. /**
  22272. * fires when the player language change
  22273. *
  22274. * @event Player#languagechange
  22275. * @type {Event}
  22276. */
  22277. this.trigger('languagechange');
  22278. }
  22279. }
  22280. }
  22281. /**
  22282. * Get the player's language dictionary
  22283. * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
  22284. * Languages specified directly in the player options have precedence
  22285. *
  22286. * @return {Array}
  22287. * An array of of supported languages
  22288. */
  22289. languages() {
  22290. return merge(Player.prototype.options_.languages, this.languages_);
  22291. }
  22292. /**
  22293. * returns a JavaScript object representing the current track
  22294. * information. **DOES not return it as JSON**
  22295. *
  22296. * @return {Object}
  22297. * Object representing the current of track info
  22298. */
  22299. toJSON() {
  22300. const options = merge(this.options_);
  22301. const tracks = options.tracks;
  22302. options.tracks = [];
  22303. for (let i = 0; i < tracks.length; i++) {
  22304. let track = tracks[i];
  22305. // deep merge tracks and null out player so no circular references
  22306. track = merge(track);
  22307. track.player = undefined;
  22308. options.tracks[i] = track;
  22309. }
  22310. return options;
  22311. }
  22312. /**
  22313. * Creates a simple modal dialog (an instance of the {@link ModalDialog}
  22314. * component) that immediately overlays the player with arbitrary
  22315. * content and removes itself when closed.
  22316. *
  22317. * @param {string|Function|Element|Array|null} content
  22318. * Same as {@link ModalDialog#content}'s param of the same name.
  22319. * The most straight-forward usage is to provide a string or DOM
  22320. * element.
  22321. *
  22322. * @param {Object} [options]
  22323. * Extra options which will be passed on to the {@link ModalDialog}.
  22324. *
  22325. * @return {ModalDialog}
  22326. * the {@link ModalDialog} that was created
  22327. */
  22328. createModal(content, options) {
  22329. options = options || {};
  22330. options.content = content || '';
  22331. const modal = new ModalDialog(this, options);
  22332. this.addChild(modal);
  22333. modal.on('dispose', () => {
  22334. this.removeChild(modal);
  22335. });
  22336. modal.open();
  22337. return modal;
  22338. }
  22339. /**
  22340. * Change breakpoint classes when the player resizes.
  22341. *
  22342. * @private
  22343. */
  22344. updateCurrentBreakpoint_() {
  22345. if (!this.responsive()) {
  22346. return;
  22347. }
  22348. const currentBreakpoint = this.currentBreakpoint();
  22349. const currentWidth = this.currentWidth();
  22350. for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
  22351. const candidateBreakpoint = BREAKPOINT_ORDER[i];
  22352. const maxWidth = this.breakpoints_[candidateBreakpoint];
  22353. if (currentWidth <= maxWidth) {
  22354. // The current breakpoint did not change, nothing to do.
  22355. if (currentBreakpoint === candidateBreakpoint) {
  22356. return;
  22357. }
  22358. // Only remove a class if there is a current breakpoint.
  22359. if (currentBreakpoint) {
  22360. this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
  22361. }
  22362. this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
  22363. this.breakpoint_ = candidateBreakpoint;
  22364. break;
  22365. }
  22366. }
  22367. }
  22368. /**
  22369. * Removes the current breakpoint.
  22370. *
  22371. * @private
  22372. */
  22373. removeCurrentBreakpoint_() {
  22374. const className = this.currentBreakpointClass();
  22375. this.breakpoint_ = '';
  22376. if (className) {
  22377. this.removeClass(className);
  22378. }
  22379. }
  22380. /**
  22381. * Get or set breakpoints on the player.
  22382. *
  22383. * Calling this method with an object or `true` will remove any previous
  22384. * custom breakpoints and start from the defaults again.
  22385. *
  22386. * @param {Object|boolean} [breakpoints]
  22387. * If an object is given, it can be used to provide custom
  22388. * breakpoints. If `true` is given, will set default breakpoints.
  22389. * If this argument is not given, will simply return the current
  22390. * breakpoints.
  22391. *
  22392. * @param {number} [breakpoints.tiny]
  22393. * The maximum width for the "vjs-layout-tiny" class.
  22394. *
  22395. * @param {number} [breakpoints.xsmall]
  22396. * The maximum width for the "vjs-layout-x-small" class.
  22397. *
  22398. * @param {number} [breakpoints.small]
  22399. * The maximum width for the "vjs-layout-small" class.
  22400. *
  22401. * @param {number} [breakpoints.medium]
  22402. * The maximum width for the "vjs-layout-medium" class.
  22403. *
  22404. * @param {number} [breakpoints.large]
  22405. * The maximum width for the "vjs-layout-large" class.
  22406. *
  22407. * @param {number} [breakpoints.xlarge]
  22408. * The maximum width for the "vjs-layout-x-large" class.
  22409. *
  22410. * @param {number} [breakpoints.huge]
  22411. * The maximum width for the "vjs-layout-huge" class.
  22412. *
  22413. * @return {Object}
  22414. * An object mapping breakpoint names to maximum width values.
  22415. */
  22416. breakpoints(breakpoints) {
  22417. // Used as a getter.
  22418. if (breakpoints === undefined) {
  22419. return Object.assign(this.breakpoints_);
  22420. }
  22421. this.breakpoint_ = '';
  22422. this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
  22423. // When breakpoint definitions change, we need to update the currently
  22424. // selected breakpoint.
  22425. this.updateCurrentBreakpoint_();
  22426. // Clone the breakpoints before returning.
  22427. return Object.assign(this.breakpoints_);
  22428. }
  22429. /**
  22430. * Get or set a flag indicating whether or not this player should adjust
  22431. * its UI based on its dimensions.
  22432. *
  22433. * @param {boolean} value
  22434. * Should be `true` if the player should adjust its UI based on its
  22435. * dimensions; otherwise, should be `false`.
  22436. *
  22437. * @return {boolean}
  22438. * Will be `true` if this player should adjust its UI based on its
  22439. * dimensions; otherwise, will be `false`.
  22440. */
  22441. responsive(value) {
  22442. // Used as a getter.
  22443. if (value === undefined) {
  22444. return this.responsive_;
  22445. }
  22446. value = Boolean(value);
  22447. const current = this.responsive_;
  22448. // Nothing changed.
  22449. if (value === current) {
  22450. return;
  22451. }
  22452. // The value actually changed, set it.
  22453. this.responsive_ = value;
  22454. // Start listening for breakpoints and set the initial breakpoint if the
  22455. // player is now responsive.
  22456. if (value) {
  22457. this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
  22458. this.updateCurrentBreakpoint_();
  22459. // Stop listening for breakpoints if the player is no longer responsive.
  22460. } else {
  22461. this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
  22462. this.removeCurrentBreakpoint_();
  22463. }
  22464. return value;
  22465. }
  22466. /**
  22467. * Get current breakpoint name, if any.
  22468. *
  22469. * @return {string}
  22470. * If there is currently a breakpoint set, returns a the key from the
  22471. * breakpoints object matching it. Otherwise, returns an empty string.
  22472. */
  22473. currentBreakpoint() {
  22474. return this.breakpoint_;
  22475. }
  22476. /**
  22477. * Get the current breakpoint class name.
  22478. *
  22479. * @return {string}
  22480. * The matching class name (e.g. `"vjs-layout-tiny"` or
  22481. * `"vjs-layout-large"`) for the current breakpoint. Empty string if
  22482. * there is no current breakpoint.
  22483. */
  22484. currentBreakpointClass() {
  22485. return BREAKPOINT_CLASSES[this.breakpoint_] || '';
  22486. }
  22487. /**
  22488. * An object that describes a single piece of media.
  22489. *
  22490. * Properties that are not part of this type description will be retained; so,
  22491. * this can be viewed as a generic metadata storage mechanism as well.
  22492. *
  22493. * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
  22494. * @typedef {Object} Player~MediaObject
  22495. *
  22496. * @property {string} [album]
  22497. * Unused, except if this object is passed to the `MediaSession`
  22498. * API.
  22499. *
  22500. * @property {string} [artist]
  22501. * Unused, except if this object is passed to the `MediaSession`
  22502. * API.
  22503. *
  22504. * @property {Object[]} [artwork]
  22505. * Unused, except if this object is passed to the `MediaSession`
  22506. * API. If not specified, will be populated via the `poster`, if
  22507. * available.
  22508. *
  22509. * @property {string} [poster]
  22510. * URL to an image that will display before playback.
  22511. *
  22512. * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
  22513. * A single source object, an array of source objects, or a string
  22514. * referencing a URL to a media source. It is _highly recommended_
  22515. * that an object or array of objects is used here, so that source
  22516. * selection algorithms can take the `type` into account.
  22517. *
  22518. * @property {string} [title]
  22519. * Unused, except if this object is passed to the `MediaSession`
  22520. * API.
  22521. *
  22522. * @property {Object[]} [textTracks]
  22523. * An array of objects to be used to create text tracks, following
  22524. * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
  22525. * For ease of removal, these will be created as "remote" text
  22526. * tracks and set to automatically clean up on source changes.
  22527. *
  22528. * These objects may have properties like `src`, `kind`, `label`,
  22529. * and `language`, see {@link Tech#createRemoteTextTrack}.
  22530. */
  22531. /**
  22532. * Populate the player using a {@link Player~MediaObject|MediaObject}.
  22533. *
  22534. * @param {Player~MediaObject} media
  22535. * A media object.
  22536. *
  22537. * @param {Function} ready
  22538. * A callback to be called when the player is ready.
  22539. */
  22540. loadMedia(media, ready) {
  22541. if (!media || typeof media !== 'object') {
  22542. return;
  22543. }
  22544. this.reset();
  22545. // Clone the media object so it cannot be mutated from outside.
  22546. this.cache_.media = merge(media);
  22547. const {
  22548. artist,
  22549. artwork,
  22550. description,
  22551. poster,
  22552. src,
  22553. textTracks,
  22554. title
  22555. } = this.cache_.media;
  22556. // If `artwork` is not given, create it using `poster`.
  22557. if (!artwork && poster) {
  22558. this.cache_.media.artwork = [{
  22559. src: poster,
  22560. type: getMimetype(poster)
  22561. }];
  22562. }
  22563. if (src) {
  22564. this.src(src);
  22565. }
  22566. if (poster) {
  22567. this.poster(poster);
  22568. }
  22569. if (Array.isArray(textTracks)) {
  22570. textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
  22571. }
  22572. if (this.titleBar) {
  22573. this.titleBar.update({
  22574. title,
  22575. description: description || artist || ''
  22576. });
  22577. }
  22578. this.ready(ready);
  22579. }
  22580. /**
  22581. * Get a clone of the current {@link Player~MediaObject} for this player.
  22582. *
  22583. * If the `loadMedia` method has not been used, will attempt to return a
  22584. * {@link Player~MediaObject} based on the current state of the player.
  22585. *
  22586. * @return {Player~MediaObject}
  22587. */
  22588. getMedia() {
  22589. if (!this.cache_.media) {
  22590. const poster = this.poster();
  22591. const src = this.currentSources();
  22592. const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
  22593. kind: tt.kind,
  22594. label: tt.label,
  22595. language: tt.language,
  22596. src: tt.src
  22597. }));
  22598. const media = {
  22599. src,
  22600. textTracks
  22601. };
  22602. if (poster) {
  22603. media.poster = poster;
  22604. media.artwork = [{
  22605. src: media.poster,
  22606. type: getMimetype(media.poster)
  22607. }];
  22608. }
  22609. return media;
  22610. }
  22611. return merge(this.cache_.media);
  22612. }
  22613. /**
  22614. * Gets tag settings
  22615. *
  22616. * @param {Element} tag
  22617. * The player tag
  22618. *
  22619. * @return {Object}
  22620. * An object containing all of the settings
  22621. * for a player tag
  22622. */
  22623. static getTagSettings(tag) {
  22624. const baseOptions = {
  22625. sources: [],
  22626. tracks: []
  22627. };
  22628. const tagOptions = getAttributes(tag);
  22629. const dataSetup = tagOptions['data-setup'];
  22630. if (hasClass(tag, 'vjs-fill')) {
  22631. tagOptions.fill = true;
  22632. }
  22633. if (hasClass(tag, 'vjs-fluid')) {
  22634. tagOptions.fluid = true;
  22635. }
  22636. // Check if data-setup attr exists.
  22637. if (dataSetup !== null) {
  22638. // Parse options JSON
  22639. // If empty string, make it a parsable json object.
  22640. const [err, data] = safeParseTuple__default["default"](dataSetup || '{}');
  22641. if (err) {
  22642. log.error(err);
  22643. }
  22644. Object.assign(tagOptions, data);
  22645. }
  22646. Object.assign(baseOptions, tagOptions);
  22647. // Get tag children settings
  22648. if (tag.hasChildNodes()) {
  22649. const children = tag.childNodes;
  22650. for (let i = 0, j = children.length; i < j; i++) {
  22651. const child = children[i];
  22652. // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
  22653. const childName = child.nodeName.toLowerCase();
  22654. if (childName === 'source') {
  22655. baseOptions.sources.push(getAttributes(child));
  22656. } else if (childName === 'track') {
  22657. baseOptions.tracks.push(getAttributes(child));
  22658. }
  22659. }
  22660. }
  22661. return baseOptions;
  22662. }
  22663. /**
  22664. * Set debug mode to enable/disable logs at info level.
  22665. *
  22666. * @param {boolean} enabled
  22667. * @fires Player#debugon
  22668. * @fires Player#debugoff
  22669. */
  22670. debug(enabled) {
  22671. if (enabled === undefined) {
  22672. return this.debugEnabled_;
  22673. }
  22674. if (enabled) {
  22675. this.trigger('debugon');
  22676. this.previousLogLevel_ = this.log.level;
  22677. this.log.level('debug');
  22678. this.debugEnabled_ = true;
  22679. } else {
  22680. this.trigger('debugoff');
  22681. this.log.level(this.previousLogLevel_);
  22682. this.previousLogLevel_ = undefined;
  22683. this.debugEnabled_ = false;
  22684. }
  22685. }
  22686. /**
  22687. * Set or get current playback rates.
  22688. * Takes an array and updates the playback rates menu with the new items.
  22689. * Pass in an empty array to hide the menu.
  22690. * Values other than arrays are ignored.
  22691. *
  22692. * @fires Player#playbackrateschange
  22693. * @param {number[]} newRates
  22694. * The new rates that the playback rates menu should update to.
  22695. * An empty array will hide the menu
  22696. * @return {number[]} When used as a getter will return the current playback rates
  22697. */
  22698. playbackRates(newRates) {
  22699. if (newRates === undefined) {
  22700. return this.cache_.playbackRates;
  22701. }
  22702. // ignore any value that isn't an array
  22703. if (!Array.isArray(newRates)) {
  22704. return;
  22705. }
  22706. // ignore any arrays that don't only contain numbers
  22707. if (!newRates.every(rate => typeof rate === 'number')) {
  22708. return;
  22709. }
  22710. this.cache_.playbackRates = newRates;
  22711. /**
  22712. * fires when the playback rates in a player are changed
  22713. *
  22714. * @event Player#playbackrateschange
  22715. * @type {Event}
  22716. */
  22717. this.trigger('playbackrateschange');
  22718. }
  22719. }
  22720. /**
  22721. * Get the {@link VideoTrackList}
  22722. *
  22723. * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
  22724. *
  22725. * @return {VideoTrackList}
  22726. * the current video track list
  22727. *
  22728. * @method Player.prototype.videoTracks
  22729. */
  22730. /**
  22731. * Get the {@link AudioTrackList}
  22732. *
  22733. * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
  22734. *
  22735. * @return {AudioTrackList}
  22736. * the current audio track list
  22737. *
  22738. * @method Player.prototype.audioTracks
  22739. */
  22740. /**
  22741. * Get the {@link TextTrackList}
  22742. *
  22743. * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
  22744. *
  22745. * @return {TextTrackList}
  22746. * the current text track list
  22747. *
  22748. * @method Player.prototype.textTracks
  22749. */
  22750. /**
  22751. * Get the remote {@link TextTrackList}
  22752. *
  22753. * @return {TextTrackList}
  22754. * The current remote text track list
  22755. *
  22756. * @method Player.prototype.remoteTextTracks
  22757. */
  22758. /**
  22759. * Get the remote {@link HtmlTrackElementList} tracks.
  22760. *
  22761. * @return {HtmlTrackElementList}
  22762. * The current remote text track element list
  22763. *
  22764. * @method Player.prototype.remoteTextTrackEls
  22765. */
  22766. ALL.names.forEach(function (name) {
  22767. const props = ALL[name];
  22768. Player.prototype[props.getterName] = function () {
  22769. if (this.tech_) {
  22770. return this.tech_[props.getterName]();
  22771. }
  22772. // if we have not yet loadTech_, we create {video,audio,text}Tracks_
  22773. // these will be passed to the tech during loading
  22774. this[props.privateName] = this[props.privateName] || new props.ListClass();
  22775. return this[props.privateName];
  22776. };
  22777. });
  22778. /**
  22779. * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
  22780. * sets the `crossOrigin` property on the `<video>` tag to control the CORS
  22781. * behavior.
  22782. *
  22783. * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
  22784. *
  22785. * @param {string} [value]
  22786. * The value to set the `Player`'s crossorigin to. If an argument is
  22787. * given, must be one of `anonymous` or `use-credentials`.
  22788. *
  22789. * @return {string|undefined}
  22790. * - The current crossorigin value of the `Player` when getting.
  22791. * - undefined when setting
  22792. */
  22793. Player.prototype.crossorigin = Player.prototype.crossOrigin;
  22794. /**
  22795. * Global enumeration of players.
  22796. *
  22797. * The keys are the player IDs and the values are either the {@link Player}
  22798. * instance or `null` for disposed players.
  22799. *
  22800. * @type {Object}
  22801. */
  22802. Player.players = {};
  22803. const navigator = window__default["default"].navigator;
  22804. /*
  22805. * Player instance options, surfaced using options
  22806. * options = Player.prototype.options_
  22807. * Make changes in options, not here.
  22808. *
  22809. * @type {Object}
  22810. * @private
  22811. */
  22812. Player.prototype.options_ = {
  22813. // Default order of fallback technology
  22814. techOrder: Tech.defaultTechOrder_,
  22815. html5: {},
  22816. // enable sourceset by default
  22817. enableSourceset: true,
  22818. // default inactivity timeout
  22819. inactivityTimeout: 2000,
  22820. // default playback rates
  22821. playbackRates: [],
  22822. // Add playback rate selection by adding rates
  22823. // 'playbackRates': [0.5, 1, 1.5, 2],
  22824. liveui: false,
  22825. // Included control sets
  22826. children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
  22827. language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
  22828. // locales and their language translations
  22829. languages: {},
  22830. // Default message to show when a video cannot be played.
  22831. notSupportedMessage: 'No compatible source was found for this media.',
  22832. normalizeAutoplay: false,
  22833. fullscreen: {
  22834. options: {
  22835. navigationUI: 'hide'
  22836. }
  22837. },
  22838. breakpoints: {},
  22839. responsive: false,
  22840. audioOnlyMode: false,
  22841. audioPosterMode: false
  22842. };
  22843. [
  22844. /**
  22845. * Returns whether or not the player is in the "ended" state.
  22846. *
  22847. * @return {Boolean} True if the player is in the ended state, false if not.
  22848. * @method Player#ended
  22849. */
  22850. 'ended',
  22851. /**
  22852. * Returns whether or not the player is in the "seeking" state.
  22853. *
  22854. * @return {Boolean} True if the player is in the seeking state, false if not.
  22855. * @method Player#seeking
  22856. */
  22857. 'seeking',
  22858. /**
  22859. * Returns the TimeRanges of the media that are currently available
  22860. * for seeking to.
  22861. *
  22862. * @return {TimeRanges} the seekable intervals of the media timeline
  22863. * @method Player#seekable
  22864. */
  22865. 'seekable',
  22866. /**
  22867. * Returns the current state of network activity for the element, from
  22868. * the codes in the list below.
  22869. * - NETWORK_EMPTY (numeric value 0)
  22870. * The element has not yet been initialised. All attributes are in
  22871. * their initial states.
  22872. * - NETWORK_IDLE (numeric value 1)
  22873. * The element's resource selection algorithm is active and has
  22874. * selected a resource, but it is not actually using the network at
  22875. * this time.
  22876. * - NETWORK_LOADING (numeric value 2)
  22877. * The user agent is actively trying to download data.
  22878. * - NETWORK_NO_SOURCE (numeric value 3)
  22879. * The element's resource selection algorithm is active, but it has
  22880. * not yet found a resource to use.
  22881. *
  22882. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
  22883. * @return {number} the current network activity state
  22884. * @method Player#networkState
  22885. */
  22886. 'networkState',
  22887. /**
  22888. * Returns a value that expresses the current state of the element
  22889. * with respect to rendering the current playback position, from the
  22890. * codes in the list below.
  22891. * - HAVE_NOTHING (numeric value 0)
  22892. * No information regarding the media resource is available.
  22893. * - HAVE_METADATA (numeric value 1)
  22894. * Enough of the resource has been obtained that the duration of the
  22895. * resource is available.
  22896. * - HAVE_CURRENT_DATA (numeric value 2)
  22897. * Data for the immediate current playback position is available.
  22898. * - HAVE_FUTURE_DATA (numeric value 3)
  22899. * Data for the immediate current playback position is available, as
  22900. * well as enough data for the user agent to advance the current
  22901. * playback position in the direction of playback.
  22902. * - HAVE_ENOUGH_DATA (numeric value 4)
  22903. * The user agent estimates that enough data is available for
  22904. * playback to proceed uninterrupted.
  22905. *
  22906. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
  22907. * @return {number} the current playback rendering state
  22908. * @method Player#readyState
  22909. */
  22910. 'readyState'].forEach(function (fn) {
  22911. Player.prototype[fn] = function () {
  22912. return this.techGet_(fn);
  22913. };
  22914. });
  22915. TECH_EVENTS_RETRIGGER.forEach(function (event) {
  22916. Player.prototype[`handleTech${toTitleCase(event)}_`] = function () {
  22917. return this.trigger(event);
  22918. };
  22919. });
  22920. /**
  22921. * Fired when the player has initial duration and dimension information
  22922. *
  22923. * @event Player#loadedmetadata
  22924. * @type {Event}
  22925. */
  22926. /**
  22927. * Fired when the player has downloaded data at the current playback position
  22928. *
  22929. * @event Player#loadeddata
  22930. * @type {Event}
  22931. */
  22932. /**
  22933. * Fired when the current playback position has changed *
  22934. * During playback this is fired every 15-250 milliseconds, depending on the
  22935. * playback technology in use.
  22936. *
  22937. * @event Player#timeupdate
  22938. * @type {Event}
  22939. */
  22940. /**
  22941. * Fired when the volume changes
  22942. *
  22943. * @event Player#volumechange
  22944. * @type {Event}
  22945. */
  22946. /**
  22947. * Reports whether or not a player has a plugin available.
  22948. *
  22949. * This does not report whether or not the plugin has ever been initialized
  22950. * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
  22951. *
  22952. * @method Player#hasPlugin
  22953. * @param {string} name
  22954. * The name of a plugin.
  22955. *
  22956. * @return {boolean}
  22957. * Whether or not this player has the requested plugin available.
  22958. */
  22959. /**
  22960. * Reports whether or not a player is using a plugin by name.
  22961. *
  22962. * For basic plugins, this only reports whether the plugin has _ever_ been
  22963. * initialized on this player.
  22964. *
  22965. * @method Player#usingPlugin
  22966. * @param {string} name
  22967. * The name of a plugin.
  22968. *
  22969. * @return {boolean}
  22970. * Whether or not this player is using the requested plugin.
  22971. */
  22972. Component.registerComponent('Player', Player);
  22973. /**
  22974. * @file plugin.js
  22975. */
  22976. /**
  22977. * The base plugin name.
  22978. *
  22979. * @private
  22980. * @constant
  22981. * @type {string}
  22982. */
  22983. const BASE_PLUGIN_NAME = 'plugin';
  22984. /**
  22985. * The key on which a player's active plugins cache is stored.
  22986. *
  22987. * @private
  22988. * @constant
  22989. * @type {string}
  22990. */
  22991. const PLUGIN_CACHE_KEY = 'activePlugins_';
  22992. /**
  22993. * Stores registered plugins in a private space.
  22994. *
  22995. * @private
  22996. * @type {Object}
  22997. */
  22998. const pluginStorage = {};
  22999. /**
  23000. * Reports whether or not a plugin has been registered.
  23001. *
  23002. * @private
  23003. * @param {string} name
  23004. * The name of a plugin.
  23005. *
  23006. * @return {boolean}
  23007. * Whether or not the plugin has been registered.
  23008. */
  23009. const pluginExists = name => pluginStorage.hasOwnProperty(name);
  23010. /**
  23011. * Get a single registered plugin by name.
  23012. *
  23013. * @private
  23014. * @param {string} name
  23015. * The name of a plugin.
  23016. *
  23017. * @return {typeof Plugin|Function|undefined}
  23018. * The plugin (or undefined).
  23019. */
  23020. const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
  23021. /**
  23022. * Marks a plugin as "active" on a player.
  23023. *
  23024. * Also, ensures that the player has an object for tracking active plugins.
  23025. *
  23026. * @private
  23027. * @param {Player} player
  23028. * A Video.js player instance.
  23029. *
  23030. * @param {string} name
  23031. * The name of a plugin.
  23032. */
  23033. const markPluginAsActive = (player, name) => {
  23034. player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
  23035. player[PLUGIN_CACHE_KEY][name] = true;
  23036. };
  23037. /**
  23038. * Triggers a pair of plugin setup events.
  23039. *
  23040. * @private
  23041. * @param {Player} player
  23042. * A Video.js player instance.
  23043. *
  23044. * @param {Plugin~PluginEventHash} hash
  23045. * A plugin event hash.
  23046. *
  23047. * @param {boolean} [before]
  23048. * If true, prefixes the event name with "before". In other words,
  23049. * use this to trigger "beforepluginsetup" instead of "pluginsetup".
  23050. */
  23051. const triggerSetupEvent = (player, hash, before) => {
  23052. const eventName = (before ? 'before' : '') + 'pluginsetup';
  23053. player.trigger(eventName, hash);
  23054. player.trigger(eventName + ':' + hash.name, hash);
  23055. };
  23056. /**
  23057. * Takes a basic plugin function and returns a wrapper function which marks
  23058. * on the player that the plugin has been activated.
  23059. *
  23060. * @private
  23061. * @param {string} name
  23062. * The name of the plugin.
  23063. *
  23064. * @param {Function} plugin
  23065. * The basic plugin.
  23066. *
  23067. * @return {Function}
  23068. * A wrapper function for the given plugin.
  23069. */
  23070. const createBasicPlugin = function (name, plugin) {
  23071. const basicPluginWrapper = function () {
  23072. // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
  23073. // regardless, but we want the hash to be consistent with the hash provided
  23074. // for advanced plugins.
  23075. //
  23076. // The only potentially counter-intuitive thing here is the `instance` in
  23077. // the "pluginsetup" event is the value returned by the `plugin` function.
  23078. triggerSetupEvent(this, {
  23079. name,
  23080. plugin,
  23081. instance: null
  23082. }, true);
  23083. const instance = plugin.apply(this, arguments);
  23084. markPluginAsActive(this, name);
  23085. triggerSetupEvent(this, {
  23086. name,
  23087. plugin,
  23088. instance
  23089. });
  23090. return instance;
  23091. };
  23092. Object.keys(plugin).forEach(function (prop) {
  23093. basicPluginWrapper[prop] = plugin[prop];
  23094. });
  23095. return basicPluginWrapper;
  23096. };
  23097. /**
  23098. * Takes a plugin sub-class and returns a factory function for generating
  23099. * instances of it.
  23100. *
  23101. * This factory function will replace itself with an instance of the requested
  23102. * sub-class of Plugin.
  23103. *
  23104. * @private
  23105. * @param {string} name
  23106. * The name of the plugin.
  23107. *
  23108. * @param {Plugin} PluginSubClass
  23109. * The advanced plugin.
  23110. *
  23111. * @return {Function}
  23112. */
  23113. const createPluginFactory = (name, PluginSubClass) => {
  23114. // Add a `name` property to the plugin prototype so that each plugin can
  23115. // refer to itself by name.
  23116. PluginSubClass.prototype.name = name;
  23117. return function (...args) {
  23118. triggerSetupEvent(this, {
  23119. name,
  23120. plugin: PluginSubClass,
  23121. instance: null
  23122. }, true);
  23123. const instance = new PluginSubClass(...[this, ...args]);
  23124. // The plugin is replaced by a function that returns the current instance.
  23125. this[name] = () => instance;
  23126. triggerSetupEvent(this, instance.getEventHash());
  23127. return instance;
  23128. };
  23129. };
  23130. /**
  23131. * Parent class for all advanced plugins.
  23132. *
  23133. * @mixes module:evented~EventedMixin
  23134. * @mixes module:stateful~StatefulMixin
  23135. * @fires Player#beforepluginsetup
  23136. * @fires Player#beforepluginsetup:$name
  23137. * @fires Player#pluginsetup
  23138. * @fires Player#pluginsetup:$name
  23139. * @listens Player#dispose
  23140. * @throws {Error}
  23141. * If attempting to instantiate the base {@link Plugin} class
  23142. * directly instead of via a sub-class.
  23143. */
  23144. class Plugin {
  23145. /**
  23146. * Creates an instance of this class.
  23147. *
  23148. * Sub-classes should call `super` to ensure plugins are properly initialized.
  23149. *
  23150. * @param {Player} player
  23151. * A Video.js player instance.
  23152. */
  23153. constructor(player) {
  23154. if (this.constructor === Plugin) {
  23155. throw new Error('Plugin must be sub-classed; not directly instantiated.');
  23156. }
  23157. this.player = player;
  23158. if (!this.log) {
  23159. this.log = this.player.log.createLogger(this.name);
  23160. }
  23161. // Make this object evented, but remove the added `trigger` method so we
  23162. // use the prototype version instead.
  23163. evented(this);
  23164. delete this.trigger;
  23165. stateful(this, this.constructor.defaultState);
  23166. markPluginAsActive(player, this.name);
  23167. // Auto-bind the dispose method so we can use it as a listener and unbind
  23168. // it later easily.
  23169. this.dispose = this.dispose.bind(this);
  23170. // If the player is disposed, dispose the plugin.
  23171. player.on('dispose', this.dispose);
  23172. }
  23173. /**
  23174. * Get the version of the plugin that was set on <pluginName>.VERSION
  23175. */
  23176. version() {
  23177. return this.constructor.VERSION;
  23178. }
  23179. /**
  23180. * Each event triggered by plugins includes a hash of additional data with
  23181. * conventional properties.
  23182. *
  23183. * This returns that object or mutates an existing hash.
  23184. *
  23185. * @param {Object} [hash={}]
  23186. * An object to be used as event an event hash.
  23187. *
  23188. * @return {Plugin~PluginEventHash}
  23189. * An event hash object with provided properties mixed-in.
  23190. */
  23191. getEventHash(hash = {}) {
  23192. hash.name = this.name;
  23193. hash.plugin = this.constructor;
  23194. hash.instance = this;
  23195. return hash;
  23196. }
  23197. /**
  23198. * Triggers an event on the plugin object and overrides
  23199. * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
  23200. *
  23201. * @param {string|Object} event
  23202. * An event type or an object with a type property.
  23203. *
  23204. * @param {Object} [hash={}]
  23205. * Additional data hash to merge with a
  23206. * {@link Plugin~PluginEventHash|PluginEventHash}.
  23207. *
  23208. * @return {boolean}
  23209. * Whether or not default was prevented.
  23210. */
  23211. trigger(event, hash = {}) {
  23212. return trigger(this.eventBusEl_, event, this.getEventHash(hash));
  23213. }
  23214. /**
  23215. * Handles "statechanged" events on the plugin. No-op by default, override by
  23216. * subclassing.
  23217. *
  23218. * @abstract
  23219. * @param {Event} e
  23220. * An event object provided by a "statechanged" event.
  23221. *
  23222. * @param {Object} e.changes
  23223. * An object describing changes that occurred with the "statechanged"
  23224. * event.
  23225. */
  23226. handleStateChanged(e) {}
  23227. /**
  23228. * Disposes a plugin.
  23229. *
  23230. * Subclasses can override this if they want, but for the sake of safety,
  23231. * it's probably best to subscribe the "dispose" event.
  23232. *
  23233. * @fires Plugin#dispose
  23234. */
  23235. dispose() {
  23236. const {
  23237. name,
  23238. player
  23239. } = this;
  23240. /**
  23241. * Signals that a advanced plugin is about to be disposed.
  23242. *
  23243. * @event Plugin#dispose
  23244. * @type {Event}
  23245. */
  23246. this.trigger('dispose');
  23247. this.off();
  23248. player.off('dispose', this.dispose);
  23249. // Eliminate any possible sources of leaking memory by clearing up
  23250. // references between the player and the plugin instance and nulling out
  23251. // the plugin's state and replacing methods with a function that throws.
  23252. player[PLUGIN_CACHE_KEY][name] = false;
  23253. this.player = this.state = null;
  23254. // Finally, replace the plugin name on the player with a new factory
  23255. // function, so that the plugin is ready to be set up again.
  23256. player[name] = createPluginFactory(name, pluginStorage[name]);
  23257. }
  23258. /**
  23259. * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
  23260. *
  23261. * @param {string|Function} plugin
  23262. * If a string, matches the name of a plugin. If a function, will be
  23263. * tested directly.
  23264. *
  23265. * @return {boolean}
  23266. * Whether or not a plugin is a basic plugin.
  23267. */
  23268. static isBasic(plugin) {
  23269. const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
  23270. return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
  23271. }
  23272. /**
  23273. * Register a Video.js plugin.
  23274. *
  23275. * @param {string} name
  23276. * The name of the plugin to be registered. Must be a string and
  23277. * must not match an existing plugin or a method on the `Player`
  23278. * prototype.
  23279. *
  23280. * @param {typeof Plugin|Function} plugin
  23281. * A sub-class of `Plugin` or a function for basic plugins.
  23282. *
  23283. * @return {typeof Plugin|Function}
  23284. * For advanced plugins, a factory function for that plugin. For
  23285. * basic plugins, a wrapper function that initializes the plugin.
  23286. */
  23287. static registerPlugin(name, plugin) {
  23288. if (typeof name !== 'string') {
  23289. throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
  23290. }
  23291. if (pluginExists(name)) {
  23292. log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
  23293. } else if (Player.prototype.hasOwnProperty(name)) {
  23294. throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
  23295. }
  23296. if (typeof plugin !== 'function') {
  23297. throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
  23298. }
  23299. pluginStorage[name] = plugin;
  23300. // Add a player prototype method for all sub-classed plugins (but not for
  23301. // the base Plugin class).
  23302. if (name !== BASE_PLUGIN_NAME) {
  23303. if (Plugin.isBasic(plugin)) {
  23304. Player.prototype[name] = createBasicPlugin(name, plugin);
  23305. } else {
  23306. Player.prototype[name] = createPluginFactory(name, plugin);
  23307. }
  23308. }
  23309. return plugin;
  23310. }
  23311. /**
  23312. * De-register a Video.js plugin.
  23313. *
  23314. * @param {string} name
  23315. * The name of the plugin to be de-registered. Must be a string that
  23316. * matches an existing plugin.
  23317. *
  23318. * @throws {Error}
  23319. * If an attempt is made to de-register the base plugin.
  23320. */
  23321. static deregisterPlugin(name) {
  23322. if (name === BASE_PLUGIN_NAME) {
  23323. throw new Error('Cannot de-register base plugin.');
  23324. }
  23325. if (pluginExists(name)) {
  23326. delete pluginStorage[name];
  23327. delete Player.prototype[name];
  23328. }
  23329. }
  23330. /**
  23331. * Gets an object containing multiple Video.js plugins.
  23332. *
  23333. * @param {Array} [names]
  23334. * If provided, should be an array of plugin names. Defaults to _all_
  23335. * plugin names.
  23336. *
  23337. * @return {Object|undefined}
  23338. * An object containing plugin(s) associated with their name(s) or
  23339. * `undefined` if no matching plugins exist).
  23340. */
  23341. static getPlugins(names = Object.keys(pluginStorage)) {
  23342. let result;
  23343. names.forEach(name => {
  23344. const plugin = getPlugin(name);
  23345. if (plugin) {
  23346. result = result || {};
  23347. result[name] = plugin;
  23348. }
  23349. });
  23350. return result;
  23351. }
  23352. /**
  23353. * Gets a plugin's version, if available
  23354. *
  23355. * @param {string} name
  23356. * The name of a plugin.
  23357. *
  23358. * @return {string}
  23359. * The plugin's version or an empty string.
  23360. */
  23361. static getPluginVersion(name) {
  23362. const plugin = getPlugin(name);
  23363. return plugin && plugin.VERSION || '';
  23364. }
  23365. }
  23366. /**
  23367. * Gets a plugin by name if it exists.
  23368. *
  23369. * @static
  23370. * @method getPlugin
  23371. * @memberOf Plugin
  23372. * @param {string} name
  23373. * The name of a plugin.
  23374. *
  23375. * @returns {typeof Plugin|Function|undefined}
  23376. * The plugin (or `undefined`).
  23377. */
  23378. Plugin.getPlugin = getPlugin;
  23379. /**
  23380. * The name of the base plugin class as it is registered.
  23381. *
  23382. * @type {string}
  23383. */
  23384. Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
  23385. Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
  23386. /**
  23387. * Documented in player.js
  23388. *
  23389. * @ignore
  23390. */
  23391. Player.prototype.usingPlugin = function (name) {
  23392. return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
  23393. };
  23394. /**
  23395. * Documented in player.js
  23396. *
  23397. * @ignore
  23398. */
  23399. Player.prototype.hasPlugin = function (name) {
  23400. return !!pluginExists(name);
  23401. };
  23402. /**
  23403. * Signals that a plugin is about to be set up on a player.
  23404. *
  23405. * @event Player#beforepluginsetup
  23406. * @type {Plugin~PluginEventHash}
  23407. */
  23408. /**
  23409. * Signals that a plugin is about to be set up on a player - by name. The name
  23410. * is the name of the plugin.
  23411. *
  23412. * @event Player#beforepluginsetup:$name
  23413. * @type {Plugin~PluginEventHash}
  23414. */
  23415. /**
  23416. * Signals that a plugin has just been set up on a player.
  23417. *
  23418. * @event Player#pluginsetup
  23419. * @type {Plugin~PluginEventHash}
  23420. */
  23421. /**
  23422. * Signals that a plugin has just been set up on a player - by name. The name
  23423. * is the name of the plugin.
  23424. *
  23425. * @event Player#pluginsetup:$name
  23426. * @type {Plugin~PluginEventHash}
  23427. */
  23428. /**
  23429. * @typedef {Object} Plugin~PluginEventHash
  23430. *
  23431. * @property {string} instance
  23432. * For basic plugins, the return value of the plugin function. For
  23433. * advanced plugins, the plugin instance on which the event is fired.
  23434. *
  23435. * @property {string} name
  23436. * The name of the plugin.
  23437. *
  23438. * @property {string} plugin
  23439. * For basic plugins, the plugin function. For advanced plugins, the
  23440. * plugin class/constructor.
  23441. */
  23442. /**
  23443. * @file deprecate.js
  23444. * @module deprecate
  23445. */
  23446. /**
  23447. * Decorate a function with a deprecation message the first time it is called.
  23448. *
  23449. * @param {string} message
  23450. * A deprecation message to log the first time the returned function
  23451. * is called.
  23452. *
  23453. * @param {Function} fn
  23454. * The function to be deprecated.
  23455. *
  23456. * @return {Function}
  23457. * A wrapper function that will log a deprecation warning the first
  23458. * time it is called. The return value will be the return value of
  23459. * the wrapped function.
  23460. */
  23461. function deprecate(message, fn) {
  23462. let warned = false;
  23463. return function (...args) {
  23464. if (!warned) {
  23465. log.warn(message);
  23466. }
  23467. warned = true;
  23468. return fn.apply(this, args);
  23469. };
  23470. }
  23471. /**
  23472. * Internal function used to mark a function as deprecated in the next major
  23473. * version with consistent messaging.
  23474. *
  23475. * @param {number} major The major version where it will be removed
  23476. * @param {string} oldName The old function name
  23477. * @param {string} newName The new function name
  23478. * @param {Function} fn The function to deprecate
  23479. * @return {Function} The decorated function
  23480. */
  23481. function deprecateForMajor(major, oldName, newName, fn) {
  23482. return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
  23483. }
  23484. /**
  23485. * @file video.js
  23486. * @module videojs
  23487. */
  23488. /**
  23489. * Normalize an `id` value by trimming off a leading `#`
  23490. *
  23491. * @private
  23492. * @param {string} id
  23493. * A string, maybe with a leading `#`.
  23494. *
  23495. * @return {string}
  23496. * The string, without any leading `#`.
  23497. */
  23498. const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
  23499. /**
  23500. * A callback that is called when a component is ready. Does not have any
  23501. * parameters and any callback value will be ignored. See: {@link Component~ReadyCallback}
  23502. *
  23503. * @callback ReadyCallback
  23504. */
  23505. /**
  23506. * The `videojs()` function doubles as the main function for users to create a
  23507. * {@link Player} instance as well as the main library namespace.
  23508. *
  23509. * It can also be used as a getter for a pre-existing {@link Player} instance.
  23510. * However, we _strongly_ recommend using `videojs.getPlayer()` for this
  23511. * purpose because it avoids any potential for unintended initialization.
  23512. *
  23513. * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
  23514. * of our JSDoc template, we cannot properly document this as both a function
  23515. * and a namespace, so its function signature is documented here.
  23516. *
  23517. * #### Arguments
  23518. * ##### id
  23519. * string|Element, **required**
  23520. *
  23521. * Video element or video element ID.
  23522. *
  23523. * ##### options
  23524. * Object, optional
  23525. *
  23526. * Options object for providing settings.
  23527. * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
  23528. *
  23529. * ##### ready
  23530. * {@link Component~ReadyCallback}, optional
  23531. *
  23532. * A function to be called when the {@link Player} and {@link Tech} are ready.
  23533. *
  23534. * #### Return Value
  23535. *
  23536. * The `videojs()` function returns a {@link Player} instance.
  23537. *
  23538. * @namespace
  23539. *
  23540. * @borrows AudioTrack as AudioTrack
  23541. * @borrows Component.getComponent as getComponent
  23542. * @borrows module:events.on as on
  23543. * @borrows module:events.one as one
  23544. * @borrows module:events.off as off
  23545. * @borrows module:events.trigger as trigger
  23546. * @borrows EventTarget as EventTarget
  23547. * @borrows module:middleware.use as use
  23548. * @borrows Player.players as players
  23549. * @borrows Plugin.registerPlugin as registerPlugin
  23550. * @borrows Plugin.deregisterPlugin as deregisterPlugin
  23551. * @borrows Plugin.getPlugins as getPlugins
  23552. * @borrows Plugin.getPlugin as getPlugin
  23553. * @borrows Plugin.getPluginVersion as getPluginVersion
  23554. * @borrows Tech.getTech as getTech
  23555. * @borrows Tech.registerTech as registerTech
  23556. * @borrows TextTrack as TextTrack
  23557. * @borrows VideoTrack as VideoTrack
  23558. *
  23559. * @param {string|Element} id
  23560. * Video element or video element ID.
  23561. *
  23562. * @param {Object} [options]
  23563. * Options object for providing settings.
  23564. * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
  23565. *
  23566. * @param {ReadyCallback} [ready]
  23567. * A function to be called when the {@link Player} and {@link Tech} are
  23568. * ready.
  23569. *
  23570. * @return {Player}
  23571. * The `videojs()` function returns a {@link Player|Player} instance.
  23572. */
  23573. function videojs(id, options, ready) {
  23574. let player = videojs.getPlayer(id);
  23575. if (player) {
  23576. if (options) {
  23577. log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
  23578. }
  23579. if (ready) {
  23580. player.ready(ready);
  23581. }
  23582. return player;
  23583. }
  23584. const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
  23585. if (!isEl(el)) {
  23586. throw new TypeError('The element or ID supplied is not valid. (videojs)');
  23587. }
  23588. // document.body.contains(el) will only check if el is contained within that one document.
  23589. // This causes problems for elements in iframes.
  23590. // Instead, use the element's ownerDocument instead of the global document.
  23591. // This will make sure that the element is indeed in the dom of that document.
  23592. // Additionally, check that the document in question has a default view.
  23593. // If the document is no longer attached to the dom, the defaultView of the document will be null.
  23594. if (!el.ownerDocument.defaultView || !el.ownerDocument.body.contains(el)) {
  23595. log.warn('The element supplied is not included in the DOM');
  23596. }
  23597. options = options || {};
  23598. // Store a copy of the el before modification, if it is to be restored in destroy()
  23599. // If div ingest, store the parent div
  23600. if (options.restoreEl === true) {
  23601. options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
  23602. }
  23603. hooks('beforesetup').forEach(hookFunction => {
  23604. const opts = hookFunction(el, merge(options));
  23605. if (!isObject(opts) || Array.isArray(opts)) {
  23606. log.error('please return an object in beforesetup hooks');
  23607. return;
  23608. }
  23609. options = merge(options, opts);
  23610. });
  23611. // We get the current "Player" component here in case an integration has
  23612. // replaced it with a custom player.
  23613. const PlayerComponent = Component.getComponent('Player');
  23614. player = new PlayerComponent(el, options, ready);
  23615. hooks('setup').forEach(hookFunction => hookFunction(player));
  23616. return player;
  23617. }
  23618. videojs.hooks_ = hooks_;
  23619. videojs.hooks = hooks;
  23620. videojs.hook = hook;
  23621. videojs.hookOnce = hookOnce;
  23622. videojs.removeHook = removeHook;
  23623. // Add default styles
  23624. if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
  23625. let style = $('.vjs-styles-defaults');
  23626. if (!style) {
  23627. style = createStyleElement('vjs-styles-defaults');
  23628. const head = $('head');
  23629. if (head) {
  23630. head.insertBefore(style, head.firstChild);
  23631. }
  23632. setTextContent(style, `
  23633. .video-js {
  23634. width: 300px;
  23635. height: 150px;
  23636. }
  23637. .vjs-fluid:not(.vjs-audio-only-mode) {
  23638. padding-top: 56.25%
  23639. }
  23640. `);
  23641. }
  23642. }
  23643. // Run Auto-load players
  23644. // You have to wait at least once in case this script is loaded after your
  23645. // video in the DOM (weird behavior only with minified version)
  23646. autoSetupTimeout(1, videojs);
  23647. /**
  23648. * Current Video.js version. Follows [semantic versioning](https://semver.org/).
  23649. *
  23650. * @type {string}
  23651. */
  23652. videojs.VERSION = version;
  23653. /**
  23654. * The global options object. These are the settings that take effect
  23655. * if no overrides are specified when the player is created.
  23656. *
  23657. * @type {Object}
  23658. */
  23659. videojs.options = Player.prototype.options_;
  23660. /**
  23661. * Get an object with the currently created players, keyed by player ID
  23662. *
  23663. * @return {Object}
  23664. * The created players
  23665. */
  23666. videojs.getPlayers = () => Player.players;
  23667. /**
  23668. * Get a single player based on an ID or DOM element.
  23669. *
  23670. * This is useful if you want to check if an element or ID has an associated
  23671. * Video.js player, but not create one if it doesn't.
  23672. *
  23673. * @param {string|Element} id
  23674. * An HTML element - `<video>`, `<audio>`, or `<video-js>` -
  23675. * or a string matching the `id` of such an element.
  23676. *
  23677. * @return {Player|undefined}
  23678. * A player instance or `undefined` if there is no player instance
  23679. * matching the argument.
  23680. */
  23681. videojs.getPlayer = id => {
  23682. const players = Player.players;
  23683. let tag;
  23684. if (typeof id === 'string') {
  23685. const nId = normalizeId(id);
  23686. const player = players[nId];
  23687. if (player) {
  23688. return player;
  23689. }
  23690. tag = $('#' + nId);
  23691. } else {
  23692. tag = id;
  23693. }
  23694. if (isEl(tag)) {
  23695. const {
  23696. player,
  23697. playerId
  23698. } = tag;
  23699. // Element may have a `player` property referring to an already created
  23700. // player instance. If so, return that.
  23701. if (player || players[playerId]) {
  23702. return player || players[playerId];
  23703. }
  23704. }
  23705. };
  23706. /**
  23707. * Returns an array of all current players.
  23708. *
  23709. * @return {Array}
  23710. * An array of all players. The array will be in the order that
  23711. * `Object.keys` provides, which could potentially vary between
  23712. * JavaScript engines.
  23713. *
  23714. */
  23715. videojs.getAllPlayers = () =>
  23716. // Disposed players leave a key with a `null` value, so we need to make sure
  23717. // we filter those out.
  23718. Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
  23719. videojs.players = Player.players;
  23720. videojs.getComponent = Component.getComponent;
  23721. /**
  23722. * Register a component so it can referred to by name. Used when adding to other
  23723. * components, either through addChild `component.addChild('myComponent')` or through
  23724. * default children options `{ children: ['myComponent'] }`.
  23725. *
  23726. * > NOTE: You could also just initialize the component before adding.
  23727. * `component.addChild(new MyComponent());`
  23728. *
  23729. * @param {string} name
  23730. * The class name of the component
  23731. *
  23732. * @param {Component} comp
  23733. * The component class
  23734. *
  23735. * @return {Component}
  23736. * The newly registered component
  23737. */
  23738. videojs.registerComponent = (name, comp) => {
  23739. if (Tech.isTech(comp)) {
  23740. log.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
  23741. }
  23742. Component.registerComponent.call(Component, name, comp);
  23743. };
  23744. videojs.getTech = Tech.getTech;
  23745. videojs.registerTech = Tech.registerTech;
  23746. videojs.use = use;
  23747. /**
  23748. * An object that can be returned by a middleware to signify
  23749. * that the middleware is being terminated.
  23750. *
  23751. * @type {object}
  23752. * @property {object} middleware.TERMINATOR
  23753. */
  23754. Object.defineProperty(videojs, 'middleware', {
  23755. value: {},
  23756. writeable: false,
  23757. enumerable: true
  23758. });
  23759. Object.defineProperty(videojs.middleware, 'TERMINATOR', {
  23760. value: TERMINATOR,
  23761. writeable: false,
  23762. enumerable: true
  23763. });
  23764. /**
  23765. * A reference to the {@link module:browser|browser utility module} as an object.
  23766. *
  23767. * @type {Object}
  23768. * @see {@link module:browser|browser}
  23769. */
  23770. videojs.browser = browser;
  23771. /**
  23772. * A reference to the {@link module:obj|obj utility module} as an object.
  23773. *
  23774. * @type {Object}
  23775. * @see {@link module:obj|obj}
  23776. */
  23777. videojs.obj = Obj;
  23778. /**
  23779. * Deprecated reference to the {@link module:obj.merge|merge function}
  23780. *
  23781. * @type {Function}
  23782. * @see {@link module:obj.merge|merge}
  23783. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
  23784. */
  23785. videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge);
  23786. /**
  23787. * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
  23788. *
  23789. * @type {Function}
  23790. * @see {@link module:obj.defineLazyProperty|defineLazyProperty}
  23791. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
  23792. */
  23793. videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
  23794. /**
  23795. * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
  23796. *
  23797. * @type {Function}
  23798. * @see {@link module:fn.bind_|fn.bind_}
  23799. * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
  23800. */
  23801. videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
  23802. videojs.registerPlugin = Plugin.registerPlugin;
  23803. videojs.deregisterPlugin = Plugin.deregisterPlugin;
  23804. /**
  23805. * Deprecated method to register a plugin with Video.js
  23806. *
  23807. * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
  23808. *
  23809. * @param {string} name
  23810. * The plugin name
  23811. *
  23812. * @param {Plugin|Function} plugin
  23813. * The plugin sub-class or function
  23814. */
  23815. videojs.plugin = (name, plugin) => {
  23816. log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
  23817. return Plugin.registerPlugin(name, plugin);
  23818. };
  23819. videojs.getPlugins = Plugin.getPlugins;
  23820. videojs.getPlugin = Plugin.getPlugin;
  23821. videojs.getPluginVersion = Plugin.getPluginVersion;
  23822. /**
  23823. * Adding languages so that they're available to all players.
  23824. * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
  23825. *
  23826. * @param {string} code
  23827. * The language code or dictionary property
  23828. *
  23829. * @param {Object} data
  23830. * The data values to be translated
  23831. *
  23832. * @return {Object}
  23833. * The resulting language dictionary object
  23834. */
  23835. videojs.addLanguage = function (code, data) {
  23836. code = ('' + code).toLowerCase();
  23837. videojs.options.languages = merge(videojs.options.languages, {
  23838. [code]: data
  23839. });
  23840. return videojs.options.languages[code];
  23841. };
  23842. /**
  23843. * A reference to the {@link module:log|log utility module} as an object.
  23844. *
  23845. * @type {Function}
  23846. * @see {@link module:log|log}
  23847. */
  23848. videojs.log = log;
  23849. videojs.createLogger = createLogger;
  23850. /**
  23851. * A reference to the {@link module:time|time utility module} as an object.
  23852. *
  23853. * @type {Object}
  23854. * @see {@link module:time|time}
  23855. */
  23856. videojs.time = Time;
  23857. /**
  23858. * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
  23859. *
  23860. * @type {Function}
  23861. * @see {@link module:time.createTimeRanges|createTimeRanges}
  23862. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
  23863. */
  23864. videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges);
  23865. /**
  23866. * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
  23867. *
  23868. * @type {Function}
  23869. * @see {@link module:time.createTimeRanges|createTimeRanges}
  23870. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
  23871. */
  23872. videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges);
  23873. /**
  23874. * Deprecated reference to the {@link module:time.formatTime|formatTime function}
  23875. *
  23876. * @type {Function}
  23877. * @see {@link module:time.formatTime|formatTime}
  23878. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
  23879. */
  23880. videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
  23881. /**
  23882. * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
  23883. *
  23884. * @type {Function}
  23885. * @see {@link module:time.setFormatTime|setFormatTime}
  23886. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
  23887. */
  23888. videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
  23889. /**
  23890. * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
  23891. *
  23892. * @type {Function}
  23893. * @see {@link module:time.resetFormatTime|resetFormatTime}
  23894. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
  23895. */
  23896. videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
  23897. /**
  23898. * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
  23899. *
  23900. * @type {Function}
  23901. * @see {@link module:url.parseUrl|parseUrl}
  23902. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
  23903. */
  23904. videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
  23905. /**
  23906. * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
  23907. *
  23908. * @type {Function}
  23909. * @see {@link module:url.isCrossOrigin|isCrossOrigin}
  23910. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
  23911. */
  23912. videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
  23913. videojs.EventTarget = EventTarget;
  23914. videojs.any = any;
  23915. videojs.on = on;
  23916. videojs.one = one;
  23917. videojs.off = off;
  23918. videojs.trigger = trigger;
  23919. /**
  23920. * A cross-browser XMLHttpRequest wrapper.
  23921. *
  23922. * @function
  23923. * @param {Object} options
  23924. * Settings for the request.
  23925. *
  23926. * @return {XMLHttpRequest|XDomainRequest}
  23927. * The request object.
  23928. *
  23929. * @see https://github.com/Raynos/xhr
  23930. */
  23931. videojs.xhr = XHR__default["default"];
  23932. videojs.TextTrack = TextTrack;
  23933. videojs.AudioTrack = AudioTrack;
  23934. videojs.VideoTrack = VideoTrack;
  23935. ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
  23936. videojs[k] = function () {
  23937. log.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
  23938. return Dom[k].apply(null, arguments);
  23939. };
  23940. });
  23941. videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
  23942. /**
  23943. * A reference to the {@link module:dom|DOM utility module} as an object.
  23944. *
  23945. * @type {Object}
  23946. * @see {@link module:dom|dom}
  23947. */
  23948. videojs.dom = Dom;
  23949. /**
  23950. * A reference to the {@link module:fn|fn utility module} as an object.
  23951. *
  23952. * @type {Object}
  23953. * @see {@link module:fn|fn}
  23954. */
  23955. videojs.fn = Fn;
  23956. /**
  23957. * A reference to the {@link module:num|num utility module} as an object.
  23958. *
  23959. * @type {Object}
  23960. * @see {@link module:num|num}
  23961. */
  23962. videojs.num = Num;
  23963. /**
  23964. * A reference to the {@link module:str|str utility module} as an object.
  23965. *
  23966. * @type {Object}
  23967. * @see {@link module:str|str}
  23968. */
  23969. videojs.str = Str;
  23970. /**
  23971. * A reference to the {@link module:url|URL utility module} as an object.
  23972. *
  23973. * @type {Object}
  23974. * @see {@link module:url|url}
  23975. */
  23976. videojs.url = Url;
  23977. module.exports = videojs;