core.js 779 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457124581245912460124611246212463124641246512466124671246812469124701247112472124731247412475124761247712478124791248012481124821248312484124851248612487124881248912490124911249212493124941249512496124971249812499125001250112502125031250412505125061250712508125091251012511125121251312514125151251612517125181251912520125211252212523125241252512526125271252812529125301253112532125331253412535125361253712538125391254012541125421254312544125451254612547125481254912550125511255212553125541255512556125571255812559125601256112562125631256412565125661256712568125691257012571125721257312574125751257612577125781257912580125811258212583125841258512586125871258812589125901259112592125931259412595125961259712598125991260012601126021260312604126051260612607126081260912610126111261212613126141261512616126171261812619126201262112622126231262412625126261262712628126291263012631126321263312634126351263612637126381263912640126411264212643126441264512646126471264812649126501265112652126531265412655126561265712658126591266012661126621266312664126651266612667126681266912670126711267212673126741267512676126771267812679126801268112682126831268412685126861268712688126891269012691126921269312694126951269612697126981269912700127011270212703127041270512706127071270812709127101271112712127131271412715127161271712718127191272012721127221272312724127251272612727127281272912730127311273212733127341273512736127371273812739127401274112742127431274412745127461274712748127491275012751127521275312754127551275612757127581275912760127611276212763127641276512766127671276812769127701277112772127731277412775127761277712778127791278012781127821278312784127851278612787127881278912790127911279212793127941279512796127971279812799128001280112802128031280412805128061280712808128091281012811128121281312814128151281612817128181281912820128211282212823128241282512826128271282812829128301283112832128331283412835128361283712838128391284012841128421284312844128451284612847128481284912850128511285212853128541285512856128571285812859128601286112862128631286412865128661286712868128691287012871128721287312874128751287612877128781287912880128811288212883128841288512886128871288812889128901289112892128931289412895128961289712898128991290012901129021290312904129051290612907129081290912910129111291212913129141291512916129171291812919129201292112922129231292412925129261292712928129291293012931129321293312934129351293612937129381293912940129411294212943129441294512946129471294812949129501295112952129531295412955129561295712958129591296012961129621296312964129651296612967129681296912970129711297212973129741297512976129771297812979129801298112982129831298412985129861298712988129891299012991129921299312994129951299612997129981299913000130011300213003130041300513006130071300813009130101301113012130131301413015130161301713018130191302013021130221302313024130251302613027130281302913030130311303213033130341303513036130371303813039130401304113042130431304413045130461304713048130491305013051130521305313054130551305613057130581305913060130611306213063130641306513066130671306813069130701307113072130731307413075130761307713078130791308013081130821308313084130851308613087130881308913090130911309213093130941309513096130971309813099131001310113102131031310413105131061310713108131091311013111131121311313114131151311613117131181311913120131211312213123131241312513126131271312813129131301313113132131331313413135131361313713138131391314013141131421314313144131451314613147131481314913150131511315213153131541315513156131571315813159131601316113162131631316413165131661316713168131691317013171131721317313174131751317613177131781317913180131811318213183131841318513186131871318813189131901319113192131931319413195131961319713198131991320013201132021320313204132051320613207132081320913210132111321213213132141321513216132171321813219132201322113222132231322413225132261322713228132291323013231132321323313234132351323613237132381323913240132411324213243132441324513246132471324813249132501325113252132531325413255132561325713258132591326013261132621326313264132651326613267132681326913270132711327213273132741327513276132771327813279132801328113282132831328413285132861328713288132891329013291132921329313294132951329613297132981329913300133011330213303133041330513306133071330813309133101331113312133131331413315133161331713318133191332013321133221332313324133251332613327133281332913330133311333213333133341333513336133371333813339133401334113342133431334413345133461334713348133491335013351133521335313354133551335613357133581335913360133611336213363133641336513366133671336813369133701337113372133731337413375133761337713378133791338013381133821338313384133851338613387133881338913390133911339213393133941339513396133971339813399134001340113402134031340413405134061340713408134091341013411134121341313414134151341613417134181341913420134211342213423134241342513426134271342813429134301343113432134331343413435134361343713438134391344013441134421344313444134451344613447134481344913450134511345213453134541345513456134571345813459134601346113462134631346413465134661346713468134691347013471134721347313474134751347613477134781347913480134811348213483134841348513486134871348813489134901349113492134931349413495134961349713498134991350013501135021350313504135051350613507135081350913510135111351213513135141351513516135171351813519135201352113522135231352413525135261352713528135291353013531135321353313534135351353613537135381353913540135411354213543135441354513546135471354813549135501355113552135531355413555135561355713558135591356013561135621356313564135651356613567135681356913570135711357213573135741357513576135771357813579135801358113582135831358413585135861358713588135891359013591135921359313594135951359613597135981359913600136011360213603136041360513606136071360813609136101361113612136131361413615136161361713618136191362013621136221362313624136251362613627136281362913630136311363213633136341363513636136371363813639136401364113642136431364413645136461364713648136491365013651136521365313654136551365613657136581365913660136611366213663136641366513666136671366813669136701367113672136731367413675136761367713678136791368013681136821368313684136851368613687136881368913690136911369213693136941369513696136971369813699137001370113702137031370413705137061370713708137091371013711137121371313714137151371613717137181371913720137211372213723137241372513726137271372813729137301373113732137331373413735137361373713738137391374013741137421374313744137451374613747137481374913750137511375213753137541375513756137571375813759137601376113762137631376413765137661376713768137691377013771137721377313774137751377613777137781377913780137811378213783137841378513786137871378813789137901379113792137931379413795137961379713798137991380013801138021380313804138051380613807138081380913810138111381213813138141381513816138171381813819138201382113822138231382413825138261382713828138291383013831138321383313834138351383613837138381383913840138411384213843138441384513846138471384813849138501385113852138531385413855138561385713858138591386013861138621386313864138651386613867138681386913870138711387213873138741387513876138771387813879138801388113882138831388413885138861388713888138891389013891138921389313894138951389613897138981389913900139011390213903139041390513906139071390813909139101391113912139131391413915139161391713918139191392013921139221392313924139251392613927139281392913930139311393213933139341393513936139371393813939139401394113942139431394413945139461394713948139491395013951139521395313954139551395613957139581395913960139611396213963139641396513966139671396813969139701397113972139731397413975139761397713978139791398013981139821398313984139851398613987139881398913990139911399213993139941399513996139971399813999140001400114002140031400414005140061400714008140091401014011140121401314014140151401614017140181401914020140211402214023140241402514026140271402814029140301403114032140331403414035140361403714038140391404014041140421404314044140451404614047140481404914050140511405214053140541405514056140571405814059140601406114062140631406414065140661406714068140691407014071140721407314074140751407614077140781407914080140811408214083140841408514086140871408814089140901409114092140931409414095140961409714098140991410014101141021410314104141051410614107141081410914110141111411214113141141411514116141171411814119141201412114122141231412414125141261412714128141291413014131141321413314134141351413614137141381413914140141411414214143141441414514146141471414814149141501415114152141531415414155141561415714158141591416014161141621416314164141651416614167141681416914170141711417214173141741417514176141771417814179141801418114182141831418414185141861418714188141891419014191141921419314194141951419614197141981419914200142011420214203142041420514206142071420814209142101421114212142131421414215142161421714218142191422014221142221422314224142251422614227142281422914230142311423214233142341423514236142371423814239142401424114242142431424414245142461424714248142491425014251142521425314254142551425614257142581425914260142611426214263142641426514266142671426814269142701427114272142731427414275142761427714278142791428014281142821428314284142851428614287142881428914290142911429214293142941429514296142971429814299143001430114302143031430414305143061430714308143091431014311143121431314314143151431614317143181431914320143211432214323143241432514326143271432814329143301433114332143331433414335143361433714338143391434014341143421434314344143451434614347143481434914350143511435214353143541435514356143571435814359143601436114362143631436414365143661436714368143691437014371143721437314374143751437614377143781437914380143811438214383143841438514386143871438814389143901439114392143931439414395143961439714398143991440014401144021440314404144051440614407144081440914410144111441214413144141441514416144171441814419144201442114422144231442414425144261442714428144291443014431144321443314434144351443614437144381443914440144411444214443144441444514446144471444814449144501445114452144531445414455144561445714458144591446014461144621446314464144651446614467144681446914470144711447214473144741447514476144771447814479144801448114482144831448414485144861448714488144891449014491144921449314494144951449614497144981449914500145011450214503145041450514506145071450814509145101451114512145131451414515145161451714518145191452014521145221452314524145251452614527145281452914530145311453214533145341453514536145371453814539145401454114542145431454414545145461454714548145491455014551145521455314554145551455614557145581455914560145611456214563145641456514566145671456814569145701457114572145731457414575145761457714578145791458014581145821458314584145851458614587145881458914590145911459214593145941459514596145971459814599146001460114602146031460414605146061460714608146091461014611146121461314614146151461614617146181461914620146211462214623146241462514626146271462814629146301463114632146331463414635146361463714638146391464014641146421464314644146451464614647146481464914650146511465214653146541465514656146571465814659146601466114662146631466414665146661466714668146691467014671146721467314674146751467614677146781467914680146811468214683146841468514686146871468814689146901469114692146931469414695146961469714698146991470014701147021470314704147051470614707147081470914710147111471214713147141471514716147171471814719147201472114722147231472414725147261472714728147291473014731147321473314734147351473614737147381473914740147411474214743147441474514746147471474814749147501475114752147531475414755147561475714758147591476014761147621476314764147651476614767147681476914770147711477214773147741477514776147771477814779147801478114782147831478414785147861478714788147891479014791147921479314794147951479614797147981479914800148011480214803148041480514806148071480814809148101481114812148131481414815148161481714818148191482014821148221482314824148251482614827148281482914830148311483214833148341483514836148371483814839148401484114842148431484414845148461484714848148491485014851148521485314854148551485614857148581485914860148611486214863148641486514866148671486814869148701487114872148731487414875148761487714878148791488014881148821488314884148851488614887148881488914890148911489214893148941489514896148971489814899149001490114902149031490414905149061490714908149091491014911149121491314914149151491614917149181491914920149211492214923149241492514926149271492814929149301493114932149331493414935149361493714938149391494014941149421494314944149451494614947149481494914950149511495214953149541495514956149571495814959149601496114962149631496414965149661496714968149691497014971149721497314974149751497614977149781497914980149811498214983149841498514986149871498814989149901499114992149931499414995149961499714998149991500015001150021500315004150051500615007150081500915010150111501215013150141501515016150171501815019150201502115022150231502415025150261502715028150291503015031150321503315034150351503615037150381503915040150411504215043150441504515046150471504815049150501505115052150531505415055150561505715058150591506015061150621506315064150651506615067150681506915070150711507215073150741507515076150771507815079150801508115082150831508415085150861508715088150891509015091150921509315094150951509615097150981509915100151011510215103151041510515106151071510815109151101511115112151131511415115151161511715118151191512015121151221512315124151251512615127151281512915130151311513215133151341513515136151371513815139151401514115142151431514415145151461514715148151491515015151151521515315154151551515615157151581515915160151611516215163151641516515166151671516815169151701517115172151731517415175151761517715178151791518015181151821518315184151851518615187151881518915190151911519215193151941519515196151971519815199152001520115202152031520415205152061520715208152091521015211152121521315214152151521615217152181521915220152211522215223152241522515226152271522815229152301523115232152331523415235152361523715238152391524015241152421524315244152451524615247152481524915250152511525215253152541525515256152571525815259152601526115262152631526415265152661526715268152691527015271152721527315274152751527615277152781527915280152811528215283152841528515286152871528815289152901529115292152931529415295152961529715298152991530015301153021530315304153051530615307153081530915310153111531215313153141531515316153171531815319153201532115322153231532415325153261532715328153291533015331153321533315334153351533615337153381533915340153411534215343153441534515346153471534815349153501535115352153531535415355153561535715358153591536015361153621536315364153651536615367153681536915370153711537215373153741537515376153771537815379153801538115382153831538415385153861538715388153891539015391153921539315394153951539615397153981539915400154011540215403154041540515406154071540815409154101541115412154131541415415154161541715418154191542015421154221542315424154251542615427154281542915430154311543215433154341543515436154371543815439154401544115442154431544415445154461544715448154491545015451154521545315454154551545615457154581545915460154611546215463154641546515466154671546815469154701547115472154731547415475154761547715478154791548015481154821548315484154851548615487154881548915490154911549215493154941549515496154971549815499155001550115502155031550415505155061550715508155091551015511155121551315514155151551615517155181551915520155211552215523155241552515526155271552815529155301553115532155331553415535155361553715538155391554015541155421554315544155451554615547155481554915550155511555215553155541555515556155571555815559155601556115562155631556415565155661556715568155691557015571155721557315574155751557615577155781557915580155811558215583155841558515586155871558815589155901559115592155931559415595155961559715598155991560015601156021560315604156051560615607156081560915610156111561215613156141561515616156171561815619156201562115622156231562415625156261562715628156291563015631156321563315634156351563615637156381563915640156411564215643156441564515646156471564815649156501565115652156531565415655156561565715658156591566015661156621566315664156651566615667156681566915670156711567215673156741567515676156771567815679156801568115682156831568415685156861568715688156891569015691156921569315694156951569615697156981569915700157011570215703157041570515706157071570815709157101571115712157131571415715157161571715718157191572015721157221572315724157251572615727157281572915730157311573215733157341573515736157371573815739157401574115742157431574415745157461574715748157491575015751157521575315754157551575615757157581575915760157611576215763157641576515766157671576815769157701577115772157731577415775157761577715778157791578015781157821578315784157851578615787157881578915790157911579215793157941579515796157971579815799158001580115802158031580415805158061580715808158091581015811158121581315814158151581615817158181581915820158211582215823158241582515826158271582815829158301583115832158331583415835158361583715838158391584015841158421584315844158451584615847158481584915850158511585215853158541585515856158571585815859158601586115862158631586415865158661586715868158691587015871158721587315874158751587615877158781587915880158811588215883158841588515886158871588815889158901589115892158931589415895158961589715898158991590015901159021590315904159051590615907159081590915910159111591215913159141591515916159171591815919159201592115922159231592415925159261592715928159291593015931159321593315934159351593615937159381593915940159411594215943159441594515946159471594815949159501595115952159531595415955159561595715958159591596015961159621596315964159651596615967159681596915970159711597215973159741597515976159771597815979159801598115982159831598415985159861598715988159891599015991159921599315994159951599615997159981599916000160011600216003160041600516006160071600816009160101601116012160131601416015160161601716018160191602016021160221602316024160251602616027160281602916030160311603216033160341603516036160371603816039160401604116042160431604416045160461604716048160491605016051160521605316054160551605616057160581605916060160611606216063160641606516066160671606816069160701607116072160731607416075160761607716078160791608016081160821608316084160851608616087160881608916090160911609216093160941609516096160971609816099161001610116102161031610416105161061610716108161091611016111161121611316114161151611616117161181611916120161211612216123161241612516126161271612816129161301613116132161331613416135161361613716138161391614016141161421614316144161451614616147161481614916150161511615216153161541615516156161571615816159161601616116162161631616416165161661616716168161691617016171161721617316174161751617616177161781617916180161811618216183161841618516186161871618816189161901619116192161931619416195161961619716198161991620016201162021620316204162051620616207162081620916210162111621216213162141621516216162171621816219162201622116222162231622416225162261622716228162291623016231162321623316234162351623616237162381623916240162411624216243162441624516246162471624816249162501625116252162531625416255162561625716258162591626016261162621626316264162651626616267162681626916270162711627216273162741627516276162771627816279162801628116282162831628416285162861628716288162891629016291162921629316294162951629616297162981629916300163011630216303163041630516306163071630816309163101631116312163131631416315163161631716318163191632016321163221632316324163251632616327163281632916330163311633216333163341633516336163371633816339163401634116342163431634416345163461634716348163491635016351163521635316354163551635616357163581635916360163611636216363163641636516366163671636816369163701637116372163731637416375163761637716378163791638016381163821638316384163851638616387163881638916390163911639216393163941639516396163971639816399164001640116402164031640416405164061640716408164091641016411164121641316414164151641616417164181641916420164211642216423164241642516426164271642816429164301643116432164331643416435164361643716438164391644016441164421644316444164451644616447164481644916450164511645216453164541645516456164571645816459164601646116462164631646416465164661646716468164691647016471164721647316474164751647616477164781647916480164811648216483164841648516486164871648816489164901649116492164931649416495164961649716498164991650016501165021650316504165051650616507165081650916510165111651216513165141651516516165171651816519165201652116522165231652416525165261652716528165291653016531165321653316534165351653616537165381653916540165411654216543165441654516546165471654816549165501655116552165531655416555165561655716558165591656016561165621656316564165651656616567165681656916570165711657216573165741657516576165771657816579165801658116582165831658416585165861658716588165891659016591165921659316594165951659616597165981659916600166011660216603166041660516606166071660816609166101661116612166131661416615166161661716618166191662016621166221662316624166251662616627166281662916630166311663216633166341663516636166371663816639166401664116642166431664416645166461664716648166491665016651166521665316654166551665616657166581665916660166611666216663166641666516666166671666816669166701667116672166731667416675166761667716678166791668016681166821668316684166851668616687166881668916690166911669216693166941669516696166971669816699167001670116702167031670416705167061670716708167091671016711167121671316714167151671616717167181671916720167211672216723167241672516726167271672816729167301673116732167331673416735167361673716738167391674016741167421674316744167451674616747167481674916750167511675216753167541675516756167571675816759167601676116762167631676416765167661676716768167691677016771167721677316774167751677616777167781677916780167811678216783167841678516786167871678816789167901679116792167931679416795167961679716798167991680016801168021680316804168051680616807168081680916810168111681216813168141681516816168171681816819168201682116822168231682416825168261682716828168291683016831168321683316834168351683616837168381683916840168411684216843168441684516846168471684816849168501685116852168531685416855168561685716858168591686016861168621686316864168651686616867168681686916870168711687216873168741687516876168771687816879168801688116882168831688416885168861688716888168891689016891168921689316894168951689616897168981689916900169011690216903169041690516906169071690816909169101691116912169131691416915169161691716918169191692016921169221692316924169251692616927169281692916930169311693216933169341693516936169371693816939169401694116942169431694416945169461694716948169491695016951169521695316954169551695616957169581695916960169611696216963169641696516966169671696816969169701697116972169731697416975169761697716978169791698016981169821698316984169851698616987169881698916990169911699216993169941699516996169971699816999170001700117002170031700417005170061700717008170091701017011170121701317014170151701617017170181701917020170211702217023170241702517026170271702817029170301703117032170331703417035170361703717038170391704017041170421704317044170451704617047170481704917050170511705217053170541705517056170571705817059170601706117062170631706417065170661706717068170691707017071170721707317074170751707617077170781707917080170811708217083170841708517086170871708817089170901709117092170931709417095170961709717098170991710017101171021710317104171051710617107171081710917110171111711217113171141711517116171171711817119171201712117122171231712417125171261712717128171291713017131171321713317134171351713617137171381713917140171411714217143171441714517146171471714817149171501715117152171531715417155171561715717158171591716017161171621716317164171651716617167171681716917170171711717217173171741717517176171771717817179171801718117182171831718417185171861718717188171891719017191171921719317194171951719617197171981719917200172011720217203172041720517206172071720817209172101721117212172131721417215172161721717218172191722017221172221722317224172251722617227172281722917230172311723217233172341723517236172371723817239172401724117242172431724417245172461724717248172491725017251172521725317254172551725617257172581725917260172611726217263172641726517266172671726817269172701727117272172731727417275172761727717278172791728017281172821728317284172851728617287172881728917290172911729217293172941729517296172971729817299173001730117302173031730417305173061730717308173091731017311173121731317314173151731617317173181731917320173211732217323173241732517326173271732817329173301733117332173331733417335173361733717338173391734017341173421734317344173451734617347173481734917350173511735217353173541735517356173571735817359173601736117362173631736417365173661736717368173691737017371173721737317374173751737617377173781737917380173811738217383173841738517386173871738817389173901739117392173931739417395173961739717398173991740017401174021740317404174051740617407174081740917410174111741217413174141741517416174171741817419174201742117422174231742417425174261742717428174291743017431174321743317434174351743617437174381743917440174411744217443174441744517446174471744817449174501745117452174531745417455174561745717458174591746017461174621746317464174651746617467174681746917470174711747217473174741747517476174771747817479174801748117482174831748417485174861748717488174891749017491174921749317494174951749617497174981749917500175011750217503175041750517506175071750817509175101751117512175131751417515175161751717518175191752017521175221752317524175251752617527175281752917530175311753217533175341753517536175371753817539175401754117542175431754417545175461754717548175491755017551175521755317554175551755617557175581755917560175611756217563175641756517566175671756817569175701757117572175731757417575175761757717578175791758017581175821758317584175851758617587175881758917590175911759217593175941759517596175971759817599176001760117602176031760417605176061760717608176091761017611176121761317614176151761617617176181761917620176211762217623176241762517626176271762817629176301763117632176331763417635176361763717638176391764017641176421764317644176451764617647176481764917650176511765217653176541765517656176571765817659176601766117662176631766417665176661766717668176691767017671176721767317674176751767617677176781767917680176811768217683176841768517686176871768817689176901769117692176931769417695176961769717698176991770017701177021770317704177051770617707177081770917710177111771217713177141771517716177171771817719177201772117722177231772417725177261772717728177291773017731177321773317734177351773617737177381773917740177411774217743177441774517746177471774817749177501775117752177531775417755177561775717758177591776017761177621776317764177651776617767177681776917770177711777217773177741777517776177771777817779177801778117782177831778417785177861778717788177891779017791177921779317794177951779617797177981779917800178011780217803178041780517806178071780817809178101781117812178131781417815178161781717818178191782017821178221782317824178251782617827178281782917830178311783217833178341783517836178371783817839178401784117842178431784417845178461784717848178491785017851178521785317854178551785617857178581785917860178611786217863178641786517866178671786817869178701787117872178731787417875178761787717878178791788017881178821788317884178851788617887178881788917890178911789217893178941789517896178971789817899179001790117902179031790417905179061790717908179091791017911179121791317914179151791617917179181791917920179211792217923179241792517926179271792817929179301793117932179331793417935179361793717938179391794017941179421794317944179451794617947179481794917950179511795217953179541795517956179571795817959179601796117962179631796417965179661796717968179691797017971179721797317974179751797617977179781797917980179811798217983179841798517986179871798817989179901799117992179931799417995179961799717998179991800018001180021800318004180051800618007180081800918010180111801218013180141801518016180171801818019180201802118022180231802418025180261802718028180291803018031180321803318034180351803618037180381803918040180411804218043180441804518046180471804818049180501805118052180531805418055180561805718058180591806018061180621806318064180651806618067180681806918070180711807218073180741807518076180771807818079180801808118082180831808418085180861808718088180891809018091180921809318094180951809618097180981809918100181011810218103181041810518106181071810818109181101811118112181131811418115181161811718118181191812018121181221812318124181251812618127181281812918130181311813218133181341813518136181371813818139181401814118142181431814418145181461814718148181491815018151181521815318154181551815618157181581815918160181611816218163181641816518166181671816818169181701817118172181731817418175181761817718178181791818018181181821818318184181851818618187181881818918190181911819218193181941819518196181971819818199182001820118202182031820418205182061820718208182091821018211182121821318214182151821618217182181821918220182211822218223182241822518226182271822818229182301823118232182331823418235182361823718238182391824018241182421824318244182451824618247182481824918250182511825218253182541825518256182571825818259182601826118262182631826418265182661826718268182691827018271182721827318274182751827618277182781827918280182811828218283182841828518286182871828818289182901829118292182931829418295182961829718298182991830018301183021830318304183051830618307183081830918310183111831218313183141831518316183171831818319183201832118322183231832418325183261832718328183291833018331183321833318334183351833618337183381833918340183411834218343183441834518346183471834818349183501835118352183531835418355183561835718358183591836018361183621836318364183651836618367183681836918370183711837218373183741837518376183771837818379183801838118382183831838418385183861838718388183891839018391183921839318394183951839618397183981839918400184011840218403184041840518406184071840818409184101841118412184131841418415184161841718418184191842018421184221842318424184251842618427184281842918430184311843218433184341843518436184371843818439184401844118442184431844418445184461844718448184491845018451184521845318454184551845618457184581845918460184611846218463184641846518466184671846818469184701847118472184731847418475184761847718478184791848018481184821848318484184851848618487184881848918490184911849218493184941849518496184971849818499185001850118502185031850418505185061850718508185091851018511185121851318514185151851618517185181851918520185211852218523185241852518526185271852818529185301853118532185331853418535185361853718538185391854018541185421854318544185451854618547185481854918550185511855218553185541855518556185571855818559185601856118562185631856418565185661856718568185691857018571185721857318574185751857618577185781857918580185811858218583185841858518586185871858818589185901859118592185931859418595185961859718598185991860018601186021860318604186051860618607186081860918610186111861218613186141861518616186171861818619186201862118622186231862418625186261862718628186291863018631186321863318634186351863618637186381863918640186411864218643186441864518646186471864818649186501865118652186531865418655186561865718658186591866018661186621866318664186651866618667186681866918670186711867218673186741867518676186771867818679186801868118682186831868418685186861868718688186891869018691186921869318694186951869618697186981869918700187011870218703187041870518706187071870818709187101871118712187131871418715187161871718718187191872018721187221872318724187251872618727187281872918730187311873218733187341873518736187371873818739187401874118742187431874418745187461874718748187491875018751187521875318754187551875618757187581875918760187611876218763187641876518766187671876818769187701877118772187731877418775187761877718778187791878018781187821878318784187851878618787187881878918790187911879218793187941879518796187971879818799188001880118802188031880418805188061880718808188091881018811188121881318814188151881618817188181881918820188211882218823188241882518826188271882818829188301883118832188331883418835188361883718838188391884018841188421884318844188451884618847188481884918850188511885218853188541885518856188571885818859188601886118862188631886418865188661886718868188691887018871188721887318874188751887618877188781887918880188811888218883188841888518886188871888818889188901889118892188931889418895188961889718898188991890018901189021890318904189051890618907189081890918910189111891218913189141891518916189171891818919189201892118922189231892418925189261892718928189291893018931189321893318934189351893618937189381893918940189411894218943189441894518946189471894818949189501895118952189531895418955189561895718958189591896018961189621896318964189651896618967189681896918970189711897218973189741897518976189771897818979189801898118982189831898418985189861898718988189891899018991189921899318994189951899618997189981899919000190011900219003190041900519006190071900819009190101901119012190131901419015190161901719018190191902019021190221902319024190251902619027190281902919030190311903219033190341903519036190371903819039190401904119042190431904419045190461904719048190491905019051190521905319054190551905619057190581905919060190611906219063190641906519066190671906819069190701907119072190731907419075190761907719078190791908019081190821908319084190851908619087190881908919090190911909219093190941909519096190971909819099191001910119102191031910419105191061910719108191091911019111191121911319114191151911619117191181911919120191211912219123191241912519126191271912819129191301913119132191331913419135191361913719138191391914019141191421914319144191451914619147191481914919150191511915219153191541915519156191571915819159191601916119162191631916419165191661916719168191691917019171191721917319174191751917619177191781917919180191811918219183191841918519186191871918819189191901919119192191931919419195191961919719198191991920019201192021920319204192051920619207192081920919210192111921219213192141921519216192171921819219192201922119222192231922419225192261922719228192291923019231192321923319234192351923619237192381923919240192411924219243192441924519246192471924819249192501925119252192531925419255192561925719258192591926019261192621926319264192651926619267192681926919270192711927219273192741927519276192771927819279192801928119282192831928419285192861928719288192891929019291192921929319294192951929619297192981929919300193011930219303193041930519306193071930819309193101931119312193131931419315193161931719318193191932019321193221932319324193251932619327193281932919330193311933219333193341933519336193371933819339193401934119342193431934419345193461934719348193491935019351193521935319354193551935619357193581935919360193611936219363193641936519366193671936819369193701937119372193731937419375193761937719378193791938019381193821938319384193851938619387193881938919390193911939219393193941939519396193971939819399194001940119402194031940419405194061940719408194091941019411194121941319414194151941619417194181941919420194211942219423194241942519426194271942819429194301943119432194331943419435194361943719438194391944019441194421944319444194451944619447194481944919450194511945219453194541945519456194571945819459194601946119462194631946419465194661946719468194691947019471194721947319474194751947619477194781947919480194811948219483194841948519486194871948819489194901949119492194931949419495194961949719498194991950019501195021950319504195051950619507195081950919510195111951219513195141951519516195171951819519195201952119522195231952419525195261952719528195291953019531195321953319534195351953619537195381953919540195411954219543195441954519546195471954819549195501955119552195531955419555195561955719558195591956019561195621956319564195651956619567195681956919570195711957219573195741957519576195771957819579195801958119582195831958419585195861958719588195891959019591195921959319594195951959619597195981959919600196011960219603196041960519606196071960819609196101961119612196131961419615196161961719618196191962019621196221962319624196251962619627196281962919630196311963219633196341963519636196371963819639196401964119642196431964419645196461964719648196491965019651196521965319654196551965619657196581965919660196611966219663196641966519666196671966819669196701967119672196731967419675196761967719678196791968019681196821968319684196851968619687196881968919690196911969219693196941969519696196971969819699197001970119702197031970419705197061970719708197091971019711197121971319714197151971619717197181971919720197211972219723197241972519726197271972819729197301973119732197331973419735197361973719738197391974019741197421974319744197451974619747197481974919750197511975219753197541975519756197571975819759197601976119762197631976419765197661976719768197691977019771197721977319774197751977619777197781977919780197811978219783197841978519786197871978819789197901979119792197931979419795197961979719798197991980019801198021980319804198051980619807198081980919810198111981219813198141981519816198171981819819198201982119822198231982419825198261982719828198291983019831198321983319834198351983619837198381983919840198411984219843198441984519846198471984819849198501985119852198531985419855198561985719858198591986019861198621986319864198651986619867198681986919870198711987219873198741987519876198771987819879198801988119882198831988419885198861988719888198891989019891198921989319894198951989619897198981989919900199011990219903199041990519906199071990819909199101991119912199131991419915199161991719918199191992019921199221992319924199251992619927199281992919930199311993219933199341993519936199371993819939199401994119942199431994419945199461994719948199491995019951199521995319954199551995619957199581995919960199611996219963199641996519966199671996819969199701997119972199731997419975199761997719978199791998019981199821998319984199851998619987199881998919990199911999219993199941999519996199971999819999200002000120002200032000420005200062000720008200092001020011200122001320014200152001620017200182001920020200212002220023200242002520026200272002820029200302003120032200332003420035200362003720038200392004020041200422004320044200452004620047200482004920050200512005220053200542005520056200572005820059200602006120062200632006420065200662006720068200692007020071200722007320074200752007620077200782007920080200812008220083200842008520086200872008820089200902009120092200932009420095200962009720098200992010020101201022010320104201052010620107201082010920110201112011220113201142011520116201172011820119201202012120122201232012420125201262012720128201292013020131201322013320134201352013620137201382013920140201412014220143201442014520146201472014820149201502015120152201532015420155201562015720158201592016020161201622016320164201652016620167201682016920170201712017220173201742017520176201772017820179201802018120182201832018420185201862018720188201892019020191201922019320194201952019620197201982019920200202012020220203202042020520206202072020820209202102021120212202132021420215202162021720218202192022020221202222022320224202252022620227202282022920230202312023220233202342023520236202372023820239202402024120242202432024420245202462024720248202492025020251202522025320254202552025620257202582025920260202612026220263202642026520266202672026820269202702027120272202732027420275202762027720278202792028020281202822028320284202852028620287202882028920290202912029220293202942029520296202972029820299203002030120302203032030420305203062030720308203092031020311203122031320314203152031620317203182031920320203212032220323203242032520326203272032820329203302033120332203332033420335203362033720338203392034020341203422034320344203452034620347203482034920350203512035220353203542035520356203572035820359203602036120362203632036420365203662036720368203692037020371203722037320374203752037620377203782037920380203812038220383203842038520386203872038820389203902039120392203932039420395203962039720398203992040020401204022040320404204052040620407204082040920410204112041220413204142041520416204172041820419204202042120422204232042420425204262042720428204292043020431204322043320434204352043620437204382043920440204412044220443204442044520446204472044820449204502045120452204532045420455204562045720458204592046020461204622046320464204652046620467204682046920470204712047220473204742047520476204772047820479204802048120482204832048420485204862048720488204892049020491204922049320494204952049620497204982049920500205012050220503205042050520506205072050820509205102051120512205132051420515205162051720518205192052020521205222052320524205252052620527205282052920530205312053220533205342053520536205372053820539205402054120542205432054420545205462054720548205492055020551205522055320554205552055620557205582055920560205612056220563205642056520566205672056820569205702057120572205732057420575205762057720578205792058020581205822058320584205852058620587205882058920590205912059220593205942059520596205972059820599206002060120602206032060420605206062060720608206092061020611206122061320614206152061620617206182061920620206212062220623206242062520626206272062820629206302063120632206332063420635206362063720638206392064020641206422064320644206452064620647206482064920650206512065220653206542065520656206572065820659206602066120662206632066420665206662066720668206692067020671206722067320674206752067620677206782067920680206812068220683206842068520686206872068820689206902069120692206932069420695206962069720698206992070020701207022070320704207052070620707207082070920710207112071220713207142071520716207172071820719207202072120722207232072420725207262072720728207292073020731207322073320734207352073620737207382073920740207412074220743207442074520746207472074820749207502075120752207532075420755207562075720758207592076020761207622076320764207652076620767207682076920770207712077220773207742077520776207772077820779207802078120782207832078420785207862078720788207892079020791207922079320794207952079620797207982079920800208012080220803208042080520806208072080820809208102081120812208132081420815208162081720818208192082020821208222082320824208252082620827208282082920830208312083220833208342083520836208372083820839208402084120842208432084420845208462084720848208492085020851208522085320854208552085620857208582085920860208612086220863208642086520866208672086820869208702087120872208732087420875208762087720878208792088020881208822088320884208852088620887208882088920890208912089220893208942089520896208972089820899209002090120902209032090420905209062090720908209092091020911209122091320914209152091620917209182091920920209212092220923209242092520926209272092820929209302093120932209332093420935209362093720938209392094020941209422094320944209452094620947209482094920950209512095220953209542095520956209572095820959209602096120962209632096420965209662096720968209692097020971209722097320974209752097620977209782097920980209812098220983209842098520986209872098820989209902099120992209932099420995209962099720998209992100021001210022100321004210052100621007210082100921010210112101221013210142101521016210172101821019210202102121022210232102421025210262102721028210292103021031210322103321034210352103621037210382103921040210412104221043210442104521046210472104821049210502105121052210532105421055210562105721058210592106021061210622106321064210652106621067210682106921070210712107221073210742107521076210772107821079210802108121082210832108421085210862108721088210892109021091210922109321094210952109621097210982109921100211012110221103211042110521106211072110821109211102111121112211132111421115211162111721118211192112021121211222112321124211252112621127211282112921130211312113221133211342113521136211372113821139211402114121142211432114421145211462114721148211492115021151211522115321154211552115621157211582115921160211612116221163211642116521166211672116821169211702117121172211732117421175211762117721178211792118021181211822118321184211852118621187211882118921190211912119221193211942119521196211972119821199212002120121202212032120421205212062120721208212092121021211212122121321214212152121621217212182121921220212212122221223212242122521226212272122821229212302123121232212332123421235212362123721238212392124021241212422124321244212452124621247212482124921250212512125221253212542125521256212572125821259212602126121262212632126421265212662126721268212692127021271212722127321274212752127621277212782127921280212812128221283212842128521286212872128821289212902129121292212932129421295212962129721298212992130021301213022130321304213052130621307213082130921310213112131221313213142131521316213172131821319213202132121322213232132421325213262132721328213292133021331213322133321334213352133621337213382133921340213412134221343213442134521346213472134821349213502135121352213532135421355213562135721358213592136021361213622136321364213652136621367213682136921370213712137221373213742137521376213772137821379213802138121382213832138421385213862138721388213892139021391213922139321394213952139621397213982139921400214012140221403214042140521406214072140821409214102141121412214132141421415214162141721418214192142021421214222142321424214252142621427214282142921430214312143221433214342143521436214372143821439214402144121442214432144421445214462144721448214492145021451214522145321454214552145621457214582145921460214612146221463214642146521466214672146821469214702147121472214732147421475214762147721478214792148021481214822148321484214852148621487214882148921490214912149221493214942149521496214972149821499215002150121502215032150421505215062150721508215092151021511215122151321514215152151621517215182151921520215212152221523215242152521526215272152821529215302153121532215332153421535215362153721538215392154021541215422154321544215452154621547215482154921550215512155221553215542155521556215572155821559215602156121562215632156421565215662156721568215692157021571215722157321574215752157621577215782157921580215812158221583215842158521586215872158821589215902159121592215932159421595215962159721598215992160021601216022160321604216052160621607216082160921610216112161221613216142161521616216172161821619216202162121622216232162421625216262162721628216292163021631216322163321634216352163621637216382163921640216412164221643216442164521646216472164821649216502165121652216532165421655216562165721658216592166021661216622166321664216652166621667216682166921670216712167221673216742167521676216772167821679216802168121682216832168421685216862168721688216892169021691216922169321694216952169621697216982169921700217012170221703217042170521706217072170821709217102171121712217132171421715217162171721718217192172021721217222172321724217252172621727217282172921730217312173221733217342173521736217372173821739217402174121742217432174421745217462174721748217492175021751217522175321754217552175621757217582175921760217612176221763217642176521766217672176821769217702177121772217732177421775217762177721778217792178021781217822178321784217852178621787217882178921790217912179221793217942179521796217972179821799218002180121802218032180421805218062180721808218092181021811218122181321814218152181621817218182181921820218212182221823218242182521826218272182821829218302183121832218332183421835218362183721838218392184021841218422184321844218452184621847218482184921850218512185221853218542185521856218572185821859218602186121862218632186421865218662186721868218692187021871218722187321874218752187621877218782187921880218812188221883218842188521886218872188821889218902189121892218932189421895218962189721898218992190021901219022190321904219052190621907219082190921910219112191221913219142191521916219172191821919219202192121922219232192421925219262192721928219292193021931219322193321934219352193621937219382193921940219412194221943219442194521946219472194821949219502195121952219532195421955219562195721958219592196021961219622196321964219652196621967219682196921970219712197221973219742197521976219772197821979219802198121982219832198421985219862198721988219892199021991219922199321994219952199621997219982199922000220012200222003220042200522006220072200822009220102201122012220132201422015220162201722018220192202022021220222202322024220252202622027220282202922030220312203222033220342203522036220372203822039220402204122042220432204422045220462204722048220492205022051220522205322054220552205622057220582205922060220612206222063220642206522066220672206822069220702207122072220732207422075220762207722078220792208022081220822208322084220852208622087220882208922090220912209222093220942209522096220972209822099221002210122102221032210422105221062210722108221092211022111221122211322114221152211622117221182211922120221212212222123221242212522126221272212822129221302213122132221332213422135221362213722138221392214022141221422214322144221452214622147221482214922150221512215222153221542215522156221572215822159221602216122162221632216422165221662216722168221692217022171221722217322174221752217622177221782217922180221812218222183221842218522186221872218822189221902219122192221932219422195221962219722198221992220022201222022220322204222052220622207222082220922210222112221222213222142221522216222172221822219222202222122222222232222422225222262222722228222292223022231222322223322234222352223622237222382223922240222412224222243222442224522246222472224822249222502225122252222532225422255222562225722258222592226022261222622226322264222652226622267222682226922270222712227222273222742227522276222772227822279222802228122282222832228422285222862228722288222892229022291222922229322294222952229622297222982229922300223012230222303223042230522306223072230822309223102231122312223132231422315223162231722318223192232022321223222232322324223252232622327223282232922330223312233222333223342233522336223372233822339223402234122342223432234422345223462234722348223492235022351223522235322354223552235622357223582235922360223612236222363223642236522366223672236822369223702237122372223732237422375223762237722378223792238022381223822238322384223852238622387223882238922390223912239222393223942239522396223972239822399224002240122402224032240422405224062240722408224092241022411224122241322414224152241622417224182241922420224212242222423224242242522426224272242822429224302243122432224332243422435224362243722438224392244022441224422244322444224452244622447224482244922450224512245222453224542245522456224572245822459224602246122462224632246422465224662246722468224692247022471224722247322474224752247622477224782247922480224812248222483224842248522486224872248822489224902249122492224932249422495224962249722498224992250022501225022250322504225052250622507225082250922510225112251222513225142251522516225172251822519225202252122522225232252422525225262252722528225292253022531225322253322534225352253622537225382253922540225412254222543225442254522546225472254822549225502255122552225532255422555225562255722558225592256022561225622256322564225652256622567225682256922570225712257222573225742257522576225772257822579225802258122582225832258422585225862258722588225892259022591225922259322594225952259622597225982259922600226012260222603226042260522606226072260822609226102261122612226132261422615226162261722618226192262022621226222262322624226252262622627226282262922630226312263222633226342263522636226372263822639226402264122642226432264422645226462264722648226492265022651226522265322654226552265622657226582265922660226612266222663226642266522666226672266822669226702267122672226732267422675226762267722678226792268022681226822268322684226852268622687226882268922690226912269222693226942269522696226972269822699227002270122702227032270422705227062270722708227092271022711227122271322714227152271622717227182271922720227212272222723227242272522726227272272822729227302273122732227332273422735227362273722738227392274022741227422274322744227452274622747227482274922750227512275222753227542275522756227572275822759227602276122762227632276422765227662276722768227692277022771227722277322774227752277622777227782277922780227812278222783227842278522786227872278822789227902279122792227932279422795227962279722798227992280022801228022280322804228052280622807228082280922810228112281222813228142281522816228172281822819228202282122822228232282422825228262282722828228292283022831228322283322834228352283622837228382283922840228412284222843228442284522846228472284822849228502285122852228532285422855228562285722858228592286022861228622286322864228652286622867228682286922870228712287222873228742287522876228772287822879228802288122882228832288422885228862288722888228892289022891228922289322894228952289622897228982289922900229012290222903229042290522906229072290822909229102291122912229132291422915229162291722918229192292022921229222292322924229252292622927229282292922930229312293222933229342293522936229372293822939229402294122942229432294422945229462294722948229492295022951229522295322954229552295622957229582295922960229612296222963229642296522966229672296822969229702297122972229732297422975229762297722978229792298022981229822298322984229852298622987229882298922990229912299222993229942299522996229972299822999230002300123002230032300423005230062300723008230092301023011230122301323014230152301623017230182301923020230212302223023230242302523026230272302823029230302303123032230332303423035230362303723038230392304023041230422304323044230452304623047230482304923050230512305223053230542305523056230572305823059230602306123062230632306423065230662306723068230692307023071230722307323074230752307623077230782307923080230812308223083230842308523086230872308823089230902309123092230932309423095230962309723098230992310023101231022310323104231052310623107231082310923110231112311223113231142311523116231172311823119231202312123122231232312423125231262312723128231292313023131231322313323134231352313623137231382313923140231412314223143231442314523146231472314823149231502315123152231532315423155231562315723158231592316023161231622316323164231652316623167231682316923170231712317223173231742317523176231772317823179231802318123182231832318423185231862318723188231892319023191231922319323194231952319623197231982319923200232012320223203232042320523206232072320823209232102321123212232132321423215232162321723218232192322023221232222322323224232252322623227232282322923230232312323223233232342323523236232372323823239232402324123242232432324423245232462324723248232492325023251232522325323254232552325623257232582325923260232612326223263232642326523266232672326823269232702327123272232732327423275232762327723278232792328023281232822328323284232852328623287232882328923290232912329223293232942329523296232972329823299233002330123302233032330423305233062330723308233092331023311233122331323314233152331623317233182331923320233212332223323233242332523326233272332823329233302333123332233332333423335233362333723338233392334023341233422334323344233452334623347233482334923350233512335223353233542335523356233572335823359233602336123362233632336423365233662336723368233692337023371233722337323374233752337623377233782337923380233812338223383233842338523386233872338823389233902339123392233932339423395233962339723398233992340023401234022340323404234052340623407234082340923410234112341223413234142341523416234172341823419234202342123422234232342423425234262342723428234292343023431234322343323434234352343623437234382343923440234412344223443234442344523446234472344823449234502345123452234532345423455234562345723458234592346023461234622346323464234652346623467234682346923470234712347223473234742347523476234772347823479234802348123482234832348423485234862348723488234892349023491234922349323494234952349623497234982349923500235012350223503235042350523506235072350823509235102351123512235132351423515235162351723518235192352023521235222352323524235252352623527235282352923530235312353223533235342353523536235372353823539235402354123542235432354423545235462354723548235492355023551235522355323554235552355623557235582355923560235612356223563235642356523566235672356823569235702357123572235732357423575235762357723578235792358023581235822358323584235852358623587235882358923590235912359223593235942359523596235972359823599236002360123602236032360423605236062360723608236092361023611236122361323614236152361623617236182361923620236212362223623236242362523626236272362823629236302363123632236332363423635236362363723638236392364023641236422364323644236452364623647236482364923650236512365223653236542365523656236572365823659236602366123662236632366423665236662366723668236692367023671236722367323674236752367623677236782367923680236812368223683236842368523686236872368823689236902369123692236932369423695236962369723698236992370023701237022370323704237052370623707237082370923710237112371223713237142371523716237172371823719237202372123722237232372423725237262372723728237292373023731237322373323734237352373623737237382373923740237412374223743237442374523746237472374823749237502375123752237532375423755237562375723758237592376023761237622376323764237652376623767237682376923770237712377223773237742377523776237772377823779237802378123782237832378423785237862378723788237892379023791237922379323794237952379623797237982379923800238012380223803238042380523806238072380823809238102381123812238132381423815238162381723818238192382023821238222382323824238252382623827238282382923830238312383223833238342383523836238372383823839238402384123842238432384423845238462384723848238492385023851238522385323854238552385623857238582385923860238612386223863238642386523866238672386823869238702387123872238732387423875238762387723878238792388023881238822388323884238852388623887238882388923890238912389223893238942389523896238972389823899239002390123902239032390423905239062390723908239092391023911239122391323914239152391623917239182391923920239212392223923239242392523926239272392823929239302393123932239332393423935239362393723938239392394023941239422394323944239452394623947239482394923950239512395223953239542395523956239572395823959239602396123962239632396423965239662396723968239692397023971239722397323974239752397623977239782397923980239812398223983239842398523986239872398823989239902399123992239932399423995239962399723998239992400024001240022400324004240052400624007240082400924010240112401224013240142401524016240172401824019240202402124022240232402424025240262402724028240292403024031240322403324034240352403624037240382403924040240412404224043240442404524046240472404824049240502405124052240532405424055240562405724058240592406024061240622406324064240652406624067240682406924070240712407224073240742407524076240772407824079240802408124082240832408424085240862408724088240892409024091240922409324094240952409624097240982409924100241012410224103241042410524106241072410824109241102411124112241132411424115241162411724118241192412024121241222412324124241252412624127241282412924130241312413224133241342413524136241372413824139241402414124142241432414424145241462414724148241492415024151241522415324154241552415624157241582415924160241612416224163241642416524166241672416824169241702417124172241732417424175241762417724178241792418024181241822418324184241852418624187241882418924190241912419224193241942419524196241972419824199242002420124202242032420424205242062420724208242092421024211242122421324214242152421624217242182421924220242212422224223242242422524226242272422824229242302423124232242332423424235242362423724238242392424024241242422424324244242452424624247242482424924250242512425224253242542425524256242572425824259242602426124262242632426424265242662426724268242692427024271242722427324274242752427624277242782427924280242812428224283242842428524286242872428824289242902429124292242932429424295242962429724298242992430024301243022430324304243052430624307243082430924310243112431224313243142431524316243172431824319243202432124322243232432424325243262432724328243292433024331243322433324334243352433624337243382433924340243412434224343243442434524346243472434824349243502435124352243532435424355243562435724358243592436024361243622436324364243652436624367243682436924370243712437224373243742437524376243772437824379243802438124382243832438424385243862438724388243892439024391243922439324394243952439624397243982439924400244012440224403244042440524406244072440824409244102441124412244132441424415244162441724418244192442024421244222442324424244252442624427244282442924430244312443224433244342443524436244372443824439244402444124442244432444424445244462444724448244492445024451244522445324454244552445624457244582445924460244612446224463244642446524466244672446824469244702447124472244732447424475244762447724478244792448024481244822448324484244852448624487244882448924490244912449224493244942449524496244972449824499245002450124502245032450424505245062450724508245092451024511245122451324514245152451624517245182451924520245212452224523245242452524526245272452824529245302453124532245332453424535245362453724538245392454024541245422454324544245452454624547245482454924550245512455224553245542455524556245572455824559245602456124562245632456424565245662456724568245692457024571245722457324574245752457624577245782457924580245812458224583245842458524586245872458824589245902459124592245932459424595245962459724598245992460024601246022460324604246052460624607246082460924610246112461224613246142461524616246172461824619246202462124622246232462424625246262462724628246292463024631246322463324634246352463624637246382463924640246412464224643246442464524646246472464824649246502465124652246532465424655246562465724658246592466024661246622466324664246652466624667246682466924670246712467224673246742467524676246772467824679246802468124682246832468424685246862468724688246892469024691246922469324694246952469624697246982469924700247012470224703247042470524706247072470824709247102471124712247132471424715247162471724718247192472024721247222472324724247252472624727247282472924730247312473224733247342473524736247372473824739247402474124742247432474424745247462474724748247492475024751247522475324754247552475624757247582475924760247612476224763247642476524766247672476824769247702477124772247732477424775247762477724778247792478024781247822478324784247852478624787247882478924790247912479224793247942479524796247972479824799248002480124802248032480424805248062480724808248092481024811248122481324814248152481624817248182481924820248212482224823248242482524826248272482824829248302483124832248332483424835248362483724838248392484024841248422484324844248452484624847248482484924850248512485224853248542485524856248572485824859248602486124862248632486424865248662486724868248692487024871248722487324874248752487624877248782487924880248812488224883248842488524886248872488824889248902489124892248932489424895248962489724898248992490024901249022490324904249052490624907249082490924910249112491224913249142491524916249172491824919249202492124922249232492424925249262492724928249292493024931249322493324934249352493624937249382493924940249412494224943249442494524946249472494824949249502495124952249532495424955249562495724958249592496024961249622496324964249652496624967249682496924970249712497224973249742497524976249772497824979249802498124982249832498424985249862498724988249892499024991249922499324994249952499624997249982499925000250012500225003250042500525006250072500825009250102501125012250132501425015250162501725018250192502025021250222502325024250252502625027250282502925030250312503225033250342503525036250372503825039250402504125042250432504425045250462504725048250492505025051250522505325054250552505625057250582505925060250612506225063250642506525066250672506825069250702507125072250732507425075250762507725078250792508025081250822508325084250852508625087250882508925090250912509225093250942509525096250972509825099251002510125102251032510425105251062510725108251092511025111251122511325114251152511625117251182511925120251212512225123251242512525126251272512825129251302513125132251332513425135251362513725138251392514025141251422514325144251452514625147251482514925150251512515225153251542515525156251572515825159251602516125162251632516425165251662516725168251692517025171251722517325174251752517625177251782517925180251812518225183251842518525186251872518825189251902519125192251932519425195251962519725198251992520025201252022520325204252052520625207252082520925210252112521225213252142521525216252172521825219252202522125222252232522425225252262522725228252292523025231252322523325234252352523625237252382523925240252412524225243252442524525246252472524825249252502525125252252532525425255252562525725258252592526025261252622526325264252652526625267252682526925270252712527225273252742527525276252772527825279252802528125282252832528425285252862528725288252892529025291252922529325294252952529625297252982529925300253012530225303253042530525306253072530825309253102531125312253132531425315253162531725318253192532025321253222532325324253252532625327253282532925330253312533225333253342533525336253372533825339253402534125342253432534425345253462534725348253492535025351253522535325354253552535625357253582535925360253612536225363253642536525366253672536825369253702537125372253732537425375253762537725378253792538025381253822538325384253852538625387253882538925390253912539225393253942539525396253972539825399254002540125402254032540425405254062540725408254092541025411254122541325414254152541625417254182541925420254212542225423254242542525426254272542825429254302543125432254332543425435254362543725438254392544025441254422544325444254452544625447254482544925450254512545225453254542545525456254572545825459254602546125462254632546425465254662546725468254692547025471254722547325474254752547625477254782547925480254812548225483254842548525486254872548825489254902549125492254932549425495254962549725498254992550025501255022550325504255052550625507255082550925510255112551225513255142551525516255172551825519255202552125522255232552425525255262552725528255292553025531255322553325534255352553625537255382553925540255412554225543255442554525546255472554825549255502555125552255532555425555255562555725558255592556025561255622556325564255652556625567255682556925570255712557225573255742557525576255772557825579255802558125582255832558425585255862558725588255892559025591255922559325594255952559625597255982559925600256012560225603256042560525606256072560825609256102561125612256132561425615256162561725618256192562025621256222562325624256252562625627256282562925630256312563225633256342563525636256372563825639256402564125642256432564425645256462564725648256492565025651256522565325654256552565625657256582565925660256612566225663256642566525666256672566825669256702567125672256732567425675256762567725678256792568025681256822568325684256852568625687256882568925690256912569225693256942569525696256972569825699257002570125702257032570425705257062570725708257092571025711257122571325714257152571625717257182571925720257212572225723257242572525726257272572825729257302573125732257332573425735257362573725738257392574025741257422574325744257452574625747257482574925750257512575225753257542575525756257572575825759257602576125762257632576425765257662576725768257692577025771257722577325774257752577625777257782577925780257812578225783257842578525786257872578825789257902579125792257932579425795257962579725798257992580025801258022580325804258052580625807258082580925810258112581225813258142581525816258172581825819258202582125822258232582425825258262582725828258292583025831258322583325834258352583625837258382583925840258412584225843258442584525846258472584825849258502585125852258532585425855258562585725858258592586025861258622586325864258652586625867258682586925870258712587225873258742587525876258772587825879258802588125882258832588425885258862588725888258892589025891258922589325894258952589625897258982589925900259012590225903259042590525906259072590825909259102591125912259132591425915259162591725918259192592025921259222592325924259252592625927259282592925930259312593225933259342593525936259372593825939259402594125942259432594425945259462594725948259492595025951259522595325954259552595625957259582595925960259612596225963259642596525966259672596825969259702597125972259732597425975259762597725978259792598025981259822598325984259852598625987259882598925990259912599225993259942599525996259972599825999260002600126002260032600426005260062600726008260092601026011260122601326014260152601626017260182601926020260212602226023260242602526026260272602826029260302603126032260332603426035260362603726038260392604026041260422604326044260452604626047260482604926050260512605226053260542605526056260572605826059260602606126062260632606426065260662606726068260692607026071260722607326074260752607626077260782607926080260812608226083260842608526086260872608826089260902609126092260932609426095260962609726098260992610026101261022610326104261052610626107261082610926110261112611226113261142611526116261172611826119261202612126122261232612426125261262612726128261292613026131261322613326134261352613626137261382613926140261412614226143261442614526146261472614826149261502615126152261532615426155261562615726158
  1. /**
  2. * @license
  3. * Video.js 8.10.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.10.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. const specApi = apiMap[0];
  123. let browserApi;
  124. // determine the supported set of functions
  125. for (let i = 0; i < apiMap.length; i++) {
  126. // check for exitFullscreen function
  127. if (apiMap[i][1] in document__default["default"]) {
  128. browserApi = apiMap[i];
  129. break;
  130. }
  131. }
  132. // map the browser API names to the spec API names
  133. if (browserApi) {
  134. for (let i = 0; i < browserApi.length; i++) {
  135. FullscreenApi[specApi[i]] = browserApi[i];
  136. }
  137. FullscreenApi.prefixed = browserApi[0] !== specApi[0];
  138. }
  139. /**
  140. * @file create-logger.js
  141. * @module create-logger
  142. */
  143. // This is the private tracking variable for the logging history.
  144. let history = [];
  145. /**
  146. * Log messages to the console and history based on the type of message
  147. *
  148. * @private
  149. * @param {string} name
  150. * The name of the console method to use.
  151. *
  152. * @param {Object} log
  153. * The arguments to be passed to the matching console method.
  154. *
  155. * @param {string} [styles]
  156. * styles for name
  157. */
  158. const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
  159. const lvl = log.levels[level];
  160. const lvlRegExp = new RegExp(`^(${lvl})$`);
  161. let resultName = name;
  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. if (styles) {
  167. resultName = `%c${name}`;
  168. args.unshift(styles);
  169. }
  170. // Add console prefix after adding to history.
  171. args.unshift(resultName + ':');
  172. // Add a clone of the args at this point to history.
  173. if (history) {
  174. history.push([].concat(args));
  175. // only store 1000 history entries
  176. const splice = history.length - 1000;
  177. history.splice(0, splice > 0 ? splice : 0);
  178. }
  179. // If there's no console then don't try to output messages, but they will
  180. // still be stored in history.
  181. if (!window__default["default"].console) {
  182. return;
  183. }
  184. // Was setting these once outside of this function, but containing them
  185. // in the function makes it easier to test cases where console doesn't exist
  186. // when the module is executed.
  187. let fn = window__default["default"].console[type];
  188. if (!fn && type === 'debug') {
  189. // Certain browsers don't have support for console.debug. For those, we
  190. // should default to the closest comparable log.
  191. fn = window__default["default"].console.info || window__default["default"].console.log;
  192. }
  193. // Bail out if there's no console or if this type is not allowed by the
  194. // current logging level.
  195. if (!fn || !lvl || !lvlRegExp.test(type)) {
  196. return;
  197. }
  198. fn[Array.isArray(args) ? 'apply' : 'call'](window__default["default"].console, args);
  199. };
  200. function createLogger$1(name, delimiter = ':', styles = '') {
  201. // This is the private tracking variable for logging level.
  202. let level = 'info';
  203. // the curried logByType bound to the specific log and history
  204. let logByType;
  205. /**
  206. * Logs plain debug messages. Similar to `console.log`.
  207. *
  208. * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
  209. * of our JSDoc template, we cannot properly document this as both a function
  210. * and a namespace, so its function signature is documented here.
  211. *
  212. * #### Arguments
  213. * ##### *args
  214. * *[]
  215. *
  216. * Any combination of values that could be passed to `console.log()`.
  217. *
  218. * #### Return Value
  219. *
  220. * `undefined`
  221. *
  222. * @namespace
  223. * @param {...*} args
  224. * One or more messages or objects that should be logged.
  225. */
  226. const log = function (...args) {
  227. logByType('log', level, args);
  228. };
  229. // This is the logByType helper that the logging methods below use
  230. logByType = LogByTypeFactory(name, log, styles);
  231. /**
  232. * Create a new subLogger which chains the old name to the new name.
  233. *
  234. * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
  235. * ```js
  236. * mylogger('foo');
  237. * // > VIDEOJS: player: foo
  238. * ```
  239. *
  240. * @param {string} subName
  241. * The name to add call the new logger
  242. * @param {string} [subDelimiter]
  243. * Optional delimiter
  244. * @param {string} [subStyles]
  245. * Optional styles
  246. * @return {Object}
  247. */
  248. log.createLogger = (subName, subDelimiter, subStyles) => {
  249. const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
  250. const resultStyles = subStyles !== undefined ? subStyles : styles;
  251. const resultName = `${name} ${resultDelimiter} ${subName}`;
  252. return createLogger$1(resultName, resultDelimiter, resultStyles);
  253. };
  254. /**
  255. * Create a new logger.
  256. *
  257. * @param {string} newName
  258. * The name for the new logger
  259. * @param {string} [newDelimiter]
  260. * Optional delimiter
  261. * @param {string} [newStyles]
  262. * Optional styles
  263. * @return {Object}
  264. */
  265. log.createNewLogger = (newName, newDelimiter, newStyles) => {
  266. return createLogger$1(newName, newDelimiter, newStyles);
  267. };
  268. /**
  269. * Enumeration of available logging levels, where the keys are the level names
  270. * and the values are `|`-separated strings containing logging methods allowed
  271. * in that logging level. These strings are used to create a regular expression
  272. * matching the function name being called.
  273. *
  274. * Levels provided by Video.js are:
  275. *
  276. * - `off`: Matches no calls. Any value that can be cast to `false` will have
  277. * this effect. The most restrictive.
  278. * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
  279. * `log.warn`, and `log.error`).
  280. * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
  281. * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
  282. * - `warn`: Matches `log.warn` and `log.error` calls.
  283. * - `error`: Matches only `log.error` calls.
  284. *
  285. * @type {Object}
  286. */
  287. log.levels = {
  288. all: 'debug|log|warn|error',
  289. off: '',
  290. debug: 'debug|log|warn|error',
  291. info: 'log|warn|error',
  292. warn: 'warn|error',
  293. error: 'error',
  294. DEFAULT: level
  295. };
  296. /**
  297. * Get or set the current logging level.
  298. *
  299. * If a string matching a key from {@link module:log.levels} is provided, acts
  300. * as a setter.
  301. *
  302. * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
  303. * Pass a valid level to set a new logging level.
  304. *
  305. * @return {string}
  306. * The current logging level.
  307. */
  308. log.level = lvl => {
  309. if (typeof lvl === 'string') {
  310. if (!log.levels.hasOwnProperty(lvl)) {
  311. throw new Error(`"${lvl}" in not a valid log level`);
  312. }
  313. level = lvl;
  314. }
  315. return level;
  316. };
  317. /**
  318. * Returns an array containing everything that has been logged to the history.
  319. *
  320. * This array is a shallow clone of the internal history record. However, its
  321. * contents are _not_ cloned; so, mutating objects inside this array will
  322. * mutate them in history.
  323. *
  324. * @return {Array}
  325. */
  326. log.history = () => history ? [].concat(history) : [];
  327. /**
  328. * Allows you to filter the history by the given logger name
  329. *
  330. * @param {string} fname
  331. * The name to filter by
  332. *
  333. * @return {Array}
  334. * The filtered list to return
  335. */
  336. log.history.filter = fname => {
  337. return (history || []).filter(historyItem => {
  338. // if the first item in each historyItem includes `fname`, then it's a match
  339. return new RegExp(`.*${fname}.*`).test(historyItem[0]);
  340. });
  341. };
  342. /**
  343. * Clears the internal history tracking, but does not prevent further history
  344. * tracking.
  345. */
  346. log.history.clear = () => {
  347. if (history) {
  348. history.length = 0;
  349. }
  350. };
  351. /**
  352. * Disable history tracking if it is currently enabled.
  353. */
  354. log.history.disable = () => {
  355. if (history !== null) {
  356. history.length = 0;
  357. history = null;
  358. }
  359. };
  360. /**
  361. * Enable history tracking if it is currently disabled.
  362. */
  363. log.history.enable = () => {
  364. if (history === null) {
  365. history = [];
  366. }
  367. };
  368. /**
  369. * Logs error messages. Similar to `console.error`.
  370. *
  371. * @param {...*} args
  372. * One or more messages or objects that should be logged as an error
  373. */
  374. log.error = (...args) => logByType('error', level, args);
  375. /**
  376. * Logs warning messages. Similar to `console.warn`.
  377. *
  378. * @param {...*} args
  379. * One or more messages or objects that should be logged as a warning.
  380. */
  381. log.warn = (...args) => logByType('warn', level, args);
  382. /**
  383. * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
  384. * log if `console.debug` is not available
  385. *
  386. * @param {...*} args
  387. * One or more messages or objects that should be logged as debug.
  388. */
  389. log.debug = (...args) => logByType('debug', level, args);
  390. return log;
  391. }
  392. /**
  393. * @file log.js
  394. * @module log
  395. */
  396. const log = createLogger$1('VIDEOJS');
  397. const createLogger = log.createLogger;
  398. /**
  399. * @file obj.js
  400. * @module obj
  401. */
  402. /**
  403. * @callback obj:EachCallback
  404. *
  405. * @param {*} value
  406. * The current key for the object that is being iterated over.
  407. *
  408. * @param {string} key
  409. * The current key-value for object that is being iterated over
  410. */
  411. /**
  412. * @callback obj:ReduceCallback
  413. *
  414. * @param {*} accum
  415. * The value that is accumulating over the reduce loop.
  416. *
  417. * @param {*} value
  418. * The current key for the object that is being iterated over.
  419. *
  420. * @param {string} key
  421. * The current key-value for object that is being iterated over
  422. *
  423. * @return {*}
  424. * The new accumulated value.
  425. */
  426. const toString = Object.prototype.toString;
  427. /**
  428. * Get the keys of an Object
  429. *
  430. * @param {Object}
  431. * The Object to get the keys from
  432. *
  433. * @return {string[]}
  434. * An array of the keys from the object. Returns an empty array if the
  435. * object passed in was invalid or had no keys.
  436. *
  437. * @private
  438. */
  439. const keys = function (object) {
  440. return isObject(object) ? Object.keys(object) : [];
  441. };
  442. /**
  443. * Array-like iteration for objects.
  444. *
  445. * @param {Object} object
  446. * The object to iterate over
  447. *
  448. * @param {obj:EachCallback} fn
  449. * The callback function which is called for each key in the object.
  450. */
  451. function each(object, fn) {
  452. keys(object).forEach(key => fn(object[key], key));
  453. }
  454. /**
  455. * Array-like reduce for objects.
  456. *
  457. * @param {Object} object
  458. * The Object that you want to reduce.
  459. *
  460. * @param {Function} fn
  461. * A callback function which is called for each key in the object. It
  462. * receives the accumulated value and the per-iteration value and key
  463. * as arguments.
  464. *
  465. * @param {*} [initial = 0]
  466. * Starting value
  467. *
  468. * @return {*}
  469. * The final accumulated value.
  470. */
  471. function reduce(object, fn, initial = 0) {
  472. return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
  473. }
  474. /**
  475. * Returns whether a value is an object of any kind - including DOM nodes,
  476. * arrays, regular expressions, etc. Not functions, though.
  477. *
  478. * This avoids the gotcha where using `typeof` on a `null` value
  479. * results in `'object'`.
  480. *
  481. * @param {Object} value
  482. * @return {boolean}
  483. */
  484. function isObject(value) {
  485. return !!value && typeof value === 'object';
  486. }
  487. /**
  488. * Returns whether an object appears to be a "plain" object - that is, a
  489. * direct instance of `Object`.
  490. *
  491. * @param {Object} value
  492. * @return {boolean}
  493. */
  494. function isPlain(value) {
  495. return isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object;
  496. }
  497. /**
  498. * Merge two objects recursively.
  499. *
  500. * Performs a deep merge like
  501. * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
  502. * plain objects (not arrays, elements, or anything else).
  503. *
  504. * Non-plain object values will be copied directly from the right-most
  505. * argument.
  506. *
  507. * @param {Object[]} sources
  508. * One or more objects to merge into a new object.
  509. *
  510. * @return {Object}
  511. * A new object that is the merged result of all sources.
  512. */
  513. function merge(...sources) {
  514. const result = {};
  515. sources.forEach(source => {
  516. if (!source) {
  517. return;
  518. }
  519. each(source, (value, key) => {
  520. if (!isPlain(value)) {
  521. result[key] = value;
  522. return;
  523. }
  524. if (!isPlain(result[key])) {
  525. result[key] = {};
  526. }
  527. result[key] = merge(result[key], value);
  528. });
  529. });
  530. return result;
  531. }
  532. /**
  533. * Returns an array of values for a given object
  534. *
  535. * @param {Object} source - target object
  536. * @return {Array<unknown>} - object values
  537. */
  538. function values(source = {}) {
  539. const result = [];
  540. for (const key in source) {
  541. if (source.hasOwnProperty(key)) {
  542. const value = source[key];
  543. result.push(value);
  544. }
  545. }
  546. return result;
  547. }
  548. /**
  549. * Object.defineProperty but "lazy", which means that the value is only set after
  550. * it is retrieved the first time, rather than being set right away.
  551. *
  552. * @param {Object} obj the object to set the property on
  553. * @param {string} key the key for the property to set
  554. * @param {Function} getValue the function used to get the value when it is needed.
  555. * @param {boolean} setter whether a setter should be allowed or not
  556. */
  557. function defineLazyProperty(obj, key, getValue, setter = true) {
  558. const set = value => Object.defineProperty(obj, key, {
  559. value,
  560. enumerable: true,
  561. writable: true
  562. });
  563. const options = {
  564. configurable: true,
  565. enumerable: true,
  566. get() {
  567. const value = getValue();
  568. set(value);
  569. return value;
  570. }
  571. };
  572. if (setter) {
  573. options.set = set;
  574. }
  575. return Object.defineProperty(obj, key, options);
  576. }
  577. var Obj = /*#__PURE__*/Object.freeze({
  578. __proto__: null,
  579. each: each,
  580. reduce: reduce,
  581. isObject: isObject,
  582. isPlain: isPlain,
  583. merge: merge,
  584. values: values,
  585. defineLazyProperty: defineLazyProperty
  586. });
  587. /**
  588. * @file browser.js
  589. * @module browser
  590. */
  591. /**
  592. * Whether or not this device is an iPod.
  593. *
  594. * @static
  595. * @type {Boolean}
  596. */
  597. let IS_IPOD = false;
  598. /**
  599. * The detected iOS version - or `null`.
  600. *
  601. * @static
  602. * @type {string|null}
  603. */
  604. let IOS_VERSION = null;
  605. /**
  606. * Whether or not this is an Android device.
  607. *
  608. * @static
  609. * @type {Boolean}
  610. */
  611. let IS_ANDROID = false;
  612. /**
  613. * The detected Android version - or `null` if not Android or indeterminable.
  614. *
  615. * @static
  616. * @type {number|string|null}
  617. */
  618. let ANDROID_VERSION;
  619. /**
  620. * Whether or not this is Mozilla Firefox.
  621. *
  622. * @static
  623. * @type {Boolean}
  624. */
  625. let IS_FIREFOX = false;
  626. /**
  627. * Whether or not this is Microsoft Edge.
  628. *
  629. * @static
  630. * @type {Boolean}
  631. */
  632. let IS_EDGE = false;
  633. /**
  634. * Whether or not this is any Chromium Browser
  635. *
  636. * @static
  637. * @type {Boolean}
  638. */
  639. let IS_CHROMIUM = false;
  640. /**
  641. * Whether or not this is any Chromium browser that is not Edge.
  642. *
  643. * This will also be `true` for Chrome on iOS, which will have different support
  644. * as it is actually Safari under the hood.
  645. *
  646. * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
  647. * IS_CHROMIUM should be used instead.
  648. * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
  649. *
  650. * @static
  651. * @deprecated
  652. * @type {Boolean}
  653. */
  654. let IS_CHROME = false;
  655. /**
  656. * The detected Chromium version - or `null`.
  657. *
  658. * @static
  659. * @type {number|null}
  660. */
  661. let CHROMIUM_VERSION = null;
  662. /**
  663. * The detected Google Chrome version - or `null`.
  664. * This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
  665. * Deprecated, use CHROMIUM_VERSION instead.
  666. *
  667. * @static
  668. * @deprecated
  669. * @type {number|null}
  670. */
  671. let CHROME_VERSION = null;
  672. /**
  673. * The detected Internet Explorer version - or `null`.
  674. *
  675. * @static
  676. * @deprecated
  677. * @type {number|null}
  678. */
  679. let IE_VERSION = null;
  680. /**
  681. * Whether or not this is desktop Safari.
  682. *
  683. * @static
  684. * @type {Boolean}
  685. */
  686. let IS_SAFARI = false;
  687. /**
  688. * Whether or not this is a Windows machine.
  689. *
  690. * @static
  691. * @type {Boolean}
  692. */
  693. let IS_WINDOWS = false;
  694. /**
  695. * Whether or not this device is an iPad.
  696. *
  697. * @static
  698. * @type {Boolean}
  699. */
  700. let IS_IPAD = false;
  701. /**
  702. * Whether or not this device is an iPhone.
  703. *
  704. * @static
  705. * @type {Boolean}
  706. */
  707. // The Facebook app's UIWebView identifies as both an iPhone and iPad, so
  708. // to identify iPhones, we need to exclude iPads.
  709. // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
  710. let IS_IPHONE = false;
  711. /**
  712. * Whether or not this device is touch-enabled.
  713. *
  714. * @static
  715. * @const
  716. * @type {Boolean}
  717. */
  718. 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));
  719. const UAD = window__default["default"].navigator && window__default["default"].navigator.userAgentData;
  720. if (UAD && UAD.platform && UAD.brands) {
  721. // If userAgentData is present, use it instead of userAgent to avoid warnings
  722. // Currently only implemented on Chromium
  723. // userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
  724. IS_ANDROID = UAD.platform === 'Android';
  725. IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
  726. IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
  727. IS_CHROME = !IS_EDGE && IS_CHROMIUM;
  728. CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
  729. IS_WINDOWS = UAD.platform === 'Windows';
  730. }
  731. // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
  732. // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
  733. // the checks need to be made agiainst the regular userAgent string.
  734. if (!IS_CHROMIUM) {
  735. const USER_AGENT = window__default["default"].navigator && window__default["default"].navigator.userAgent || '';
  736. IS_IPOD = /iPod/i.test(USER_AGENT);
  737. IOS_VERSION = function () {
  738. const match = USER_AGENT.match(/OS (\d+)_/i);
  739. if (match && match[1]) {
  740. return match[1];
  741. }
  742. return null;
  743. }();
  744. IS_ANDROID = /Android/i.test(USER_AGENT);
  745. ANDROID_VERSION = function () {
  746. // This matches Android Major.Minor.Patch versions
  747. // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
  748. const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
  749. if (!match) {
  750. return null;
  751. }
  752. const major = match[1] && parseFloat(match[1]);
  753. const minor = match[2] && parseFloat(match[2]);
  754. if (major && minor) {
  755. return parseFloat(match[1] + '.' + match[2]);
  756. } else if (major) {
  757. return major;
  758. }
  759. return null;
  760. }();
  761. IS_FIREFOX = /Firefox/i.test(USER_AGENT);
  762. IS_EDGE = /Edg/i.test(USER_AGENT);
  763. IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
  764. IS_CHROME = !IS_EDGE && IS_CHROMIUM;
  765. CHROMIUM_VERSION = CHROME_VERSION = function () {
  766. const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
  767. if (match && match[2]) {
  768. return parseFloat(match[2]);
  769. }
  770. return null;
  771. }();
  772. IE_VERSION = function () {
  773. const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
  774. let version = result && parseFloat(result[1]);
  775. if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
  776. // IE 11 has a different user agent string than other IE versions
  777. version = 11.0;
  778. }
  779. return version;
  780. }();
  781. IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE;
  782. IS_WINDOWS = /Windows/i.test(USER_AGENT);
  783. IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
  784. IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
  785. }
  786. /**
  787. * Whether or not this is an iOS device.
  788. *
  789. * @static
  790. * @const
  791. * @type {Boolean}
  792. */
  793. const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
  794. /**
  795. * Whether or not this is any flavor of Safari - including iOS.
  796. *
  797. * @static
  798. * @const
  799. * @type {Boolean}
  800. */
  801. const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
  802. var browser = /*#__PURE__*/Object.freeze({
  803. __proto__: null,
  804. get IS_IPOD () { return IS_IPOD; },
  805. get IOS_VERSION () { return IOS_VERSION; },
  806. get IS_ANDROID () { return IS_ANDROID; },
  807. get ANDROID_VERSION () { return ANDROID_VERSION; },
  808. get IS_FIREFOX () { return IS_FIREFOX; },
  809. get IS_EDGE () { return IS_EDGE; },
  810. get IS_CHROMIUM () { return IS_CHROMIUM; },
  811. get IS_CHROME () { return IS_CHROME; },
  812. get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
  813. get CHROME_VERSION () { return CHROME_VERSION; },
  814. get IE_VERSION () { return IE_VERSION; },
  815. get IS_SAFARI () { return IS_SAFARI; },
  816. get IS_WINDOWS () { return IS_WINDOWS; },
  817. get IS_IPAD () { return IS_IPAD; },
  818. get IS_IPHONE () { return IS_IPHONE; },
  819. TOUCH_ENABLED: TOUCH_ENABLED,
  820. IS_IOS: IS_IOS,
  821. IS_ANY_SAFARI: IS_ANY_SAFARI
  822. });
  823. /**
  824. * @file dom.js
  825. * @module dom
  826. */
  827. /**
  828. * Detect if a value is a string with any non-whitespace characters.
  829. *
  830. * @private
  831. * @param {string} str
  832. * The string to check
  833. *
  834. * @return {boolean}
  835. * Will be `true` if the string is non-blank, `false` otherwise.
  836. *
  837. */
  838. function isNonBlankString(str) {
  839. // we use str.trim as it will trim any whitespace characters
  840. // from the front or back of non-whitespace characters. aka
  841. // Any string that contains non-whitespace characters will
  842. // still contain them after `trim` but whitespace only strings
  843. // will have a length of 0, failing this check.
  844. return typeof str === 'string' && Boolean(str.trim());
  845. }
  846. /**
  847. * Throws an error if the passed string has whitespace. This is used by
  848. * class methods to be relatively consistent with the classList API.
  849. *
  850. * @private
  851. * @param {string} str
  852. * The string to check for whitespace.
  853. *
  854. * @throws {Error}
  855. * Throws an error if there is whitespace in the string.
  856. */
  857. function throwIfWhitespace(str) {
  858. // str.indexOf instead of regex because str.indexOf is faster performance wise.
  859. if (str.indexOf(' ') >= 0) {
  860. throw new Error('class has illegal whitespace characters');
  861. }
  862. }
  863. /**
  864. * Whether the current DOM interface appears to be real (i.e. not simulated).
  865. *
  866. * @return {boolean}
  867. * Will be `true` if the DOM appears to be real, `false` otherwise.
  868. */
  869. function isReal() {
  870. // Both document and window will never be undefined thanks to `global`.
  871. return document__default["default"] === window__default["default"].document;
  872. }
  873. /**
  874. * Determines, via duck typing, whether or not a value is a DOM element.
  875. *
  876. * @param {*} value
  877. * The value to check.
  878. *
  879. * @return {boolean}
  880. * Will be `true` if the value is a DOM element, `false` otherwise.
  881. */
  882. function isEl(value) {
  883. return isObject(value) && value.nodeType === 1;
  884. }
  885. /**
  886. * Determines if the current DOM is embedded in an iframe.
  887. *
  888. * @return {boolean}
  889. * Will be `true` if the DOM is embedded in an iframe, `false`
  890. * otherwise.
  891. */
  892. function isInFrame() {
  893. // We need a try/catch here because Safari will throw errors when attempting
  894. // to get either `parent` or `self`
  895. try {
  896. return window__default["default"].parent !== window__default["default"].self;
  897. } catch (x) {
  898. return true;
  899. }
  900. }
  901. /**
  902. * Creates functions to query the DOM using a given method.
  903. *
  904. * @private
  905. * @param {string} method
  906. * The method to create the query with.
  907. *
  908. * @return {Function}
  909. * The query method
  910. */
  911. function createQuerier(method) {
  912. return function (selector, context) {
  913. if (!isNonBlankString(selector)) {
  914. return document__default["default"][method](null);
  915. }
  916. if (isNonBlankString(context)) {
  917. context = document__default["default"].querySelector(context);
  918. }
  919. const ctx = isEl(context) ? context : document__default["default"];
  920. return ctx[method] && ctx[method](selector);
  921. };
  922. }
  923. /**
  924. * Creates an element and applies properties, attributes, and inserts content.
  925. *
  926. * @param {string} [tagName='div']
  927. * Name of tag to be created.
  928. *
  929. * @param {Object} [properties={}]
  930. * Element properties to be applied.
  931. *
  932. * @param {Object} [attributes={}]
  933. * Element attributes to be applied.
  934. *
  935. * @param {ContentDescriptor} [content]
  936. * A content descriptor object.
  937. *
  938. * @return {Element}
  939. * The element that was created.
  940. */
  941. function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
  942. const el = document__default["default"].createElement(tagName);
  943. Object.getOwnPropertyNames(properties).forEach(function (propName) {
  944. const val = properties[propName];
  945. // Handle textContent since it's not supported everywhere and we have a
  946. // method for it.
  947. if (propName === 'textContent') {
  948. textContent(el, val);
  949. } else if (el[propName] !== val || propName === 'tabIndex') {
  950. el[propName] = val;
  951. }
  952. });
  953. Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
  954. el.setAttribute(attrName, attributes[attrName]);
  955. });
  956. if (content) {
  957. appendContent(el, content);
  958. }
  959. return el;
  960. }
  961. /**
  962. * Injects text into an element, replacing any existing contents entirely.
  963. *
  964. * @param {HTMLElement} el
  965. * The element to add text content into
  966. *
  967. * @param {string} text
  968. * The text content to add.
  969. *
  970. * @return {Element}
  971. * The element with added text content.
  972. */
  973. function textContent(el, text) {
  974. if (typeof el.textContent === 'undefined') {
  975. el.innerText = text;
  976. } else {
  977. el.textContent = text;
  978. }
  979. return el;
  980. }
  981. /**
  982. * Insert an element as the first child node of another
  983. *
  984. * @param {Element} child
  985. * Element to insert
  986. *
  987. * @param {Element} parent
  988. * Element to insert child into
  989. */
  990. function prependTo(child, parent) {
  991. if (parent.firstChild) {
  992. parent.insertBefore(child, parent.firstChild);
  993. } else {
  994. parent.appendChild(child);
  995. }
  996. }
  997. /**
  998. * Check if an element has a class name.
  999. *
  1000. * @param {Element} element
  1001. * Element to check
  1002. *
  1003. * @param {string} classToCheck
  1004. * Class name to check for
  1005. *
  1006. * @return {boolean}
  1007. * Will be `true` if the element has a class, `false` otherwise.
  1008. *
  1009. * @throws {Error}
  1010. * Throws an error if `classToCheck` has white space.
  1011. */
  1012. function hasClass(element, classToCheck) {
  1013. throwIfWhitespace(classToCheck);
  1014. return element.classList.contains(classToCheck);
  1015. }
  1016. /**
  1017. * Add a class name to an element.
  1018. *
  1019. * @param {Element} element
  1020. * Element to add class name to.
  1021. *
  1022. * @param {...string} classesToAdd
  1023. * One or more class name to add.
  1024. *
  1025. * @return {Element}
  1026. * The DOM element with the added class name.
  1027. */
  1028. function addClass(element, ...classesToAdd) {
  1029. element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
  1030. return element;
  1031. }
  1032. /**
  1033. * Remove a class name from an element.
  1034. *
  1035. * @param {Element} element
  1036. * Element to remove a class name from.
  1037. *
  1038. * @param {...string} classesToRemove
  1039. * One or more class name to remove.
  1040. *
  1041. * @return {Element}
  1042. * The DOM element with class name removed.
  1043. */
  1044. function removeClass(element, ...classesToRemove) {
  1045. // Protect in case the player gets disposed
  1046. if (!element) {
  1047. log.warn("removeClass was called with an element that doesn't exist");
  1048. return null;
  1049. }
  1050. element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
  1051. return element;
  1052. }
  1053. /**
  1054. * The callback definition for toggleClass.
  1055. *
  1056. * @callback module:dom~PredicateCallback
  1057. * @param {Element} element
  1058. * The DOM element of the Component.
  1059. *
  1060. * @param {string} classToToggle
  1061. * The `className` that wants to be toggled
  1062. *
  1063. * @return {boolean|undefined}
  1064. * If `true` is returned, the `classToToggle` will be added to the
  1065. * `element`. If `false`, the `classToToggle` will be removed from
  1066. * the `element`. If `undefined`, the callback will be ignored.
  1067. */
  1068. /**
  1069. * Adds or removes a class name to/from an element depending on an optional
  1070. * condition or the presence/absence of the class name.
  1071. *
  1072. * @param {Element} element
  1073. * The element to toggle a class name on.
  1074. *
  1075. * @param {string} classToToggle
  1076. * The class that should be toggled.
  1077. *
  1078. * @param {boolean|module:dom~PredicateCallback} [predicate]
  1079. * See the return value for {@link module:dom~PredicateCallback}
  1080. *
  1081. * @return {Element}
  1082. * The element with a class that has been toggled.
  1083. */
  1084. function toggleClass(element, classToToggle, predicate) {
  1085. if (typeof predicate === 'function') {
  1086. predicate = predicate(element, classToToggle);
  1087. }
  1088. if (typeof predicate !== 'boolean') {
  1089. predicate = undefined;
  1090. }
  1091. classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
  1092. return element;
  1093. }
  1094. /**
  1095. * Apply attributes to an HTML element.
  1096. *
  1097. * @param {Element} el
  1098. * Element to add attributes to.
  1099. *
  1100. * @param {Object} [attributes]
  1101. * Attributes to be applied.
  1102. */
  1103. function setAttributes(el, attributes) {
  1104. Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
  1105. const attrValue = attributes[attrName];
  1106. if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
  1107. el.removeAttribute(attrName);
  1108. } else {
  1109. el.setAttribute(attrName, attrValue === true ? '' : attrValue);
  1110. }
  1111. });
  1112. }
  1113. /**
  1114. * Get an element's attribute values, as defined on the HTML tag.
  1115. *
  1116. * Attributes are not the same as properties. They're defined on the tag
  1117. * or with setAttribute.
  1118. *
  1119. * @param {Element} tag
  1120. * Element from which to get tag attributes.
  1121. *
  1122. * @return {Object}
  1123. * All attributes of the element. Boolean attributes will be `true` or
  1124. * `false`, others will be strings.
  1125. */
  1126. function getAttributes(tag) {
  1127. const obj = {};
  1128. // known boolean attributes
  1129. // we can check for matching boolean properties, but not all browsers
  1130. // and not all tags know about these attributes, so, we still want to check them manually
  1131. const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
  1132. if (tag && tag.attributes && tag.attributes.length > 0) {
  1133. const attrs = tag.attributes;
  1134. for (let i = attrs.length - 1; i >= 0; i--) {
  1135. const attrName = attrs[i].name;
  1136. /** @type {boolean|string} */
  1137. let attrVal = attrs[i].value;
  1138. // check for known booleans
  1139. // the matching element property will return a value for typeof
  1140. if (knownBooleans.includes(attrName)) {
  1141. // the value of an included boolean attribute is typically an empty
  1142. // string ('') which would equal false if we just check for a false value.
  1143. // we also don't want support bad code like autoplay='false'
  1144. attrVal = attrVal !== null ? true : false;
  1145. }
  1146. obj[attrName] = attrVal;
  1147. }
  1148. }
  1149. return obj;
  1150. }
  1151. /**
  1152. * Get the value of an element's attribute.
  1153. *
  1154. * @param {Element} el
  1155. * A DOM element.
  1156. *
  1157. * @param {string} attribute
  1158. * Attribute to get the value of.
  1159. *
  1160. * @return {string}
  1161. * The value of the attribute.
  1162. */
  1163. function getAttribute(el, attribute) {
  1164. return el.getAttribute(attribute);
  1165. }
  1166. /**
  1167. * Set the value of an element's attribute.
  1168. *
  1169. * @param {Element} el
  1170. * A DOM element.
  1171. *
  1172. * @param {string} attribute
  1173. * Attribute to set.
  1174. *
  1175. * @param {string} value
  1176. * Value to set the attribute to.
  1177. */
  1178. function setAttribute(el, attribute, value) {
  1179. el.setAttribute(attribute, value);
  1180. }
  1181. /**
  1182. * Remove an element's attribute.
  1183. *
  1184. * @param {Element} el
  1185. * A DOM element.
  1186. *
  1187. * @param {string} attribute
  1188. * Attribute to remove.
  1189. */
  1190. function removeAttribute(el, attribute) {
  1191. el.removeAttribute(attribute);
  1192. }
  1193. /**
  1194. * Attempt to block the ability to select text.
  1195. */
  1196. function blockTextSelection() {
  1197. document__default["default"].body.focus();
  1198. document__default["default"].onselectstart = function () {
  1199. return false;
  1200. };
  1201. }
  1202. /**
  1203. * Turn off text selection blocking.
  1204. */
  1205. function unblockTextSelection() {
  1206. document__default["default"].onselectstart = function () {
  1207. return true;
  1208. };
  1209. }
  1210. /**
  1211. * Identical to the native `getBoundingClientRect` function, but ensures that
  1212. * the method is supported at all (it is in all browsers we claim to support)
  1213. * and that the element is in the DOM before continuing.
  1214. *
  1215. * This wrapper function also shims properties which are not provided by some
  1216. * older browsers (namely, IE8).
  1217. *
  1218. * Additionally, some browsers do not support adding properties to a
  1219. * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
  1220. * properties (except `x` and `y` which are not widely supported). This helps
  1221. * avoid implementations where keys are non-enumerable.
  1222. *
  1223. * @param {Element} el
  1224. * Element whose `ClientRect` we want to calculate.
  1225. *
  1226. * @return {Object|undefined}
  1227. * Always returns a plain object - or `undefined` if it cannot.
  1228. */
  1229. function getBoundingClientRect(el) {
  1230. if (el && el.getBoundingClientRect && el.parentNode) {
  1231. const rect = el.getBoundingClientRect();
  1232. const result = {};
  1233. ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
  1234. if (rect[k] !== undefined) {
  1235. result[k] = rect[k];
  1236. }
  1237. });
  1238. if (!result.height) {
  1239. result.height = parseFloat(computedStyle(el, 'height'));
  1240. }
  1241. if (!result.width) {
  1242. result.width = parseFloat(computedStyle(el, 'width'));
  1243. }
  1244. return result;
  1245. }
  1246. }
  1247. /**
  1248. * Represents the position of a DOM element on the page.
  1249. *
  1250. * @typedef {Object} module:dom~Position
  1251. *
  1252. * @property {number} left
  1253. * Pixels to the left.
  1254. *
  1255. * @property {number} top
  1256. * Pixels from the top.
  1257. */
  1258. /**
  1259. * Get the position of an element in the DOM.
  1260. *
  1261. * Uses `getBoundingClientRect` technique from John Resig.
  1262. *
  1263. * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
  1264. *
  1265. * @param {Element} el
  1266. * Element from which to get offset.
  1267. *
  1268. * @return {module:dom~Position}
  1269. * The position of the element that was passed in.
  1270. */
  1271. function findPosition(el) {
  1272. if (!el || el && !el.offsetParent) {
  1273. return {
  1274. left: 0,
  1275. top: 0,
  1276. width: 0,
  1277. height: 0
  1278. };
  1279. }
  1280. const width = el.offsetWidth;
  1281. const height = el.offsetHeight;
  1282. let left = 0;
  1283. let top = 0;
  1284. while (el.offsetParent && el !== document__default["default"][FullscreenApi.fullscreenElement]) {
  1285. left += el.offsetLeft;
  1286. top += el.offsetTop;
  1287. el = el.offsetParent;
  1288. }
  1289. return {
  1290. left,
  1291. top,
  1292. width,
  1293. height
  1294. };
  1295. }
  1296. /**
  1297. * Represents x and y coordinates for a DOM element or mouse pointer.
  1298. *
  1299. * @typedef {Object} module:dom~Coordinates
  1300. *
  1301. * @property {number} x
  1302. * x coordinate in pixels
  1303. *
  1304. * @property {number} y
  1305. * y coordinate in pixels
  1306. */
  1307. /**
  1308. * Get the pointer position within an element.
  1309. *
  1310. * The base on the coordinates are the bottom left of the element.
  1311. *
  1312. * @param {Element} el
  1313. * Element on which to get the pointer position on.
  1314. *
  1315. * @param {Event} event
  1316. * Event object.
  1317. *
  1318. * @return {module:dom~Coordinates}
  1319. * A coordinates object corresponding to the mouse position.
  1320. *
  1321. */
  1322. function getPointerPosition(el, event) {
  1323. const translated = {
  1324. x: 0,
  1325. y: 0
  1326. };
  1327. if (IS_IOS) {
  1328. let item = el;
  1329. while (item && item.nodeName.toLowerCase() !== 'html') {
  1330. const transform = computedStyle(item, 'transform');
  1331. if (/^matrix/.test(transform)) {
  1332. const values = transform.slice(7, -1).split(/,\s/).map(Number);
  1333. translated.x += values[4];
  1334. translated.y += values[5];
  1335. } else if (/^matrix3d/.test(transform)) {
  1336. const values = transform.slice(9, -1).split(/,\s/).map(Number);
  1337. translated.x += values[12];
  1338. translated.y += values[13];
  1339. }
  1340. item = item.parentNode;
  1341. }
  1342. }
  1343. const position = {};
  1344. const boxTarget = findPosition(event.target);
  1345. const box = findPosition(el);
  1346. const boxW = box.width;
  1347. const boxH = box.height;
  1348. let offsetY = event.offsetY - (box.top - boxTarget.top);
  1349. let offsetX = event.offsetX - (box.left - boxTarget.left);
  1350. if (event.changedTouches) {
  1351. offsetX = event.changedTouches[0].pageX - box.left;
  1352. offsetY = event.changedTouches[0].pageY + box.top;
  1353. if (IS_IOS) {
  1354. offsetX -= translated.x;
  1355. offsetY -= translated.y;
  1356. }
  1357. }
  1358. position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
  1359. position.x = Math.max(0, Math.min(1, offsetX / boxW));
  1360. return position;
  1361. }
  1362. /**
  1363. * Determines, via duck typing, whether or not a value is a text node.
  1364. *
  1365. * @param {*} value
  1366. * Check if this value is a text node.
  1367. *
  1368. * @return {boolean}
  1369. * Will be `true` if the value is a text node, `false` otherwise.
  1370. */
  1371. function isTextNode(value) {
  1372. return isObject(value) && value.nodeType === 3;
  1373. }
  1374. /**
  1375. * Empties the contents of an element.
  1376. *
  1377. * @param {Element} el
  1378. * The element to empty children from
  1379. *
  1380. * @return {Element}
  1381. * The element with no children
  1382. */
  1383. function emptyEl(el) {
  1384. while (el.firstChild) {
  1385. el.removeChild(el.firstChild);
  1386. }
  1387. return el;
  1388. }
  1389. /**
  1390. * This is a mixed value that describes content to be injected into the DOM
  1391. * via some method. It can be of the following types:
  1392. *
  1393. * Type | Description
  1394. * -----------|-------------
  1395. * `string` | The value will be normalized into a text node.
  1396. * `Element` | The value will be accepted as-is.
  1397. * `Text` | A TextNode. The value will be accepted as-is.
  1398. * `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).
  1399. * `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.
  1400. *
  1401. * @typedef {string|Element|Text|Array|Function} ContentDescriptor
  1402. */
  1403. /**
  1404. * Normalizes content for eventual insertion into the DOM.
  1405. *
  1406. * This allows a wide range of content definition methods, but helps protect
  1407. * from falling into the trap of simply writing to `innerHTML`, which could
  1408. * be an XSS concern.
  1409. *
  1410. * The content for an element can be passed in multiple types and
  1411. * combinations, whose behavior is as follows:
  1412. *
  1413. * @param {ContentDescriptor} content
  1414. * A content descriptor value.
  1415. *
  1416. * @return {Array}
  1417. * All of the content that was passed in, normalized to an array of
  1418. * elements or text nodes.
  1419. */
  1420. function normalizeContent(content) {
  1421. // First, invoke content if it is a function. If it produces an array,
  1422. // that needs to happen before normalization.
  1423. if (typeof content === 'function') {
  1424. content = content();
  1425. }
  1426. // Next up, normalize to an array, so one or many items can be normalized,
  1427. // filtered, and returned.
  1428. return (Array.isArray(content) ? content : [content]).map(value => {
  1429. // First, invoke value if it is a function to produce a new value,
  1430. // which will be subsequently normalized to a Node of some kind.
  1431. if (typeof value === 'function') {
  1432. value = value();
  1433. }
  1434. if (isEl(value) || isTextNode(value)) {
  1435. return value;
  1436. }
  1437. if (typeof value === 'string' && /\S/.test(value)) {
  1438. return document__default["default"].createTextNode(value);
  1439. }
  1440. }).filter(value => value);
  1441. }
  1442. /**
  1443. * Normalizes and appends content to an element.
  1444. *
  1445. * @param {Element} el
  1446. * Element to append normalized content to.
  1447. *
  1448. * @param {ContentDescriptor} content
  1449. * A content descriptor value.
  1450. *
  1451. * @return {Element}
  1452. * The element with appended normalized content.
  1453. */
  1454. function appendContent(el, content) {
  1455. normalizeContent(content).forEach(node => el.appendChild(node));
  1456. return el;
  1457. }
  1458. /**
  1459. * Normalizes and inserts content into an element; this is identical to
  1460. * `appendContent()`, except it empties the element first.
  1461. *
  1462. * @param {Element} el
  1463. * Element to insert normalized content into.
  1464. *
  1465. * @param {ContentDescriptor} content
  1466. * A content descriptor value.
  1467. *
  1468. * @return {Element}
  1469. * The element with inserted normalized content.
  1470. */
  1471. function insertContent(el, content) {
  1472. return appendContent(emptyEl(el), content);
  1473. }
  1474. /**
  1475. * Check if an event was a single left click.
  1476. *
  1477. * @param {MouseEvent} event
  1478. * Event object.
  1479. *
  1480. * @return {boolean}
  1481. * Will be `true` if a single left click, `false` otherwise.
  1482. */
  1483. function isSingleLeftClick(event) {
  1484. // Note: if you create something draggable, be sure to
  1485. // call it on both `mousedown` and `mousemove` event,
  1486. // otherwise `mousedown` should be enough for a button
  1487. if (event.button === undefined && event.buttons === undefined) {
  1488. // Why do we need `buttons` ?
  1489. // Because, middle mouse sometimes have this:
  1490. // e.button === 0 and e.buttons === 4
  1491. // Furthermore, we want to prevent combination click, something like
  1492. // HOLD middlemouse then left click, that would be
  1493. // e.button === 0, e.buttons === 5
  1494. // just `button` is not gonna work
  1495. // Alright, then what this block does ?
  1496. // this is for chrome `simulate mobile devices`
  1497. // I want to support this as well
  1498. return true;
  1499. }
  1500. if (event.button === 0 && event.buttons === undefined) {
  1501. // Touch screen, sometimes on some specific device, `buttons`
  1502. // doesn't have anything (safari on ios, blackberry...)
  1503. return true;
  1504. }
  1505. // `mouseup` event on a single left click has
  1506. // `button` and `buttons` equal to 0
  1507. if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
  1508. return true;
  1509. }
  1510. if (event.button !== 0 || event.buttons !== 1) {
  1511. // This is the reason we have those if else block above
  1512. // if any special case we can catch and let it slide
  1513. // we do it above, when get to here, this definitely
  1514. // is-not-left-click
  1515. return false;
  1516. }
  1517. return true;
  1518. }
  1519. /**
  1520. * Finds a single DOM element matching `selector` within the optional
  1521. * `context` of another DOM element (defaulting to `document`).
  1522. *
  1523. * @param {string} selector
  1524. * A valid CSS selector, which will be passed to `querySelector`.
  1525. *
  1526. * @param {Element|String} [context=document]
  1527. * A DOM element within which to query. Can also be a selector
  1528. * string in which case the first matching element will be used
  1529. * as context. If missing (or no element matches selector), falls
  1530. * back to `document`.
  1531. *
  1532. * @return {Element|null}
  1533. * The element that was found or null.
  1534. */
  1535. const $ = createQuerier('querySelector');
  1536. /**
  1537. * Finds a all DOM elements matching `selector` within the optional
  1538. * `context` of another DOM element (defaulting to `document`).
  1539. *
  1540. * @param {string} selector
  1541. * A valid CSS selector, which will be passed to `querySelectorAll`.
  1542. *
  1543. * @param {Element|String} [context=document]
  1544. * A DOM element within which to query. Can also be a selector
  1545. * string in which case the first matching element will be used
  1546. * as context. If missing (or no element matches selector), falls
  1547. * back to `document`.
  1548. *
  1549. * @return {NodeList}
  1550. * A element list of elements that were found. Will be empty if none
  1551. * were found.
  1552. *
  1553. */
  1554. const $$ = createQuerier('querySelectorAll');
  1555. /**
  1556. * A safe getComputedStyle.
  1557. *
  1558. * This is needed because in Firefox, if the player is loaded in an iframe with
  1559. * `display:none`, then `getComputedStyle` returns `null`, so, we do a
  1560. * null-check to make sure that the player doesn't break in these cases.
  1561. *
  1562. * @param {Element} el
  1563. * The element you want the computed style of
  1564. *
  1565. * @param {string} prop
  1566. * The property name you want
  1567. *
  1568. * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
  1569. */
  1570. function computedStyle(el, prop) {
  1571. if (!el || !prop) {
  1572. return '';
  1573. }
  1574. if (typeof window__default["default"].getComputedStyle === 'function') {
  1575. let computedStyleValue;
  1576. try {
  1577. computedStyleValue = window__default["default"].getComputedStyle(el);
  1578. } catch (e) {
  1579. return '';
  1580. }
  1581. return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
  1582. }
  1583. return '';
  1584. }
  1585. /**
  1586. * Copy document style sheets to another window.
  1587. *
  1588. * @param {Window} win
  1589. * The window element you want to copy the document style sheets to.
  1590. *
  1591. */
  1592. function copyStyleSheetsToWindow(win) {
  1593. [...document__default["default"].styleSheets].forEach(styleSheet => {
  1594. try {
  1595. const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
  1596. const style = document__default["default"].createElement('style');
  1597. style.textContent = cssRules;
  1598. win.document.head.appendChild(style);
  1599. } catch (e) {
  1600. const link = document__default["default"].createElement('link');
  1601. link.rel = 'stylesheet';
  1602. link.type = styleSheet.type;
  1603. // For older Safari this has to be the string; on other browsers setting the MediaList works
  1604. link.media = styleSheet.media.mediaText;
  1605. link.href = styleSheet.href;
  1606. win.document.head.appendChild(link);
  1607. }
  1608. });
  1609. }
  1610. var Dom = /*#__PURE__*/Object.freeze({
  1611. __proto__: null,
  1612. isReal: isReal,
  1613. isEl: isEl,
  1614. isInFrame: isInFrame,
  1615. createEl: createEl,
  1616. textContent: textContent,
  1617. prependTo: prependTo,
  1618. hasClass: hasClass,
  1619. addClass: addClass,
  1620. removeClass: removeClass,
  1621. toggleClass: toggleClass,
  1622. setAttributes: setAttributes,
  1623. getAttributes: getAttributes,
  1624. getAttribute: getAttribute,
  1625. setAttribute: setAttribute,
  1626. removeAttribute: removeAttribute,
  1627. blockTextSelection: blockTextSelection,
  1628. unblockTextSelection: unblockTextSelection,
  1629. getBoundingClientRect: getBoundingClientRect,
  1630. findPosition: findPosition,
  1631. getPointerPosition: getPointerPosition,
  1632. isTextNode: isTextNode,
  1633. emptyEl: emptyEl,
  1634. normalizeContent: normalizeContent,
  1635. appendContent: appendContent,
  1636. insertContent: insertContent,
  1637. isSingleLeftClick: isSingleLeftClick,
  1638. $: $,
  1639. $$: $$,
  1640. computedStyle: computedStyle,
  1641. copyStyleSheetsToWindow: copyStyleSheetsToWindow
  1642. });
  1643. /**
  1644. * @file setup.js - Functions for setting up a player without
  1645. * user interaction based on the data-setup `attribute` of the video tag.
  1646. *
  1647. * @module setup
  1648. */
  1649. let _windowLoaded = false;
  1650. let videojs$1;
  1651. /**
  1652. * Set up any tags that have a data-setup `attribute` when the player is started.
  1653. */
  1654. const autoSetup = function () {
  1655. if (videojs$1.options.autoSetup === false) {
  1656. return;
  1657. }
  1658. const vids = Array.prototype.slice.call(document__default["default"].getElementsByTagName('video'));
  1659. const audios = Array.prototype.slice.call(document__default["default"].getElementsByTagName('audio'));
  1660. const divs = Array.prototype.slice.call(document__default["default"].getElementsByTagName('video-js'));
  1661. const mediaEls = vids.concat(audios, divs);
  1662. // Check if any media elements exist
  1663. if (mediaEls && mediaEls.length > 0) {
  1664. for (let i = 0, e = mediaEls.length; i < e; i++) {
  1665. const mediaEl = mediaEls[i];
  1666. // Check if element exists, has getAttribute func.
  1667. if (mediaEl && mediaEl.getAttribute) {
  1668. // Make sure this player hasn't already been set up.
  1669. if (mediaEl.player === undefined) {
  1670. const options = mediaEl.getAttribute('data-setup');
  1671. // Check if data-setup attr exists.
  1672. // We only auto-setup if they've added the data-setup attr.
  1673. if (options !== null) {
  1674. // Create new video.js instance.
  1675. videojs$1(mediaEl);
  1676. }
  1677. }
  1678. // If getAttribute isn't defined, we need to wait for the DOM.
  1679. } else {
  1680. autoSetupTimeout(1);
  1681. break;
  1682. }
  1683. }
  1684. // No videos were found, so keep looping unless page is finished loading.
  1685. } else if (!_windowLoaded) {
  1686. autoSetupTimeout(1);
  1687. }
  1688. };
  1689. /**
  1690. * Wait until the page is loaded before running autoSetup. This will be called in
  1691. * autoSetup if `hasLoaded` returns false.
  1692. *
  1693. * @param {number} wait
  1694. * How long to wait in ms
  1695. *
  1696. * @param {module:videojs} [vjs]
  1697. * The videojs library function
  1698. */
  1699. function autoSetupTimeout(wait, vjs) {
  1700. // Protect against breakage in non-browser environments
  1701. if (!isReal()) {
  1702. return;
  1703. }
  1704. if (vjs) {
  1705. videojs$1 = vjs;
  1706. }
  1707. window__default["default"].setTimeout(autoSetup, wait);
  1708. }
  1709. /**
  1710. * Used to set the internal tracking of window loaded state to true.
  1711. *
  1712. * @private
  1713. */
  1714. function setWindowLoaded() {
  1715. _windowLoaded = true;
  1716. window__default["default"].removeEventListener('load', setWindowLoaded);
  1717. }
  1718. if (isReal()) {
  1719. if (document__default["default"].readyState === 'complete') {
  1720. setWindowLoaded();
  1721. } else {
  1722. /**
  1723. * Listen for the load event on window, and set _windowLoaded to true.
  1724. *
  1725. * We use a standard event listener here to avoid incrementing the GUID
  1726. * before any players are created.
  1727. *
  1728. * @listens load
  1729. */
  1730. window__default["default"].addEventListener('load', setWindowLoaded);
  1731. }
  1732. }
  1733. /**
  1734. * @file stylesheet.js
  1735. * @module stylesheet
  1736. */
  1737. /**
  1738. * Create a DOM style element given a className for it.
  1739. *
  1740. * @param {string} className
  1741. * The className to add to the created style element.
  1742. *
  1743. * @return {Element}
  1744. * The element that was created.
  1745. */
  1746. const createStyleElement = function (className) {
  1747. const style = document__default["default"].createElement('style');
  1748. style.className = className;
  1749. return style;
  1750. };
  1751. /**
  1752. * Add text to a DOM element.
  1753. *
  1754. * @param {Element} el
  1755. * The Element to add text content to.
  1756. *
  1757. * @param {string} content
  1758. * The text to add to the element.
  1759. */
  1760. const setTextContent = function (el, content) {
  1761. if (el.styleSheet) {
  1762. el.styleSheet.cssText = content;
  1763. } else {
  1764. el.textContent = content;
  1765. }
  1766. };
  1767. /**
  1768. * @file dom-data.js
  1769. * @module dom-data
  1770. */
  1771. /**
  1772. * Element Data Store.
  1773. *
  1774. * Allows for binding data to an element without putting it directly on the
  1775. * element. Ex. Event listeners are stored here.
  1776. * (also from jsninja.com, slightly modified and updated for closure compiler)
  1777. *
  1778. * @type {Object}
  1779. * @private
  1780. */
  1781. var DomData = new WeakMap();
  1782. /**
  1783. * @file guid.js
  1784. * @module guid
  1785. */
  1786. // Default value for GUIDs. This allows us to reset the GUID counter in tests.
  1787. //
  1788. // The initial GUID is 3 because some users have come to rely on the first
  1789. // default player ID ending up as `vjs_video_3`.
  1790. //
  1791. // See: https://github.com/videojs/video.js/pull/6216
  1792. const _initialGuid = 3;
  1793. /**
  1794. * Unique ID for an element or function
  1795. *
  1796. * @type {Number}
  1797. */
  1798. let _guid = _initialGuid;
  1799. /**
  1800. * Get a unique auto-incrementing ID by number that has not been returned before.
  1801. *
  1802. * @return {number}
  1803. * A new unique ID.
  1804. */
  1805. function newGUID() {
  1806. return _guid++;
  1807. }
  1808. /**
  1809. * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
  1810. * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
  1811. * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
  1812. * robust as jquery's, so there's probably some differences.
  1813. *
  1814. * @file events.js
  1815. * @module events
  1816. */
  1817. /**
  1818. * Clean up the listener cache and dispatchers
  1819. *
  1820. * @param {Element|Object} elem
  1821. * Element to clean up
  1822. *
  1823. * @param {string} type
  1824. * Type of event to clean up
  1825. */
  1826. function _cleanUpEvents(elem, type) {
  1827. if (!DomData.has(elem)) {
  1828. return;
  1829. }
  1830. const data = DomData.get(elem);
  1831. // Remove the events of a particular type if there are none left
  1832. if (data.handlers[type].length === 0) {
  1833. delete data.handlers[type];
  1834. // data.handlers[type] = null;
  1835. // Setting to null was causing an error with data.handlers
  1836. // Remove the meta-handler from the element
  1837. if (elem.removeEventListener) {
  1838. elem.removeEventListener(type, data.dispatcher, false);
  1839. } else if (elem.detachEvent) {
  1840. elem.detachEvent('on' + type, data.dispatcher);
  1841. }
  1842. }
  1843. // Remove the events object if there are no types left
  1844. if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
  1845. delete data.handlers;
  1846. delete data.dispatcher;
  1847. delete data.disabled;
  1848. }
  1849. // Finally remove the element data if there is no data left
  1850. if (Object.getOwnPropertyNames(data).length === 0) {
  1851. DomData.delete(elem);
  1852. }
  1853. }
  1854. /**
  1855. * Loops through an array of event types and calls the requested method for each type.
  1856. *
  1857. * @param {Function} fn
  1858. * The event method we want to use.
  1859. *
  1860. * @param {Element|Object} elem
  1861. * Element or object to bind listeners to
  1862. *
  1863. * @param {string[]} types
  1864. * Type of event to bind to.
  1865. *
  1866. * @param {Function} callback
  1867. * Event listener.
  1868. */
  1869. function _handleMultipleEvents(fn, elem, types, callback) {
  1870. types.forEach(function (type) {
  1871. // Call the event method for each one of the types
  1872. fn(elem, type, callback);
  1873. });
  1874. }
  1875. /**
  1876. * Fix a native event to have standard property values
  1877. *
  1878. * @param {Object} event
  1879. * Event object to fix.
  1880. *
  1881. * @return {Object}
  1882. * Fixed event object.
  1883. */
  1884. function fixEvent(event) {
  1885. if (event.fixed_) {
  1886. return event;
  1887. }
  1888. function returnTrue() {
  1889. return true;
  1890. }
  1891. function returnFalse() {
  1892. return false;
  1893. }
  1894. // Test if fixing up is needed
  1895. // Used to check if !event.stopPropagation instead of isPropagationStopped
  1896. // But native events return true for stopPropagation, but don't have
  1897. // other expected methods like isPropagationStopped. Seems to be a problem
  1898. // with the Javascript Ninja code. So we're just overriding all events now.
  1899. if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
  1900. const old = event || window__default["default"].event;
  1901. event = {};
  1902. // Clone the old object so that we can modify the values event = {};
  1903. // IE8 Doesn't like when you mess with native event properties
  1904. // Firefox returns false for event.hasOwnProperty('type') and other props
  1905. // which makes copying more difficult.
  1906. // TODO: Probably best to create a whitelist of event props
  1907. for (const key in old) {
  1908. // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
  1909. // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
  1910. // and webkitMovementX/Y
  1911. // Lighthouse complains if Event.path is copied
  1912. if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
  1913. // Chrome 32+ warns if you try to copy deprecated returnValue, but
  1914. // we still want to if preventDefault isn't supported (IE8).
  1915. if (!(key === 'returnValue' && old.preventDefault)) {
  1916. event[key] = old[key];
  1917. }
  1918. }
  1919. }
  1920. // The event occurred on this element
  1921. if (!event.target) {
  1922. event.target = event.srcElement || document__default["default"];
  1923. }
  1924. // Handle which other element the event is related to
  1925. if (!event.relatedTarget) {
  1926. event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
  1927. }
  1928. // Stop the default browser action
  1929. event.preventDefault = function () {
  1930. if (old.preventDefault) {
  1931. old.preventDefault();
  1932. }
  1933. event.returnValue = false;
  1934. old.returnValue = false;
  1935. event.defaultPrevented = true;
  1936. };
  1937. event.defaultPrevented = false;
  1938. // Stop the event from bubbling
  1939. event.stopPropagation = function () {
  1940. if (old.stopPropagation) {
  1941. old.stopPropagation();
  1942. }
  1943. event.cancelBubble = true;
  1944. old.cancelBubble = true;
  1945. event.isPropagationStopped = returnTrue;
  1946. };
  1947. event.isPropagationStopped = returnFalse;
  1948. // Stop the event from bubbling and executing other handlers
  1949. event.stopImmediatePropagation = function () {
  1950. if (old.stopImmediatePropagation) {
  1951. old.stopImmediatePropagation();
  1952. }
  1953. event.isImmediatePropagationStopped = returnTrue;
  1954. event.stopPropagation();
  1955. };
  1956. event.isImmediatePropagationStopped = returnFalse;
  1957. // Handle mouse position
  1958. if (event.clientX !== null && event.clientX !== undefined) {
  1959. const doc = document__default["default"].documentElement;
  1960. const body = document__default["default"].body;
  1961. event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
  1962. event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
  1963. }
  1964. // Handle key presses
  1965. event.which = event.charCode || event.keyCode;
  1966. // Fix button for mouse clicks:
  1967. // 0 == left; 1 == middle; 2 == right
  1968. if (event.button !== null && event.button !== undefined) {
  1969. // The following is disabled because it does not pass videojs-standard
  1970. // and... yikes.
  1971. /* eslint-disable */
  1972. event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
  1973. /* eslint-enable */
  1974. }
  1975. }
  1976. event.fixed_ = true;
  1977. // Returns fixed-up instance
  1978. return event;
  1979. }
  1980. /**
  1981. * Whether passive event listeners are supported
  1982. */
  1983. let _supportsPassive;
  1984. const supportsPassive = function () {
  1985. if (typeof _supportsPassive !== 'boolean') {
  1986. _supportsPassive = false;
  1987. try {
  1988. const opts = Object.defineProperty({}, 'passive', {
  1989. get() {
  1990. _supportsPassive = true;
  1991. }
  1992. });
  1993. window__default["default"].addEventListener('test', null, opts);
  1994. window__default["default"].removeEventListener('test', null, opts);
  1995. } catch (e) {
  1996. // disregard
  1997. }
  1998. }
  1999. return _supportsPassive;
  2000. };
  2001. /**
  2002. * Touch events Chrome expects to be passive
  2003. */
  2004. const passiveEvents = ['touchstart', 'touchmove'];
  2005. /**
  2006. * Add an event listener to element
  2007. * It stores the handler function in a separate cache object
  2008. * and adds a generic handler to the element's event,
  2009. * along with a unique id (guid) to the element.
  2010. *
  2011. * @param {Element|Object} elem
  2012. * Element or object to bind listeners to
  2013. *
  2014. * @param {string|string[]} type
  2015. * Type of event to bind to.
  2016. *
  2017. * @param {Function} fn
  2018. * Event listener.
  2019. */
  2020. function on(elem, type, fn) {
  2021. if (Array.isArray(type)) {
  2022. return _handleMultipleEvents(on, elem, type, fn);
  2023. }
  2024. if (!DomData.has(elem)) {
  2025. DomData.set(elem, {});
  2026. }
  2027. const data = DomData.get(elem);
  2028. // We need a place to store all our handler data
  2029. if (!data.handlers) {
  2030. data.handlers = {};
  2031. }
  2032. if (!data.handlers[type]) {
  2033. data.handlers[type] = [];
  2034. }
  2035. if (!fn.guid) {
  2036. fn.guid = newGUID();
  2037. }
  2038. data.handlers[type].push(fn);
  2039. if (!data.dispatcher) {
  2040. data.disabled = false;
  2041. data.dispatcher = function (event, hash) {
  2042. if (data.disabled) {
  2043. return;
  2044. }
  2045. event = fixEvent(event);
  2046. const handlers = data.handlers[event.type];
  2047. if (handlers) {
  2048. // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
  2049. const handlersCopy = handlers.slice(0);
  2050. for (let m = 0, n = handlersCopy.length; m < n; m++) {
  2051. if (event.isImmediatePropagationStopped()) {
  2052. break;
  2053. } else {
  2054. try {
  2055. handlersCopy[m].call(elem, event, hash);
  2056. } catch (e) {
  2057. log.error(e);
  2058. }
  2059. }
  2060. }
  2061. }
  2062. };
  2063. }
  2064. if (data.handlers[type].length === 1) {
  2065. if (elem.addEventListener) {
  2066. let options = false;
  2067. if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
  2068. options = {
  2069. passive: true
  2070. };
  2071. }
  2072. elem.addEventListener(type, data.dispatcher, options);
  2073. } else if (elem.attachEvent) {
  2074. elem.attachEvent('on' + type, data.dispatcher);
  2075. }
  2076. }
  2077. }
  2078. /**
  2079. * Removes event listeners from an element
  2080. *
  2081. * @param {Element|Object} elem
  2082. * Object to remove listeners from.
  2083. *
  2084. * @param {string|string[]} [type]
  2085. * Type of listener to remove. Don't include to remove all events from element.
  2086. *
  2087. * @param {Function} [fn]
  2088. * Specific listener to remove. Don't include to remove listeners for an event
  2089. * type.
  2090. */
  2091. function off(elem, type, fn) {
  2092. // Don't want to add a cache object through getElData if not needed
  2093. if (!DomData.has(elem)) {
  2094. return;
  2095. }
  2096. const data = DomData.get(elem);
  2097. // If no events exist, nothing to unbind
  2098. if (!data.handlers) {
  2099. return;
  2100. }
  2101. if (Array.isArray(type)) {
  2102. return _handleMultipleEvents(off, elem, type, fn);
  2103. }
  2104. // Utility function
  2105. const removeType = function (el, t) {
  2106. data.handlers[t] = [];
  2107. _cleanUpEvents(el, t);
  2108. };
  2109. // Are we removing all bound events?
  2110. if (type === undefined) {
  2111. for (const t in data.handlers) {
  2112. if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
  2113. removeType(elem, t);
  2114. }
  2115. }
  2116. return;
  2117. }
  2118. const handlers = data.handlers[type];
  2119. // If no handlers exist, nothing to unbind
  2120. if (!handlers) {
  2121. return;
  2122. }
  2123. // If no listener was provided, remove all listeners for type
  2124. if (!fn) {
  2125. removeType(elem, type);
  2126. return;
  2127. }
  2128. // We're only removing a single handler
  2129. if (fn.guid) {
  2130. for (let n = 0; n < handlers.length; n++) {
  2131. if (handlers[n].guid === fn.guid) {
  2132. handlers.splice(n--, 1);
  2133. }
  2134. }
  2135. }
  2136. _cleanUpEvents(elem, type);
  2137. }
  2138. /**
  2139. * Trigger an event for an element
  2140. *
  2141. * @param {Element|Object} elem
  2142. * Element to trigger an event on
  2143. *
  2144. * @param {EventTarget~Event|string} event
  2145. * A string (the type) or an event object with a type attribute
  2146. *
  2147. * @param {Object} [hash]
  2148. * data hash to pass along with the event
  2149. *
  2150. * @return {boolean|undefined}
  2151. * Returns the opposite of `defaultPrevented` if default was
  2152. * prevented. Otherwise, returns `undefined`
  2153. */
  2154. function trigger(elem, event, hash) {
  2155. // Fetches element data and a reference to the parent (for bubbling).
  2156. // Don't want to add a data object to cache for every parent,
  2157. // so checking hasElData first.
  2158. const elemData = DomData.has(elem) ? DomData.get(elem) : {};
  2159. const parent = elem.parentNode || elem.ownerDocument;
  2160. // type = event.type || event,
  2161. // handler;
  2162. // If an event name was passed as a string, creates an event out of it
  2163. if (typeof event === 'string') {
  2164. event = {
  2165. type: event,
  2166. target: elem
  2167. };
  2168. } else if (!event.target) {
  2169. event.target = elem;
  2170. }
  2171. // Normalizes the event properties.
  2172. event = fixEvent(event);
  2173. // If the passed element has a dispatcher, executes the established handlers.
  2174. if (elemData.dispatcher) {
  2175. elemData.dispatcher.call(elem, event, hash);
  2176. }
  2177. // Unless explicitly stopped or the event does not bubble (e.g. media events)
  2178. // recursively calls this function to bubble the event up the DOM.
  2179. if (parent && !event.isPropagationStopped() && event.bubbles === true) {
  2180. trigger.call(null, parent, event, hash);
  2181. // If at the top of the DOM, triggers the default action unless disabled.
  2182. } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
  2183. if (!DomData.has(event.target)) {
  2184. DomData.set(event.target, {});
  2185. }
  2186. const targetData = DomData.get(event.target);
  2187. // Checks if the target has a default action for this event.
  2188. if (event.target[event.type]) {
  2189. // Temporarily disables event dispatching on the target as we have already executed the handler.
  2190. targetData.disabled = true;
  2191. // Executes the default action.
  2192. if (typeof event.target[event.type] === 'function') {
  2193. event.target[event.type]();
  2194. }
  2195. // Re-enables event dispatching.
  2196. targetData.disabled = false;
  2197. }
  2198. }
  2199. // Inform the triggerer if the default was prevented by returning false
  2200. return !event.defaultPrevented;
  2201. }
  2202. /**
  2203. * Trigger a listener only once for an event.
  2204. *
  2205. * @param {Element|Object} elem
  2206. * Element or object to bind to.
  2207. *
  2208. * @param {string|string[]} type
  2209. * Name/type of event
  2210. *
  2211. * @param {Event~EventListener} fn
  2212. * Event listener function
  2213. */
  2214. function one(elem, type, fn) {
  2215. if (Array.isArray(type)) {
  2216. return _handleMultipleEvents(one, elem, type, fn);
  2217. }
  2218. const func = function () {
  2219. off(elem, type, func);
  2220. fn.apply(this, arguments);
  2221. };
  2222. // copy the guid to the new function so it can removed using the original function's ID
  2223. func.guid = fn.guid = fn.guid || newGUID();
  2224. on(elem, type, func);
  2225. }
  2226. /**
  2227. * Trigger a listener only once and then turn if off for all
  2228. * configured events
  2229. *
  2230. * @param {Element|Object} elem
  2231. * Element or object to bind to.
  2232. *
  2233. * @param {string|string[]} type
  2234. * Name/type of event
  2235. *
  2236. * @param {Event~EventListener} fn
  2237. * Event listener function
  2238. */
  2239. function any(elem, type, fn) {
  2240. const func = function () {
  2241. off(elem, type, func);
  2242. fn.apply(this, arguments);
  2243. };
  2244. // copy the guid to the new function so it can removed using the original function's ID
  2245. func.guid = fn.guid = fn.guid || newGUID();
  2246. // multiple ons, but one off for everything
  2247. on(elem, type, func);
  2248. }
  2249. var Events = /*#__PURE__*/Object.freeze({
  2250. __proto__: null,
  2251. fixEvent: fixEvent,
  2252. on: on,
  2253. off: off,
  2254. trigger: trigger,
  2255. one: one,
  2256. any: any
  2257. });
  2258. /**
  2259. * @file fn.js
  2260. * @module fn
  2261. */
  2262. const UPDATE_REFRESH_INTERVAL = 30;
  2263. /**
  2264. * A private, internal-only function for changing the context of a function.
  2265. *
  2266. * It also stores a unique id on the function so it can be easily removed from
  2267. * events.
  2268. *
  2269. * @private
  2270. * @function
  2271. * @param {*} context
  2272. * The object to bind as scope.
  2273. *
  2274. * @param {Function} fn
  2275. * The function to be bound to a scope.
  2276. *
  2277. * @param {number} [uid]
  2278. * An optional unique ID for the function to be set
  2279. *
  2280. * @return {Function}
  2281. * The new function that will be bound into the context given
  2282. */
  2283. const bind_ = function (context, fn, uid) {
  2284. // Make sure the function has a unique ID
  2285. if (!fn.guid) {
  2286. fn.guid = newGUID();
  2287. }
  2288. // Create the new function that changes the context
  2289. const bound = fn.bind(context);
  2290. // Allow for the ability to individualize this function
  2291. // Needed in the case where multiple objects might share the same prototype
  2292. // IF both items add an event listener with the same function, then you try to remove just one
  2293. // it will remove both because they both have the same guid.
  2294. // when using this, you need to use the bind method when you remove the listener as well.
  2295. // currently used in text tracks
  2296. bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
  2297. return bound;
  2298. };
  2299. /**
  2300. * Wraps the given function, `fn`, with a new function that only invokes `fn`
  2301. * at most once per every `wait` milliseconds.
  2302. *
  2303. * @function
  2304. * @param {Function} fn
  2305. * The function to be throttled.
  2306. *
  2307. * @param {number} wait
  2308. * The number of milliseconds by which to throttle.
  2309. *
  2310. * @return {Function}
  2311. */
  2312. const throttle = function (fn, wait) {
  2313. let last = window__default["default"].performance.now();
  2314. const throttled = function (...args) {
  2315. const now = window__default["default"].performance.now();
  2316. if (now - last >= wait) {
  2317. fn(...args);
  2318. last = now;
  2319. }
  2320. };
  2321. return throttled;
  2322. };
  2323. /**
  2324. * Creates a debounced function that delays invoking `func` until after `wait`
  2325. * milliseconds have elapsed since the last time the debounced function was
  2326. * invoked.
  2327. *
  2328. * Inspired by lodash and underscore implementations.
  2329. *
  2330. * @function
  2331. * @param {Function} func
  2332. * The function to wrap with debounce behavior.
  2333. *
  2334. * @param {number} wait
  2335. * The number of milliseconds to wait after the last invocation.
  2336. *
  2337. * @param {boolean} [immediate]
  2338. * Whether or not to invoke the function immediately upon creation.
  2339. *
  2340. * @param {Object} [context=window]
  2341. * The "context" in which the debounced function should debounce. For
  2342. * example, if this function should be tied to a Video.js player,
  2343. * the player can be passed here. Alternatively, defaults to the
  2344. * global `window` object.
  2345. *
  2346. * @return {Function}
  2347. * A debounced function.
  2348. */
  2349. const debounce = function (func, wait, immediate, context = window__default["default"]) {
  2350. let timeout;
  2351. const cancel = () => {
  2352. context.clearTimeout(timeout);
  2353. timeout = null;
  2354. };
  2355. /* eslint-disable consistent-this */
  2356. const debounced = function () {
  2357. const self = this;
  2358. const args = arguments;
  2359. let later = function () {
  2360. timeout = null;
  2361. later = null;
  2362. if (!immediate) {
  2363. func.apply(self, args);
  2364. }
  2365. };
  2366. if (!timeout && immediate) {
  2367. func.apply(self, args);
  2368. }
  2369. context.clearTimeout(timeout);
  2370. timeout = context.setTimeout(later, wait);
  2371. };
  2372. /* eslint-enable consistent-this */
  2373. debounced.cancel = cancel;
  2374. return debounced;
  2375. };
  2376. var Fn = /*#__PURE__*/Object.freeze({
  2377. __proto__: null,
  2378. UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
  2379. bind_: bind_,
  2380. throttle: throttle,
  2381. debounce: debounce
  2382. });
  2383. /**
  2384. * @file src/js/event-target.js
  2385. */
  2386. let EVENT_MAP;
  2387. /**
  2388. * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
  2389. * adds shorthand functions that wrap around lengthy functions. For example:
  2390. * the `on` function is a wrapper around `addEventListener`.
  2391. *
  2392. * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
  2393. * @class EventTarget
  2394. */
  2395. class EventTarget {
  2396. /**
  2397. * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
  2398. * function that will get called when an event with a certain name gets triggered.
  2399. *
  2400. * @param {string|string[]} type
  2401. * An event name or an array of event names.
  2402. *
  2403. * @param {Function} fn
  2404. * The function to call with `EventTarget`s
  2405. */
  2406. on(type, fn) {
  2407. // Remove the addEventListener alias before calling Events.on
  2408. // so we don't get into an infinite type loop
  2409. const ael = this.addEventListener;
  2410. this.addEventListener = () => {};
  2411. on(this, type, fn);
  2412. this.addEventListener = ael;
  2413. }
  2414. /**
  2415. * Removes an `event listener` for a specific event from an instance of `EventTarget`.
  2416. * This makes it so that the `event listener` will no longer get called when the
  2417. * named event happens.
  2418. *
  2419. * @param {string|string[]} type
  2420. * An event name or an array of event names.
  2421. *
  2422. * @param {Function} fn
  2423. * The function to remove.
  2424. */
  2425. off(type, fn) {
  2426. off(this, type, fn);
  2427. }
  2428. /**
  2429. * This function will add an `event listener` that gets triggered only once. After the
  2430. * first trigger it will get removed. This is like adding an `event listener`
  2431. * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
  2432. *
  2433. * @param {string|string[]} type
  2434. * An event name or an array of event names.
  2435. *
  2436. * @param {Function} fn
  2437. * The function to be called once for each event name.
  2438. */
  2439. one(type, fn) {
  2440. // Remove the addEventListener aliasing Events.on
  2441. // so we don't get into an infinite type loop
  2442. const ael = this.addEventListener;
  2443. this.addEventListener = () => {};
  2444. one(this, type, fn);
  2445. this.addEventListener = ael;
  2446. }
  2447. /**
  2448. * This function will add an `event listener` that gets triggered only once and is
  2449. * removed from all events. This is like adding an array of `event listener`s
  2450. * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
  2451. * first time it is triggered.
  2452. *
  2453. * @param {string|string[]} type
  2454. * An event name or an array of event names.
  2455. *
  2456. * @param {Function} fn
  2457. * The function to be called once for each event name.
  2458. */
  2459. any(type, fn) {
  2460. // Remove the addEventListener aliasing Events.on
  2461. // so we don't get into an infinite type loop
  2462. const ael = this.addEventListener;
  2463. this.addEventListener = () => {};
  2464. any(this, type, fn);
  2465. this.addEventListener = ael;
  2466. }
  2467. /**
  2468. * This function causes an event to happen. This will then cause any `event listeners`
  2469. * that are waiting for that event, to get called. If there are no `event listeners`
  2470. * for an event then nothing will happen.
  2471. *
  2472. * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
  2473. * Trigger will also call the `on` + `uppercaseEventName` function.
  2474. *
  2475. * Example:
  2476. * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
  2477. * `onClick` if it exists.
  2478. *
  2479. * @param {string|EventTarget~Event|Object} event
  2480. * The name of the event, an `Event`, or an object with a key of type set to
  2481. * an event name.
  2482. */
  2483. trigger(event) {
  2484. const type = event.type || event;
  2485. // deprecation
  2486. // In a future version we should default target to `this`
  2487. // similar to how we default the target to `elem` in
  2488. // `Events.trigger`. Right now the default `target` will be
  2489. // `document` due to the `Event.fixEvent` call.
  2490. if (typeof event === 'string') {
  2491. event = {
  2492. type
  2493. };
  2494. }
  2495. event = fixEvent(event);
  2496. if (this.allowedEvents_[type] && this['on' + type]) {
  2497. this['on' + type](event);
  2498. }
  2499. trigger(this, event);
  2500. }
  2501. queueTrigger(event) {
  2502. // only set up EVENT_MAP if it'll be used
  2503. if (!EVENT_MAP) {
  2504. EVENT_MAP = new Map();
  2505. }
  2506. const type = event.type || event;
  2507. let map = EVENT_MAP.get(this);
  2508. if (!map) {
  2509. map = new Map();
  2510. EVENT_MAP.set(this, map);
  2511. }
  2512. const oldTimeout = map.get(type);
  2513. map.delete(type);
  2514. window__default["default"].clearTimeout(oldTimeout);
  2515. const timeout = window__default["default"].setTimeout(() => {
  2516. map.delete(type);
  2517. // if we cleared out all timeouts for the current target, delete its map
  2518. if (map.size === 0) {
  2519. map = null;
  2520. EVENT_MAP.delete(this);
  2521. }
  2522. this.trigger(event);
  2523. }, 0);
  2524. map.set(type, timeout);
  2525. }
  2526. }
  2527. /**
  2528. * A Custom DOM event.
  2529. *
  2530. * @typedef {CustomEvent} Event
  2531. * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
  2532. */
  2533. /**
  2534. * All event listeners should follow the following format.
  2535. *
  2536. * @callback EventListener
  2537. * @this {EventTarget}
  2538. *
  2539. * @param {Event} event
  2540. * the event that triggered this function
  2541. *
  2542. * @param {Object} [hash]
  2543. * hash of data sent during the event
  2544. */
  2545. /**
  2546. * An object containing event names as keys and booleans as values.
  2547. *
  2548. * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
  2549. * will have extra functionality. See that function for more information.
  2550. *
  2551. * @property EventTarget.prototype.allowedEvents_
  2552. * @protected
  2553. */
  2554. EventTarget.prototype.allowedEvents_ = {};
  2555. /**
  2556. * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
  2557. * the standard DOM API.
  2558. *
  2559. * @function
  2560. * @see {@link EventTarget#on}
  2561. */
  2562. EventTarget.prototype.addEventListener = EventTarget.prototype.on;
  2563. /**
  2564. * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
  2565. * the standard DOM API.
  2566. *
  2567. * @function
  2568. * @see {@link EventTarget#off}
  2569. */
  2570. EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
  2571. /**
  2572. * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
  2573. * the standard DOM API.
  2574. *
  2575. * @function
  2576. * @see {@link EventTarget#trigger}
  2577. */
  2578. EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
  2579. /**
  2580. * @file mixins/evented.js
  2581. * @module evented
  2582. */
  2583. const objName = obj => {
  2584. if (typeof obj.name === 'function') {
  2585. return obj.name();
  2586. }
  2587. if (typeof obj.name === 'string') {
  2588. return obj.name;
  2589. }
  2590. if (obj.name_) {
  2591. return obj.name_;
  2592. }
  2593. if (obj.constructor && obj.constructor.name) {
  2594. return obj.constructor.name;
  2595. }
  2596. return typeof obj;
  2597. };
  2598. /**
  2599. * Returns whether or not an object has had the evented mixin applied.
  2600. *
  2601. * @param {Object} object
  2602. * An object to test.
  2603. *
  2604. * @return {boolean}
  2605. * Whether or not the object appears to be evented.
  2606. */
  2607. const isEvented = object => object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
  2608. /**
  2609. * Adds a callback to run after the evented mixin applied.
  2610. *
  2611. * @param {Object} target
  2612. * An object to Add
  2613. * @param {Function} callback
  2614. * The callback to run.
  2615. */
  2616. const addEventedCallback = (target, callback) => {
  2617. if (isEvented(target)) {
  2618. callback();
  2619. } else {
  2620. if (!target.eventedCallbacks) {
  2621. target.eventedCallbacks = [];
  2622. }
  2623. target.eventedCallbacks.push(callback);
  2624. }
  2625. };
  2626. /**
  2627. * Whether a value is a valid event type - non-empty string or array.
  2628. *
  2629. * @private
  2630. * @param {string|Array} type
  2631. * The type value to test.
  2632. *
  2633. * @return {boolean}
  2634. * Whether or not the type is a valid event type.
  2635. */
  2636. const isValidEventType = type =>
  2637. // The regex here verifies that the `type` contains at least one non-
  2638. // whitespace character.
  2639. typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
  2640. /**
  2641. * Validates a value to determine if it is a valid event target. Throws if not.
  2642. *
  2643. * @private
  2644. * @throws {Error}
  2645. * If the target does not appear to be a valid event target.
  2646. *
  2647. * @param {Object} target
  2648. * The object to test.
  2649. *
  2650. * @param {Object} obj
  2651. * The evented object we are validating for
  2652. *
  2653. * @param {string} fnName
  2654. * The name of the evented mixin function that called this.
  2655. */
  2656. const validateTarget = (target, obj, fnName) => {
  2657. if (!target || !target.nodeName && !isEvented(target)) {
  2658. throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
  2659. }
  2660. };
  2661. /**
  2662. * Validates a value to determine if it is a valid event target. Throws if not.
  2663. *
  2664. * @private
  2665. * @throws {Error}
  2666. * If the type does not appear to be a valid event type.
  2667. *
  2668. * @param {string|Array} type
  2669. * The type to test.
  2670. *
  2671. * @param {Object} obj
  2672. * The evented object we are validating for
  2673. *
  2674. * @param {string} fnName
  2675. * The name of the evented mixin function that called this.
  2676. */
  2677. const validateEventType = (type, obj, fnName) => {
  2678. if (!isValidEventType(type)) {
  2679. throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
  2680. }
  2681. };
  2682. /**
  2683. * Validates a value to determine if it is a valid listener. Throws if not.
  2684. *
  2685. * @private
  2686. * @throws {Error}
  2687. * If the listener is not a function.
  2688. *
  2689. * @param {Function} listener
  2690. * The listener to test.
  2691. *
  2692. * @param {Object} obj
  2693. * The evented object we are validating for
  2694. *
  2695. * @param {string} fnName
  2696. * The name of the evented mixin function that called this.
  2697. */
  2698. const validateListener = (listener, obj, fnName) => {
  2699. if (typeof listener !== 'function') {
  2700. throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
  2701. }
  2702. };
  2703. /**
  2704. * Takes an array of arguments given to `on()` or `one()`, validates them, and
  2705. * normalizes them into an object.
  2706. *
  2707. * @private
  2708. * @param {Object} self
  2709. * The evented object on which `on()` or `one()` was called. This
  2710. * object will be bound as the `this` value for the listener.
  2711. *
  2712. * @param {Array} args
  2713. * An array of arguments passed to `on()` or `one()`.
  2714. *
  2715. * @param {string} fnName
  2716. * The name of the evented mixin function that called this.
  2717. *
  2718. * @return {Object}
  2719. * An object containing useful values for `on()` or `one()` calls.
  2720. */
  2721. const normalizeListenArgs = (self, args, fnName) => {
  2722. // If the number of arguments is less than 3, the target is always the
  2723. // evented object itself.
  2724. const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
  2725. let target;
  2726. let type;
  2727. let listener;
  2728. if (isTargetingSelf) {
  2729. target = self.eventBusEl_;
  2730. // Deal with cases where we got 3 arguments, but we are still listening to
  2731. // the evented object itself.
  2732. if (args.length >= 3) {
  2733. args.shift();
  2734. }
  2735. [type, listener] = args;
  2736. } else {
  2737. [target, type, listener] = args;
  2738. }
  2739. validateTarget(target, self, fnName);
  2740. validateEventType(type, self, fnName);
  2741. validateListener(listener, self, fnName);
  2742. listener = bind_(self, listener);
  2743. return {
  2744. isTargetingSelf,
  2745. target,
  2746. type,
  2747. listener
  2748. };
  2749. };
  2750. /**
  2751. * Adds the listener to the event type(s) on the target, normalizing for
  2752. * the type of target.
  2753. *
  2754. * @private
  2755. * @param {Element|Object} target
  2756. * A DOM node or evented object.
  2757. *
  2758. * @param {string} method
  2759. * The event binding method to use ("on" or "one").
  2760. *
  2761. * @param {string|Array} type
  2762. * One or more event type(s).
  2763. *
  2764. * @param {Function} listener
  2765. * A listener function.
  2766. */
  2767. const listen = (target, method, type, listener) => {
  2768. validateTarget(target, target, method);
  2769. if (target.nodeName) {
  2770. Events[method](target, type, listener);
  2771. } else {
  2772. target[method](type, listener);
  2773. }
  2774. };
  2775. /**
  2776. * Contains methods that provide event capabilities to an object which is passed
  2777. * to {@link module:evented|evented}.
  2778. *
  2779. * @mixin EventedMixin
  2780. */
  2781. const EventedMixin = {
  2782. /**
  2783. * Add a listener to an event (or events) on this object or another evented
  2784. * object.
  2785. *
  2786. * @param {string|Array|Element|Object} targetOrType
  2787. * If this is a string or array, it represents the event type(s)
  2788. * that will trigger the listener.
  2789. *
  2790. * Another evented object can be passed here instead, which will
  2791. * cause the listener to listen for events on _that_ object.
  2792. *
  2793. * In either case, the listener's `this` value will be bound to
  2794. * this object.
  2795. *
  2796. * @param {string|Array|Function} typeOrListener
  2797. * If the first argument was a string or array, this should be the
  2798. * listener function. Otherwise, this is a string or array of event
  2799. * type(s).
  2800. *
  2801. * @param {Function} [listener]
  2802. * If the first argument was another evented object, this will be
  2803. * the listener function.
  2804. */
  2805. on(...args) {
  2806. const {
  2807. isTargetingSelf,
  2808. target,
  2809. type,
  2810. listener
  2811. } = normalizeListenArgs(this, args, 'on');
  2812. listen(target, 'on', type, listener);
  2813. // If this object is listening to another evented object.
  2814. if (!isTargetingSelf) {
  2815. // If this object is disposed, remove the listener.
  2816. const removeListenerOnDispose = () => this.off(target, type, listener);
  2817. // Use the same function ID as the listener so we can remove it later it
  2818. // using the ID of the original listener.
  2819. removeListenerOnDispose.guid = listener.guid;
  2820. // Add a listener to the target's dispose event as well. This ensures
  2821. // that if the target is disposed BEFORE this object, we remove the
  2822. // removal listener that was just added. Otherwise, we create a memory leak.
  2823. const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
  2824. // Use the same function ID as the listener so we can remove it later
  2825. // it using the ID of the original listener.
  2826. removeRemoverOnTargetDispose.guid = listener.guid;
  2827. listen(this, 'on', 'dispose', removeListenerOnDispose);
  2828. listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
  2829. }
  2830. },
  2831. /**
  2832. * Add a listener to an event (or events) on this object or another evented
  2833. * object. The listener will be called once per event and then removed.
  2834. *
  2835. * @param {string|Array|Element|Object} targetOrType
  2836. * If this is a string or array, it represents the event type(s)
  2837. * that will trigger the listener.
  2838. *
  2839. * Another evented object can be passed here instead, which will
  2840. * cause the listener to listen for events on _that_ object.
  2841. *
  2842. * In either case, the listener's `this` value will be bound to
  2843. * this object.
  2844. *
  2845. * @param {string|Array|Function} typeOrListener
  2846. * If the first argument was a string or array, this should be the
  2847. * listener function. Otherwise, this is a string or array of event
  2848. * type(s).
  2849. *
  2850. * @param {Function} [listener]
  2851. * If the first argument was another evented object, this will be
  2852. * the listener function.
  2853. */
  2854. one(...args) {
  2855. const {
  2856. isTargetingSelf,
  2857. target,
  2858. type,
  2859. listener
  2860. } = normalizeListenArgs(this, args, 'one');
  2861. // Targeting this evented object.
  2862. if (isTargetingSelf) {
  2863. listen(target, 'one', type, listener);
  2864. // Targeting another evented object.
  2865. } else {
  2866. // TODO: This wrapper is incorrect! It should only
  2867. // remove the wrapper for the event type that called it.
  2868. // Instead all listeners are removed on the first trigger!
  2869. // see https://github.com/videojs/video.js/issues/5962
  2870. const wrapper = (...largs) => {
  2871. this.off(target, type, wrapper);
  2872. listener.apply(null, largs);
  2873. };
  2874. // Use the same function ID as the listener so we can remove it later
  2875. // it using the ID of the original listener.
  2876. wrapper.guid = listener.guid;
  2877. listen(target, 'one', type, wrapper);
  2878. }
  2879. },
  2880. /**
  2881. * Add a listener to an event (or events) on this object or another evented
  2882. * object. The listener will only be called once for the first event that is triggered
  2883. * then removed.
  2884. *
  2885. * @param {string|Array|Element|Object} targetOrType
  2886. * If this is a string or array, it represents the event type(s)
  2887. * that will trigger the listener.
  2888. *
  2889. * Another evented object can be passed here instead, which will
  2890. * cause the listener to listen for events on _that_ object.
  2891. *
  2892. * In either case, the listener's `this` value will be bound to
  2893. * this object.
  2894. *
  2895. * @param {string|Array|Function} typeOrListener
  2896. * If the first argument was a string or array, this should be the
  2897. * listener function. Otherwise, this is a string or array of event
  2898. * type(s).
  2899. *
  2900. * @param {Function} [listener]
  2901. * If the first argument was another evented object, this will be
  2902. * the listener function.
  2903. */
  2904. any(...args) {
  2905. const {
  2906. isTargetingSelf,
  2907. target,
  2908. type,
  2909. listener
  2910. } = normalizeListenArgs(this, args, 'any');
  2911. // Targeting this evented object.
  2912. if (isTargetingSelf) {
  2913. listen(target, 'any', type, listener);
  2914. // Targeting another evented object.
  2915. } else {
  2916. const wrapper = (...largs) => {
  2917. this.off(target, type, wrapper);
  2918. listener.apply(null, largs);
  2919. };
  2920. // Use the same function ID as the listener so we can remove it later
  2921. // it using the ID of the original listener.
  2922. wrapper.guid = listener.guid;
  2923. listen(target, 'any', type, wrapper);
  2924. }
  2925. },
  2926. /**
  2927. * Removes listener(s) from event(s) on an evented object.
  2928. *
  2929. * @param {string|Array|Element|Object} [targetOrType]
  2930. * If this is a string or array, it represents the event type(s).
  2931. *
  2932. * Another evented object can be passed here instead, in which case
  2933. * ALL 3 arguments are _required_.
  2934. *
  2935. * @param {string|Array|Function} [typeOrListener]
  2936. * If the first argument was a string or array, this may be the
  2937. * listener function. Otherwise, this is a string or array of event
  2938. * type(s).
  2939. *
  2940. * @param {Function} [listener]
  2941. * If the first argument was another evented object, this will be
  2942. * the listener function; otherwise, _all_ listeners bound to the
  2943. * event type(s) will be removed.
  2944. */
  2945. off(targetOrType, typeOrListener, listener) {
  2946. // Targeting this evented object.
  2947. if (!targetOrType || isValidEventType(targetOrType)) {
  2948. off(this.eventBusEl_, targetOrType, typeOrListener);
  2949. // Targeting another evented object.
  2950. } else {
  2951. const target = targetOrType;
  2952. const type = typeOrListener;
  2953. // Fail fast and in a meaningful way!
  2954. validateTarget(target, this, 'off');
  2955. validateEventType(type, this, 'off');
  2956. validateListener(listener, this, 'off');
  2957. // Ensure there's at least a guid, even if the function hasn't been used
  2958. listener = bind_(this, listener);
  2959. // Remove the dispose listener on this evented object, which was given
  2960. // the same guid as the event listener in on().
  2961. this.off('dispose', listener);
  2962. if (target.nodeName) {
  2963. off(target, type, listener);
  2964. off(target, 'dispose', listener);
  2965. } else if (isEvented(target)) {
  2966. target.off(type, listener);
  2967. target.off('dispose', listener);
  2968. }
  2969. }
  2970. },
  2971. /**
  2972. * Fire an event on this evented object, causing its listeners to be called.
  2973. *
  2974. * @param {string|Object} event
  2975. * An event type or an object with a type property.
  2976. *
  2977. * @param {Object} [hash]
  2978. * An additional object to pass along to listeners.
  2979. *
  2980. * @return {boolean}
  2981. * Whether or not the default behavior was prevented.
  2982. */
  2983. trigger(event, hash) {
  2984. validateTarget(this.eventBusEl_, this, 'trigger');
  2985. const type = event && typeof event !== 'string' ? event.type : event;
  2986. if (!isValidEventType(type)) {
  2987. 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.');
  2988. }
  2989. return trigger(this.eventBusEl_, event, hash);
  2990. }
  2991. };
  2992. /**
  2993. * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
  2994. *
  2995. * @param {Object} target
  2996. * The object to which to add event methods.
  2997. *
  2998. * @param {Object} [options={}]
  2999. * Options for customizing the mixin behavior.
  3000. *
  3001. * @param {string} [options.eventBusKey]
  3002. * By default, adds a `eventBusEl_` DOM element to the target object,
  3003. * which is used as an event bus. If the target object already has a
  3004. * DOM element that should be used, pass its key here.
  3005. *
  3006. * @return {Object}
  3007. * The target object.
  3008. */
  3009. function evented(target, options = {}) {
  3010. const {
  3011. eventBusKey
  3012. } = options;
  3013. // Set or create the eventBusEl_.
  3014. if (eventBusKey) {
  3015. if (!target[eventBusKey].nodeName) {
  3016. throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
  3017. }
  3018. target.eventBusEl_ = target[eventBusKey];
  3019. } else {
  3020. target.eventBusEl_ = createEl('span', {
  3021. className: 'vjs-event-bus'
  3022. });
  3023. }
  3024. Object.assign(target, EventedMixin);
  3025. if (target.eventedCallbacks) {
  3026. target.eventedCallbacks.forEach(callback => {
  3027. callback();
  3028. });
  3029. }
  3030. // When any evented object is disposed, it removes all its listeners.
  3031. target.on('dispose', () => {
  3032. target.off();
  3033. [target, target.el_, target.eventBusEl_].forEach(function (val) {
  3034. if (val && DomData.has(val)) {
  3035. DomData.delete(val);
  3036. }
  3037. });
  3038. window__default["default"].setTimeout(() => {
  3039. target.eventBusEl_ = null;
  3040. }, 0);
  3041. });
  3042. return target;
  3043. }
  3044. /**
  3045. * @file mixins/stateful.js
  3046. * @module stateful
  3047. */
  3048. /**
  3049. * Contains methods that provide statefulness to an object which is passed
  3050. * to {@link module:stateful}.
  3051. *
  3052. * @mixin StatefulMixin
  3053. */
  3054. const StatefulMixin = {
  3055. /**
  3056. * A hash containing arbitrary keys and values representing the state of
  3057. * the object.
  3058. *
  3059. * @type {Object}
  3060. */
  3061. state: {},
  3062. /**
  3063. * Set the state of an object by mutating its
  3064. * {@link module:stateful~StatefulMixin.state|state} object in place.
  3065. *
  3066. * @fires module:stateful~StatefulMixin#statechanged
  3067. * @param {Object|Function} stateUpdates
  3068. * A new set of properties to shallow-merge into the plugin state.
  3069. * Can be a plain object or a function returning a plain object.
  3070. *
  3071. * @return {Object|undefined}
  3072. * An object containing changes that occurred. If no changes
  3073. * occurred, returns `undefined`.
  3074. */
  3075. setState(stateUpdates) {
  3076. // Support providing the `stateUpdates` state as a function.
  3077. if (typeof stateUpdates === 'function') {
  3078. stateUpdates = stateUpdates();
  3079. }
  3080. let changes;
  3081. each(stateUpdates, (value, key) => {
  3082. // Record the change if the value is different from what's in the
  3083. // current state.
  3084. if (this.state[key] !== value) {
  3085. changes = changes || {};
  3086. changes[key] = {
  3087. from: this.state[key],
  3088. to: value
  3089. };
  3090. }
  3091. this.state[key] = value;
  3092. });
  3093. // Only trigger "statechange" if there were changes AND we have a trigger
  3094. // function. This allows us to not require that the target object be an
  3095. // evented object.
  3096. if (changes && isEvented(this)) {
  3097. /**
  3098. * An event triggered on an object that is both
  3099. * {@link module:stateful|stateful} and {@link module:evented|evented}
  3100. * indicating that its state has changed.
  3101. *
  3102. * @event module:stateful~StatefulMixin#statechanged
  3103. * @type {Object}
  3104. * @property {Object} changes
  3105. * A hash containing the properties that were changed and
  3106. * the values they were changed `from` and `to`.
  3107. */
  3108. this.trigger({
  3109. changes,
  3110. type: 'statechanged'
  3111. });
  3112. }
  3113. return changes;
  3114. }
  3115. };
  3116. /**
  3117. * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
  3118. * object.
  3119. *
  3120. * If the target object is {@link module:evented|evented} and has a
  3121. * `handleStateChanged` method, that method will be automatically bound to the
  3122. * `statechanged` event on itself.
  3123. *
  3124. * @param {Object} target
  3125. * The object to be made stateful.
  3126. *
  3127. * @param {Object} [defaultState]
  3128. * A default set of properties to populate the newly-stateful object's
  3129. * `state` property.
  3130. *
  3131. * @return {Object}
  3132. * Returns the `target`.
  3133. */
  3134. function stateful(target, defaultState) {
  3135. Object.assign(target, StatefulMixin);
  3136. // This happens after the mixing-in because we need to replace the `state`
  3137. // added in that step.
  3138. target.state = Object.assign({}, target.state, defaultState);
  3139. // Auto-bind the `handleStateChanged` method of the target object if it exists.
  3140. if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
  3141. target.on('statechanged', target.handleStateChanged);
  3142. }
  3143. return target;
  3144. }
  3145. /**
  3146. * @file str.js
  3147. * @module to-lower-case
  3148. */
  3149. /**
  3150. * Lowercase the first letter of a string.
  3151. *
  3152. * @param {string} string
  3153. * String to be lowercased
  3154. *
  3155. * @return {string}
  3156. * The string with a lowercased first letter
  3157. */
  3158. const toLowerCase = function (string) {
  3159. if (typeof string !== 'string') {
  3160. return string;
  3161. }
  3162. return string.replace(/./, w => w.toLowerCase());
  3163. };
  3164. /**
  3165. * Uppercase the first letter of a string.
  3166. *
  3167. * @param {string} string
  3168. * String to be uppercased
  3169. *
  3170. * @return {string}
  3171. * The string with an uppercased first letter
  3172. */
  3173. const toTitleCase = function (string) {
  3174. if (typeof string !== 'string') {
  3175. return string;
  3176. }
  3177. return string.replace(/./, w => w.toUpperCase());
  3178. };
  3179. /**
  3180. * Compares the TitleCase versions of the two strings for equality.
  3181. *
  3182. * @param {string} str1
  3183. * The first string to compare
  3184. *
  3185. * @param {string} str2
  3186. * The second string to compare
  3187. *
  3188. * @return {boolean}
  3189. * Whether the TitleCase versions of the strings are equal
  3190. */
  3191. const titleCaseEquals = function (str1, str2) {
  3192. return toTitleCase(str1) === toTitleCase(str2);
  3193. };
  3194. var Str = /*#__PURE__*/Object.freeze({
  3195. __proto__: null,
  3196. toLowerCase: toLowerCase,
  3197. toTitleCase: toTitleCase,
  3198. titleCaseEquals: titleCaseEquals
  3199. });
  3200. /**
  3201. * Player Component - Base class for all UI objects
  3202. *
  3203. * @file component.js
  3204. */
  3205. /**
  3206. * Base class for all UI Components.
  3207. * Components are UI objects which represent both a javascript object and an element
  3208. * in the DOM. They can be children of other components, and can have
  3209. * children themselves.
  3210. *
  3211. * Components can also use methods from {@link EventTarget}
  3212. */
  3213. class Component {
  3214. /**
  3215. * A callback that is called when a component is ready. Does not have any
  3216. * parameters and any callback value will be ignored.
  3217. *
  3218. * @callback ReadyCallback
  3219. * @this Component
  3220. */
  3221. /**
  3222. * Creates an instance of this class.
  3223. *
  3224. * @param { import('./player').default } player
  3225. * The `Player` that this class should be attached to.
  3226. *
  3227. * @param {Object} [options]
  3228. * The key/value store of component options.
  3229. *
  3230. * @param {Object[]} [options.children]
  3231. * An array of children objects to initialize this component with. Children objects have
  3232. * a name property that will be used if more than one component of the same type needs to be
  3233. * added.
  3234. *
  3235. * @param {string} [options.className]
  3236. * A class or space separated list of classes to add the component
  3237. *
  3238. * @param {ReadyCallback} [ready]
  3239. * Function that gets called when the `Component` is ready.
  3240. */
  3241. constructor(player, options, ready) {
  3242. // The component might be the player itself and we can't pass `this` to super
  3243. if (!player && this.play) {
  3244. this.player_ = player = this; // eslint-disable-line
  3245. } else {
  3246. this.player_ = player;
  3247. }
  3248. this.isDisposed_ = false;
  3249. // Hold the reference to the parent component via `addChild` method
  3250. this.parentComponent_ = null;
  3251. // Make a copy of prototype.options_ to protect against overriding defaults
  3252. this.options_ = merge({}, this.options_);
  3253. // Updated options with supplied options
  3254. options = this.options_ = merge(this.options_, options);
  3255. // Get ID from options or options element if one is supplied
  3256. this.id_ = options.id || options.el && options.el.id;
  3257. // If there was no ID from the options, generate one
  3258. if (!this.id_) {
  3259. // Don't require the player ID function in the case of mock players
  3260. const id = player && player.id && player.id() || 'no_player';
  3261. this.id_ = `${id}_component_${newGUID()}`;
  3262. }
  3263. this.name_ = options.name || null;
  3264. // Create element if one wasn't provided in options
  3265. if (options.el) {
  3266. this.el_ = options.el;
  3267. } else if (options.createEl !== false) {
  3268. this.el_ = this.createEl();
  3269. }
  3270. if (options.className && this.el_) {
  3271. options.className.split(' ').forEach(c => this.addClass(c));
  3272. }
  3273. // Remove the placeholder event methods. If the component is evented, the
  3274. // real methods are added next
  3275. ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
  3276. this[fn] = undefined;
  3277. });
  3278. // if evented is anything except false, we want to mixin in evented
  3279. if (options.evented !== false) {
  3280. // Make this an evented object and use `el_`, if available, as its event bus
  3281. evented(this, {
  3282. eventBusKey: this.el_ ? 'el_' : null
  3283. });
  3284. this.handleLanguagechange = this.handleLanguagechange.bind(this);
  3285. this.on(this.player_, 'languagechange', this.handleLanguagechange);
  3286. }
  3287. stateful(this, this.constructor.defaultState);
  3288. this.children_ = [];
  3289. this.childIndex_ = {};
  3290. this.childNameIndex_ = {};
  3291. this.setTimeoutIds_ = new Set();
  3292. this.setIntervalIds_ = new Set();
  3293. this.rafIds_ = new Set();
  3294. this.namedRafs_ = new Map();
  3295. this.clearingTimersOnDispose_ = false;
  3296. // Add any child components in options
  3297. if (options.initChildren !== false) {
  3298. this.initChildren();
  3299. }
  3300. // Don't want to trigger ready here or it will go before init is actually
  3301. // finished for all children that run this constructor
  3302. this.ready(ready);
  3303. if (options.reportTouchActivity !== false) {
  3304. this.enableTouchActivity();
  3305. }
  3306. }
  3307. // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
  3308. // They are replaced or removed in the constructor
  3309. /**
  3310. * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
  3311. * function that will get called when an event with a certain name gets triggered.
  3312. *
  3313. * @param {string|string[]} type
  3314. * An event name or an array of event names.
  3315. *
  3316. * @param {Function} fn
  3317. * The function to call with `EventTarget`s
  3318. */
  3319. on(type, fn) {}
  3320. /**
  3321. * Removes an `event listener` for a specific event from an instance of `EventTarget`.
  3322. * This makes it so that the `event listener` will no longer get called when the
  3323. * named event happens.
  3324. *
  3325. * @param {string|string[]} type
  3326. * An event name or an array of event names.
  3327. *
  3328. * @param {Function} [fn]
  3329. * The function to remove. If not specified, all listeners managed by Video.js will be removed.
  3330. */
  3331. off(type, fn) {}
  3332. /**
  3333. * This function will add an `event listener` that gets triggered only once. After the
  3334. * first trigger it will get removed. This is like adding an `event listener`
  3335. * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
  3336. *
  3337. * @param {string|string[]} type
  3338. * An event name or an array of event names.
  3339. *
  3340. * @param {Function} fn
  3341. * The function to be called once for each event name.
  3342. */
  3343. one(type, fn) {}
  3344. /**
  3345. * This function will add an `event listener` that gets triggered only once and is
  3346. * removed from all events. This is like adding an array of `event listener`s
  3347. * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
  3348. * first time it is triggered.
  3349. *
  3350. * @param {string|string[]} type
  3351. * An event name or an array of event names.
  3352. *
  3353. * @param {Function} fn
  3354. * The function to be called once for each event name.
  3355. */
  3356. any(type, fn) {}
  3357. /**
  3358. * This function causes an event to happen. This will then cause any `event listeners`
  3359. * that are waiting for that event, to get called. If there are no `event listeners`
  3360. * for an event then nothing will happen.
  3361. *
  3362. * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
  3363. * Trigger will also call the `on` + `uppercaseEventName` function.
  3364. *
  3365. * Example:
  3366. * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
  3367. * `onClick` if it exists.
  3368. *
  3369. * @param {string|Event|Object} event
  3370. * The name of the event, an `Event`, or an object with a key of type set to
  3371. * an event name.
  3372. *
  3373. * @param {Object} [hash]
  3374. * Optionally extra argument to pass through to an event listener
  3375. */
  3376. trigger(event, hash) {}
  3377. /**
  3378. * Dispose of the `Component` and all child components.
  3379. *
  3380. * @fires Component#dispose
  3381. *
  3382. * @param {Object} options
  3383. * @param {Element} options.originalEl element with which to replace player element
  3384. */
  3385. dispose(options = {}) {
  3386. // Bail out if the component has already been disposed.
  3387. if (this.isDisposed_) {
  3388. return;
  3389. }
  3390. if (this.readyQueue_) {
  3391. this.readyQueue_.length = 0;
  3392. }
  3393. /**
  3394. * Triggered when a `Component` is disposed.
  3395. *
  3396. * @event Component#dispose
  3397. * @type {Event}
  3398. *
  3399. * @property {boolean} [bubbles=false]
  3400. * set to false so that the dispose event does not
  3401. * bubble up
  3402. */
  3403. this.trigger({
  3404. type: 'dispose',
  3405. bubbles: false
  3406. });
  3407. this.isDisposed_ = true;
  3408. // Dispose all children.
  3409. if (this.children_) {
  3410. for (let i = this.children_.length - 1; i >= 0; i--) {
  3411. if (this.children_[i].dispose) {
  3412. this.children_[i].dispose();
  3413. }
  3414. }
  3415. }
  3416. // Delete child references
  3417. this.children_ = null;
  3418. this.childIndex_ = null;
  3419. this.childNameIndex_ = null;
  3420. this.parentComponent_ = null;
  3421. if (this.el_) {
  3422. // Remove element from DOM
  3423. if (this.el_.parentNode) {
  3424. if (options.restoreEl) {
  3425. this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
  3426. } else {
  3427. this.el_.parentNode.removeChild(this.el_);
  3428. }
  3429. }
  3430. this.el_ = null;
  3431. }
  3432. // remove reference to the player after disposing of the element
  3433. this.player_ = null;
  3434. }
  3435. /**
  3436. * Determine whether or not this component has been disposed.
  3437. *
  3438. * @return {boolean}
  3439. * If the component has been disposed, will be `true`. Otherwise, `false`.
  3440. */
  3441. isDisposed() {
  3442. return Boolean(this.isDisposed_);
  3443. }
  3444. /**
  3445. * Return the {@link Player} that the `Component` has attached to.
  3446. *
  3447. * @return { import('./player').default }
  3448. * The player that this `Component` has attached to.
  3449. */
  3450. player() {
  3451. return this.player_;
  3452. }
  3453. /**
  3454. * Deep merge of options objects with new options.
  3455. * > Note: When both `obj` and `options` contain properties whose values are objects.
  3456. * The two properties get merged using {@link module:obj.merge}
  3457. *
  3458. * @param {Object} obj
  3459. * The object that contains new options.
  3460. *
  3461. * @return {Object}
  3462. * A new object of `this.options_` and `obj` merged together.
  3463. */
  3464. options(obj) {
  3465. if (!obj) {
  3466. return this.options_;
  3467. }
  3468. this.options_ = merge(this.options_, obj);
  3469. return this.options_;
  3470. }
  3471. /**
  3472. * Get the `Component`s DOM element
  3473. *
  3474. * @return {Element}
  3475. * The DOM element for this `Component`.
  3476. */
  3477. el() {
  3478. return this.el_;
  3479. }
  3480. /**
  3481. * Create the `Component`s DOM element.
  3482. *
  3483. * @param {string} [tagName]
  3484. * Element's DOM node type. e.g. 'div'
  3485. *
  3486. * @param {Object} [properties]
  3487. * An object of properties that should be set.
  3488. *
  3489. * @param {Object} [attributes]
  3490. * An object of attributes that should be set.
  3491. *
  3492. * @return {Element}
  3493. * The element that gets created.
  3494. */
  3495. createEl(tagName, properties, attributes) {
  3496. return createEl(tagName, properties, attributes);
  3497. }
  3498. /**
  3499. * Localize a string given the string in english.
  3500. *
  3501. * If tokens are provided, it'll try and run a simple token replacement on the provided string.
  3502. * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
  3503. *
  3504. * If a `defaultValue` is provided, it'll use that over `string`,
  3505. * if a value isn't found in provided language files.
  3506. * This is useful if you want to have a descriptive key for token replacement
  3507. * but have a succinct localized string and not require `en.json` to be included.
  3508. *
  3509. * Currently, it is used for the progress bar timing.
  3510. * ```js
  3511. * {
  3512. * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
  3513. * }
  3514. * ```
  3515. * It is then used like so:
  3516. * ```js
  3517. * this.localize('progress bar timing: currentTime={1} duration{2}',
  3518. * [this.player_.currentTime(), this.player_.duration()],
  3519. * '{1} of {2}');
  3520. * ```
  3521. *
  3522. * Which outputs something like: `01:23 of 24:56`.
  3523. *
  3524. *
  3525. * @param {string} string
  3526. * The string to localize and the key to lookup in the language files.
  3527. * @param {string[]} [tokens]
  3528. * If the current item has token replacements, provide the tokens here.
  3529. * @param {string} [defaultValue]
  3530. * Defaults to `string`. Can be a default value to use for token replacement
  3531. * if the lookup key is needed to be separate.
  3532. *
  3533. * @return {string}
  3534. * The localized string or if no localization exists the english string.
  3535. */
  3536. localize(string, tokens, defaultValue = string) {
  3537. const code = this.player_.language && this.player_.language();
  3538. const languages = this.player_.languages && this.player_.languages();
  3539. const language = languages && languages[code];
  3540. const primaryCode = code && code.split('-')[0];
  3541. const primaryLang = languages && languages[primaryCode];
  3542. let localizedString = defaultValue;
  3543. if (language && language[string]) {
  3544. localizedString = language[string];
  3545. } else if (primaryLang && primaryLang[string]) {
  3546. localizedString = primaryLang[string];
  3547. }
  3548. if (tokens) {
  3549. localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
  3550. const value = tokens[index - 1];
  3551. let ret = value;
  3552. if (typeof value === 'undefined') {
  3553. ret = match;
  3554. }
  3555. return ret;
  3556. });
  3557. }
  3558. return localizedString;
  3559. }
  3560. /**
  3561. * Handles language change for the player in components. Should be overridden by sub-components.
  3562. *
  3563. * @abstract
  3564. */
  3565. handleLanguagechange() {}
  3566. /**
  3567. * Return the `Component`s DOM element. This is where children get inserted.
  3568. * This will usually be the the same as the element returned in {@link Component#el}.
  3569. *
  3570. * @return {Element}
  3571. * The content element for this `Component`.
  3572. */
  3573. contentEl() {
  3574. return this.contentEl_ || this.el_;
  3575. }
  3576. /**
  3577. * Get this `Component`s ID
  3578. *
  3579. * @return {string}
  3580. * The id of this `Component`
  3581. */
  3582. id() {
  3583. return this.id_;
  3584. }
  3585. /**
  3586. * Get the `Component`s name. The name gets used to reference the `Component`
  3587. * and is set during registration.
  3588. *
  3589. * @return {string}
  3590. * The name of this `Component`.
  3591. */
  3592. name() {
  3593. return this.name_;
  3594. }
  3595. /**
  3596. * Get an array of all child components
  3597. *
  3598. * @return {Array}
  3599. * The children
  3600. */
  3601. children() {
  3602. return this.children_;
  3603. }
  3604. /**
  3605. * Returns the child `Component` with the given `id`.
  3606. *
  3607. * @param {string} id
  3608. * The id of the child `Component` to get.
  3609. *
  3610. * @return {Component|undefined}
  3611. * The child `Component` with the given `id` or undefined.
  3612. */
  3613. getChildById(id) {
  3614. return this.childIndex_[id];
  3615. }
  3616. /**
  3617. * Returns the child `Component` with the given `name`.
  3618. *
  3619. * @param {string} name
  3620. * The name of the child `Component` to get.
  3621. *
  3622. * @return {Component|undefined}
  3623. * The child `Component` with the given `name` or undefined.
  3624. */
  3625. getChild(name) {
  3626. if (!name) {
  3627. return;
  3628. }
  3629. return this.childNameIndex_[name];
  3630. }
  3631. /**
  3632. * Returns the descendant `Component` following the givent
  3633. * descendant `names`. For instance ['foo', 'bar', 'baz'] would
  3634. * try to get 'foo' on the current component, 'bar' on the 'foo'
  3635. * component and 'baz' on the 'bar' component and return undefined
  3636. * if any of those don't exist.
  3637. *
  3638. * @param {...string[]|...string} names
  3639. * The name of the child `Component` to get.
  3640. *
  3641. * @return {Component|undefined}
  3642. * The descendant `Component` following the given descendant
  3643. * `names` or undefined.
  3644. */
  3645. getDescendant(...names) {
  3646. // flatten array argument into the main array
  3647. names = names.reduce((acc, n) => acc.concat(n), []);
  3648. let currentChild = this;
  3649. for (let i = 0; i < names.length; i++) {
  3650. currentChild = currentChild.getChild(names[i]);
  3651. if (!currentChild || !currentChild.getChild) {
  3652. return;
  3653. }
  3654. }
  3655. return currentChild;
  3656. }
  3657. /**
  3658. * Adds an SVG icon element to another element or component.
  3659. *
  3660. * @param {string} iconName
  3661. * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
  3662. *
  3663. * @param {Element} [el=this.el()]
  3664. * Element to set the title on. Defaults to the current Component's element.
  3665. *
  3666. * @return {Element}
  3667. * The newly created icon element.
  3668. */
  3669. setIcon(iconName, el = this.el()) {
  3670. // TODO: In v9 of video.js, we will want to remove font icons entirely.
  3671. // This means this check, as well as the others throughout the code, and
  3672. // the unecessary CSS for font icons, will need to be removed.
  3673. // See https://github.com/videojs/video.js/pull/8260 as to which components
  3674. // need updating.
  3675. if (!this.player_.options_.experimentalSvgIcons) {
  3676. return;
  3677. }
  3678. const xmlnsURL = 'http://www.w3.org/2000/svg';
  3679. // The below creates an element in the format of:
  3680. // <span><svg><use>....</use></svg></span>
  3681. const iconContainer = createEl('span', {
  3682. className: 'vjs-icon-placeholder vjs-svg-icon'
  3683. }, {
  3684. 'aria-hidden': 'true'
  3685. });
  3686. const svgEl = document__default["default"].createElementNS(xmlnsURL, 'svg');
  3687. svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
  3688. const useEl = document__default["default"].createElementNS(xmlnsURL, 'use');
  3689. svgEl.appendChild(useEl);
  3690. useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
  3691. iconContainer.appendChild(svgEl);
  3692. // Replace a pre-existing icon if one exists.
  3693. if (this.iconIsSet_) {
  3694. el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
  3695. } else {
  3696. el.appendChild(iconContainer);
  3697. }
  3698. this.iconIsSet_ = true;
  3699. return iconContainer;
  3700. }
  3701. /**
  3702. * Add a child `Component` inside the current `Component`.
  3703. *
  3704. * @param {string|Component} child
  3705. * The name or instance of a child to add.
  3706. *
  3707. * @param {Object} [options={}]
  3708. * The key/value store of options that will get passed to children of
  3709. * the child.
  3710. *
  3711. * @param {number} [index=this.children_.length]
  3712. * The index to attempt to add a child into.
  3713. *
  3714. *
  3715. * @return {Component}
  3716. * The `Component` that gets added as a child. When using a string the
  3717. * `Component` will get created by this process.
  3718. */
  3719. addChild(child, options = {}, index = this.children_.length) {
  3720. let component;
  3721. let componentName;
  3722. // If child is a string, create component with options
  3723. if (typeof child === 'string') {
  3724. componentName = toTitleCase(child);
  3725. const componentClassName = options.componentClass || componentName;
  3726. // Set name through options
  3727. options.name = componentName;
  3728. // Create a new object & element for this controls set
  3729. // If there's no .player_, this is a player
  3730. const ComponentClass = Component.getComponent(componentClassName);
  3731. if (!ComponentClass) {
  3732. throw new Error(`Component ${componentClassName} does not exist`);
  3733. }
  3734. // data stored directly on the videojs object may be
  3735. // misidentified as a component to retain
  3736. // backwards-compatibility with 4.x. check to make sure the
  3737. // component class can be instantiated.
  3738. if (typeof ComponentClass !== 'function') {
  3739. return null;
  3740. }
  3741. component = new ComponentClass(this.player_ || this, options);
  3742. // child is a component instance
  3743. } else {
  3744. component = child;
  3745. }
  3746. if (component.parentComponent_) {
  3747. component.parentComponent_.removeChild(component);
  3748. }
  3749. this.children_.splice(index, 0, component);
  3750. component.parentComponent_ = this;
  3751. if (typeof component.id === 'function') {
  3752. this.childIndex_[component.id()] = component;
  3753. }
  3754. // If a name wasn't used to create the component, check if we can use the
  3755. // name function of the component
  3756. componentName = componentName || component.name && toTitleCase(component.name());
  3757. if (componentName) {
  3758. this.childNameIndex_[componentName] = component;
  3759. this.childNameIndex_[toLowerCase(componentName)] = component;
  3760. }
  3761. // Add the UI object's element to the container div (box)
  3762. // Having an element is not required
  3763. if (typeof component.el === 'function' && component.el()) {
  3764. // If inserting before a component, insert before that component's element
  3765. let refNode = null;
  3766. if (this.children_[index + 1]) {
  3767. // Most children are components, but the video tech is an HTML element
  3768. if (this.children_[index + 1].el_) {
  3769. refNode = this.children_[index + 1].el_;
  3770. } else if (isEl(this.children_[index + 1])) {
  3771. refNode = this.children_[index + 1];
  3772. }
  3773. }
  3774. this.contentEl().insertBefore(component.el(), refNode);
  3775. }
  3776. // Return so it can stored on parent object if desired.
  3777. return component;
  3778. }
  3779. /**
  3780. * Remove a child `Component` from this `Component`s list of children. Also removes
  3781. * the child `Component`s element from this `Component`s element.
  3782. *
  3783. * @param {Component} component
  3784. * The child `Component` to remove.
  3785. */
  3786. removeChild(component) {
  3787. if (typeof component === 'string') {
  3788. component = this.getChild(component);
  3789. }
  3790. if (!component || !this.children_) {
  3791. return;
  3792. }
  3793. let childFound = false;
  3794. for (let i = this.children_.length - 1; i >= 0; i--) {
  3795. if (this.children_[i] === component) {
  3796. childFound = true;
  3797. this.children_.splice(i, 1);
  3798. break;
  3799. }
  3800. }
  3801. if (!childFound) {
  3802. return;
  3803. }
  3804. component.parentComponent_ = null;
  3805. this.childIndex_[component.id()] = null;
  3806. this.childNameIndex_[toTitleCase(component.name())] = null;
  3807. this.childNameIndex_[toLowerCase(component.name())] = null;
  3808. const compEl = component.el();
  3809. if (compEl && compEl.parentNode === this.contentEl()) {
  3810. this.contentEl().removeChild(component.el());
  3811. }
  3812. }
  3813. /**
  3814. * Add and initialize default child `Component`s based upon options.
  3815. */
  3816. initChildren() {
  3817. const children = this.options_.children;
  3818. if (children) {
  3819. // `this` is `parent`
  3820. const parentOptions = this.options_;
  3821. const handleAdd = child => {
  3822. const name = child.name;
  3823. let opts = child.opts;
  3824. // Allow options for children to be set at the parent options
  3825. // e.g. videojs(id, { controlBar: false });
  3826. // instead of videojs(id, { children: { controlBar: false });
  3827. if (parentOptions[name] !== undefined) {
  3828. opts = parentOptions[name];
  3829. }
  3830. // Allow for disabling default components
  3831. // e.g. options['children']['posterImage'] = false
  3832. if (opts === false) {
  3833. return;
  3834. }
  3835. // Allow options to be passed as a simple boolean if no configuration
  3836. // is necessary.
  3837. if (opts === true) {
  3838. opts = {};
  3839. }
  3840. // We also want to pass the original player options
  3841. // to each component as well so they don't need to
  3842. // reach back into the player for options later.
  3843. opts.playerOptions = this.options_.playerOptions;
  3844. // Create and add the child component.
  3845. // Add a direct reference to the child by name on the parent instance.
  3846. // If two of the same component are used, different names should be supplied
  3847. // for each
  3848. const newChild = this.addChild(name, opts);
  3849. if (newChild) {
  3850. this[name] = newChild;
  3851. }
  3852. };
  3853. // Allow for an array of children details to passed in the options
  3854. let workingChildren;
  3855. const Tech = Component.getComponent('Tech');
  3856. if (Array.isArray(children)) {
  3857. workingChildren = children;
  3858. } else {
  3859. workingChildren = Object.keys(children);
  3860. }
  3861. workingChildren
  3862. // children that are in this.options_ but also in workingChildren would
  3863. // give us extra children we do not want. So, we want to filter them out.
  3864. .concat(Object.keys(this.options_).filter(function (child) {
  3865. return !workingChildren.some(function (wchild) {
  3866. if (typeof wchild === 'string') {
  3867. return child === wchild;
  3868. }
  3869. return child === wchild.name;
  3870. });
  3871. })).map(child => {
  3872. let name;
  3873. let opts;
  3874. if (typeof child === 'string') {
  3875. name = child;
  3876. opts = children[name] || this.options_[name] || {};
  3877. } else {
  3878. name = child.name;
  3879. opts = child;
  3880. }
  3881. return {
  3882. name,
  3883. opts
  3884. };
  3885. }).filter(child => {
  3886. // we have to make sure that child.name isn't in the techOrder since
  3887. // techs are registered as Components but can't aren't compatible
  3888. // See https://github.com/videojs/video.js/issues/2772
  3889. const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name));
  3890. return c && !Tech.isTech(c);
  3891. }).forEach(handleAdd);
  3892. }
  3893. }
  3894. /**
  3895. * Builds the default DOM class name. Should be overridden by sub-components.
  3896. *
  3897. * @return {string}
  3898. * The DOM class name for this object.
  3899. *
  3900. * @abstract
  3901. */
  3902. buildCSSClass() {
  3903. // Child classes can include a function that does:
  3904. // return 'CLASS NAME' + this._super();
  3905. return '';
  3906. }
  3907. /**
  3908. * Bind a listener to the component's ready state.
  3909. * Different from event listeners in that if the ready event has already happened
  3910. * it will trigger the function immediately.
  3911. *
  3912. * @param {ReadyCallback} fn
  3913. * Function that gets called when the `Component` is ready.
  3914. *
  3915. * @return {Component}
  3916. * Returns itself; method can be chained.
  3917. */
  3918. ready(fn, sync = false) {
  3919. if (!fn) {
  3920. return;
  3921. }
  3922. if (!this.isReady_) {
  3923. this.readyQueue_ = this.readyQueue_ || [];
  3924. this.readyQueue_.push(fn);
  3925. return;
  3926. }
  3927. if (sync) {
  3928. fn.call(this);
  3929. } else {
  3930. // Call the function asynchronously by default for consistency
  3931. this.setTimeout(fn, 1);
  3932. }
  3933. }
  3934. /**
  3935. * Trigger all the ready listeners for this `Component`.
  3936. *
  3937. * @fires Component#ready
  3938. */
  3939. triggerReady() {
  3940. this.isReady_ = true;
  3941. // Ensure ready is triggered asynchronously
  3942. this.setTimeout(function () {
  3943. const readyQueue = this.readyQueue_;
  3944. // Reset Ready Queue
  3945. this.readyQueue_ = [];
  3946. if (readyQueue && readyQueue.length > 0) {
  3947. readyQueue.forEach(function (fn) {
  3948. fn.call(this);
  3949. }, this);
  3950. }
  3951. // Allow for using event listeners also
  3952. /**
  3953. * Triggered when a `Component` is ready.
  3954. *
  3955. * @event Component#ready
  3956. * @type {Event}
  3957. */
  3958. this.trigger('ready');
  3959. }, 1);
  3960. }
  3961. /**
  3962. * Find a single DOM element matching a `selector`. This can be within the `Component`s
  3963. * `contentEl()` or another custom context.
  3964. *
  3965. * @param {string} selector
  3966. * A valid CSS selector, which will be passed to `querySelector`.
  3967. *
  3968. * @param {Element|string} [context=this.contentEl()]
  3969. * A DOM element within which to query. Can also be a selector string in
  3970. * which case the first matching element will get used as context. If
  3971. * missing `this.contentEl()` gets used. If `this.contentEl()` returns
  3972. * nothing it falls back to `document`.
  3973. *
  3974. * @return {Element|null}
  3975. * the dom element that was found, or null
  3976. *
  3977. * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
  3978. */
  3979. $(selector, context) {
  3980. return $(selector, context || this.contentEl());
  3981. }
  3982. /**
  3983. * Finds all DOM element matching a `selector`. This can be within the `Component`s
  3984. * `contentEl()` or another custom context.
  3985. *
  3986. * @param {string} selector
  3987. * A valid CSS selector, which will be passed to `querySelectorAll`.
  3988. *
  3989. * @param {Element|string} [context=this.contentEl()]
  3990. * A DOM element within which to query. Can also be a selector string in
  3991. * which case the first matching element will get used as context. If
  3992. * missing `this.contentEl()` gets used. If `this.contentEl()` returns
  3993. * nothing it falls back to `document`.
  3994. *
  3995. * @return {NodeList}
  3996. * a list of dom elements that were found
  3997. *
  3998. * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
  3999. */
  4000. $$(selector, context) {
  4001. return $$(selector, context || this.contentEl());
  4002. }
  4003. /**
  4004. * Check if a component's element has a CSS class name.
  4005. *
  4006. * @param {string} classToCheck
  4007. * CSS class name to check.
  4008. *
  4009. * @return {boolean}
  4010. * - True if the `Component` has the class.
  4011. * - False if the `Component` does not have the class`
  4012. */
  4013. hasClass(classToCheck) {
  4014. return hasClass(this.el_, classToCheck);
  4015. }
  4016. /**
  4017. * Add a CSS class name to the `Component`s element.
  4018. *
  4019. * @param {...string} classesToAdd
  4020. * One or more CSS class name to add.
  4021. */
  4022. addClass(...classesToAdd) {
  4023. addClass(this.el_, ...classesToAdd);
  4024. }
  4025. /**
  4026. * Remove a CSS class name from the `Component`s element.
  4027. *
  4028. * @param {...string} classesToRemove
  4029. * One or more CSS class name to remove.
  4030. */
  4031. removeClass(...classesToRemove) {
  4032. removeClass(this.el_, ...classesToRemove);
  4033. }
  4034. /**
  4035. * Add or remove a CSS class name from the component's element.
  4036. * - `classToToggle` gets added when {@link Component#hasClass} would return false.
  4037. * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
  4038. *
  4039. * @param {string} classToToggle
  4040. * The class to add or remove based on (@link Component#hasClass}
  4041. *
  4042. * @param {boolean|Dom~predicate} [predicate]
  4043. * An {@link Dom~predicate} function or a boolean
  4044. */
  4045. toggleClass(classToToggle, predicate) {
  4046. toggleClass(this.el_, classToToggle, predicate);
  4047. }
  4048. /**
  4049. * Show the `Component`s element if it is hidden by removing the
  4050. * 'vjs-hidden' class name from it.
  4051. */
  4052. show() {
  4053. this.removeClass('vjs-hidden');
  4054. }
  4055. /**
  4056. * Hide the `Component`s element if it is currently showing by adding the
  4057. * 'vjs-hidden` class name to it.
  4058. */
  4059. hide() {
  4060. this.addClass('vjs-hidden');
  4061. }
  4062. /**
  4063. * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
  4064. * class name to it. Used during fadeIn/fadeOut.
  4065. *
  4066. * @private
  4067. */
  4068. lockShowing() {
  4069. this.addClass('vjs-lock-showing');
  4070. }
  4071. /**
  4072. * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
  4073. * class name from it. Used during fadeIn/fadeOut.
  4074. *
  4075. * @private
  4076. */
  4077. unlockShowing() {
  4078. this.removeClass('vjs-lock-showing');
  4079. }
  4080. /**
  4081. * Get the value of an attribute on the `Component`s element.
  4082. *
  4083. * @param {string} attribute
  4084. * Name of the attribute to get the value from.
  4085. *
  4086. * @return {string|null}
  4087. * - The value of the attribute that was asked for.
  4088. * - Can be an empty string on some browsers if the attribute does not exist
  4089. * or has no value
  4090. * - Most browsers will return null if the attribute does not exist or has
  4091. * no value.
  4092. *
  4093. * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
  4094. */
  4095. getAttribute(attribute) {
  4096. return getAttribute(this.el_, attribute);
  4097. }
  4098. /**
  4099. * Set the value of an attribute on the `Component`'s element
  4100. *
  4101. * @param {string} attribute
  4102. * Name of the attribute to set.
  4103. *
  4104. * @param {string} value
  4105. * Value to set the attribute to.
  4106. *
  4107. * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
  4108. */
  4109. setAttribute(attribute, value) {
  4110. setAttribute(this.el_, attribute, value);
  4111. }
  4112. /**
  4113. * Remove an attribute from the `Component`s element.
  4114. *
  4115. * @param {string} attribute
  4116. * Name of the attribute to remove.
  4117. *
  4118. * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
  4119. */
  4120. removeAttribute(attribute) {
  4121. removeAttribute(this.el_, attribute);
  4122. }
  4123. /**
  4124. * Get or set the width of the component based upon the CSS styles.
  4125. * See {@link Component#dimension} for more detailed information.
  4126. *
  4127. * @param {number|string} [num]
  4128. * The width that you want to set postfixed with '%', 'px' or nothing.
  4129. *
  4130. * @param {boolean} [skipListeners]
  4131. * Skip the componentresize event trigger
  4132. *
  4133. * @return {number|undefined}
  4134. * The width when getting, zero if there is no width
  4135. */
  4136. width(num, skipListeners) {
  4137. return this.dimension('width', num, skipListeners);
  4138. }
  4139. /**
  4140. * Get or set the height of the component based upon the CSS styles.
  4141. * See {@link Component#dimension} for more detailed information.
  4142. *
  4143. * @param {number|string} [num]
  4144. * The height that you want to set postfixed with '%', 'px' or nothing.
  4145. *
  4146. * @param {boolean} [skipListeners]
  4147. * Skip the componentresize event trigger
  4148. *
  4149. * @return {number|undefined}
  4150. * The height when getting, zero if there is no height
  4151. */
  4152. height(num, skipListeners) {
  4153. return this.dimension('height', num, skipListeners);
  4154. }
  4155. /**
  4156. * Set both the width and height of the `Component` element at the same time.
  4157. *
  4158. * @param {number|string} width
  4159. * Width to set the `Component`s element to.
  4160. *
  4161. * @param {number|string} height
  4162. * Height to set the `Component`s element to.
  4163. */
  4164. dimensions(width, height) {
  4165. // Skip componentresize listeners on width for optimization
  4166. this.width(width, true);
  4167. this.height(height);
  4168. }
  4169. /**
  4170. * Get or set width or height of the `Component` element. This is the shared code
  4171. * for the {@link Component#width} and {@link Component#height}.
  4172. *
  4173. * Things to know:
  4174. * - If the width or height in an number this will return the number postfixed with 'px'.
  4175. * - If the width/height is a percent this will return the percent postfixed with '%'
  4176. * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
  4177. * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
  4178. * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
  4179. * for more information
  4180. * - If you want the computed style of the component, use {@link Component#currentWidth}
  4181. * and {@link {Component#currentHeight}
  4182. *
  4183. * @fires Component#componentresize
  4184. *
  4185. * @param {string} widthOrHeight
  4186. 8 'width' or 'height'
  4187. *
  4188. * @param {number|string} [num]
  4189. 8 New dimension
  4190. *
  4191. * @param {boolean} [skipListeners]
  4192. * Skip componentresize event trigger
  4193. *
  4194. * @return {number|undefined}
  4195. * The dimension when getting or 0 if unset
  4196. */
  4197. dimension(widthOrHeight, num, skipListeners) {
  4198. if (num !== undefined) {
  4199. // Set to zero if null or literally NaN (NaN !== NaN)
  4200. if (num === null || num !== num) {
  4201. num = 0;
  4202. }
  4203. // Check if using css width/height (% or px) and adjust
  4204. if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
  4205. this.el_.style[widthOrHeight] = num;
  4206. } else if (num === 'auto') {
  4207. this.el_.style[widthOrHeight] = '';
  4208. } else {
  4209. this.el_.style[widthOrHeight] = num + 'px';
  4210. }
  4211. // skipListeners allows us to avoid triggering the resize event when setting both width and height
  4212. if (!skipListeners) {
  4213. /**
  4214. * Triggered when a component is resized.
  4215. *
  4216. * @event Component#componentresize
  4217. * @type {Event}
  4218. */
  4219. this.trigger('componentresize');
  4220. }
  4221. return;
  4222. }
  4223. // Not setting a value, so getting it
  4224. // Make sure element exists
  4225. if (!this.el_) {
  4226. return 0;
  4227. }
  4228. // Get dimension value from style
  4229. const val = this.el_.style[widthOrHeight];
  4230. const pxIndex = val.indexOf('px');
  4231. if (pxIndex !== -1) {
  4232. // Return the pixel value with no 'px'
  4233. return parseInt(val.slice(0, pxIndex), 10);
  4234. }
  4235. // No px so using % or no style was set, so falling back to offsetWidth/height
  4236. // If component has display:none, offset will return 0
  4237. // TODO: handle display:none and no dimension style using px
  4238. return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
  4239. }
  4240. /**
  4241. * Get the computed width or the height of the component's element.
  4242. *
  4243. * Uses `window.getComputedStyle`.
  4244. *
  4245. * @param {string} widthOrHeight
  4246. * A string containing 'width' or 'height'. Whichever one you want to get.
  4247. *
  4248. * @return {number}
  4249. * The dimension that gets asked for or 0 if nothing was set
  4250. * for that dimension.
  4251. */
  4252. currentDimension(widthOrHeight) {
  4253. let computedWidthOrHeight = 0;
  4254. if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
  4255. throw new Error('currentDimension only accepts width or height value');
  4256. }
  4257. computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
  4258. // remove 'px' from variable and parse as integer
  4259. computedWidthOrHeight = parseFloat(computedWidthOrHeight);
  4260. // if the computed value is still 0, it's possible that the browser is lying
  4261. // and we want to check the offset values.
  4262. // This code also runs wherever getComputedStyle doesn't exist.
  4263. if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
  4264. const rule = `offset${toTitleCase(widthOrHeight)}`;
  4265. computedWidthOrHeight = this.el_[rule];
  4266. }
  4267. return computedWidthOrHeight;
  4268. }
  4269. /**
  4270. * An object that contains width and height values of the `Component`s
  4271. * computed style. Uses `window.getComputedStyle`.
  4272. *
  4273. * @typedef {Object} Component~DimensionObject
  4274. *
  4275. * @property {number} width
  4276. * The width of the `Component`s computed style.
  4277. *
  4278. * @property {number} height
  4279. * The height of the `Component`s computed style.
  4280. */
  4281. /**
  4282. * Get an object that contains computed width and height values of the
  4283. * component's element.
  4284. *
  4285. * Uses `window.getComputedStyle`.
  4286. *
  4287. * @return {Component~DimensionObject}
  4288. * The computed dimensions of the component's element.
  4289. */
  4290. currentDimensions() {
  4291. return {
  4292. width: this.currentDimension('width'),
  4293. height: this.currentDimension('height')
  4294. };
  4295. }
  4296. /**
  4297. * Get the computed width of the component's element.
  4298. *
  4299. * Uses `window.getComputedStyle`.
  4300. *
  4301. * @return {number}
  4302. * The computed width of the component's element.
  4303. */
  4304. currentWidth() {
  4305. return this.currentDimension('width');
  4306. }
  4307. /**
  4308. * Get the computed height of the component's element.
  4309. *
  4310. * Uses `window.getComputedStyle`.
  4311. *
  4312. * @return {number}
  4313. * The computed height of the component's element.
  4314. */
  4315. currentHeight() {
  4316. return this.currentDimension('height');
  4317. }
  4318. /**
  4319. * Set the focus to this component
  4320. */
  4321. focus() {
  4322. this.el_.focus();
  4323. }
  4324. /**
  4325. * Remove the focus from this component
  4326. */
  4327. blur() {
  4328. this.el_.blur();
  4329. }
  4330. /**
  4331. * When this Component receives a `keydown` event which it does not process,
  4332. * it passes the event to the Player for handling.
  4333. *
  4334. * @param {KeyboardEvent} event
  4335. * The `keydown` event that caused this function to be called.
  4336. */
  4337. handleKeyDown(event) {
  4338. if (this.player_) {
  4339. // We only stop propagation here because we want unhandled events to fall
  4340. // back to the browser. Exclude Tab for focus trapping.
  4341. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  4342. event.stopPropagation();
  4343. }
  4344. this.player_.handleKeyDown(event);
  4345. }
  4346. }
  4347. /**
  4348. * Many components used to have a `handleKeyPress` method, which was poorly
  4349. * named because it listened to a `keydown` event. This method name now
  4350. * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
  4351. * will not see their method calls stop working.
  4352. *
  4353. * @param {KeyboardEvent} event
  4354. * The event that caused this function to be called.
  4355. */
  4356. handleKeyPress(event) {
  4357. this.handleKeyDown(event);
  4358. }
  4359. /**
  4360. * Emit a 'tap' events when touch event support gets detected. This gets used to
  4361. * support toggling the controls through a tap on the video. They get enabled
  4362. * because every sub-component would have extra overhead otherwise.
  4363. *
  4364. * @protected
  4365. * @fires Component#tap
  4366. * @listens Component#touchstart
  4367. * @listens Component#touchmove
  4368. * @listens Component#touchleave
  4369. * @listens Component#touchcancel
  4370. * @listens Component#touchend
  4371. */
  4372. emitTapEvents() {
  4373. // Track the start time so we can determine how long the touch lasted
  4374. let touchStart = 0;
  4375. let firstTouch = null;
  4376. // Maximum movement allowed during a touch event to still be considered a tap
  4377. // Other popular libs use anywhere from 2 (hammer.js) to 15,
  4378. // so 10 seems like a nice, round number.
  4379. const tapMovementThreshold = 10;
  4380. // The maximum length a touch can be while still being considered a tap
  4381. const touchTimeThreshold = 200;
  4382. let couldBeTap;
  4383. this.on('touchstart', function (event) {
  4384. // If more than one finger, don't consider treating this as a click
  4385. if (event.touches.length === 1) {
  4386. // Copy pageX/pageY from the object
  4387. firstTouch = {
  4388. pageX: event.touches[0].pageX,
  4389. pageY: event.touches[0].pageY
  4390. };
  4391. // Record start time so we can detect a tap vs. "touch and hold"
  4392. touchStart = window__default["default"].performance.now();
  4393. // Reset couldBeTap tracking
  4394. couldBeTap = true;
  4395. }
  4396. });
  4397. this.on('touchmove', function (event) {
  4398. // If more than one finger, don't consider treating this as a click
  4399. if (event.touches.length > 1) {
  4400. couldBeTap = false;
  4401. } else if (firstTouch) {
  4402. // Some devices will throw touchmoves for all but the slightest of taps.
  4403. // So, if we moved only a small distance, this could still be a tap
  4404. const xdiff = event.touches[0].pageX - firstTouch.pageX;
  4405. const ydiff = event.touches[0].pageY - firstTouch.pageY;
  4406. const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
  4407. if (touchDistance > tapMovementThreshold) {
  4408. couldBeTap = false;
  4409. }
  4410. }
  4411. });
  4412. const noTap = function () {
  4413. couldBeTap = false;
  4414. };
  4415. // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
  4416. this.on('touchleave', noTap);
  4417. this.on('touchcancel', noTap);
  4418. // When the touch ends, measure how long it took and trigger the appropriate
  4419. // event
  4420. this.on('touchend', function (event) {
  4421. firstTouch = null;
  4422. // Proceed only if the touchmove/leave/cancel event didn't happen
  4423. if (couldBeTap === true) {
  4424. // Measure how long the touch lasted
  4425. const touchTime = window__default["default"].performance.now() - touchStart;
  4426. // Make sure the touch was less than the threshold to be considered a tap
  4427. if (touchTime < touchTimeThreshold) {
  4428. // Don't let browser turn this into a click
  4429. event.preventDefault();
  4430. /**
  4431. * Triggered when a `Component` is tapped.
  4432. *
  4433. * @event Component#tap
  4434. * @type {MouseEvent}
  4435. */
  4436. this.trigger('tap');
  4437. // It may be good to copy the touchend event object and change the
  4438. // type to tap, if the other event properties aren't exact after
  4439. // Events.fixEvent runs (e.g. event.target)
  4440. }
  4441. }
  4442. });
  4443. }
  4444. /**
  4445. * This function reports user activity whenever touch events happen. This can get
  4446. * turned off by any sub-components that wants touch events to act another way.
  4447. *
  4448. * Report user touch activity when touch events occur. User activity gets used to
  4449. * determine when controls should show/hide. It is simple when it comes to mouse
  4450. * events, because any mouse event should show the controls. So we capture mouse
  4451. * events that bubble up to the player and report activity when that happens.
  4452. * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
  4453. * controls. So touch events can't help us at the player level either.
  4454. *
  4455. * User activity gets checked asynchronously. So what could happen is a tap event
  4456. * on the video turns the controls off. Then the `touchend` event bubbles up to
  4457. * the player. Which, if it reported user activity, would turn the controls right
  4458. * back on. We also don't want to completely block touch events from bubbling up.
  4459. * Furthermore a `touchmove` event and anything other than a tap, should not turn
  4460. * controls back on.
  4461. *
  4462. * @listens Component#touchstart
  4463. * @listens Component#touchmove
  4464. * @listens Component#touchend
  4465. * @listens Component#touchcancel
  4466. */
  4467. enableTouchActivity() {
  4468. // Don't continue if the root player doesn't support reporting user activity
  4469. if (!this.player() || !this.player().reportUserActivity) {
  4470. return;
  4471. }
  4472. // listener for reporting that the user is active
  4473. const report = bind_(this.player(), this.player().reportUserActivity);
  4474. let touchHolding;
  4475. this.on('touchstart', function () {
  4476. report();
  4477. // For as long as the they are touching the device or have their mouse down,
  4478. // we consider them active even if they're not moving their finger or mouse.
  4479. // So we want to continue to update that they are active
  4480. this.clearInterval(touchHolding);
  4481. // report at the same interval as activityCheck
  4482. touchHolding = this.setInterval(report, 250);
  4483. });
  4484. const touchEnd = function (event) {
  4485. report();
  4486. // stop the interval that maintains activity if the touch is holding
  4487. this.clearInterval(touchHolding);
  4488. };
  4489. this.on('touchmove', report);
  4490. this.on('touchend', touchEnd);
  4491. this.on('touchcancel', touchEnd);
  4492. }
  4493. /**
  4494. * A callback that has no parameters and is bound into `Component`s context.
  4495. *
  4496. * @callback Component~GenericCallback
  4497. * @this Component
  4498. */
  4499. /**
  4500. * Creates a function that runs after an `x` millisecond timeout. This function is a
  4501. * wrapper around `window.setTimeout`. There are a few reasons to use this one
  4502. * instead though:
  4503. * 1. It gets cleared via {@link Component#clearTimeout} when
  4504. * {@link Component#dispose} gets called.
  4505. * 2. The function callback will gets turned into a {@link Component~GenericCallback}
  4506. *
  4507. * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
  4508. * will cause its dispose listener not to get cleaned up! Please use
  4509. * {@link Component#clearTimeout} or {@link Component#dispose} instead.
  4510. *
  4511. * @param {Component~GenericCallback} fn
  4512. * The function that will be run after `timeout`.
  4513. *
  4514. * @param {number} timeout
  4515. * Timeout in milliseconds to delay before executing the specified function.
  4516. *
  4517. * @return {number}
  4518. * Returns a timeout ID that gets used to identify the timeout. It can also
  4519. * get used in {@link Component#clearTimeout} to clear the timeout that
  4520. * was set.
  4521. *
  4522. * @listens Component#dispose
  4523. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
  4524. */
  4525. setTimeout(fn, timeout) {
  4526. // declare as variables so they are properly available in timeout function
  4527. // eslint-disable-next-line
  4528. var timeoutId;
  4529. fn = bind_(this, fn);
  4530. this.clearTimersOnDispose_();
  4531. timeoutId = window__default["default"].setTimeout(() => {
  4532. if (this.setTimeoutIds_.has(timeoutId)) {
  4533. this.setTimeoutIds_.delete(timeoutId);
  4534. }
  4535. fn();
  4536. }, timeout);
  4537. this.setTimeoutIds_.add(timeoutId);
  4538. return timeoutId;
  4539. }
  4540. /**
  4541. * Clears a timeout that gets created via `window.setTimeout` or
  4542. * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
  4543. * use this function instead of `window.clearTimout`. If you don't your dispose
  4544. * listener will not get cleaned up until {@link Component#dispose}!
  4545. *
  4546. * @param {number} timeoutId
  4547. * The id of the timeout to clear. The return value of
  4548. * {@link Component#setTimeout} or `window.setTimeout`.
  4549. *
  4550. * @return {number}
  4551. * Returns the timeout id that was cleared.
  4552. *
  4553. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
  4554. */
  4555. clearTimeout(timeoutId) {
  4556. if (this.setTimeoutIds_.has(timeoutId)) {
  4557. this.setTimeoutIds_.delete(timeoutId);
  4558. window__default["default"].clearTimeout(timeoutId);
  4559. }
  4560. return timeoutId;
  4561. }
  4562. /**
  4563. * Creates a function that gets run every `x` milliseconds. This function is a wrapper
  4564. * around `window.setInterval`. There are a few reasons to use this one instead though.
  4565. * 1. It gets cleared via {@link Component#clearInterval} when
  4566. * {@link Component#dispose} gets called.
  4567. * 2. The function callback will be a {@link Component~GenericCallback}
  4568. *
  4569. * @param {Component~GenericCallback} fn
  4570. * The function to run every `x` seconds.
  4571. *
  4572. * @param {number} interval
  4573. * Execute the specified function every `x` milliseconds.
  4574. *
  4575. * @return {number}
  4576. * Returns an id that can be used to identify the interval. It can also be be used in
  4577. * {@link Component#clearInterval} to clear the interval.
  4578. *
  4579. * @listens Component#dispose
  4580. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
  4581. */
  4582. setInterval(fn, interval) {
  4583. fn = bind_(this, fn);
  4584. this.clearTimersOnDispose_();
  4585. const intervalId = window__default["default"].setInterval(fn, interval);
  4586. this.setIntervalIds_.add(intervalId);
  4587. return intervalId;
  4588. }
  4589. /**
  4590. * Clears an interval that gets created via `window.setInterval` or
  4591. * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
  4592. * use this function instead of `window.clearInterval`. If you don't your dispose
  4593. * listener will not get cleaned up until {@link Component#dispose}!
  4594. *
  4595. * @param {number} intervalId
  4596. * The id of the interval to clear. The return value of
  4597. * {@link Component#setInterval} or `window.setInterval`.
  4598. *
  4599. * @return {number}
  4600. * Returns the interval id that was cleared.
  4601. *
  4602. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
  4603. */
  4604. clearInterval(intervalId) {
  4605. if (this.setIntervalIds_.has(intervalId)) {
  4606. this.setIntervalIds_.delete(intervalId);
  4607. window__default["default"].clearInterval(intervalId);
  4608. }
  4609. return intervalId;
  4610. }
  4611. /**
  4612. * Queues up a callback to be passed to requestAnimationFrame (rAF), but
  4613. * with a few extra bonuses:
  4614. *
  4615. * - Supports browsers that do not support rAF by falling back to
  4616. * {@link Component#setTimeout}.
  4617. *
  4618. * - The callback is turned into a {@link Component~GenericCallback} (i.e.
  4619. * bound to the component).
  4620. *
  4621. * - Automatic cancellation of the rAF callback is handled if the component
  4622. * is disposed before it is called.
  4623. *
  4624. * @param {Component~GenericCallback} fn
  4625. * A function that will be bound to this component and executed just
  4626. * before the browser's next repaint.
  4627. *
  4628. * @return {number}
  4629. * Returns an rAF ID that gets used to identify the timeout. It can
  4630. * also be used in {@link Component#cancelAnimationFrame} to cancel
  4631. * the animation frame callback.
  4632. *
  4633. * @listens Component#dispose
  4634. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
  4635. */
  4636. requestAnimationFrame(fn) {
  4637. this.clearTimersOnDispose_();
  4638. // declare as variables so they are properly available in rAF function
  4639. // eslint-disable-next-line
  4640. var id;
  4641. fn = bind_(this, fn);
  4642. id = window__default["default"].requestAnimationFrame(() => {
  4643. if (this.rafIds_.has(id)) {
  4644. this.rafIds_.delete(id);
  4645. }
  4646. fn();
  4647. });
  4648. this.rafIds_.add(id);
  4649. return id;
  4650. }
  4651. /**
  4652. * Request an animation frame, but only one named animation
  4653. * frame will be queued. Another will never be added until
  4654. * the previous one finishes.
  4655. *
  4656. * @param {string} name
  4657. * The name to give this requestAnimationFrame
  4658. *
  4659. * @param {Component~GenericCallback} fn
  4660. * A function that will be bound to this component and executed just
  4661. * before the browser's next repaint.
  4662. */
  4663. requestNamedAnimationFrame(name, fn) {
  4664. if (this.namedRafs_.has(name)) {
  4665. return;
  4666. }
  4667. this.clearTimersOnDispose_();
  4668. fn = bind_(this, fn);
  4669. const id = this.requestAnimationFrame(() => {
  4670. fn();
  4671. if (this.namedRafs_.has(name)) {
  4672. this.namedRafs_.delete(name);
  4673. }
  4674. });
  4675. this.namedRafs_.set(name, id);
  4676. return name;
  4677. }
  4678. /**
  4679. * Cancels a current named animation frame if it exists.
  4680. *
  4681. * @param {string} name
  4682. * The name of the requestAnimationFrame to cancel.
  4683. */
  4684. cancelNamedAnimationFrame(name) {
  4685. if (!this.namedRafs_.has(name)) {
  4686. return;
  4687. }
  4688. this.cancelAnimationFrame(this.namedRafs_.get(name));
  4689. this.namedRafs_.delete(name);
  4690. }
  4691. /**
  4692. * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
  4693. * (rAF).
  4694. *
  4695. * If you queue an rAF callback via {@link Component#requestAnimationFrame},
  4696. * use this function instead of `window.cancelAnimationFrame`. If you don't,
  4697. * your dispose listener will not get cleaned up until {@link Component#dispose}!
  4698. *
  4699. * @param {number} id
  4700. * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
  4701. *
  4702. * @return {number}
  4703. * Returns the rAF ID that was cleared.
  4704. *
  4705. * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
  4706. */
  4707. cancelAnimationFrame(id) {
  4708. if (this.rafIds_.has(id)) {
  4709. this.rafIds_.delete(id);
  4710. window__default["default"].cancelAnimationFrame(id);
  4711. }
  4712. return id;
  4713. }
  4714. /**
  4715. * A function to setup `requestAnimationFrame`, `setTimeout`,
  4716. * and `setInterval`, clearing on dispose.
  4717. *
  4718. * > Previously each timer added and removed dispose listeners on it's own.
  4719. * For better performance it was decided to batch them all, and use `Set`s
  4720. * to track outstanding timer ids.
  4721. *
  4722. * @private
  4723. */
  4724. clearTimersOnDispose_() {
  4725. if (this.clearingTimersOnDispose_) {
  4726. return;
  4727. }
  4728. this.clearingTimersOnDispose_ = true;
  4729. this.one('dispose', () => {
  4730. [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
  4731. // for a `Set` key will actually be the value again
  4732. // so forEach((val, val) =>` but for maps we want to use
  4733. // the key.
  4734. this[idName].forEach((val, key) => this[cancelName](key));
  4735. });
  4736. this.clearingTimersOnDispose_ = false;
  4737. });
  4738. }
  4739. /**
  4740. * Register a `Component` with `videojs` given the name and the component.
  4741. *
  4742. * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
  4743. * should be registered using {@link Tech.registerTech} or
  4744. * {@link videojs:videojs.registerTech}.
  4745. *
  4746. * > NOTE: This function can also be seen on videojs as
  4747. * {@link videojs:videojs.registerComponent}.
  4748. *
  4749. * @param {string} name
  4750. * The name of the `Component` to register.
  4751. *
  4752. * @param {Component} ComponentToRegister
  4753. * The `Component` class to register.
  4754. *
  4755. * @return {Component}
  4756. * The `Component` that was registered.
  4757. */
  4758. static registerComponent(name, ComponentToRegister) {
  4759. if (typeof name !== 'string' || !name) {
  4760. throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
  4761. }
  4762. const Tech = Component.getComponent('Tech');
  4763. // We need to make sure this check is only done if Tech has been registered.
  4764. const isTech = Tech && Tech.isTech(ComponentToRegister);
  4765. const isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
  4766. if (isTech || !isComp) {
  4767. let reason;
  4768. if (isTech) {
  4769. reason = 'techs must be registered using Tech.registerTech()';
  4770. } else {
  4771. reason = 'must be a Component subclass';
  4772. }
  4773. throw new Error(`Illegal component, "${name}"; ${reason}.`);
  4774. }
  4775. name = toTitleCase(name);
  4776. if (!Component.components_) {
  4777. Component.components_ = {};
  4778. }
  4779. const Player = Component.getComponent('Player');
  4780. if (name === 'Player' && Player && Player.players) {
  4781. const players = Player.players;
  4782. const playerNames = Object.keys(players);
  4783. // If we have players that were disposed, then their name will still be
  4784. // in Players.players. So, we must loop through and verify that the value
  4785. // for each item is not null. This allows registration of the Player component
  4786. // after all players have been disposed or before any were created.
  4787. if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
  4788. throw new Error('Can not register Player component after player has been created.');
  4789. }
  4790. }
  4791. Component.components_[name] = ComponentToRegister;
  4792. Component.components_[toLowerCase(name)] = ComponentToRegister;
  4793. return ComponentToRegister;
  4794. }
  4795. /**
  4796. * Get a `Component` based on the name it was registered with.
  4797. *
  4798. * @param {string} name
  4799. * The Name of the component to get.
  4800. *
  4801. * @return {typeof Component}
  4802. * The `Component` that got registered under the given name.
  4803. */
  4804. static getComponent(name) {
  4805. if (!name || !Component.components_) {
  4806. return;
  4807. }
  4808. return Component.components_[name];
  4809. }
  4810. }
  4811. Component.registerComponent('Component', Component);
  4812. /**
  4813. * @file time.js
  4814. * @module time
  4815. */
  4816. /**
  4817. * Returns the time for the specified index at the start or end
  4818. * of a TimeRange object.
  4819. *
  4820. * @typedef {Function} TimeRangeIndex
  4821. *
  4822. * @param {number} [index=0]
  4823. * The range number to return the time for.
  4824. *
  4825. * @return {number}
  4826. * The time offset at the specified index.
  4827. *
  4828. * @deprecated The index argument must be provided.
  4829. * In the future, leaving it out will throw an error.
  4830. */
  4831. /**
  4832. * An object that contains ranges of time, which mimics {@link TimeRanges}.
  4833. *
  4834. * @typedef {Object} TimeRange
  4835. *
  4836. * @property {number} length
  4837. * The number of time ranges represented by this object.
  4838. *
  4839. * @property {module:time~TimeRangeIndex} start
  4840. * Returns the time offset at which a specified time range begins.
  4841. *
  4842. * @property {module:time~TimeRangeIndex} end
  4843. * Returns the time offset at which a specified time range ends.
  4844. *
  4845. * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
  4846. */
  4847. /**
  4848. * Check if any of the time ranges are over the maximum index.
  4849. *
  4850. * @private
  4851. * @param {string} fnName
  4852. * The function name to use for logging
  4853. *
  4854. * @param {number} index
  4855. * The index to check
  4856. *
  4857. * @param {number} maxIndex
  4858. * The maximum possible index
  4859. *
  4860. * @throws {Error} if the timeRanges provided are over the maxIndex
  4861. */
  4862. function rangeCheck(fnName, index, maxIndex) {
  4863. if (typeof index !== 'number' || index < 0 || index > maxIndex) {
  4864. throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
  4865. }
  4866. }
  4867. /**
  4868. * Get the time for the specified index at the start or end
  4869. * of a TimeRange object.
  4870. *
  4871. * @private
  4872. * @param {string} fnName
  4873. * The function name to use for logging
  4874. *
  4875. * @param {string} valueIndex
  4876. * The property that should be used to get the time. should be
  4877. * 'start' or 'end'
  4878. *
  4879. * @param {Array} ranges
  4880. * An array of time ranges
  4881. *
  4882. * @param {Array} [rangeIndex=0]
  4883. * The index to start the search at
  4884. *
  4885. * @return {number}
  4886. * The time that offset at the specified index.
  4887. *
  4888. * @deprecated rangeIndex must be set to a value, in the future this will throw an error.
  4889. * @throws {Error} if rangeIndex is more than the length of ranges
  4890. */
  4891. function getRange(fnName, valueIndex, ranges, rangeIndex) {
  4892. rangeCheck(fnName, rangeIndex, ranges.length - 1);
  4893. return ranges[rangeIndex][valueIndex];
  4894. }
  4895. /**
  4896. * Create a time range object given ranges of time.
  4897. *
  4898. * @private
  4899. * @param {Array} [ranges]
  4900. * An array of time ranges.
  4901. *
  4902. * @return {TimeRange}
  4903. */
  4904. function createTimeRangesObj(ranges) {
  4905. let timeRangesObj;
  4906. if (ranges === undefined || ranges.length === 0) {
  4907. timeRangesObj = {
  4908. length: 0,
  4909. start() {
  4910. throw new Error('This TimeRanges object is empty');
  4911. },
  4912. end() {
  4913. throw new Error('This TimeRanges object is empty');
  4914. }
  4915. };
  4916. } else {
  4917. timeRangesObj = {
  4918. length: ranges.length,
  4919. start: getRange.bind(null, 'start', 0, ranges),
  4920. end: getRange.bind(null, 'end', 1, ranges)
  4921. };
  4922. }
  4923. if (window__default["default"].Symbol && window__default["default"].Symbol.iterator) {
  4924. timeRangesObj[window__default["default"].Symbol.iterator] = () => (ranges || []).values();
  4925. }
  4926. return timeRangesObj;
  4927. }
  4928. /**
  4929. * Create a `TimeRange` object which mimics an
  4930. * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
  4931. *
  4932. * @param {number|Array[]} start
  4933. * The start of a single range (a number) or an array of ranges (an
  4934. * array of arrays of two numbers each).
  4935. *
  4936. * @param {number} end
  4937. * The end of a single range. Cannot be used with the array form of
  4938. * the `start` argument.
  4939. *
  4940. * @return {TimeRange}
  4941. */
  4942. function createTimeRanges(start, end) {
  4943. if (Array.isArray(start)) {
  4944. return createTimeRangesObj(start);
  4945. } else if (start === undefined || end === undefined) {
  4946. return createTimeRangesObj();
  4947. }
  4948. return createTimeRangesObj([[start, end]]);
  4949. }
  4950. /**
  4951. * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
  4952. * seconds) will force a number of leading zeros to cover the length of the
  4953. * guide.
  4954. *
  4955. * @private
  4956. * @param {number} seconds
  4957. * Number of seconds to be turned into a string
  4958. *
  4959. * @param {number} guide
  4960. * Number (in seconds) to model the string after
  4961. *
  4962. * @return {string}
  4963. * Time formatted as H:MM:SS or M:SS
  4964. */
  4965. const defaultImplementation = function (seconds, guide) {
  4966. seconds = seconds < 0 ? 0 : seconds;
  4967. let s = Math.floor(seconds % 60);
  4968. let m = Math.floor(seconds / 60 % 60);
  4969. let h = Math.floor(seconds / 3600);
  4970. const gm = Math.floor(guide / 60 % 60);
  4971. const gh = Math.floor(guide / 3600);
  4972. // handle invalid times
  4973. if (isNaN(seconds) || seconds === Infinity) {
  4974. // '-' is false for all relational operators (e.g. <, >=) so this setting
  4975. // will add the minimum number of fields specified by the guide
  4976. h = m = s = '-';
  4977. }
  4978. // Check if we need to show hours
  4979. h = h > 0 || gh > 0 ? h + ':' : '';
  4980. // If hours are showing, we may need to add a leading zero.
  4981. // Always show at least one digit of minutes.
  4982. m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
  4983. // Check if leading zero is need for seconds
  4984. s = s < 10 ? '0' + s : s;
  4985. return h + m + s;
  4986. };
  4987. // Internal pointer to the current implementation.
  4988. let implementation = defaultImplementation;
  4989. /**
  4990. * Replaces the default formatTime implementation with a custom implementation.
  4991. *
  4992. * @param {Function} customImplementation
  4993. * A function which will be used in place of the default formatTime
  4994. * implementation. Will receive the current time in seconds and the
  4995. * guide (in seconds) as arguments.
  4996. */
  4997. function setFormatTime(customImplementation) {
  4998. implementation = customImplementation;
  4999. }
  5000. /**
  5001. * Resets formatTime to the default implementation.
  5002. */
  5003. function resetFormatTime() {
  5004. implementation = defaultImplementation;
  5005. }
  5006. /**
  5007. * Delegates to either the default time formatting function or a custom
  5008. * function supplied via `setFormatTime`.
  5009. *
  5010. * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
  5011. * guide (in seconds) will force a number of leading zeros to cover the
  5012. * length of the guide.
  5013. *
  5014. * @example formatTime(125, 600) === "02:05"
  5015. * @param {number} seconds
  5016. * Number of seconds to be turned into a string
  5017. *
  5018. * @param {number} guide
  5019. * Number (in seconds) to model the string after
  5020. *
  5021. * @return {string}
  5022. * Time formatted as H:MM:SS or M:SS
  5023. */
  5024. function formatTime(seconds, guide = seconds) {
  5025. return implementation(seconds, guide);
  5026. }
  5027. var Time = /*#__PURE__*/Object.freeze({
  5028. __proto__: null,
  5029. createTimeRanges: createTimeRanges,
  5030. createTimeRange: createTimeRanges,
  5031. setFormatTime: setFormatTime,
  5032. resetFormatTime: resetFormatTime,
  5033. formatTime: formatTime
  5034. });
  5035. /**
  5036. * @file buffer.js
  5037. * @module buffer
  5038. */
  5039. /**
  5040. * Compute the percentage of the media that has been buffered.
  5041. *
  5042. * @param { import('./time').TimeRange } buffered
  5043. * The current `TimeRanges` object representing buffered time ranges
  5044. *
  5045. * @param {number} duration
  5046. * Total duration of the media
  5047. *
  5048. * @return {number}
  5049. * Percent buffered of the total duration in decimal form.
  5050. */
  5051. function bufferedPercent(buffered, duration) {
  5052. let bufferedDuration = 0;
  5053. let start;
  5054. let end;
  5055. if (!duration) {
  5056. return 0;
  5057. }
  5058. if (!buffered || !buffered.length) {
  5059. buffered = createTimeRanges(0, 0);
  5060. }
  5061. for (let i = 0; i < buffered.length; i++) {
  5062. start = buffered.start(i);
  5063. end = buffered.end(i);
  5064. // buffered end can be bigger than duration by a very small fraction
  5065. if (end > duration) {
  5066. end = duration;
  5067. }
  5068. bufferedDuration += end - start;
  5069. }
  5070. return bufferedDuration / duration;
  5071. }
  5072. /**
  5073. * @file media-error.js
  5074. */
  5075. /**
  5076. * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
  5077. *
  5078. * @param {number|string|Object|MediaError} value
  5079. * This can be of multiple types:
  5080. * - number: should be a standard error code
  5081. * - string: an error message (the code will be 0)
  5082. * - Object: arbitrary properties
  5083. * - `MediaError` (native): used to populate a video.js `MediaError` object
  5084. * - `MediaError` (video.js): will return itself if it's already a
  5085. * video.js `MediaError` object.
  5086. *
  5087. * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
  5088. * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
  5089. *
  5090. * @class MediaError
  5091. */
  5092. function MediaError(value) {
  5093. // Allow redundant calls to this constructor to avoid having `instanceof`
  5094. // checks peppered around the code.
  5095. if (value instanceof MediaError) {
  5096. return value;
  5097. }
  5098. if (typeof value === 'number') {
  5099. this.code = value;
  5100. } else if (typeof value === 'string') {
  5101. // default code is zero, so this is a custom error
  5102. this.message = value;
  5103. } else if (isObject(value)) {
  5104. // We assign the `code` property manually because native `MediaError` objects
  5105. // do not expose it as an own/enumerable property of the object.
  5106. if (typeof value.code === 'number') {
  5107. this.code = value.code;
  5108. }
  5109. Object.assign(this, value);
  5110. }
  5111. if (!this.message) {
  5112. this.message = MediaError.defaultMessages[this.code] || '';
  5113. }
  5114. }
  5115. /**
  5116. * The error code that refers two one of the defined `MediaError` types
  5117. *
  5118. * @type {Number}
  5119. */
  5120. MediaError.prototype.code = 0;
  5121. /**
  5122. * An optional message that to show with the error. Message is not part of the HTML5
  5123. * video spec but allows for more informative custom errors.
  5124. *
  5125. * @type {String}
  5126. */
  5127. MediaError.prototype.message = '';
  5128. /**
  5129. * An optional status code that can be set by plugins to allow even more detail about
  5130. * the error. For example a plugin might provide a specific HTTP status code and an
  5131. * error message for that code. Then when the plugin gets that error this class will
  5132. * know how to display an error message for it. This allows a custom message to show
  5133. * up on the `Player` error overlay.
  5134. *
  5135. * @type {Array}
  5136. */
  5137. MediaError.prototype.status = null;
  5138. /**
  5139. * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
  5140. * specification listed under {@link MediaError} for more information.
  5141. *
  5142. * @enum {array}
  5143. * @readonly
  5144. * @property {string} 0 - MEDIA_ERR_CUSTOM
  5145. * @property {string} 1 - MEDIA_ERR_ABORTED
  5146. * @property {string} 2 - MEDIA_ERR_NETWORK
  5147. * @property {string} 3 - MEDIA_ERR_DECODE
  5148. * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
  5149. * @property {string} 5 - MEDIA_ERR_ENCRYPTED
  5150. */
  5151. MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
  5152. /**
  5153. * The default `MediaError` messages based on the {@link MediaError.errorTypes}.
  5154. *
  5155. * @type {Array}
  5156. * @constant
  5157. */
  5158. MediaError.defaultMessages = {
  5159. 1: 'You aborted the media playback',
  5160. 2: 'A network error caused the media download to fail part-way.',
  5161. 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
  5162. 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
  5163. 5: 'The media is encrypted and we do not have the keys to decrypt it.'
  5164. };
  5165. // Add types as properties on MediaError
  5166. // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
  5167. for (let errNum = 0; errNum < MediaError.errorTypes.length; errNum++) {
  5168. MediaError[MediaError.errorTypes[errNum]] = errNum;
  5169. // values should be accessible on both the class and instance
  5170. MediaError.prototype[MediaError.errorTypes[errNum]] = errNum;
  5171. }
  5172. /**
  5173. * Returns whether an object is `Promise`-like (i.e. has a `then` method).
  5174. *
  5175. * @param {Object} value
  5176. * An object that may or may not be `Promise`-like.
  5177. *
  5178. * @return {boolean}
  5179. * Whether or not the object is `Promise`-like.
  5180. */
  5181. function isPromise(value) {
  5182. return value !== undefined && value !== null && typeof value.then === 'function';
  5183. }
  5184. /**
  5185. * Silence a Promise-like object.
  5186. *
  5187. * This is useful for avoiding non-harmful, but potentially confusing "uncaught
  5188. * play promise" rejection error messages.
  5189. *
  5190. * @param {Object} value
  5191. * An object that may or may not be `Promise`-like.
  5192. */
  5193. function silencePromise(value) {
  5194. if (isPromise(value)) {
  5195. value.then(null, e => {});
  5196. }
  5197. }
  5198. /**
  5199. * @file text-track-list-converter.js Utilities for capturing text track state and
  5200. * re-creating tracks based on a capture.
  5201. *
  5202. * @module text-track-list-converter
  5203. */
  5204. /**
  5205. * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
  5206. * represents the {@link TextTrack}'s state.
  5207. *
  5208. * @param {TextTrack} track
  5209. * The text track to query.
  5210. *
  5211. * @return {Object}
  5212. * A serializable javascript representation of the TextTrack.
  5213. * @private
  5214. */
  5215. const trackToJson_ = function (track) {
  5216. const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
  5217. if (track[prop]) {
  5218. acc[prop] = track[prop];
  5219. }
  5220. return acc;
  5221. }, {
  5222. cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
  5223. return {
  5224. startTime: cue.startTime,
  5225. endTime: cue.endTime,
  5226. text: cue.text,
  5227. id: cue.id
  5228. };
  5229. })
  5230. });
  5231. return ret;
  5232. };
  5233. /**
  5234. * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
  5235. * state of all {@link TextTrack}s currently configured. The return array is compatible with
  5236. * {@link text-track-list-converter:jsonToTextTracks}.
  5237. *
  5238. * @param { import('../tech/tech').default } tech
  5239. * The tech object to query
  5240. *
  5241. * @return {Array}
  5242. * A serializable javascript representation of the {@link Tech}s
  5243. * {@link TextTrackList}.
  5244. */
  5245. const textTracksToJson = function (tech) {
  5246. const trackEls = tech.$$('track');
  5247. const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
  5248. const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
  5249. const json = trackToJson_(trackEl.track);
  5250. if (trackEl.src) {
  5251. json.src = trackEl.src;
  5252. }
  5253. return json;
  5254. });
  5255. return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
  5256. return trackObjs.indexOf(track) === -1;
  5257. }).map(trackToJson_));
  5258. };
  5259. /**
  5260. * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
  5261. * object {@link TextTrack} representations.
  5262. *
  5263. * @param {Array} json
  5264. * An array of `TextTrack` representation objects, like those that would be
  5265. * produced by `textTracksToJson`.
  5266. *
  5267. * @param {Tech} tech
  5268. * The `Tech` to create the `TextTrack`s on.
  5269. */
  5270. const jsonToTextTracks = function (json, tech) {
  5271. json.forEach(function (track) {
  5272. const addedTrack = tech.addRemoteTextTrack(track).track;
  5273. if (!track.src && track.cues) {
  5274. track.cues.forEach(cue => addedTrack.addCue(cue));
  5275. }
  5276. });
  5277. return tech.textTracks();
  5278. };
  5279. var textTrackConverter = {
  5280. textTracksToJson,
  5281. jsonToTextTracks,
  5282. trackToJson_
  5283. };
  5284. /**
  5285. * @file modal-dialog.js
  5286. */
  5287. const MODAL_CLASS_NAME = 'vjs-modal-dialog';
  5288. /**
  5289. * The `ModalDialog` displays over the video and its controls, which blocks
  5290. * interaction with the player until it is closed.
  5291. *
  5292. * Modal dialogs include a "Close" button and will close when that button
  5293. * is activated - or when ESC is pressed anywhere.
  5294. *
  5295. * @extends Component
  5296. */
  5297. class ModalDialog extends Component {
  5298. /**
  5299. * Create an instance of this class.
  5300. *
  5301. * @param { import('./player').default } player
  5302. * The `Player` that this class should be attached to.
  5303. *
  5304. * @param {Object} [options]
  5305. * The key/value store of player options.
  5306. *
  5307. * @param { import('./utils/dom').ContentDescriptor} [options.content=undefined]
  5308. * Provide customized content for this modal.
  5309. *
  5310. * @param {string} [options.description]
  5311. * A text description for the modal, primarily for accessibility.
  5312. *
  5313. * @param {boolean} [options.fillAlways=false]
  5314. * Normally, modals are automatically filled only the first time
  5315. * they open. This tells the modal to refresh its content
  5316. * every time it opens.
  5317. *
  5318. * @param {string} [options.label]
  5319. * A text label for the modal, primarily for accessibility.
  5320. *
  5321. * @param {boolean} [options.pauseOnOpen=true]
  5322. * If `true`, playback will will be paused if playing when
  5323. * the modal opens, and resumed when it closes.
  5324. *
  5325. * @param {boolean} [options.temporary=true]
  5326. * If `true`, the modal can only be opened once; it will be
  5327. * disposed as soon as it's closed.
  5328. *
  5329. * @param {boolean} [options.uncloseable=false]
  5330. * If `true`, the user will not be able to close the modal
  5331. * through the UI in the normal ways. Programmatic closing is
  5332. * still possible.
  5333. */
  5334. constructor(player, options) {
  5335. super(player, options);
  5336. this.handleKeyDown_ = e => this.handleKeyDown(e);
  5337. this.close_ = e => this.close(e);
  5338. this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
  5339. this.closeable(!this.options_.uncloseable);
  5340. this.content(this.options_.content);
  5341. // Make sure the contentEl is defined AFTER any children are initialized
  5342. // because we only want the contents of the modal in the contentEl
  5343. // (not the UI elements like the close button).
  5344. this.contentEl_ = createEl('div', {
  5345. className: `${MODAL_CLASS_NAME}-content`
  5346. }, {
  5347. role: 'document'
  5348. });
  5349. this.descEl_ = createEl('p', {
  5350. className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
  5351. id: this.el().getAttribute('aria-describedby')
  5352. });
  5353. textContent(this.descEl_, this.description());
  5354. this.el_.appendChild(this.descEl_);
  5355. this.el_.appendChild(this.contentEl_);
  5356. }
  5357. /**
  5358. * Create the `ModalDialog`'s DOM element
  5359. *
  5360. * @return {Element}
  5361. * The DOM element that gets created.
  5362. */
  5363. createEl() {
  5364. return super.createEl('div', {
  5365. className: this.buildCSSClass(),
  5366. tabIndex: -1
  5367. }, {
  5368. 'aria-describedby': `${this.id()}_description`,
  5369. 'aria-hidden': 'true',
  5370. 'aria-label': this.label(),
  5371. 'role': 'dialog'
  5372. });
  5373. }
  5374. dispose() {
  5375. this.contentEl_ = null;
  5376. this.descEl_ = null;
  5377. this.previouslyActiveEl_ = null;
  5378. super.dispose();
  5379. }
  5380. /**
  5381. * Builds the default DOM `className`.
  5382. *
  5383. * @return {string}
  5384. * The DOM `className` for this object.
  5385. */
  5386. buildCSSClass() {
  5387. return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
  5388. }
  5389. /**
  5390. * Returns the label string for this modal. Primarily used for accessibility.
  5391. *
  5392. * @return {string}
  5393. * the localized or raw label of this modal.
  5394. */
  5395. label() {
  5396. return this.localize(this.options_.label || 'Modal Window');
  5397. }
  5398. /**
  5399. * Returns the description string for this modal. Primarily used for
  5400. * accessibility.
  5401. *
  5402. * @return {string}
  5403. * The localized or raw description of this modal.
  5404. */
  5405. description() {
  5406. let desc = this.options_.description || this.localize('This is a modal window.');
  5407. // Append a universal closeability message if the modal is closeable.
  5408. if (this.closeable()) {
  5409. desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
  5410. }
  5411. return desc;
  5412. }
  5413. /**
  5414. * Opens the modal.
  5415. *
  5416. * @fires ModalDialog#beforemodalopen
  5417. * @fires ModalDialog#modalopen
  5418. */
  5419. open() {
  5420. if (!this.opened_) {
  5421. const player = this.player();
  5422. /**
  5423. * Fired just before a `ModalDialog` is opened.
  5424. *
  5425. * @event ModalDialog#beforemodalopen
  5426. * @type {Event}
  5427. */
  5428. this.trigger('beforemodalopen');
  5429. this.opened_ = true;
  5430. // Fill content if the modal has never opened before and
  5431. // never been filled.
  5432. if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
  5433. this.fill();
  5434. }
  5435. // If the player was playing, pause it and take note of its previously
  5436. // playing state.
  5437. this.wasPlaying_ = !player.paused();
  5438. if (this.options_.pauseOnOpen && this.wasPlaying_) {
  5439. player.pause();
  5440. }
  5441. this.on('keydown', this.handleKeyDown_);
  5442. // Hide controls and note if they were enabled.
  5443. this.hadControls_ = player.controls();
  5444. player.controls(false);
  5445. this.show();
  5446. this.conditionalFocus_();
  5447. this.el().setAttribute('aria-hidden', 'false');
  5448. /**
  5449. * Fired just after a `ModalDialog` is opened.
  5450. *
  5451. * @event ModalDialog#modalopen
  5452. * @type {Event}
  5453. */
  5454. this.trigger('modalopen');
  5455. this.hasBeenOpened_ = true;
  5456. }
  5457. }
  5458. /**
  5459. * If the `ModalDialog` is currently open or closed.
  5460. *
  5461. * @param {boolean} [value]
  5462. * If given, it will open (`true`) or close (`false`) the modal.
  5463. *
  5464. * @return {boolean}
  5465. * the current open state of the modaldialog
  5466. */
  5467. opened(value) {
  5468. if (typeof value === 'boolean') {
  5469. this[value ? 'open' : 'close']();
  5470. }
  5471. return this.opened_;
  5472. }
  5473. /**
  5474. * Closes the modal, does nothing if the `ModalDialog` is
  5475. * not open.
  5476. *
  5477. * @fires ModalDialog#beforemodalclose
  5478. * @fires ModalDialog#modalclose
  5479. */
  5480. close() {
  5481. if (!this.opened_) {
  5482. return;
  5483. }
  5484. const player = this.player();
  5485. /**
  5486. * Fired just before a `ModalDialog` is closed.
  5487. *
  5488. * @event ModalDialog#beforemodalclose
  5489. * @type {Event}
  5490. */
  5491. this.trigger('beforemodalclose');
  5492. this.opened_ = false;
  5493. if (this.wasPlaying_ && this.options_.pauseOnOpen) {
  5494. player.play();
  5495. }
  5496. this.off('keydown', this.handleKeyDown_);
  5497. if (this.hadControls_) {
  5498. player.controls(true);
  5499. }
  5500. this.hide();
  5501. this.el().setAttribute('aria-hidden', 'true');
  5502. /**
  5503. * Fired just after a `ModalDialog` is closed.
  5504. *
  5505. * @event ModalDialog#modalclose
  5506. * @type {Event}
  5507. */
  5508. this.trigger('modalclose');
  5509. this.conditionalBlur_();
  5510. if (this.options_.temporary) {
  5511. this.dispose();
  5512. }
  5513. }
  5514. /**
  5515. * Check to see if the `ModalDialog` is closeable via the UI.
  5516. *
  5517. * @param {boolean} [value]
  5518. * If given as a boolean, it will set the `closeable` option.
  5519. *
  5520. * @return {boolean}
  5521. * Returns the final value of the closable option.
  5522. */
  5523. closeable(value) {
  5524. if (typeof value === 'boolean') {
  5525. const closeable = this.closeable_ = !!value;
  5526. let close = this.getChild('closeButton');
  5527. // If this is being made closeable and has no close button, add one.
  5528. if (closeable && !close) {
  5529. // The close button should be a child of the modal - not its
  5530. // content element, so temporarily change the content element.
  5531. const temp = this.contentEl_;
  5532. this.contentEl_ = this.el_;
  5533. close = this.addChild('closeButton', {
  5534. controlText: 'Close Modal Dialog'
  5535. });
  5536. this.contentEl_ = temp;
  5537. this.on(close, 'close', this.close_);
  5538. }
  5539. // If this is being made uncloseable and has a close button, remove it.
  5540. if (!closeable && close) {
  5541. this.off(close, 'close', this.close_);
  5542. this.removeChild(close);
  5543. close.dispose();
  5544. }
  5545. }
  5546. return this.closeable_;
  5547. }
  5548. /**
  5549. * Fill the modal's content element with the modal's "content" option.
  5550. * The content element will be emptied before this change takes place.
  5551. */
  5552. fill() {
  5553. this.fillWith(this.content());
  5554. }
  5555. /**
  5556. * Fill the modal's content element with arbitrary content.
  5557. * The content element will be emptied before this change takes place.
  5558. *
  5559. * @fires ModalDialog#beforemodalfill
  5560. * @fires ModalDialog#modalfill
  5561. *
  5562. * @param { import('./utils/dom').ContentDescriptor} [content]
  5563. * The same rules apply to this as apply to the `content` option.
  5564. */
  5565. fillWith(content) {
  5566. const contentEl = this.contentEl();
  5567. const parentEl = contentEl.parentNode;
  5568. const nextSiblingEl = contentEl.nextSibling;
  5569. /**
  5570. * Fired just before a `ModalDialog` is filled with content.
  5571. *
  5572. * @event ModalDialog#beforemodalfill
  5573. * @type {Event}
  5574. */
  5575. this.trigger('beforemodalfill');
  5576. this.hasBeenFilled_ = true;
  5577. // Detach the content element from the DOM before performing
  5578. // manipulation to avoid modifying the live DOM multiple times.
  5579. parentEl.removeChild(contentEl);
  5580. this.empty();
  5581. insertContent(contentEl, content);
  5582. /**
  5583. * Fired just after a `ModalDialog` is filled with content.
  5584. *
  5585. * @event ModalDialog#modalfill
  5586. * @type {Event}
  5587. */
  5588. this.trigger('modalfill');
  5589. // Re-inject the re-filled content element.
  5590. if (nextSiblingEl) {
  5591. parentEl.insertBefore(contentEl, nextSiblingEl);
  5592. } else {
  5593. parentEl.appendChild(contentEl);
  5594. }
  5595. // make sure that the close button is last in the dialog DOM
  5596. const closeButton = this.getChild('closeButton');
  5597. if (closeButton) {
  5598. parentEl.appendChild(closeButton.el_);
  5599. }
  5600. }
  5601. /**
  5602. * Empties the content element. This happens anytime the modal is filled.
  5603. *
  5604. * @fires ModalDialog#beforemodalempty
  5605. * @fires ModalDialog#modalempty
  5606. */
  5607. empty() {
  5608. /**
  5609. * Fired just before a `ModalDialog` is emptied.
  5610. *
  5611. * @event ModalDialog#beforemodalempty
  5612. * @type {Event}
  5613. */
  5614. this.trigger('beforemodalempty');
  5615. emptyEl(this.contentEl());
  5616. /**
  5617. * Fired just after a `ModalDialog` is emptied.
  5618. *
  5619. * @event ModalDialog#modalempty
  5620. * @type {Event}
  5621. */
  5622. this.trigger('modalempty');
  5623. }
  5624. /**
  5625. * Gets or sets the modal content, which gets normalized before being
  5626. * rendered into the DOM.
  5627. *
  5628. * This does not update the DOM or fill the modal, but it is called during
  5629. * that process.
  5630. *
  5631. * @param { import('./utils/dom').ContentDescriptor} [value]
  5632. * If defined, sets the internal content value to be used on the
  5633. * next call(s) to `fill`. This value is normalized before being
  5634. * inserted. To "clear" the internal content value, pass `null`.
  5635. *
  5636. * @return { import('./utils/dom').ContentDescriptor}
  5637. * The current content of the modal dialog
  5638. */
  5639. content(value) {
  5640. if (typeof value !== 'undefined') {
  5641. this.content_ = value;
  5642. }
  5643. return this.content_;
  5644. }
  5645. /**
  5646. * conditionally focus the modal dialog if focus was previously on the player.
  5647. *
  5648. * @private
  5649. */
  5650. conditionalFocus_() {
  5651. const activeEl = document__default["default"].activeElement;
  5652. const playerEl = this.player_.el_;
  5653. this.previouslyActiveEl_ = null;
  5654. if (playerEl.contains(activeEl) || playerEl === activeEl) {
  5655. this.previouslyActiveEl_ = activeEl;
  5656. this.focus();
  5657. }
  5658. }
  5659. /**
  5660. * conditionally blur the element and refocus the last focused element
  5661. *
  5662. * @private
  5663. */
  5664. conditionalBlur_() {
  5665. if (this.previouslyActiveEl_) {
  5666. this.previouslyActiveEl_.focus();
  5667. this.previouslyActiveEl_ = null;
  5668. }
  5669. }
  5670. /**
  5671. * Keydown handler. Attached when modal is focused.
  5672. *
  5673. * @listens keydown
  5674. */
  5675. handleKeyDown(event) {
  5676. // Do not allow keydowns to reach out of the modal dialog.
  5677. event.stopPropagation();
  5678. if (keycode__default["default"].isEventKey(event, 'Escape') && this.closeable()) {
  5679. event.preventDefault();
  5680. this.close();
  5681. return;
  5682. }
  5683. // exit early if it isn't a tab key
  5684. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  5685. return;
  5686. }
  5687. const focusableEls = this.focusableEls_();
  5688. const activeEl = this.el_.querySelector(':focus');
  5689. let focusIndex;
  5690. for (let i = 0; i < focusableEls.length; i++) {
  5691. if (activeEl === focusableEls[i]) {
  5692. focusIndex = i;
  5693. break;
  5694. }
  5695. }
  5696. if (document__default["default"].activeElement === this.el_) {
  5697. focusIndex = 0;
  5698. }
  5699. if (event.shiftKey && focusIndex === 0) {
  5700. focusableEls[focusableEls.length - 1].focus();
  5701. event.preventDefault();
  5702. } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
  5703. focusableEls[0].focus();
  5704. event.preventDefault();
  5705. }
  5706. }
  5707. /**
  5708. * get all focusable elements
  5709. *
  5710. * @private
  5711. */
  5712. focusableEls_() {
  5713. const allChildren = this.el_.querySelectorAll('*');
  5714. return Array.prototype.filter.call(allChildren, child => {
  5715. 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');
  5716. });
  5717. }
  5718. }
  5719. /**
  5720. * Default options for `ModalDialog` default options.
  5721. *
  5722. * @type {Object}
  5723. * @private
  5724. */
  5725. ModalDialog.prototype.options_ = {
  5726. pauseOnOpen: true,
  5727. temporary: true
  5728. };
  5729. Component.registerComponent('ModalDialog', ModalDialog);
  5730. /**
  5731. * @file track-list.js
  5732. */
  5733. /**
  5734. * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
  5735. * {@link VideoTrackList}
  5736. *
  5737. * @extends EventTarget
  5738. */
  5739. class TrackList extends EventTarget {
  5740. /**
  5741. * Create an instance of this class
  5742. *
  5743. * @param { import('./track').default[] } tracks
  5744. * A list of tracks to initialize the list with.
  5745. *
  5746. * @abstract
  5747. */
  5748. constructor(tracks = []) {
  5749. super();
  5750. this.tracks_ = [];
  5751. /**
  5752. * @memberof TrackList
  5753. * @member {number} length
  5754. * The current number of `Track`s in the this Trackist.
  5755. * @instance
  5756. */
  5757. Object.defineProperty(this, 'length', {
  5758. get() {
  5759. return this.tracks_.length;
  5760. }
  5761. });
  5762. for (let i = 0; i < tracks.length; i++) {
  5763. this.addTrack(tracks[i]);
  5764. }
  5765. }
  5766. /**
  5767. * Add a {@link Track} to the `TrackList`
  5768. *
  5769. * @param { import('./track').default } track
  5770. * The audio, video, or text track to add to the list.
  5771. *
  5772. * @fires TrackList#addtrack
  5773. */
  5774. addTrack(track) {
  5775. const index = this.tracks_.length;
  5776. if (!('' + index in this)) {
  5777. Object.defineProperty(this, index, {
  5778. get() {
  5779. return this.tracks_[index];
  5780. }
  5781. });
  5782. }
  5783. // Do not add duplicate tracks
  5784. if (this.tracks_.indexOf(track) === -1) {
  5785. this.tracks_.push(track);
  5786. /**
  5787. * Triggered when a track is added to a track list.
  5788. *
  5789. * @event TrackList#addtrack
  5790. * @type {Event}
  5791. * @property {Track} track
  5792. * A reference to track that was added.
  5793. */
  5794. this.trigger({
  5795. track,
  5796. type: 'addtrack',
  5797. target: this
  5798. });
  5799. }
  5800. /**
  5801. * Triggered when a track label is changed.
  5802. *
  5803. * @event TrackList#addtrack
  5804. * @type {Event}
  5805. * @property {Track} track
  5806. * A reference to track that was added.
  5807. */
  5808. track.labelchange_ = () => {
  5809. this.trigger({
  5810. track,
  5811. type: 'labelchange',
  5812. target: this
  5813. });
  5814. };
  5815. if (isEvented(track)) {
  5816. track.addEventListener('labelchange', track.labelchange_);
  5817. }
  5818. }
  5819. /**
  5820. * Remove a {@link Track} from the `TrackList`
  5821. *
  5822. * @param { import('./track').default } rtrack
  5823. * The audio, video, or text track to remove from the list.
  5824. *
  5825. * @fires TrackList#removetrack
  5826. */
  5827. removeTrack(rtrack) {
  5828. let track;
  5829. for (let i = 0, l = this.length; i < l; i++) {
  5830. if (this[i] === rtrack) {
  5831. track = this[i];
  5832. if (track.off) {
  5833. track.off();
  5834. }
  5835. this.tracks_.splice(i, 1);
  5836. break;
  5837. }
  5838. }
  5839. if (!track) {
  5840. return;
  5841. }
  5842. /**
  5843. * Triggered when a track is removed from track list.
  5844. *
  5845. * @event TrackList#removetrack
  5846. * @type {Event}
  5847. * @property {Track} track
  5848. * A reference to track that was removed.
  5849. */
  5850. this.trigger({
  5851. track,
  5852. type: 'removetrack',
  5853. target: this
  5854. });
  5855. }
  5856. /**
  5857. * Get a Track from the TrackList by a tracks id
  5858. *
  5859. * @param {string} id - the id of the track to get
  5860. * @method getTrackById
  5861. * @return { import('./track').default }
  5862. * @private
  5863. */
  5864. getTrackById(id) {
  5865. let result = null;
  5866. for (let i = 0, l = this.length; i < l; i++) {
  5867. const track = this[i];
  5868. if (track.id === id) {
  5869. result = track;
  5870. break;
  5871. }
  5872. }
  5873. return result;
  5874. }
  5875. }
  5876. /**
  5877. * Triggered when a different track is selected/enabled.
  5878. *
  5879. * @event TrackList#change
  5880. * @type {Event}
  5881. */
  5882. /**
  5883. * Events that can be called with on + eventName. See {@link EventHandler}.
  5884. *
  5885. * @property {Object} TrackList#allowedEvents_
  5886. * @protected
  5887. */
  5888. TrackList.prototype.allowedEvents_ = {
  5889. change: 'change',
  5890. addtrack: 'addtrack',
  5891. removetrack: 'removetrack',
  5892. labelchange: 'labelchange'
  5893. };
  5894. // emulate attribute EventHandler support to allow for feature detection
  5895. for (const event in TrackList.prototype.allowedEvents_) {
  5896. TrackList.prototype['on' + event] = null;
  5897. }
  5898. /**
  5899. * @file audio-track-list.js
  5900. */
  5901. /**
  5902. * Anywhere we call this function we diverge from the spec
  5903. * as we only support one enabled audiotrack at a time
  5904. *
  5905. * @param {AudioTrackList} list
  5906. * list to work on
  5907. *
  5908. * @param { import('./audio-track').default } track
  5909. * The track to skip
  5910. *
  5911. * @private
  5912. */
  5913. const disableOthers$1 = function (list, track) {
  5914. for (let i = 0; i < list.length; i++) {
  5915. if (!Object.keys(list[i]).length || track.id === list[i].id) {
  5916. continue;
  5917. }
  5918. // another audio track is enabled, disable it
  5919. list[i].enabled = false;
  5920. }
  5921. };
  5922. /**
  5923. * The current list of {@link AudioTrack} for a media file.
  5924. *
  5925. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
  5926. * @extends TrackList
  5927. */
  5928. class AudioTrackList extends TrackList {
  5929. /**
  5930. * Create an instance of this class.
  5931. *
  5932. * @param { import('./audio-track').default[] } [tracks=[]]
  5933. * A list of `AudioTrack` to instantiate the list with.
  5934. */
  5935. constructor(tracks = []) {
  5936. // make sure only 1 track is enabled
  5937. // sorted from last index to first index
  5938. for (let i = tracks.length - 1; i >= 0; i--) {
  5939. if (tracks[i].enabled) {
  5940. disableOthers$1(tracks, tracks[i]);
  5941. break;
  5942. }
  5943. }
  5944. super(tracks);
  5945. this.changing_ = false;
  5946. }
  5947. /**
  5948. * Add an {@link AudioTrack} to the `AudioTrackList`.
  5949. *
  5950. * @param { import('./audio-track').default } track
  5951. * The AudioTrack to add to the list
  5952. *
  5953. * @fires TrackList#addtrack
  5954. */
  5955. addTrack(track) {
  5956. if (track.enabled) {
  5957. disableOthers$1(this, track);
  5958. }
  5959. super.addTrack(track);
  5960. // native tracks don't have this
  5961. if (!track.addEventListener) {
  5962. return;
  5963. }
  5964. track.enabledChange_ = () => {
  5965. // when we are disabling other tracks (since we don't support
  5966. // more than one track at a time) we will set changing_
  5967. // to true so that we don't trigger additional change events
  5968. if (this.changing_) {
  5969. return;
  5970. }
  5971. this.changing_ = true;
  5972. disableOthers$1(this, track);
  5973. this.changing_ = false;
  5974. this.trigger('change');
  5975. };
  5976. /**
  5977. * @listens AudioTrack#enabledchange
  5978. * @fires TrackList#change
  5979. */
  5980. track.addEventListener('enabledchange', track.enabledChange_);
  5981. }
  5982. removeTrack(rtrack) {
  5983. super.removeTrack(rtrack);
  5984. if (rtrack.removeEventListener && rtrack.enabledChange_) {
  5985. rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
  5986. rtrack.enabledChange_ = null;
  5987. }
  5988. }
  5989. }
  5990. /**
  5991. * @file video-track-list.js
  5992. */
  5993. /**
  5994. * Un-select all other {@link VideoTrack}s that are selected.
  5995. *
  5996. * @param {VideoTrackList} list
  5997. * list to work on
  5998. *
  5999. * @param { import('./video-track').default } track
  6000. * The track to skip
  6001. *
  6002. * @private
  6003. */
  6004. const disableOthers = function (list, track) {
  6005. for (let i = 0; i < list.length; i++) {
  6006. if (!Object.keys(list[i]).length || track.id === list[i].id) {
  6007. continue;
  6008. }
  6009. // another video track is enabled, disable it
  6010. list[i].selected = false;
  6011. }
  6012. };
  6013. /**
  6014. * The current list of {@link VideoTrack} for a video.
  6015. *
  6016. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
  6017. * @extends TrackList
  6018. */
  6019. class VideoTrackList extends TrackList {
  6020. /**
  6021. * Create an instance of this class.
  6022. *
  6023. * @param {VideoTrack[]} [tracks=[]]
  6024. * A list of `VideoTrack` to instantiate the list with.
  6025. */
  6026. constructor(tracks = []) {
  6027. // make sure only 1 track is enabled
  6028. // sorted from last index to first index
  6029. for (let i = tracks.length - 1; i >= 0; i--) {
  6030. if (tracks[i].selected) {
  6031. disableOthers(tracks, tracks[i]);
  6032. break;
  6033. }
  6034. }
  6035. super(tracks);
  6036. this.changing_ = false;
  6037. /**
  6038. * @member {number} VideoTrackList#selectedIndex
  6039. * The current index of the selected {@link VideoTrack`}.
  6040. */
  6041. Object.defineProperty(this, 'selectedIndex', {
  6042. get() {
  6043. for (let i = 0; i < this.length; i++) {
  6044. if (this[i].selected) {
  6045. return i;
  6046. }
  6047. }
  6048. return -1;
  6049. },
  6050. set() {}
  6051. });
  6052. }
  6053. /**
  6054. * Add a {@link VideoTrack} to the `VideoTrackList`.
  6055. *
  6056. * @param { import('./video-track').default } track
  6057. * The VideoTrack to add to the list
  6058. *
  6059. * @fires TrackList#addtrack
  6060. */
  6061. addTrack(track) {
  6062. if (track.selected) {
  6063. disableOthers(this, track);
  6064. }
  6065. super.addTrack(track);
  6066. // native tracks don't have this
  6067. if (!track.addEventListener) {
  6068. return;
  6069. }
  6070. track.selectedChange_ = () => {
  6071. if (this.changing_) {
  6072. return;
  6073. }
  6074. this.changing_ = true;
  6075. disableOthers(this, track);
  6076. this.changing_ = false;
  6077. this.trigger('change');
  6078. };
  6079. /**
  6080. * @listens VideoTrack#selectedchange
  6081. * @fires TrackList#change
  6082. */
  6083. track.addEventListener('selectedchange', track.selectedChange_);
  6084. }
  6085. removeTrack(rtrack) {
  6086. super.removeTrack(rtrack);
  6087. if (rtrack.removeEventListener && rtrack.selectedChange_) {
  6088. rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
  6089. rtrack.selectedChange_ = null;
  6090. }
  6091. }
  6092. }
  6093. /**
  6094. * @file text-track-list.js
  6095. */
  6096. /**
  6097. * The current list of {@link TextTrack} for a media file.
  6098. *
  6099. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
  6100. * @extends TrackList
  6101. */
  6102. class TextTrackList extends TrackList {
  6103. /**
  6104. * Add a {@link TextTrack} to the `TextTrackList`
  6105. *
  6106. * @param { import('./text-track').default } track
  6107. * The text track to add to the list.
  6108. *
  6109. * @fires TrackList#addtrack
  6110. */
  6111. addTrack(track) {
  6112. super.addTrack(track);
  6113. if (!this.queueChange_) {
  6114. this.queueChange_ = () => this.queueTrigger('change');
  6115. }
  6116. if (!this.triggerSelectedlanguagechange) {
  6117. this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
  6118. }
  6119. /**
  6120. * @listens TextTrack#modechange
  6121. * @fires TrackList#change
  6122. */
  6123. track.addEventListener('modechange', this.queueChange_);
  6124. const nonLanguageTextTrackKind = ['metadata', 'chapters'];
  6125. if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
  6126. track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
  6127. }
  6128. }
  6129. removeTrack(rtrack) {
  6130. super.removeTrack(rtrack);
  6131. // manually remove the event handlers we added
  6132. if (rtrack.removeEventListener) {
  6133. if (this.queueChange_) {
  6134. rtrack.removeEventListener('modechange', this.queueChange_);
  6135. }
  6136. if (this.selectedlanguagechange_) {
  6137. rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
  6138. }
  6139. }
  6140. }
  6141. }
  6142. /**
  6143. * @file html-track-element-list.js
  6144. */
  6145. /**
  6146. * The current list of {@link HtmlTrackElement}s.
  6147. */
  6148. class HtmlTrackElementList {
  6149. /**
  6150. * Create an instance of this class.
  6151. *
  6152. * @param {HtmlTrackElement[]} [tracks=[]]
  6153. * A list of `HtmlTrackElement` to instantiate the list with.
  6154. */
  6155. constructor(trackElements = []) {
  6156. this.trackElements_ = [];
  6157. /**
  6158. * @memberof HtmlTrackElementList
  6159. * @member {number} length
  6160. * The current number of `Track`s in the this Trackist.
  6161. * @instance
  6162. */
  6163. Object.defineProperty(this, 'length', {
  6164. get() {
  6165. return this.trackElements_.length;
  6166. }
  6167. });
  6168. for (let i = 0, length = trackElements.length; i < length; i++) {
  6169. this.addTrackElement_(trackElements[i]);
  6170. }
  6171. }
  6172. /**
  6173. * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
  6174. *
  6175. * @param {HtmlTrackElement} trackElement
  6176. * The track element to add to the list.
  6177. *
  6178. * @private
  6179. */
  6180. addTrackElement_(trackElement) {
  6181. const index = this.trackElements_.length;
  6182. if (!('' + index in this)) {
  6183. Object.defineProperty(this, index, {
  6184. get() {
  6185. return this.trackElements_[index];
  6186. }
  6187. });
  6188. }
  6189. // Do not add duplicate elements
  6190. if (this.trackElements_.indexOf(trackElement) === -1) {
  6191. this.trackElements_.push(trackElement);
  6192. }
  6193. }
  6194. /**
  6195. * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
  6196. * {@link TextTrack}.
  6197. *
  6198. * @param {TextTrack} track
  6199. * The track associated with a track element.
  6200. *
  6201. * @return {HtmlTrackElement|undefined}
  6202. * The track element that was found or undefined.
  6203. *
  6204. * @private
  6205. */
  6206. getTrackElementByTrack_(track) {
  6207. let trackElement_;
  6208. for (let i = 0, length = this.trackElements_.length; i < length; i++) {
  6209. if (track === this.trackElements_[i].track) {
  6210. trackElement_ = this.trackElements_[i];
  6211. break;
  6212. }
  6213. }
  6214. return trackElement_;
  6215. }
  6216. /**
  6217. * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
  6218. *
  6219. * @param {HtmlTrackElement} trackElement
  6220. * The track element to remove from the list.
  6221. *
  6222. * @private
  6223. */
  6224. removeTrackElement_(trackElement) {
  6225. for (let i = 0, length = this.trackElements_.length; i < length; i++) {
  6226. if (trackElement === this.trackElements_[i]) {
  6227. if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
  6228. this.trackElements_[i].track.off();
  6229. }
  6230. if (typeof this.trackElements_[i].off === 'function') {
  6231. this.trackElements_[i].off();
  6232. }
  6233. this.trackElements_.splice(i, 1);
  6234. break;
  6235. }
  6236. }
  6237. }
  6238. }
  6239. /**
  6240. * @file text-track-cue-list.js
  6241. */
  6242. /**
  6243. * @typedef {Object} TextTrackCueList~TextTrackCue
  6244. *
  6245. * @property {string} id
  6246. * The unique id for this text track cue
  6247. *
  6248. * @property {number} startTime
  6249. * The start time for this text track cue
  6250. *
  6251. * @property {number} endTime
  6252. * The end time for this text track cue
  6253. *
  6254. * @property {boolean} pauseOnExit
  6255. * Pause when the end time is reached if true.
  6256. *
  6257. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
  6258. */
  6259. /**
  6260. * A List of TextTrackCues.
  6261. *
  6262. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
  6263. */
  6264. class TextTrackCueList {
  6265. /**
  6266. * Create an instance of this class..
  6267. *
  6268. * @param {Array} cues
  6269. * A list of cues to be initialized with
  6270. */
  6271. constructor(cues) {
  6272. TextTrackCueList.prototype.setCues_.call(this, cues);
  6273. /**
  6274. * @memberof TextTrackCueList
  6275. * @member {number} length
  6276. * The current number of `TextTrackCue`s in the TextTrackCueList.
  6277. * @instance
  6278. */
  6279. Object.defineProperty(this, 'length', {
  6280. get() {
  6281. return this.length_;
  6282. }
  6283. });
  6284. }
  6285. /**
  6286. * A setter for cues in this list. Creates getters
  6287. * an an index for the cues.
  6288. *
  6289. * @param {Array} cues
  6290. * An array of cues to set
  6291. *
  6292. * @private
  6293. */
  6294. setCues_(cues) {
  6295. const oldLength = this.length || 0;
  6296. let i = 0;
  6297. const l = cues.length;
  6298. this.cues_ = cues;
  6299. this.length_ = cues.length;
  6300. const defineProp = function (index) {
  6301. if (!('' + index in this)) {
  6302. Object.defineProperty(this, '' + index, {
  6303. get() {
  6304. return this.cues_[index];
  6305. }
  6306. });
  6307. }
  6308. };
  6309. if (oldLength < l) {
  6310. i = oldLength;
  6311. for (; i < l; i++) {
  6312. defineProp.call(this, i);
  6313. }
  6314. }
  6315. }
  6316. /**
  6317. * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
  6318. *
  6319. * @param {string} id
  6320. * The id of the cue that should be searched for.
  6321. *
  6322. * @return {TextTrackCueList~TextTrackCue|null}
  6323. * A single cue or null if none was found.
  6324. */
  6325. getCueById(id) {
  6326. let result = null;
  6327. for (let i = 0, l = this.length; i < l; i++) {
  6328. const cue = this[i];
  6329. if (cue.id === id) {
  6330. result = cue;
  6331. break;
  6332. }
  6333. }
  6334. return result;
  6335. }
  6336. }
  6337. /**
  6338. * @file track-kinds.js
  6339. */
  6340. /**
  6341. * All possible `VideoTrackKind`s
  6342. *
  6343. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
  6344. * @typedef VideoTrack~Kind
  6345. * @enum
  6346. */
  6347. const VideoTrackKind = {
  6348. alternative: 'alternative',
  6349. captions: 'captions',
  6350. main: 'main',
  6351. sign: 'sign',
  6352. subtitles: 'subtitles',
  6353. commentary: 'commentary'
  6354. };
  6355. /**
  6356. * All possible `AudioTrackKind`s
  6357. *
  6358. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
  6359. * @typedef AudioTrack~Kind
  6360. * @enum
  6361. */
  6362. const AudioTrackKind = {
  6363. 'alternative': 'alternative',
  6364. 'descriptions': 'descriptions',
  6365. 'main': 'main',
  6366. 'main-desc': 'main-desc',
  6367. 'translation': 'translation',
  6368. 'commentary': 'commentary'
  6369. };
  6370. /**
  6371. * All possible `TextTrackKind`s
  6372. *
  6373. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
  6374. * @typedef TextTrack~Kind
  6375. * @enum
  6376. */
  6377. const TextTrackKind = {
  6378. subtitles: 'subtitles',
  6379. captions: 'captions',
  6380. descriptions: 'descriptions',
  6381. chapters: 'chapters',
  6382. metadata: 'metadata'
  6383. };
  6384. /**
  6385. * All possible `TextTrackMode`s
  6386. *
  6387. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
  6388. * @typedef TextTrack~Mode
  6389. * @enum
  6390. */
  6391. const TextTrackMode = {
  6392. disabled: 'disabled',
  6393. hidden: 'hidden',
  6394. showing: 'showing'
  6395. };
  6396. /**
  6397. * @file track.js
  6398. */
  6399. /**
  6400. * A Track class that contains all of the common functionality for {@link AudioTrack},
  6401. * {@link VideoTrack}, and {@link TextTrack}.
  6402. *
  6403. * > Note: This class should not be used directly
  6404. *
  6405. * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
  6406. * @extends EventTarget
  6407. * @abstract
  6408. */
  6409. class Track extends EventTarget {
  6410. /**
  6411. * Create an instance of this class.
  6412. *
  6413. * @param {Object} [options={}]
  6414. * Object of option names and values
  6415. *
  6416. * @param {string} [options.kind='']
  6417. * A valid kind for the track type you are creating.
  6418. *
  6419. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  6420. * A unique id for this AudioTrack.
  6421. *
  6422. * @param {string} [options.label='']
  6423. * The menu label for this track.
  6424. *
  6425. * @param {string} [options.language='']
  6426. * A valid two character language code.
  6427. *
  6428. * @abstract
  6429. */
  6430. constructor(options = {}) {
  6431. super();
  6432. const trackProps = {
  6433. id: options.id || 'vjs_track_' + newGUID(),
  6434. kind: options.kind || '',
  6435. language: options.language || ''
  6436. };
  6437. let label = options.label || '';
  6438. /**
  6439. * @memberof Track
  6440. * @member {string} id
  6441. * The id of this track. Cannot be changed after creation.
  6442. * @instance
  6443. *
  6444. * @readonly
  6445. */
  6446. /**
  6447. * @memberof Track
  6448. * @member {string} kind
  6449. * The kind of track that this is. Cannot be changed after creation.
  6450. * @instance
  6451. *
  6452. * @readonly
  6453. */
  6454. /**
  6455. * @memberof Track
  6456. * @member {string} language
  6457. * The two letter language code for this track. Cannot be changed after
  6458. * creation.
  6459. * @instance
  6460. *
  6461. * @readonly
  6462. */
  6463. for (const key in trackProps) {
  6464. Object.defineProperty(this, key, {
  6465. get() {
  6466. return trackProps[key];
  6467. },
  6468. set() {}
  6469. });
  6470. }
  6471. /**
  6472. * @memberof Track
  6473. * @member {string} label
  6474. * The label of this track. Cannot be changed after creation.
  6475. * @instance
  6476. *
  6477. * @fires Track#labelchange
  6478. */
  6479. Object.defineProperty(this, 'label', {
  6480. get() {
  6481. return label;
  6482. },
  6483. set(newLabel) {
  6484. if (newLabel !== label) {
  6485. label = newLabel;
  6486. /**
  6487. * An event that fires when label changes on this track.
  6488. *
  6489. * > Note: This is not part of the spec!
  6490. *
  6491. * @event Track#labelchange
  6492. * @type {Event}
  6493. */
  6494. this.trigger('labelchange');
  6495. }
  6496. }
  6497. });
  6498. }
  6499. }
  6500. /**
  6501. * @file url.js
  6502. * @module url
  6503. */
  6504. /**
  6505. * @typedef {Object} url:URLObject
  6506. *
  6507. * @property {string} protocol
  6508. * The protocol of the url that was parsed.
  6509. *
  6510. * @property {string} hostname
  6511. * The hostname of the url that was parsed.
  6512. *
  6513. * @property {string} port
  6514. * The port of the url that was parsed.
  6515. *
  6516. * @property {string} pathname
  6517. * The pathname of the url that was parsed.
  6518. *
  6519. * @property {string} search
  6520. * The search query of the url that was parsed.
  6521. *
  6522. * @property {string} hash
  6523. * The hash of the url that was parsed.
  6524. *
  6525. * @property {string} host
  6526. * The host of the url that was parsed.
  6527. */
  6528. /**
  6529. * Resolve and parse the elements of a URL.
  6530. *
  6531. * @function
  6532. * @param {String} url
  6533. * The url to parse
  6534. *
  6535. * @return {url:URLObject}
  6536. * An object of url details
  6537. */
  6538. const parseUrl = function (url) {
  6539. // This entire method can be replace with URL once we are able to drop IE11
  6540. const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
  6541. // add the url to an anchor and let the browser parse the URL
  6542. const a = document__default["default"].createElement('a');
  6543. a.href = url;
  6544. // Copy the specific URL properties to a new object
  6545. // This is also needed for IE because the anchor loses its
  6546. // properties when it's removed from the dom
  6547. const details = {};
  6548. for (let i = 0; i < props.length; i++) {
  6549. details[props[i]] = a[props[i]];
  6550. }
  6551. // IE adds the port to the host property unlike everyone else. If
  6552. // a port identifier is added for standard ports, strip it.
  6553. if (details.protocol === 'http:') {
  6554. details.host = details.host.replace(/:80$/, '');
  6555. }
  6556. if (details.protocol === 'https:') {
  6557. details.host = details.host.replace(/:443$/, '');
  6558. }
  6559. if (!details.protocol) {
  6560. details.protocol = window__default["default"].location.protocol;
  6561. }
  6562. /* istanbul ignore if */
  6563. if (!details.host) {
  6564. details.host = window__default["default"].location.host;
  6565. }
  6566. return details;
  6567. };
  6568. /**
  6569. * Get absolute version of relative URL.
  6570. *
  6571. * @function
  6572. * @param {string} url
  6573. * URL to make absolute
  6574. *
  6575. * @return {string}
  6576. * Absolute URL
  6577. *
  6578. * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
  6579. */
  6580. const getAbsoluteURL = function (url) {
  6581. // Check if absolute URL
  6582. if (!url.match(/^https?:\/\//)) {
  6583. // Add the url to an anchor and let the browser parse it to convert to an absolute url
  6584. const a = document__default["default"].createElement('a');
  6585. a.href = url;
  6586. url = a.href;
  6587. }
  6588. return url;
  6589. };
  6590. /**
  6591. * Returns the extension of the passed file name. It will return an empty string
  6592. * if passed an invalid path.
  6593. *
  6594. * @function
  6595. * @param {string} path
  6596. * The fileName path like '/path/to/file.mp4'
  6597. *
  6598. * @return {string}
  6599. * The extension in lower case or an empty string if no
  6600. * extension could be found.
  6601. */
  6602. const getFileExtension = function (path) {
  6603. if (typeof path === 'string') {
  6604. const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
  6605. const pathParts = splitPathRe.exec(path);
  6606. if (pathParts) {
  6607. return pathParts.pop().toLowerCase();
  6608. }
  6609. }
  6610. return '';
  6611. };
  6612. /**
  6613. * Returns whether the url passed is a cross domain request or not.
  6614. *
  6615. * @function
  6616. * @param {string} url
  6617. * The url to check.
  6618. *
  6619. * @param {Object} [winLoc]
  6620. * the domain to check the url against, defaults to window.location
  6621. *
  6622. * @param {string} [winLoc.protocol]
  6623. * The window location protocol defaults to window.location.protocol
  6624. *
  6625. * @param {string} [winLoc.host]
  6626. * The window location host defaults to window.location.host
  6627. *
  6628. * @return {boolean}
  6629. * Whether it is a cross domain request or not.
  6630. */
  6631. const isCrossOrigin = function (url, winLoc = window__default["default"].location) {
  6632. const urlInfo = parseUrl(url);
  6633. // IE8 protocol relative urls will return ':' for protocol
  6634. const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
  6635. // Check if url is for another domain/origin
  6636. // IE8 doesn't know location.origin, so we won't rely on it here
  6637. const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
  6638. return crossOrigin;
  6639. };
  6640. var Url = /*#__PURE__*/Object.freeze({
  6641. __proto__: null,
  6642. parseUrl: parseUrl,
  6643. getAbsoluteURL: getAbsoluteURL,
  6644. getFileExtension: getFileExtension,
  6645. isCrossOrigin: isCrossOrigin
  6646. });
  6647. /**
  6648. * @file text-track.js
  6649. */
  6650. /**
  6651. * Takes a webvtt file contents and parses it into cues
  6652. *
  6653. * @param {string} srcContent
  6654. * webVTT file contents
  6655. *
  6656. * @param {TextTrack} track
  6657. * TextTrack to add cues to. Cues come from the srcContent.
  6658. *
  6659. * @private
  6660. */
  6661. const parseCues = function (srcContent, track) {
  6662. const parser = new window__default["default"].WebVTT.Parser(window__default["default"], window__default["default"].vttjs, window__default["default"].WebVTT.StringDecoder());
  6663. const errors = [];
  6664. parser.oncue = function (cue) {
  6665. track.addCue(cue);
  6666. };
  6667. parser.onparsingerror = function (error) {
  6668. errors.push(error);
  6669. };
  6670. parser.onflush = function () {
  6671. track.trigger({
  6672. type: 'loadeddata',
  6673. target: track
  6674. });
  6675. };
  6676. parser.parse(srcContent);
  6677. if (errors.length > 0) {
  6678. if (window__default["default"].console && window__default["default"].console.groupCollapsed) {
  6679. window__default["default"].console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
  6680. }
  6681. errors.forEach(error => log.error(error));
  6682. if (window__default["default"].console && window__default["default"].console.groupEnd) {
  6683. window__default["default"].console.groupEnd();
  6684. }
  6685. }
  6686. parser.flush();
  6687. };
  6688. /**
  6689. * Load a `TextTrack` from a specified url.
  6690. *
  6691. * @param {string} src
  6692. * Url to load track from.
  6693. *
  6694. * @param {TextTrack} track
  6695. * Track to add cues to. Comes from the content at the end of `url`.
  6696. *
  6697. * @private
  6698. */
  6699. const loadTrack = function (src, track) {
  6700. const opts = {
  6701. uri: src
  6702. };
  6703. const crossOrigin = isCrossOrigin(src);
  6704. if (crossOrigin) {
  6705. opts.cors = crossOrigin;
  6706. }
  6707. const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
  6708. if (withCredentials) {
  6709. opts.withCredentials = withCredentials;
  6710. }
  6711. XHR__default["default"](opts, bind_(this, function (err, response, responseBody) {
  6712. if (err) {
  6713. return log.error(err, response);
  6714. }
  6715. track.loaded_ = true;
  6716. // Make sure that vttjs has loaded, otherwise, wait till it finished loading
  6717. // NOTE: this is only used for the alt/video.novtt.js build
  6718. if (typeof window__default["default"].WebVTT !== 'function') {
  6719. if (track.tech_) {
  6720. // to prevent use before define eslint error, we define loadHandler
  6721. // as a let here
  6722. track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
  6723. if (event.type === 'vttjserror') {
  6724. log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
  6725. return;
  6726. }
  6727. return parseCues(responseBody, track);
  6728. });
  6729. }
  6730. } else {
  6731. parseCues(responseBody, track);
  6732. }
  6733. }));
  6734. };
  6735. /**
  6736. * A representation of a single `TextTrack`.
  6737. *
  6738. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
  6739. * @extends Track
  6740. */
  6741. class TextTrack extends Track {
  6742. /**
  6743. * Create an instance of this class.
  6744. *
  6745. * @param {Object} options={}
  6746. * Object of option names and values
  6747. *
  6748. * @param { import('../tech/tech').default } options.tech
  6749. * A reference to the tech that owns this TextTrack.
  6750. *
  6751. * @param {TextTrack~Kind} [options.kind='subtitles']
  6752. * A valid text track kind.
  6753. *
  6754. * @param {TextTrack~Mode} [options.mode='disabled']
  6755. * A valid text track mode.
  6756. *
  6757. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  6758. * A unique id for this TextTrack.
  6759. *
  6760. * @param {string} [options.label='']
  6761. * The menu label for this track.
  6762. *
  6763. * @param {string} [options.language='']
  6764. * A valid two character language code.
  6765. *
  6766. * @param {string} [options.srclang='']
  6767. * A valid two character language code. An alternative, but deprioritized
  6768. * version of `options.language`
  6769. *
  6770. * @param {string} [options.src]
  6771. * A url to TextTrack cues.
  6772. *
  6773. * @param {boolean} [options.default]
  6774. * If this track should default to on or off.
  6775. */
  6776. constructor(options = {}) {
  6777. if (!options.tech) {
  6778. throw new Error('A tech was not provided.');
  6779. }
  6780. const settings = merge(options, {
  6781. kind: TextTrackKind[options.kind] || 'subtitles',
  6782. language: options.language || options.srclang || ''
  6783. });
  6784. let mode = TextTrackMode[settings.mode] || 'disabled';
  6785. const default_ = settings.default;
  6786. if (settings.kind === 'metadata' || settings.kind === 'chapters') {
  6787. mode = 'hidden';
  6788. }
  6789. super(settings);
  6790. this.tech_ = settings.tech;
  6791. this.cues_ = [];
  6792. this.activeCues_ = [];
  6793. this.preload_ = this.tech_.preloadTextTracks !== false;
  6794. const cues = new TextTrackCueList(this.cues_);
  6795. const activeCues = new TextTrackCueList(this.activeCues_);
  6796. let changed = false;
  6797. this.timeupdateHandler = bind_(this, function (event = {}) {
  6798. if (this.tech_.isDisposed()) {
  6799. return;
  6800. }
  6801. if (!this.tech_.isReady_) {
  6802. if (event.type !== 'timeupdate') {
  6803. this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
  6804. }
  6805. return;
  6806. }
  6807. // Accessing this.activeCues for the side-effects of updating itself
  6808. // due to its nature as a getter function. Do not remove or cues will
  6809. // stop updating!
  6810. // Use the setter to prevent deletion from uglify (pure_getters rule)
  6811. this.activeCues = this.activeCues;
  6812. if (changed) {
  6813. this.trigger('cuechange');
  6814. changed = false;
  6815. }
  6816. if (event.type !== 'timeupdate') {
  6817. this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
  6818. }
  6819. });
  6820. const disposeHandler = () => {
  6821. this.stopTracking();
  6822. };
  6823. this.tech_.one('dispose', disposeHandler);
  6824. if (mode !== 'disabled') {
  6825. this.startTracking();
  6826. }
  6827. Object.defineProperties(this, {
  6828. /**
  6829. * @memberof TextTrack
  6830. * @member {boolean} default
  6831. * If this track was set to be on or off by default. Cannot be changed after
  6832. * creation.
  6833. * @instance
  6834. *
  6835. * @readonly
  6836. */
  6837. default: {
  6838. get() {
  6839. return default_;
  6840. },
  6841. set() {}
  6842. },
  6843. /**
  6844. * @memberof TextTrack
  6845. * @member {string} mode
  6846. * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
  6847. * not be set if setting to an invalid mode.
  6848. * @instance
  6849. *
  6850. * @fires TextTrack#modechange
  6851. */
  6852. mode: {
  6853. get() {
  6854. return mode;
  6855. },
  6856. set(newMode) {
  6857. if (!TextTrackMode[newMode]) {
  6858. return;
  6859. }
  6860. if (mode === newMode) {
  6861. return;
  6862. }
  6863. mode = newMode;
  6864. if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
  6865. // On-demand load.
  6866. loadTrack(this.src, this);
  6867. }
  6868. this.stopTracking();
  6869. if (mode !== 'disabled') {
  6870. this.startTracking();
  6871. }
  6872. /**
  6873. * An event that fires when mode changes on this track. This allows
  6874. * the TextTrackList that holds this track to act accordingly.
  6875. *
  6876. * > Note: This is not part of the spec!
  6877. *
  6878. * @event TextTrack#modechange
  6879. * @type {Event}
  6880. */
  6881. this.trigger('modechange');
  6882. }
  6883. },
  6884. /**
  6885. * @memberof TextTrack
  6886. * @member {TextTrackCueList} cues
  6887. * The text track cue list for this TextTrack.
  6888. * @instance
  6889. */
  6890. cues: {
  6891. get() {
  6892. if (!this.loaded_) {
  6893. return null;
  6894. }
  6895. return cues;
  6896. },
  6897. set() {}
  6898. },
  6899. /**
  6900. * @memberof TextTrack
  6901. * @member {TextTrackCueList} activeCues
  6902. * The list text track cues that are currently active for this TextTrack.
  6903. * @instance
  6904. */
  6905. activeCues: {
  6906. get() {
  6907. if (!this.loaded_) {
  6908. return null;
  6909. }
  6910. // nothing to do
  6911. if (this.cues.length === 0) {
  6912. return activeCues;
  6913. }
  6914. const ct = this.tech_.currentTime();
  6915. const active = [];
  6916. for (let i = 0, l = this.cues.length; i < l; i++) {
  6917. const cue = this.cues[i];
  6918. if (cue.startTime <= ct && cue.endTime >= ct) {
  6919. active.push(cue);
  6920. }
  6921. }
  6922. changed = false;
  6923. if (active.length !== this.activeCues_.length) {
  6924. changed = true;
  6925. } else {
  6926. for (let i = 0; i < active.length; i++) {
  6927. if (this.activeCues_.indexOf(active[i]) === -1) {
  6928. changed = true;
  6929. }
  6930. }
  6931. }
  6932. this.activeCues_ = active;
  6933. activeCues.setCues_(this.activeCues_);
  6934. return activeCues;
  6935. },
  6936. // /!\ Keep this setter empty (see the timeupdate handler above)
  6937. set() {}
  6938. }
  6939. });
  6940. if (settings.src) {
  6941. this.src = settings.src;
  6942. if (!this.preload_) {
  6943. // Tracks will load on-demand.
  6944. // Act like we're loaded for other purposes.
  6945. this.loaded_ = true;
  6946. }
  6947. if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
  6948. loadTrack(this.src, this);
  6949. }
  6950. } else {
  6951. this.loaded_ = true;
  6952. }
  6953. }
  6954. startTracking() {
  6955. // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
  6956. this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
  6957. // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
  6958. this.tech_.on('timeupdate', this.timeupdateHandler);
  6959. }
  6960. stopTracking() {
  6961. if (this.rvf_) {
  6962. this.tech_.cancelVideoFrameCallback(this.rvf_);
  6963. this.rvf_ = undefined;
  6964. }
  6965. this.tech_.off('timeupdate', this.timeupdateHandler);
  6966. }
  6967. /**
  6968. * Add a cue to the internal list of cues.
  6969. *
  6970. * @param {TextTrack~Cue} cue
  6971. * The cue to add to our internal list
  6972. */
  6973. addCue(originalCue) {
  6974. let cue = originalCue;
  6975. // Testing if the cue is a VTTCue in a way that survives minification
  6976. if (!('getCueAsHTML' in cue)) {
  6977. cue = new window__default["default"].vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
  6978. for (const prop in originalCue) {
  6979. if (!(prop in cue)) {
  6980. cue[prop] = originalCue[prop];
  6981. }
  6982. }
  6983. // make sure that `id` is copied over
  6984. cue.id = originalCue.id;
  6985. cue.originalCue_ = originalCue;
  6986. }
  6987. const tracks = this.tech_.textTracks();
  6988. for (let i = 0; i < tracks.length; i++) {
  6989. if (tracks[i] !== this) {
  6990. tracks[i].removeCue(cue);
  6991. }
  6992. }
  6993. this.cues_.push(cue);
  6994. this.cues.setCues_(this.cues_);
  6995. }
  6996. /**
  6997. * Remove a cue from our internal list
  6998. *
  6999. * @param {TextTrack~Cue} removeCue
  7000. * The cue to remove from our internal list
  7001. */
  7002. removeCue(removeCue) {
  7003. let i = this.cues_.length;
  7004. while (i--) {
  7005. const cue = this.cues_[i];
  7006. if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
  7007. this.cues_.splice(i, 1);
  7008. this.cues.setCues_(this.cues_);
  7009. break;
  7010. }
  7011. }
  7012. }
  7013. }
  7014. /**
  7015. * cuechange - One or more cues in the track have become active or stopped being active.
  7016. * @protected
  7017. */
  7018. TextTrack.prototype.allowedEvents_ = {
  7019. cuechange: 'cuechange'
  7020. };
  7021. /**
  7022. * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
  7023. * only one `AudioTrack` in the list will be enabled at a time.
  7024. *
  7025. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
  7026. * @extends Track
  7027. */
  7028. class AudioTrack extends Track {
  7029. /**
  7030. * Create an instance of this class.
  7031. *
  7032. * @param {Object} [options={}]
  7033. * Object of option names and values
  7034. *
  7035. * @param {AudioTrack~Kind} [options.kind='']
  7036. * A valid audio track kind
  7037. *
  7038. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  7039. * A unique id for this AudioTrack.
  7040. *
  7041. * @param {string} [options.label='']
  7042. * The menu label for this track.
  7043. *
  7044. * @param {string} [options.language='']
  7045. * A valid two character language code.
  7046. *
  7047. * @param {boolean} [options.enabled]
  7048. * If this track is the one that is currently playing. If this track is part of
  7049. * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
  7050. */
  7051. constructor(options = {}) {
  7052. const settings = merge(options, {
  7053. kind: AudioTrackKind[options.kind] || ''
  7054. });
  7055. super(settings);
  7056. let enabled = false;
  7057. /**
  7058. * @memberof AudioTrack
  7059. * @member {boolean} enabled
  7060. * If this `AudioTrack` is enabled or not. When setting this will
  7061. * fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
  7062. * @instance
  7063. *
  7064. * @fires VideoTrack#selectedchange
  7065. */
  7066. Object.defineProperty(this, 'enabled', {
  7067. get() {
  7068. return enabled;
  7069. },
  7070. set(newEnabled) {
  7071. // an invalid or unchanged value
  7072. if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
  7073. return;
  7074. }
  7075. enabled = newEnabled;
  7076. /**
  7077. * An event that fires when enabled changes on this track. This allows
  7078. * the AudioTrackList that holds this track to act accordingly.
  7079. *
  7080. * > Note: This is not part of the spec! Native tracks will do
  7081. * this internally without an event.
  7082. *
  7083. * @event AudioTrack#enabledchange
  7084. * @type {Event}
  7085. */
  7086. this.trigger('enabledchange');
  7087. }
  7088. });
  7089. // if the user sets this track to selected then
  7090. // set selected to that true value otherwise
  7091. // we keep it false
  7092. if (settings.enabled) {
  7093. this.enabled = settings.enabled;
  7094. }
  7095. this.loaded_ = true;
  7096. }
  7097. }
  7098. /**
  7099. * A representation of a single `VideoTrack`.
  7100. *
  7101. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
  7102. * @extends Track
  7103. */
  7104. class VideoTrack extends Track {
  7105. /**
  7106. * Create an instance of this class.
  7107. *
  7108. * @param {Object} [options={}]
  7109. * Object of option names and values
  7110. *
  7111. * @param {string} [options.kind='']
  7112. * A valid {@link VideoTrack~Kind}
  7113. *
  7114. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  7115. * A unique id for this AudioTrack.
  7116. *
  7117. * @param {string} [options.label='']
  7118. * The menu label for this track.
  7119. *
  7120. * @param {string} [options.language='']
  7121. * A valid two character language code.
  7122. *
  7123. * @param {boolean} [options.selected]
  7124. * If this track is the one that is currently playing.
  7125. */
  7126. constructor(options = {}) {
  7127. const settings = merge(options, {
  7128. kind: VideoTrackKind[options.kind] || ''
  7129. });
  7130. super(settings);
  7131. let selected = false;
  7132. /**
  7133. * @memberof VideoTrack
  7134. * @member {boolean} selected
  7135. * If this `VideoTrack` is selected or not. When setting this will
  7136. * fire {@link VideoTrack#selectedchange} if the state of selected changed.
  7137. * @instance
  7138. *
  7139. * @fires VideoTrack#selectedchange
  7140. */
  7141. Object.defineProperty(this, 'selected', {
  7142. get() {
  7143. return selected;
  7144. },
  7145. set(newSelected) {
  7146. // an invalid or unchanged value
  7147. if (typeof newSelected !== 'boolean' || newSelected === selected) {
  7148. return;
  7149. }
  7150. selected = newSelected;
  7151. /**
  7152. * An event that fires when selected changes on this track. This allows
  7153. * the VideoTrackList that holds this track to act accordingly.
  7154. *
  7155. * > Note: This is not part of the spec! Native tracks will do
  7156. * this internally without an event.
  7157. *
  7158. * @event VideoTrack#selectedchange
  7159. * @type {Event}
  7160. */
  7161. this.trigger('selectedchange');
  7162. }
  7163. });
  7164. // if the user sets this track to selected then
  7165. // set selected to that true value otherwise
  7166. // we keep it false
  7167. if (settings.selected) {
  7168. this.selected = settings.selected;
  7169. }
  7170. }
  7171. }
  7172. /**
  7173. * @file html-track-element.js
  7174. */
  7175. /**
  7176. * A single track represented in the DOM.
  7177. *
  7178. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
  7179. * @extends EventTarget
  7180. */
  7181. class HTMLTrackElement extends EventTarget {
  7182. /**
  7183. * Create an instance of this class.
  7184. *
  7185. * @param {Object} options={}
  7186. * Object of option names and values
  7187. *
  7188. * @param { import('../tech/tech').default } options.tech
  7189. * A reference to the tech that owns this HTMLTrackElement.
  7190. *
  7191. * @param {TextTrack~Kind} [options.kind='subtitles']
  7192. * A valid text track kind.
  7193. *
  7194. * @param {TextTrack~Mode} [options.mode='disabled']
  7195. * A valid text track mode.
  7196. *
  7197. * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
  7198. * A unique id for this TextTrack.
  7199. *
  7200. * @param {string} [options.label='']
  7201. * The menu label for this track.
  7202. *
  7203. * @param {string} [options.language='']
  7204. * A valid two character language code.
  7205. *
  7206. * @param {string} [options.srclang='']
  7207. * A valid two character language code. An alternative, but deprioritized
  7208. * version of `options.language`
  7209. *
  7210. * @param {string} [options.src]
  7211. * A url to TextTrack cues.
  7212. *
  7213. * @param {boolean} [options.default]
  7214. * If this track should default to on or off.
  7215. */
  7216. constructor(options = {}) {
  7217. super();
  7218. let readyState;
  7219. const track = new TextTrack(options);
  7220. this.kind = track.kind;
  7221. this.src = track.src;
  7222. this.srclang = track.language;
  7223. this.label = track.label;
  7224. this.default = track.default;
  7225. Object.defineProperties(this, {
  7226. /**
  7227. * @memberof HTMLTrackElement
  7228. * @member {HTMLTrackElement~ReadyState} readyState
  7229. * The current ready state of the track element.
  7230. * @instance
  7231. */
  7232. readyState: {
  7233. get() {
  7234. return readyState;
  7235. }
  7236. },
  7237. /**
  7238. * @memberof HTMLTrackElement
  7239. * @member {TextTrack} track
  7240. * The underlying TextTrack object.
  7241. * @instance
  7242. *
  7243. */
  7244. track: {
  7245. get() {
  7246. return track;
  7247. }
  7248. }
  7249. });
  7250. readyState = HTMLTrackElement.NONE;
  7251. /**
  7252. * @listens TextTrack#loadeddata
  7253. * @fires HTMLTrackElement#load
  7254. */
  7255. track.addEventListener('loadeddata', () => {
  7256. readyState = HTMLTrackElement.LOADED;
  7257. this.trigger({
  7258. type: 'load',
  7259. target: this
  7260. });
  7261. });
  7262. }
  7263. }
  7264. /**
  7265. * @protected
  7266. */
  7267. HTMLTrackElement.prototype.allowedEvents_ = {
  7268. load: 'load'
  7269. };
  7270. /**
  7271. * The text track not loaded state.
  7272. *
  7273. * @type {number}
  7274. * @static
  7275. */
  7276. HTMLTrackElement.NONE = 0;
  7277. /**
  7278. * The text track loading state.
  7279. *
  7280. * @type {number}
  7281. * @static
  7282. */
  7283. HTMLTrackElement.LOADING = 1;
  7284. /**
  7285. * The text track loaded state.
  7286. *
  7287. * @type {number}
  7288. * @static
  7289. */
  7290. HTMLTrackElement.LOADED = 2;
  7291. /**
  7292. * The text track failed to load state.
  7293. *
  7294. * @type {number}
  7295. * @static
  7296. */
  7297. HTMLTrackElement.ERROR = 3;
  7298. /*
  7299. * This file contains all track properties that are used in
  7300. * player.js, tech.js, html5.js and possibly other techs in the future.
  7301. */
  7302. const NORMAL = {
  7303. audio: {
  7304. ListClass: AudioTrackList,
  7305. TrackClass: AudioTrack,
  7306. capitalName: 'Audio'
  7307. },
  7308. video: {
  7309. ListClass: VideoTrackList,
  7310. TrackClass: VideoTrack,
  7311. capitalName: 'Video'
  7312. },
  7313. text: {
  7314. ListClass: TextTrackList,
  7315. TrackClass: TextTrack,
  7316. capitalName: 'Text'
  7317. }
  7318. };
  7319. Object.keys(NORMAL).forEach(function (type) {
  7320. NORMAL[type].getterName = `${type}Tracks`;
  7321. NORMAL[type].privateName = `${type}Tracks_`;
  7322. });
  7323. const REMOTE = {
  7324. remoteText: {
  7325. ListClass: TextTrackList,
  7326. TrackClass: TextTrack,
  7327. capitalName: 'RemoteText',
  7328. getterName: 'remoteTextTracks',
  7329. privateName: 'remoteTextTracks_'
  7330. },
  7331. remoteTextEl: {
  7332. ListClass: HtmlTrackElementList,
  7333. TrackClass: HTMLTrackElement,
  7334. capitalName: 'RemoteTextTrackEls',
  7335. getterName: 'remoteTextTrackEls',
  7336. privateName: 'remoteTextTrackEls_'
  7337. }
  7338. };
  7339. const ALL = Object.assign({}, NORMAL, REMOTE);
  7340. REMOTE.names = Object.keys(REMOTE);
  7341. NORMAL.names = Object.keys(NORMAL);
  7342. ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
  7343. /**
  7344. * @file tech.js
  7345. */
  7346. /**
  7347. * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
  7348. * that just contains the src url alone.
  7349. * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
  7350. * `var SourceString = 'http://example.com/some-video.mp4';`
  7351. *
  7352. * @typedef {Object|string} SourceObject
  7353. *
  7354. * @property {string} src
  7355. * The url to the source
  7356. *
  7357. * @property {string} type
  7358. * The mime type of the source
  7359. */
  7360. /**
  7361. * A function used by {@link Tech} to create a new {@link TextTrack}.
  7362. *
  7363. * @private
  7364. *
  7365. * @param {Tech} self
  7366. * An instance of the Tech class.
  7367. *
  7368. * @param {string} kind
  7369. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
  7370. *
  7371. * @param {string} [label]
  7372. * Label to identify the text track
  7373. *
  7374. * @param {string} [language]
  7375. * Two letter language abbreviation
  7376. *
  7377. * @param {Object} [options={}]
  7378. * An object with additional text track options
  7379. *
  7380. * @return {TextTrack}
  7381. * The text track that was created.
  7382. */
  7383. function createTrackHelper(self, kind, label, language, options = {}) {
  7384. const tracks = self.textTracks();
  7385. options.kind = kind;
  7386. if (label) {
  7387. options.label = label;
  7388. }
  7389. if (language) {
  7390. options.language = language;
  7391. }
  7392. options.tech = self;
  7393. const track = new ALL.text.TrackClass(options);
  7394. tracks.addTrack(track);
  7395. return track;
  7396. }
  7397. /**
  7398. * This is the base class for media playback technology controllers, such as
  7399. * {@link HTML5}
  7400. *
  7401. * @extends Component
  7402. */
  7403. class Tech extends Component {
  7404. /**
  7405. * Create an instance of this Tech.
  7406. *
  7407. * @param {Object} [options]
  7408. * The key/value store of player options.
  7409. *
  7410. * @param {Function} [ready]
  7411. * Callback function to call when the `HTML5` Tech is ready.
  7412. */
  7413. constructor(options = {}, ready = function () {}) {
  7414. // we don't want the tech to report user activity automatically.
  7415. // This is done manually in addControlsListeners
  7416. options.reportTouchActivity = false;
  7417. super(null, options, ready);
  7418. this.onDurationChange_ = e => this.onDurationChange(e);
  7419. this.trackProgress_ = e => this.trackProgress(e);
  7420. this.trackCurrentTime_ = e => this.trackCurrentTime(e);
  7421. this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
  7422. this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
  7423. this.queuedHanders_ = new Set();
  7424. // keep track of whether the current source has played at all to
  7425. // implement a very limited played()
  7426. this.hasStarted_ = false;
  7427. this.on('playing', function () {
  7428. this.hasStarted_ = true;
  7429. });
  7430. this.on('loadstart', function () {
  7431. this.hasStarted_ = false;
  7432. });
  7433. ALL.names.forEach(name => {
  7434. const props = ALL[name];
  7435. if (options && options[props.getterName]) {
  7436. this[props.privateName] = options[props.getterName];
  7437. }
  7438. });
  7439. // Manually track progress in cases where the browser/tech doesn't report it.
  7440. if (!this.featuresProgressEvents) {
  7441. this.manualProgressOn();
  7442. }
  7443. // Manually track timeupdates in cases where the browser/tech doesn't report it.
  7444. if (!this.featuresTimeupdateEvents) {
  7445. this.manualTimeUpdatesOn();
  7446. }
  7447. ['Text', 'Audio', 'Video'].forEach(track => {
  7448. if (options[`native${track}Tracks`] === false) {
  7449. this[`featuresNative${track}Tracks`] = false;
  7450. }
  7451. });
  7452. if (options.nativeCaptions === false || options.nativeTextTracks === false) {
  7453. this.featuresNativeTextTracks = false;
  7454. } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
  7455. this.featuresNativeTextTracks = true;
  7456. }
  7457. if (!this.featuresNativeTextTracks) {
  7458. this.emulateTextTracks();
  7459. }
  7460. this.preloadTextTracks = options.preloadTextTracks !== false;
  7461. this.autoRemoteTextTracks_ = new ALL.text.ListClass();
  7462. this.initTrackListeners();
  7463. // Turn on component tap events only if not using native controls
  7464. if (!options.nativeControlsForTouch) {
  7465. this.emitTapEvents();
  7466. }
  7467. if (this.constructor) {
  7468. this.name_ = this.constructor.name || 'Unknown Tech';
  7469. }
  7470. }
  7471. /**
  7472. * A special function to trigger source set in a way that will allow player
  7473. * to re-trigger if the player or tech are not ready yet.
  7474. *
  7475. * @fires Tech#sourceset
  7476. * @param {string} src The source string at the time of the source changing.
  7477. */
  7478. triggerSourceset(src) {
  7479. if (!this.isReady_) {
  7480. // on initial ready we have to trigger source set
  7481. // 1ms after ready so that player can watch for it.
  7482. this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
  7483. }
  7484. /**
  7485. * Fired when the source is set on the tech causing the media element
  7486. * to reload.
  7487. *
  7488. * @see {@link Player#event:sourceset}
  7489. * @event Tech#sourceset
  7490. * @type {Event}
  7491. */
  7492. this.trigger({
  7493. src,
  7494. type: 'sourceset'
  7495. });
  7496. }
  7497. /* Fallbacks for unsupported event types
  7498. ================================================================================ */
  7499. /**
  7500. * Polyfill the `progress` event for browsers that don't support it natively.
  7501. *
  7502. * @see {@link Tech#trackProgress}
  7503. */
  7504. manualProgressOn() {
  7505. this.on('durationchange', this.onDurationChange_);
  7506. this.manualProgress = true;
  7507. // Trigger progress watching when a source begins loading
  7508. this.one('ready', this.trackProgress_);
  7509. }
  7510. /**
  7511. * Turn off the polyfill for `progress` events that was created in
  7512. * {@link Tech#manualProgressOn}
  7513. */
  7514. manualProgressOff() {
  7515. this.manualProgress = false;
  7516. this.stopTrackingProgress();
  7517. this.off('durationchange', this.onDurationChange_);
  7518. }
  7519. /**
  7520. * This is used to trigger a `progress` event when the buffered percent changes. It
  7521. * sets an interval function that will be called every 500 milliseconds to check if the
  7522. * buffer end percent has changed.
  7523. *
  7524. * > This function is called by {@link Tech#manualProgressOn}
  7525. *
  7526. * @param {Event} event
  7527. * The `ready` event that caused this to run.
  7528. *
  7529. * @listens Tech#ready
  7530. * @fires Tech#progress
  7531. */
  7532. trackProgress(event) {
  7533. this.stopTrackingProgress();
  7534. this.progressInterval = this.setInterval(bind_(this, function () {
  7535. // Don't trigger unless buffered amount is greater than last time
  7536. const numBufferedPercent = this.bufferedPercent();
  7537. if (this.bufferedPercent_ !== numBufferedPercent) {
  7538. /**
  7539. * See {@link Player#progress}
  7540. *
  7541. * @event Tech#progress
  7542. * @type {Event}
  7543. */
  7544. this.trigger('progress');
  7545. }
  7546. this.bufferedPercent_ = numBufferedPercent;
  7547. if (numBufferedPercent === 1) {
  7548. this.stopTrackingProgress();
  7549. }
  7550. }), 500);
  7551. }
  7552. /**
  7553. * Update our internal duration on a `durationchange` event by calling
  7554. * {@link Tech#duration}.
  7555. *
  7556. * @param {Event} event
  7557. * The `durationchange` event that caused this to run.
  7558. *
  7559. * @listens Tech#durationchange
  7560. */
  7561. onDurationChange(event) {
  7562. this.duration_ = this.duration();
  7563. }
  7564. /**
  7565. * Get and create a `TimeRange` object for buffering.
  7566. *
  7567. * @return { import('../utils/time').TimeRange }
  7568. * The time range object that was created.
  7569. */
  7570. buffered() {
  7571. return createTimeRanges(0, 0);
  7572. }
  7573. /**
  7574. * Get the percentage of the current video that is currently buffered.
  7575. *
  7576. * @return {number}
  7577. * A number from 0 to 1 that represents the decimal percentage of the
  7578. * video that is buffered.
  7579. *
  7580. */
  7581. bufferedPercent() {
  7582. return bufferedPercent(this.buffered(), this.duration_);
  7583. }
  7584. /**
  7585. * Turn off the polyfill for `progress` events that was created in
  7586. * {@link Tech#manualProgressOn}
  7587. * Stop manually tracking progress events by clearing the interval that was set in
  7588. * {@link Tech#trackProgress}.
  7589. */
  7590. stopTrackingProgress() {
  7591. this.clearInterval(this.progressInterval);
  7592. }
  7593. /**
  7594. * Polyfill the `timeupdate` event for browsers that don't support it.
  7595. *
  7596. * @see {@link Tech#trackCurrentTime}
  7597. */
  7598. manualTimeUpdatesOn() {
  7599. this.manualTimeUpdates = true;
  7600. this.on('play', this.trackCurrentTime_);
  7601. this.on('pause', this.stopTrackingCurrentTime_);
  7602. }
  7603. /**
  7604. * Turn off the polyfill for `timeupdate` events that was created in
  7605. * {@link Tech#manualTimeUpdatesOn}
  7606. */
  7607. manualTimeUpdatesOff() {
  7608. this.manualTimeUpdates = false;
  7609. this.stopTrackingCurrentTime();
  7610. this.off('play', this.trackCurrentTime_);
  7611. this.off('pause', this.stopTrackingCurrentTime_);
  7612. }
  7613. /**
  7614. * Sets up an interval function to track current time and trigger `timeupdate` every
  7615. * 250 milliseconds.
  7616. *
  7617. * @listens Tech#play
  7618. * @triggers Tech#timeupdate
  7619. */
  7620. trackCurrentTime() {
  7621. if (this.currentTimeInterval) {
  7622. this.stopTrackingCurrentTime();
  7623. }
  7624. this.currentTimeInterval = this.setInterval(function () {
  7625. /**
  7626. * Triggered at an interval of 250ms to indicated that time is passing in the video.
  7627. *
  7628. * @event Tech#timeupdate
  7629. * @type {Event}
  7630. */
  7631. this.trigger({
  7632. type: 'timeupdate',
  7633. target: this,
  7634. manuallyTriggered: true
  7635. });
  7636. // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
  7637. }, 250);
  7638. }
  7639. /**
  7640. * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
  7641. * `timeupdate` event is no longer triggered.
  7642. *
  7643. * @listens {Tech#pause}
  7644. */
  7645. stopTrackingCurrentTime() {
  7646. this.clearInterval(this.currentTimeInterval);
  7647. // #1002 - if the video ends right before the next timeupdate would happen,
  7648. // the progress bar won't make it all the way to the end
  7649. this.trigger({
  7650. type: 'timeupdate',
  7651. target: this,
  7652. manuallyTriggered: true
  7653. });
  7654. }
  7655. /**
  7656. * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
  7657. * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
  7658. *
  7659. * @fires Component#dispose
  7660. */
  7661. dispose() {
  7662. // clear out all tracks because we can't reuse them between techs
  7663. this.clearTracks(NORMAL.names);
  7664. // Turn off any manual progress or timeupdate tracking
  7665. if (this.manualProgress) {
  7666. this.manualProgressOff();
  7667. }
  7668. if (this.manualTimeUpdates) {
  7669. this.manualTimeUpdatesOff();
  7670. }
  7671. super.dispose();
  7672. }
  7673. /**
  7674. * Clear out a single `TrackList` or an array of `TrackLists` given their names.
  7675. *
  7676. * > Note: Techs without source handlers should call this between sources for `video`
  7677. * & `audio` tracks. You don't want to use them between tracks!
  7678. *
  7679. * @param {string[]|string} types
  7680. * TrackList names to clear, valid names are `video`, `audio`, and
  7681. * `text`.
  7682. */
  7683. clearTracks(types) {
  7684. types = [].concat(types);
  7685. // clear out all tracks because we can't reuse them between techs
  7686. types.forEach(type => {
  7687. const list = this[`${type}Tracks`]() || [];
  7688. let i = list.length;
  7689. while (i--) {
  7690. const track = list[i];
  7691. if (type === 'text') {
  7692. this.removeRemoteTextTrack(track);
  7693. }
  7694. list.removeTrack(track);
  7695. }
  7696. });
  7697. }
  7698. /**
  7699. * Remove any TextTracks added via addRemoteTextTrack that are
  7700. * flagged for automatic garbage collection
  7701. */
  7702. cleanupAutoTextTracks() {
  7703. const list = this.autoRemoteTextTracks_ || [];
  7704. let i = list.length;
  7705. while (i--) {
  7706. const track = list[i];
  7707. this.removeRemoteTextTrack(track);
  7708. }
  7709. }
  7710. /**
  7711. * Reset the tech, which will removes all sources and reset the internal readyState.
  7712. *
  7713. * @abstract
  7714. */
  7715. reset() {}
  7716. /**
  7717. * Get the value of `crossOrigin` from the tech.
  7718. *
  7719. * @abstract
  7720. *
  7721. * @see {Html5#crossOrigin}
  7722. */
  7723. crossOrigin() {}
  7724. /**
  7725. * Set the value of `crossOrigin` on the tech.
  7726. *
  7727. * @abstract
  7728. *
  7729. * @param {string} crossOrigin the crossOrigin value
  7730. * @see {Html5#setCrossOrigin}
  7731. */
  7732. setCrossOrigin() {}
  7733. /**
  7734. * Get or set an error on the Tech.
  7735. *
  7736. * @param {MediaError} [err]
  7737. * Error to set on the Tech
  7738. *
  7739. * @return {MediaError|null}
  7740. * The current error object on the tech, or null if there isn't one.
  7741. */
  7742. error(err) {
  7743. if (err !== undefined) {
  7744. this.error_ = new MediaError(err);
  7745. this.trigger('error');
  7746. }
  7747. return this.error_;
  7748. }
  7749. /**
  7750. * Returns the `TimeRange`s that have been played through for the current source.
  7751. *
  7752. * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
  7753. * It only checks whether the source has played at all or not.
  7754. *
  7755. * @return { import('../utils/time').TimeRange }
  7756. * - A single time range if this video has played
  7757. * - An empty set of ranges if not.
  7758. */
  7759. played() {
  7760. if (this.hasStarted_) {
  7761. return createTimeRanges(0, 0);
  7762. }
  7763. return createTimeRanges();
  7764. }
  7765. /**
  7766. * Start playback
  7767. *
  7768. * @abstract
  7769. *
  7770. * @see {Html5#play}
  7771. */
  7772. play() {}
  7773. /**
  7774. * Set whether we are scrubbing or not
  7775. *
  7776. * @abstract
  7777. * @param {boolean} _isScrubbing
  7778. * - true for we are currently scrubbing
  7779. * - false for we are no longer scrubbing
  7780. *
  7781. * @see {Html5#setScrubbing}
  7782. */
  7783. setScrubbing(_isScrubbing) {}
  7784. /**
  7785. * Get whether we are scrubbing or not
  7786. *
  7787. * @abstract
  7788. *
  7789. * @see {Html5#scrubbing}
  7790. */
  7791. scrubbing() {}
  7792. /**
  7793. * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
  7794. * previously called.
  7795. *
  7796. * @param {number} _seconds
  7797. * Set the current time of the media to this.
  7798. * @fires Tech#timeupdate
  7799. */
  7800. setCurrentTime(_seconds) {
  7801. // improve the accuracy of manual timeupdates
  7802. if (this.manualTimeUpdates) {
  7803. /**
  7804. * A manual `timeupdate` event.
  7805. *
  7806. * @event Tech#timeupdate
  7807. * @type {Event}
  7808. */
  7809. this.trigger({
  7810. type: 'timeupdate',
  7811. target: this,
  7812. manuallyTriggered: true
  7813. });
  7814. }
  7815. }
  7816. /**
  7817. * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
  7818. * {@link TextTrackList} events.
  7819. *
  7820. * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
  7821. *
  7822. * @fires Tech#audiotrackchange
  7823. * @fires Tech#videotrackchange
  7824. * @fires Tech#texttrackchange
  7825. */
  7826. initTrackListeners() {
  7827. /**
  7828. * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
  7829. *
  7830. * @event Tech#audiotrackchange
  7831. * @type {Event}
  7832. */
  7833. /**
  7834. * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
  7835. *
  7836. * @event Tech#videotrackchange
  7837. * @type {Event}
  7838. */
  7839. /**
  7840. * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
  7841. *
  7842. * @event Tech#texttrackchange
  7843. * @type {Event}
  7844. */
  7845. NORMAL.names.forEach(name => {
  7846. const props = NORMAL[name];
  7847. const trackListChanges = () => {
  7848. this.trigger(`${name}trackchange`);
  7849. };
  7850. const tracks = this[props.getterName]();
  7851. tracks.addEventListener('removetrack', trackListChanges);
  7852. tracks.addEventListener('addtrack', trackListChanges);
  7853. this.on('dispose', () => {
  7854. tracks.removeEventListener('removetrack', trackListChanges);
  7855. tracks.removeEventListener('addtrack', trackListChanges);
  7856. });
  7857. });
  7858. }
  7859. /**
  7860. * Emulate TextTracks using vtt.js if necessary
  7861. *
  7862. * @fires Tech#vttjsloaded
  7863. * @fires Tech#vttjserror
  7864. */
  7865. addWebVttScript_() {
  7866. if (window__default["default"].WebVTT) {
  7867. return;
  7868. }
  7869. // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
  7870. // signals that the Tech is ready at which point Tech.el_ is part of the DOM
  7871. // before inserting the WebVTT script
  7872. if (document__default["default"].body.contains(this.el())) {
  7873. // load via require if available and vtt.js script location was not passed in
  7874. // as an option. novtt builds will turn the above require call into an empty object
  7875. // which will cause this if check to always fail.
  7876. if (!this.options_['vtt.js'] && isPlain(vtt__default["default"]) && Object.keys(vtt__default["default"]).length > 0) {
  7877. this.trigger('vttjsloaded');
  7878. return;
  7879. }
  7880. // load vtt.js via the script location option or the cdn of no location was
  7881. // passed in
  7882. const script = document__default["default"].createElement('script');
  7883. script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
  7884. script.onload = () => {
  7885. /**
  7886. * Fired when vtt.js is loaded.
  7887. *
  7888. * @event Tech#vttjsloaded
  7889. * @type {Event}
  7890. */
  7891. this.trigger('vttjsloaded');
  7892. };
  7893. script.onerror = () => {
  7894. /**
  7895. * Fired when vtt.js was not loaded due to an error
  7896. *
  7897. * @event Tech#vttjsloaded
  7898. * @type {Event}
  7899. */
  7900. this.trigger('vttjserror');
  7901. };
  7902. this.on('dispose', () => {
  7903. script.onload = null;
  7904. script.onerror = null;
  7905. });
  7906. // but have not loaded yet and we set it to true before the inject so that
  7907. // we don't overwrite the injected window.WebVTT if it loads right away
  7908. window__default["default"].WebVTT = true;
  7909. this.el().parentNode.appendChild(script);
  7910. } else {
  7911. this.ready(this.addWebVttScript_);
  7912. }
  7913. }
  7914. /**
  7915. * Emulate texttracks
  7916. *
  7917. */
  7918. emulateTextTracks() {
  7919. const tracks = this.textTracks();
  7920. const remoteTracks = this.remoteTextTracks();
  7921. const handleAddTrack = e => tracks.addTrack(e.track);
  7922. const handleRemoveTrack = e => tracks.removeTrack(e.track);
  7923. remoteTracks.on('addtrack', handleAddTrack);
  7924. remoteTracks.on('removetrack', handleRemoveTrack);
  7925. this.addWebVttScript_();
  7926. const updateDisplay = () => this.trigger('texttrackchange');
  7927. const textTracksChanges = () => {
  7928. updateDisplay();
  7929. for (let i = 0; i < tracks.length; i++) {
  7930. const track = tracks[i];
  7931. track.removeEventListener('cuechange', updateDisplay);
  7932. if (track.mode === 'showing') {
  7933. track.addEventListener('cuechange', updateDisplay);
  7934. }
  7935. }
  7936. };
  7937. textTracksChanges();
  7938. tracks.addEventListener('change', textTracksChanges);
  7939. tracks.addEventListener('addtrack', textTracksChanges);
  7940. tracks.addEventListener('removetrack', textTracksChanges);
  7941. this.on('dispose', function () {
  7942. remoteTracks.off('addtrack', handleAddTrack);
  7943. remoteTracks.off('removetrack', handleRemoveTrack);
  7944. tracks.removeEventListener('change', textTracksChanges);
  7945. tracks.removeEventListener('addtrack', textTracksChanges);
  7946. tracks.removeEventListener('removetrack', textTracksChanges);
  7947. for (let i = 0; i < tracks.length; i++) {
  7948. const track = tracks[i];
  7949. track.removeEventListener('cuechange', updateDisplay);
  7950. }
  7951. });
  7952. }
  7953. /**
  7954. * Create and returns a remote {@link TextTrack} object.
  7955. *
  7956. * @param {string} kind
  7957. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
  7958. *
  7959. * @param {string} [label]
  7960. * Label to identify the text track
  7961. *
  7962. * @param {string} [language]
  7963. * Two letter language abbreviation
  7964. *
  7965. * @return {TextTrack}
  7966. * The TextTrack that gets created.
  7967. */
  7968. addTextTrack(kind, label, language) {
  7969. if (!kind) {
  7970. throw new Error('TextTrack kind is required but was not provided');
  7971. }
  7972. return createTrackHelper(this, kind, label, language);
  7973. }
  7974. /**
  7975. * Create an emulated TextTrack for use by addRemoteTextTrack
  7976. *
  7977. * This is intended to be overridden by classes that inherit from
  7978. * Tech in order to create native or custom TextTracks.
  7979. *
  7980. * @param {Object} options
  7981. * The object should contain the options to initialize the TextTrack with.
  7982. *
  7983. * @param {string} [options.kind]
  7984. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
  7985. *
  7986. * @param {string} [options.label].
  7987. * Label to identify the text track
  7988. *
  7989. * @param {string} [options.language]
  7990. * Two letter language abbreviation.
  7991. *
  7992. * @return {HTMLTrackElement}
  7993. * The track element that gets created.
  7994. */
  7995. createRemoteTextTrack(options) {
  7996. const track = merge(options, {
  7997. tech: this
  7998. });
  7999. return new REMOTE.remoteTextEl.TrackClass(track);
  8000. }
  8001. /**
  8002. * Creates a remote text track object and returns an html track element.
  8003. *
  8004. * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
  8005. *
  8006. * @param {Object} options
  8007. * See {@link Tech#createRemoteTextTrack} for more detailed properties.
  8008. *
  8009. * @param {boolean} [manualCleanup=false]
  8010. * - When false: the TextTrack will be automatically removed from the video
  8011. * element whenever the source changes
  8012. * - When True: The TextTrack will have to be cleaned up manually
  8013. *
  8014. * @return {HTMLTrackElement}
  8015. * An Html Track Element.
  8016. *
  8017. */
  8018. addRemoteTextTrack(options = {}, manualCleanup) {
  8019. const htmlTrackElement = this.createRemoteTextTrack(options);
  8020. if (typeof manualCleanup !== 'boolean') {
  8021. manualCleanup = false;
  8022. }
  8023. // store HTMLTrackElement and TextTrack to remote list
  8024. this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
  8025. this.remoteTextTracks().addTrack(htmlTrackElement.track);
  8026. if (manualCleanup === false) {
  8027. // create the TextTrackList if it doesn't exist
  8028. this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
  8029. }
  8030. return htmlTrackElement;
  8031. }
  8032. /**
  8033. * Remove a remote text track from the remote `TextTrackList`.
  8034. *
  8035. * @param {TextTrack} track
  8036. * `TextTrack` to remove from the `TextTrackList`
  8037. */
  8038. removeRemoteTextTrack(track) {
  8039. const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
  8040. // remove HTMLTrackElement and TextTrack from remote list
  8041. this.remoteTextTrackEls().removeTrackElement_(trackElement);
  8042. this.remoteTextTracks().removeTrack(track);
  8043. this.autoRemoteTextTracks_.removeTrack(track);
  8044. }
  8045. /**
  8046. * Gets available media playback quality metrics as specified by the W3C's Media
  8047. * Playback Quality API.
  8048. *
  8049. * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
  8050. *
  8051. * @return {Object}
  8052. * An object with supported media playback quality metrics
  8053. *
  8054. * @abstract
  8055. */
  8056. getVideoPlaybackQuality() {
  8057. return {};
  8058. }
  8059. /**
  8060. * Attempt to create a floating video window always on top of other windows
  8061. * so that users may continue consuming media while they interact with other
  8062. * content sites, or applications on their device.
  8063. *
  8064. * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
  8065. *
  8066. * @return {Promise|undefined}
  8067. * A promise with a Picture-in-Picture window if the browser supports
  8068. * Promises (or one was passed in as an option). It returns undefined
  8069. * otherwise.
  8070. *
  8071. * @abstract
  8072. */
  8073. requestPictureInPicture() {
  8074. return Promise.reject();
  8075. }
  8076. /**
  8077. * A method to check for the value of the 'disablePictureInPicture' <video> property.
  8078. * Defaults to true, as it should be considered disabled if the tech does not support pip
  8079. *
  8080. * @abstract
  8081. */
  8082. disablePictureInPicture() {
  8083. return true;
  8084. }
  8085. /**
  8086. * A method to set or unset the 'disablePictureInPicture' <video> property.
  8087. *
  8088. * @abstract
  8089. */
  8090. setDisablePictureInPicture() {}
  8091. /**
  8092. * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
  8093. *
  8094. * @param {function} cb
  8095. * @return {number} request id
  8096. */
  8097. requestVideoFrameCallback(cb) {
  8098. const id = newGUID();
  8099. if (!this.isReady_ || this.paused()) {
  8100. this.queuedHanders_.add(id);
  8101. this.one('playing', () => {
  8102. if (this.queuedHanders_.has(id)) {
  8103. this.queuedHanders_.delete(id);
  8104. cb();
  8105. }
  8106. });
  8107. } else {
  8108. this.requestNamedAnimationFrame(id, cb);
  8109. }
  8110. return id;
  8111. }
  8112. /**
  8113. * A fallback implementation of cancelVideoFrameCallback
  8114. *
  8115. * @param {number} id id of callback to be cancelled
  8116. */
  8117. cancelVideoFrameCallback(id) {
  8118. if (this.queuedHanders_.has(id)) {
  8119. this.queuedHanders_.delete(id);
  8120. } else {
  8121. this.cancelNamedAnimationFrame(id);
  8122. }
  8123. }
  8124. /**
  8125. * A method to set a poster from a `Tech`.
  8126. *
  8127. * @abstract
  8128. */
  8129. setPoster() {}
  8130. /**
  8131. * A method to check for the presence of the 'playsinline' <video> attribute.
  8132. *
  8133. * @abstract
  8134. */
  8135. playsinline() {}
  8136. /**
  8137. * A method to set or unset the 'playsinline' <video> attribute.
  8138. *
  8139. * @abstract
  8140. */
  8141. setPlaysinline() {}
  8142. /**
  8143. * Attempt to force override of native audio tracks.
  8144. *
  8145. * @param {boolean} override - If set to true native audio will be overridden,
  8146. * otherwise native audio will potentially be used.
  8147. *
  8148. * @abstract
  8149. */
  8150. overrideNativeAudioTracks(override) {}
  8151. /**
  8152. * Attempt to force override of native video tracks.
  8153. *
  8154. * @param {boolean} override - If set to true native video will be overridden,
  8155. * otherwise native video will potentially be used.
  8156. *
  8157. * @abstract
  8158. */
  8159. overrideNativeVideoTracks(override) {}
  8160. /**
  8161. * Check if the tech can support the given mime-type.
  8162. *
  8163. * The base tech does not support any type, but source handlers might
  8164. * overwrite this.
  8165. *
  8166. * @param {string} _type
  8167. * The mimetype to check for support
  8168. *
  8169. * @return {string}
  8170. * 'probably', 'maybe', or empty string
  8171. *
  8172. * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
  8173. *
  8174. * @abstract
  8175. */
  8176. canPlayType(_type) {
  8177. return '';
  8178. }
  8179. /**
  8180. * Check if the type is supported by this tech.
  8181. *
  8182. * The base tech does not support any type, but source handlers might
  8183. * overwrite this.
  8184. *
  8185. * @param {string} _type
  8186. * The media type to check
  8187. * @return {string} Returns the native video element's response
  8188. */
  8189. static canPlayType(_type) {
  8190. return '';
  8191. }
  8192. /**
  8193. * Check if the tech can support the given source
  8194. *
  8195. * @param {Object} srcObj
  8196. * The source object
  8197. * @param {Object} options
  8198. * The options passed to the tech
  8199. * @return {string} 'probably', 'maybe', or '' (empty string)
  8200. */
  8201. static canPlaySource(srcObj, options) {
  8202. return Tech.canPlayType(srcObj.type);
  8203. }
  8204. /*
  8205. * Return whether the argument is a Tech or not.
  8206. * Can be passed either a Class like `Html5` or a instance like `player.tech_`
  8207. *
  8208. * @param {Object} component
  8209. * The item to check
  8210. *
  8211. * @return {boolean}
  8212. * Whether it is a tech or not
  8213. * - True if it is a tech
  8214. * - False if it is not
  8215. */
  8216. static isTech(component) {
  8217. return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
  8218. }
  8219. /**
  8220. * Registers a `Tech` into a shared list for videojs.
  8221. *
  8222. * @param {string} name
  8223. * Name of the `Tech` to register.
  8224. *
  8225. * @param {Object} tech
  8226. * The `Tech` class to register.
  8227. */
  8228. static registerTech(name, tech) {
  8229. if (!Tech.techs_) {
  8230. Tech.techs_ = {};
  8231. }
  8232. if (!Tech.isTech(tech)) {
  8233. throw new Error(`Tech ${name} must be a Tech`);
  8234. }
  8235. if (!Tech.canPlayType) {
  8236. throw new Error('Techs must have a static canPlayType method on them');
  8237. }
  8238. if (!Tech.canPlaySource) {
  8239. throw new Error('Techs must have a static canPlaySource method on them');
  8240. }
  8241. name = toTitleCase(name);
  8242. Tech.techs_[name] = tech;
  8243. Tech.techs_[toLowerCase(name)] = tech;
  8244. if (name !== 'Tech') {
  8245. // camel case the techName for use in techOrder
  8246. Tech.defaultTechOrder_.push(name);
  8247. }
  8248. return tech;
  8249. }
  8250. /**
  8251. * Get a `Tech` from the shared list by name.
  8252. *
  8253. * @param {string} name
  8254. * `camelCase` or `TitleCase` name of the Tech to get
  8255. *
  8256. * @return {Tech|undefined}
  8257. * The `Tech` or undefined if there was no tech with the name requested.
  8258. */
  8259. static getTech(name) {
  8260. if (!name) {
  8261. return;
  8262. }
  8263. if (Tech.techs_ && Tech.techs_[name]) {
  8264. return Tech.techs_[name];
  8265. }
  8266. name = toTitleCase(name);
  8267. if (window__default["default"] && window__default["default"].videojs && window__default["default"].videojs[name]) {
  8268. log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
  8269. return window__default["default"].videojs[name];
  8270. }
  8271. }
  8272. }
  8273. /**
  8274. * Get the {@link VideoTrackList}
  8275. *
  8276. * @returns {VideoTrackList}
  8277. * @method Tech.prototype.videoTracks
  8278. */
  8279. /**
  8280. * Get the {@link AudioTrackList}
  8281. *
  8282. * @returns {AudioTrackList}
  8283. * @method Tech.prototype.audioTracks
  8284. */
  8285. /**
  8286. * Get the {@link TextTrackList}
  8287. *
  8288. * @returns {TextTrackList}
  8289. * @method Tech.prototype.textTracks
  8290. */
  8291. /**
  8292. * Get the remote element {@link TextTrackList}
  8293. *
  8294. * @returns {TextTrackList}
  8295. * @method Tech.prototype.remoteTextTracks
  8296. */
  8297. /**
  8298. * Get the remote element {@link HtmlTrackElementList}
  8299. *
  8300. * @returns {HtmlTrackElementList}
  8301. * @method Tech.prototype.remoteTextTrackEls
  8302. */
  8303. ALL.names.forEach(function (name) {
  8304. const props = ALL[name];
  8305. Tech.prototype[props.getterName] = function () {
  8306. this[props.privateName] = this[props.privateName] || new props.ListClass();
  8307. return this[props.privateName];
  8308. };
  8309. });
  8310. /**
  8311. * List of associated text tracks
  8312. *
  8313. * @type {TextTrackList}
  8314. * @private
  8315. * @property Tech#textTracks_
  8316. */
  8317. /**
  8318. * List of associated audio tracks.
  8319. *
  8320. * @type {AudioTrackList}
  8321. * @private
  8322. * @property Tech#audioTracks_
  8323. */
  8324. /**
  8325. * List of associated video tracks.
  8326. *
  8327. * @type {VideoTrackList}
  8328. * @private
  8329. * @property Tech#videoTracks_
  8330. */
  8331. /**
  8332. * Boolean indicating whether the `Tech` supports volume control.
  8333. *
  8334. * @type {boolean}
  8335. * @default
  8336. */
  8337. Tech.prototype.featuresVolumeControl = true;
  8338. /**
  8339. * Boolean indicating whether the `Tech` supports muting volume.
  8340. *
  8341. * @type {boolean}
  8342. * @default
  8343. */
  8344. Tech.prototype.featuresMuteControl = true;
  8345. /**
  8346. * Boolean indicating whether the `Tech` supports fullscreen resize control.
  8347. * Resizing plugins using request fullscreen reloads the plugin
  8348. *
  8349. * @type {boolean}
  8350. * @default
  8351. */
  8352. Tech.prototype.featuresFullscreenResize = false;
  8353. /**
  8354. * Boolean indicating whether the `Tech` supports changing the speed at which the video
  8355. * plays. Examples:
  8356. * - Set player to play 2x (twice) as fast
  8357. * - Set player to play 0.5x (half) as fast
  8358. *
  8359. * @type {boolean}
  8360. * @default
  8361. */
  8362. Tech.prototype.featuresPlaybackRate = false;
  8363. /**
  8364. * Boolean indicating whether the `Tech` supports the `progress` event.
  8365. * This will be used to determine if {@link Tech#manualProgressOn} should be called.
  8366. *
  8367. * @type {boolean}
  8368. * @default
  8369. */
  8370. Tech.prototype.featuresProgressEvents = false;
  8371. /**
  8372. * Boolean indicating whether the `Tech` supports the `sourceset` event.
  8373. *
  8374. * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
  8375. * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
  8376. * a new source.
  8377. *
  8378. * @type {boolean}
  8379. * @default
  8380. */
  8381. Tech.prototype.featuresSourceset = false;
  8382. /**
  8383. * Boolean indicating whether the `Tech` supports the `timeupdate` event.
  8384. * This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
  8385. *
  8386. * @type {boolean}
  8387. * @default
  8388. */
  8389. Tech.prototype.featuresTimeupdateEvents = false;
  8390. /**
  8391. * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
  8392. * This will help us integrate with native `TextTrack`s if the browser supports them.
  8393. *
  8394. * @type {boolean}
  8395. * @default
  8396. */
  8397. Tech.prototype.featuresNativeTextTracks = false;
  8398. /**
  8399. * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
  8400. *
  8401. * @type {boolean}
  8402. * @default
  8403. */
  8404. Tech.prototype.featuresVideoFrameCallback = false;
  8405. /**
  8406. * A functional mixin for techs that want to use the Source Handler pattern.
  8407. * Source handlers are scripts for handling specific formats.
  8408. * The source handler pattern is used for adaptive formats (HLS, DASH) that
  8409. * manually load video data and feed it into a Source Buffer (Media Source Extensions)
  8410. * Example: `Tech.withSourceHandlers.call(MyTech);`
  8411. *
  8412. * @param {Tech} _Tech
  8413. * The tech to add source handler functions to.
  8414. *
  8415. * @mixes Tech~SourceHandlerAdditions
  8416. */
  8417. Tech.withSourceHandlers = function (_Tech) {
  8418. /**
  8419. * Register a source handler
  8420. *
  8421. * @param {Function} handler
  8422. * The source handler class
  8423. *
  8424. * @param {number} [index]
  8425. * Register it at the following index
  8426. */
  8427. _Tech.registerSourceHandler = function (handler, index) {
  8428. let handlers = _Tech.sourceHandlers;
  8429. if (!handlers) {
  8430. handlers = _Tech.sourceHandlers = [];
  8431. }
  8432. if (index === undefined) {
  8433. // add to the end of the list
  8434. index = handlers.length;
  8435. }
  8436. handlers.splice(index, 0, handler);
  8437. };
  8438. /**
  8439. * Check if the tech can support the given type. Also checks the
  8440. * Techs sourceHandlers.
  8441. *
  8442. * @param {string} type
  8443. * The mimetype to check.
  8444. *
  8445. * @return {string}
  8446. * 'probably', 'maybe', or '' (empty string)
  8447. */
  8448. _Tech.canPlayType = function (type) {
  8449. const handlers = _Tech.sourceHandlers || [];
  8450. let can;
  8451. for (let i = 0; i < handlers.length; i++) {
  8452. can = handlers[i].canPlayType(type);
  8453. if (can) {
  8454. return can;
  8455. }
  8456. }
  8457. return '';
  8458. };
  8459. /**
  8460. * Returns the first source handler that supports the source.
  8461. *
  8462. * TODO: Answer question: should 'probably' be prioritized over 'maybe'
  8463. *
  8464. * @param {SourceObject} source
  8465. * The source object
  8466. *
  8467. * @param {Object} options
  8468. * The options passed to the tech
  8469. *
  8470. * @return {SourceHandler|null}
  8471. * The first source handler that supports the source or null if
  8472. * no SourceHandler supports the source
  8473. */
  8474. _Tech.selectSourceHandler = function (source, options) {
  8475. const handlers = _Tech.sourceHandlers || [];
  8476. let can;
  8477. for (let i = 0; i < handlers.length; i++) {
  8478. can = handlers[i].canHandleSource(source, options);
  8479. if (can) {
  8480. return handlers[i];
  8481. }
  8482. }
  8483. return null;
  8484. };
  8485. /**
  8486. * Check if the tech can support the given source.
  8487. *
  8488. * @param {SourceObject} srcObj
  8489. * The source object
  8490. *
  8491. * @param {Object} options
  8492. * The options passed to the tech
  8493. *
  8494. * @return {string}
  8495. * 'probably', 'maybe', or '' (empty string)
  8496. */
  8497. _Tech.canPlaySource = function (srcObj, options) {
  8498. const sh = _Tech.selectSourceHandler(srcObj, options);
  8499. if (sh) {
  8500. return sh.canHandleSource(srcObj, options);
  8501. }
  8502. return '';
  8503. };
  8504. /**
  8505. * When using a source handler, prefer its implementation of
  8506. * any function normally provided by the tech.
  8507. */
  8508. const deferrable = ['seekable', 'seeking', 'duration'];
  8509. /**
  8510. * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
  8511. * function if it exists, with a fallback to the Techs seekable function.
  8512. *
  8513. * @method _Tech.seekable
  8514. */
  8515. /**
  8516. * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
  8517. * function if it exists, otherwise it will fallback to the techs duration function.
  8518. *
  8519. * @method _Tech.duration
  8520. */
  8521. deferrable.forEach(function (fnName) {
  8522. const originalFn = this[fnName];
  8523. if (typeof originalFn !== 'function') {
  8524. return;
  8525. }
  8526. this[fnName] = function () {
  8527. if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
  8528. return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
  8529. }
  8530. return originalFn.apply(this, arguments);
  8531. };
  8532. }, _Tech.prototype);
  8533. /**
  8534. * Create a function for setting the source using a source object
  8535. * and source handlers.
  8536. * Should never be called unless a source handler was found.
  8537. *
  8538. * @param {SourceObject} source
  8539. * A source object with src and type keys
  8540. */
  8541. _Tech.prototype.setSource = function (source) {
  8542. let sh = _Tech.selectSourceHandler(source, this.options_);
  8543. if (!sh) {
  8544. // Fall back to a native source handler when unsupported sources are
  8545. // deliberately set
  8546. if (_Tech.nativeSourceHandler) {
  8547. sh = _Tech.nativeSourceHandler;
  8548. } else {
  8549. log.error('No source handler found for the current source.');
  8550. }
  8551. }
  8552. // Dispose any existing source handler
  8553. this.disposeSourceHandler();
  8554. this.off('dispose', this.disposeSourceHandler_);
  8555. if (sh !== _Tech.nativeSourceHandler) {
  8556. this.currentSource_ = source;
  8557. }
  8558. this.sourceHandler_ = sh.handleSource(source, this, this.options_);
  8559. this.one('dispose', this.disposeSourceHandler_);
  8560. };
  8561. /**
  8562. * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
  8563. *
  8564. * @listens Tech#dispose
  8565. */
  8566. _Tech.prototype.disposeSourceHandler = function () {
  8567. // if we have a source and get another one
  8568. // then we are loading something new
  8569. // than clear all of our current tracks
  8570. if (this.currentSource_) {
  8571. this.clearTracks(['audio', 'video']);
  8572. this.currentSource_ = null;
  8573. }
  8574. // always clean up auto-text tracks
  8575. this.cleanupAutoTextTracks();
  8576. if (this.sourceHandler_) {
  8577. if (this.sourceHandler_.dispose) {
  8578. this.sourceHandler_.dispose();
  8579. }
  8580. this.sourceHandler_ = null;
  8581. }
  8582. };
  8583. };
  8584. // The base Tech class needs to be registered as a Component. It is the only
  8585. // Tech that can be registered as a Component.
  8586. Component.registerComponent('Tech', Tech);
  8587. Tech.registerTech('Tech', Tech);
  8588. /**
  8589. * A list of techs that should be added to techOrder on Players
  8590. *
  8591. * @private
  8592. */
  8593. Tech.defaultTechOrder_ = [];
  8594. /**
  8595. * @file middleware.js
  8596. * @module middleware
  8597. */
  8598. const middlewares = {};
  8599. const middlewareInstances = {};
  8600. const TERMINATOR = {};
  8601. /**
  8602. * A middleware object is a plain JavaScript object that has methods that
  8603. * match the {@link Tech} methods found in the lists of allowed
  8604. * {@link module:middleware.allowedGetters|getters},
  8605. * {@link module:middleware.allowedSetters|setters}, and
  8606. * {@link module:middleware.allowedMediators|mediators}.
  8607. *
  8608. * @typedef {Object} MiddlewareObject
  8609. */
  8610. /**
  8611. * A middleware factory function that should return a
  8612. * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
  8613. *
  8614. * This factory will be called for each player when needed, with the player
  8615. * passed in as an argument.
  8616. *
  8617. * @callback MiddlewareFactory
  8618. * @param { import('../player').default } player
  8619. * A Video.js player.
  8620. */
  8621. /**
  8622. * Define a middleware that the player should use by way of a factory function
  8623. * that returns a middleware object.
  8624. *
  8625. * @param {string} type
  8626. * The MIME type to match or `"*"` for all MIME types.
  8627. *
  8628. * @param {MiddlewareFactory} middleware
  8629. * A middleware factory function that will be executed for
  8630. * matching types.
  8631. */
  8632. function use(type, middleware) {
  8633. middlewares[type] = middlewares[type] || [];
  8634. middlewares[type].push(middleware);
  8635. }
  8636. /**
  8637. * Asynchronously sets a source using middleware by recursing through any
  8638. * matching middlewares and calling `setSource` on each, passing along the
  8639. * previous returned value each time.
  8640. *
  8641. * @param { import('../player').default } player
  8642. * A {@link Player} instance.
  8643. *
  8644. * @param {Tech~SourceObject} src
  8645. * A source object.
  8646. *
  8647. * @param {Function}
  8648. * The next middleware to run.
  8649. */
  8650. function setSource(player, src, next) {
  8651. player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
  8652. }
  8653. /**
  8654. * When the tech is set, passes the tech to each middleware's `setTech` method.
  8655. *
  8656. * @param {Object[]} middleware
  8657. * An array of middleware instances.
  8658. *
  8659. * @param { import('../tech/tech').default } tech
  8660. * A Video.js tech.
  8661. */
  8662. function setTech(middleware, tech) {
  8663. middleware.forEach(mw => mw.setTech && mw.setTech(tech));
  8664. }
  8665. /**
  8666. * Calls a getter on the tech first, through each middleware
  8667. * from right to left to the player.
  8668. *
  8669. * @param {Object[]} middleware
  8670. * An array of middleware instances.
  8671. *
  8672. * @param { import('../tech/tech').default } tech
  8673. * The current tech.
  8674. *
  8675. * @param {string} method
  8676. * A method name.
  8677. *
  8678. * @return {*}
  8679. * The final value from the tech after middleware has intercepted it.
  8680. */
  8681. function get(middleware, tech, method) {
  8682. return middleware.reduceRight(middlewareIterator(method), tech[method]());
  8683. }
  8684. /**
  8685. * Takes the argument given to the player and calls the setter method on each
  8686. * middleware from left to right to the tech.
  8687. *
  8688. * @param {Object[]} middleware
  8689. * An array of middleware instances.
  8690. *
  8691. * @param { import('../tech/tech').default } tech
  8692. * The current tech.
  8693. *
  8694. * @param {string} method
  8695. * A method name.
  8696. *
  8697. * @param {*} arg
  8698. * The value to set on the tech.
  8699. *
  8700. * @return {*}
  8701. * The return value of the `method` of the `tech`.
  8702. */
  8703. function set(middleware, tech, method, arg) {
  8704. return tech[method](middleware.reduce(middlewareIterator(method), arg));
  8705. }
  8706. /**
  8707. * Takes the argument given to the player and calls the `call` version of the
  8708. * method on each middleware from left to right.
  8709. *
  8710. * Then, call the passed in method on the tech and return the result unchanged
  8711. * back to the player, through middleware, this time from right to left.
  8712. *
  8713. * @param {Object[]} middleware
  8714. * An array of middleware instances.
  8715. *
  8716. * @param { import('../tech/tech').default } tech
  8717. * The current tech.
  8718. *
  8719. * @param {string} method
  8720. * A method name.
  8721. *
  8722. * @param {*} arg
  8723. * The value to set on the tech.
  8724. *
  8725. * @return {*}
  8726. * The return value of the `method` of the `tech`, regardless of the
  8727. * return values of middlewares.
  8728. */
  8729. function mediate(middleware, tech, method, arg = null) {
  8730. const callMethod = 'call' + toTitleCase(method);
  8731. const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
  8732. const terminated = middlewareValue === TERMINATOR;
  8733. // deprecated. The `null` return value should instead return TERMINATOR to
  8734. // prevent confusion if a techs method actually returns null.
  8735. const returnValue = terminated ? null : tech[method](middlewareValue);
  8736. executeRight(middleware, method, returnValue, terminated);
  8737. return returnValue;
  8738. }
  8739. /**
  8740. * Enumeration of allowed getters where the keys are method names.
  8741. *
  8742. * @type {Object}
  8743. */
  8744. const allowedGetters = {
  8745. buffered: 1,
  8746. currentTime: 1,
  8747. duration: 1,
  8748. muted: 1,
  8749. played: 1,
  8750. paused: 1,
  8751. seekable: 1,
  8752. volume: 1,
  8753. ended: 1
  8754. };
  8755. /**
  8756. * Enumeration of allowed setters where the keys are method names.
  8757. *
  8758. * @type {Object}
  8759. */
  8760. const allowedSetters = {
  8761. setCurrentTime: 1,
  8762. setMuted: 1,
  8763. setVolume: 1
  8764. };
  8765. /**
  8766. * Enumeration of allowed mediators where the keys are method names.
  8767. *
  8768. * @type {Object}
  8769. */
  8770. const allowedMediators = {
  8771. play: 1,
  8772. pause: 1
  8773. };
  8774. function middlewareIterator(method) {
  8775. return (value, mw) => {
  8776. // if the previous middleware terminated, pass along the termination
  8777. if (value === TERMINATOR) {
  8778. return TERMINATOR;
  8779. }
  8780. if (mw[method]) {
  8781. return mw[method](value);
  8782. }
  8783. return value;
  8784. };
  8785. }
  8786. function executeRight(mws, method, value, terminated) {
  8787. for (let i = mws.length - 1; i >= 0; i--) {
  8788. const mw = mws[i];
  8789. if (mw[method]) {
  8790. mw[method](terminated, value);
  8791. }
  8792. }
  8793. }
  8794. /**
  8795. * Clear the middleware cache for a player.
  8796. *
  8797. * @param { import('../player').default } player
  8798. * A {@link Player} instance.
  8799. */
  8800. function clearCacheForPlayer(player) {
  8801. middlewareInstances[player.id()] = null;
  8802. }
  8803. /**
  8804. * {
  8805. * [playerId]: [[mwFactory, mwInstance], ...]
  8806. * }
  8807. *
  8808. * @private
  8809. */
  8810. function getOrCreateFactory(player, mwFactory) {
  8811. const mws = middlewareInstances[player.id()];
  8812. let mw = null;
  8813. if (mws === undefined || mws === null) {
  8814. mw = mwFactory(player);
  8815. middlewareInstances[player.id()] = [[mwFactory, mw]];
  8816. return mw;
  8817. }
  8818. for (let i = 0; i < mws.length; i++) {
  8819. const [mwf, mwi] = mws[i];
  8820. if (mwf !== mwFactory) {
  8821. continue;
  8822. }
  8823. mw = mwi;
  8824. }
  8825. if (mw === null) {
  8826. mw = mwFactory(player);
  8827. mws.push([mwFactory, mw]);
  8828. }
  8829. return mw;
  8830. }
  8831. function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
  8832. const [mwFactory, ...mwrest] = middleware;
  8833. // if mwFactory is a string, then we're at a fork in the road
  8834. if (typeof mwFactory === 'string') {
  8835. setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
  8836. // if we have an mwFactory, call it with the player to get the mw,
  8837. // then call the mw's setSource method
  8838. } else if (mwFactory) {
  8839. const mw = getOrCreateFactory(player, mwFactory);
  8840. // if setSource isn't present, implicitly select this middleware
  8841. if (!mw.setSource) {
  8842. acc.push(mw);
  8843. return setSourceHelper(src, mwrest, next, player, acc, lastRun);
  8844. }
  8845. mw.setSource(Object.assign({}, src), function (err, _src) {
  8846. // something happened, try the next middleware on the current level
  8847. // make sure to use the old src
  8848. if (err) {
  8849. return setSourceHelper(src, mwrest, next, player, acc, lastRun);
  8850. }
  8851. // we've succeeded, now we need to go deeper
  8852. acc.push(mw);
  8853. // if it's the same type, continue down the current chain
  8854. // otherwise, we want to go down the new chain
  8855. setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
  8856. });
  8857. } else if (mwrest.length) {
  8858. setSourceHelper(src, mwrest, next, player, acc, lastRun);
  8859. } else if (lastRun) {
  8860. next(src, acc);
  8861. } else {
  8862. setSourceHelper(src, middlewares['*'], next, player, acc, true);
  8863. }
  8864. }
  8865. /**
  8866. * Mimetypes
  8867. *
  8868. * @see https://www.iana.org/assignments/media-types/media-types.xhtml
  8869. * @typedef Mimetypes~Kind
  8870. * @enum
  8871. */
  8872. const MimetypesKind = {
  8873. opus: 'video/ogg',
  8874. ogv: 'video/ogg',
  8875. mp4: 'video/mp4',
  8876. mov: 'video/mp4',
  8877. m4v: 'video/mp4',
  8878. mkv: 'video/x-matroska',
  8879. m4a: 'audio/mp4',
  8880. mp3: 'audio/mpeg',
  8881. aac: 'audio/aac',
  8882. caf: 'audio/x-caf',
  8883. flac: 'audio/flac',
  8884. oga: 'audio/ogg',
  8885. wav: 'audio/wav',
  8886. m3u8: 'application/x-mpegURL',
  8887. mpd: 'application/dash+xml',
  8888. jpg: 'image/jpeg',
  8889. jpeg: 'image/jpeg',
  8890. gif: 'image/gif',
  8891. png: 'image/png',
  8892. svg: 'image/svg+xml',
  8893. webp: 'image/webp'
  8894. };
  8895. /**
  8896. * Get the mimetype of a given src url if possible
  8897. *
  8898. * @param {string} src
  8899. * The url to the src
  8900. *
  8901. * @return {string}
  8902. * return the mimetype if it was known or empty string otherwise
  8903. */
  8904. const getMimetype = function (src = '') {
  8905. const ext = getFileExtension(src);
  8906. const mimetype = MimetypesKind[ext.toLowerCase()];
  8907. return mimetype || '';
  8908. };
  8909. /**
  8910. * Find the mime type of a given source string if possible. Uses the player
  8911. * source cache.
  8912. *
  8913. * @param { import('../player').default } player
  8914. * The player object
  8915. *
  8916. * @param {string} src
  8917. * The source string
  8918. *
  8919. * @return {string}
  8920. * The type that was found
  8921. */
  8922. const findMimetype = (player, src) => {
  8923. if (!src) {
  8924. return '';
  8925. }
  8926. // 1. check for the type in the `source` cache
  8927. if (player.cache_.source.src === src && player.cache_.source.type) {
  8928. return player.cache_.source.type;
  8929. }
  8930. // 2. see if we have this source in our `currentSources` cache
  8931. const matchingSources = player.cache_.sources.filter(s => s.src === src);
  8932. if (matchingSources.length) {
  8933. return matchingSources[0].type;
  8934. }
  8935. // 3. look for the src url in source elements and use the type there
  8936. const sources = player.$$('source');
  8937. for (let i = 0; i < sources.length; i++) {
  8938. const s = sources[i];
  8939. if (s.type && s.src && s.src === src) {
  8940. return s.type;
  8941. }
  8942. }
  8943. // 4. finally fallback to our list of mime types based on src url extension
  8944. return getMimetype(src);
  8945. };
  8946. /**
  8947. * @module filter-source
  8948. */
  8949. /**
  8950. * Filter out single bad source objects or multiple source objects in an
  8951. * array. Also flattens nested source object arrays into a 1 dimensional
  8952. * array of source objects.
  8953. *
  8954. * @param {Tech~SourceObject|Tech~SourceObject[]} src
  8955. * The src object to filter
  8956. *
  8957. * @return {Tech~SourceObject[]}
  8958. * An array of sourceobjects containing only valid sources
  8959. *
  8960. * @private
  8961. */
  8962. const filterSource = function (src) {
  8963. // traverse array
  8964. if (Array.isArray(src)) {
  8965. let newsrc = [];
  8966. src.forEach(function (srcobj) {
  8967. srcobj = filterSource(srcobj);
  8968. if (Array.isArray(srcobj)) {
  8969. newsrc = newsrc.concat(srcobj);
  8970. } else if (isObject(srcobj)) {
  8971. newsrc.push(srcobj);
  8972. }
  8973. });
  8974. src = newsrc;
  8975. } else if (typeof src === 'string' && src.trim()) {
  8976. // convert string into object
  8977. src = [fixSource({
  8978. src
  8979. })];
  8980. } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
  8981. // src is already valid
  8982. src = [fixSource(src)];
  8983. } else {
  8984. // invalid source, turn it into an empty array
  8985. src = [];
  8986. }
  8987. return src;
  8988. };
  8989. /**
  8990. * Checks src mimetype, adding it when possible
  8991. *
  8992. * @param {Tech~SourceObject} src
  8993. * The src object to check
  8994. * @return {Tech~SourceObject}
  8995. * src Object with known type
  8996. */
  8997. function fixSource(src) {
  8998. if (!src.type) {
  8999. const mimetype = getMimetype(src.src);
  9000. if (mimetype) {
  9001. src.type = mimetype;
  9002. }
  9003. }
  9004. return src;
  9005. }
  9006. var icons = "<svg xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play\">\n <path d=\"M16 10v28l22-14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-pause\">\n <path d=\"M12 38h8V10h-8v28zm16-28v28h8V10h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-audio\">\n <path d=\"M24 2C14.06 2 6 10.06 6 20v14c0 3.31 2.69 6 6 6h6V24h-8v-4c0-7.73 6.27-14 14-14s14 6.27 14 14v4h-8v16h6c3.31 0 6-2.69 6-6V20c0-9.94-8.06-18-18-18z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-captions\">\n <path d=\"M38 8H10c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM22 22h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2zm14 0h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-subtitles\">\n <path d=\"M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM8 24h8v4H8v-4zm20 12H8v-4h20v4zm12 0h-8v-4h8v4zm0-8H20v-4h20v4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-enter\">\n <path d=\"M14 28h-4v10h10v-4h-6v-6zm-4-8h4v-6h6v-4H10v10zm24 14h-6v4h10V28h-4v6zm-6-24v4h6v6h4V10H28z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-exit\">\n <path d=\"M10 32h6v6h4V28H10v4zm6-16h-6v4h10V10h-4v6zm12 22h4v-6h6v-4H28v10zm4-22v-6h-4v10h10v-4h-6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play-circle\">\n <path d=\"M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-mute\">\n <path d=\"M33 24c0-3.53-2.04-6.58-5-8.05v4.42l4.91 4.91c.06-.42.09-.85.09-1.28zm5 0c0 1.88-.41 3.65-1.08 5.28l3.03 3.03C41.25 29.82 42 27 42 24c0-8.56-5.99-15.72-14-17.54v4.13c5.78 1.72 10 7.07 10 13.41zM8.55 6L6 8.55 15.45 18H6v12h8l10 10V26.55l8.51 8.51c-1.34 1.03-2.85 1.86-4.51 2.36v4.13a17.94 17.94 0 0 0 7.37-3.62L39.45 42 42 39.45l-18-18L8.55 6zM24 8l-4.18 4.18L24 16.36V8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-low\">\n <path d=\"M14 18v12h8l10 10V8L22 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-medium\">\n <path d=\"M37 24c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zm-27-6v12h8l10 10V8L18 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-high\">\n <path d=\"M6 18v12h8l10 10V8L14 18H6zm27 6c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zM28 6.46v4.13c5.78 1.72 10 7.07 10 13.41s-4.22 11.69-10 13.41v4.13c8.01-1.82 14-8.97 14-17.54S36.01 8.28 28 6.46z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-spinner\">\n <path d=\"M18.8 21l9.53-16.51C26.94 4.18 25.49 4 24 4c-4.8 0-9.19 1.69-12.64 4.51l7.33 12.69.11-.2zm24.28-3c-1.84-5.85-6.3-10.52-11.99-12.68L23.77 18h19.31zm.52 2H28.62l.58 1 9.53 16.5C41.99 33.94 44 29.21 44 24c0-1.37-.14-2.71-.4-4zm-26.53 4l-7.8-13.5C6.01 14.06 4 18.79 4 24c0 1.37.14 2.71.4 4h14.98l-2.31-4zM4.92 30c1.84 5.85 6.3 10.52 11.99 12.68L24.23 30H4.92zm22.54 0l-7.8 13.51c1.4.31 2.85.49 4.34.49 4.8 0 9.19-1.69 12.64-4.51L29.31 26.8 27.46 30z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 24 24\" id=\"vjs-icon-hd\">\n <path d=\"M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm2-6h4c.55 0 1 .45 1 1v4c0 .55-.45 1-1 1h-4V9zm1.5 4.5h2v-3h-2v3z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-chapters\">\n <path d=\"M6 26h4v-4H6v4zm0 8h4v-4H6v4zm0-16h4v-4H6v4zm8 8h28v-4H14v4zm0 8h28v-4H14v4zm0-20v4h28v-4H14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 40 40\" id=\"vjs-icon-downloading\">\n <path d=\"M18.208 36.875q-3.208-.292-5.979-1.729-2.771-1.438-4.812-3.729-2.042-2.292-3.188-5.229-1.146-2.938-1.146-6.23 0-6.583 4.334-11.416 4.333-4.834 10.833-5.5v3.166q-5.167.75-8.583 4.646Q6.25 14.75 6.25 19.958q0 5.209 3.396 9.104 3.396 3.896 8.562 4.646zM20 28.417L11.542 20l2.083-2.083 4.917 4.916v-11.25h2.916v11.25l4.875-4.916L28.417 20zm1.792 8.458v-3.167q1.833-.25 3.541-.958 1.709-.708 3.167-1.875l2.333 2.292q-1.958 1.583-4.25 2.541-2.291.959-4.791 1.167zm6.791-27.792q-1.541-1.125-3.25-1.854-1.708-.729-3.541-1.021V3.042q2.5.25 4.77 1.208 2.271.958 4.271 2.5zm4.584 21.584l-2.25-2.25q1.166-1.5 1.854-3.209.687-1.708.937-3.541h3.209q-.292 2.5-1.229 4.791-.938 2.292-2.521 4.209zm.541-12.417q-.291-1.833-.958-3.562-.667-1.73-1.833-3.188l2.375-2.208q1.541 1.916 2.458 4.208.917 2.292 1.167 4.75z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download\">\n <path d=\"M10.8 40.55q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h26.35v-7.7h3.4v7.7q0 1.4-1 2.4t-2.4 1zM24 32.1L13.9 22.05l2.45-2.45 5.95 5.95V7.15h3.4v18.4l5.95-5.95 2.45 2.45z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-done\">\n <path d=\"M9.8 40.5v-3.45h28.4v3.45zm9.2-9.05L7.4 19.85l2.45-2.35L19 26.65l19.2-19.2 2.4 2.4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-off\">\n <path d=\"M4.9 4.75L43.25 43.1 41 45.3l-4.75-4.75q-.05.05-.075.025-.025-.025-.075-.025H10.8q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h22.05l-7-7-1.85 1.8L13.9 21.9l1.85-1.85L2.7 7zm26.75 14.7l2.45 2.45-3.75 3.8-2.45-2.5zM25.7 7.15V21.1l-3.4-3.45V7.15z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-share\">\n <path d=\"M36 32.17c-1.52 0-2.89.59-3.93 1.54L17.82 25.4c.11-.45.18-.92.18-1.4s-.07-.95-.18-1.4l14.1-8.23c1.07 1 2.5 1.62 4.08 1.62 3.31 0 6-2.69 6-6s-2.69-6-6-6-6 2.69-6 6c0 .48.07.95.18 1.4l-14.1 8.23c-1.07-1-2.5-1.62-4.08-1.62-3.31 0-6 2.69-6 6s2.69 6 6 6c1.58 0 3.01-.62 4.08-1.62l14.25 8.31c-.1.42-.16.86-.16 1.31A5.83 5.83 0 1 0 36 32.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cog\">\n <path d=\"M38.86 25.95c.08-.64.14-1.29.14-1.95s-.06-1.31-.14-1.95l4.23-3.31c.38-.3.49-.84.24-1.28l-4-6.93c-.25-.43-.77-.61-1.22-.43l-4.98 2.01c-1.03-.79-2.16-1.46-3.38-1.97L29 4.84c-.09-.47-.5-.84-1-.84h-8c-.5 0-.91.37-.99.84l-.75 5.3a14.8 14.8 0 0 0-3.38 1.97L9.9 10.1a1 1 0 0 0-1.22.43l-4 6.93c-.25.43-.14.97.24 1.28l4.22 3.31C9.06 22.69 9 23.34 9 24s.06 1.31.14 1.95l-4.22 3.31c-.38.3-.49.84-.24 1.28l4 6.93c.25.43.77.61 1.22.43l4.98-2.01c1.03.79 2.16 1.46 3.38 1.97l.75 5.3c.08.47.49.84.99.84h8c.5 0 .91-.37.99-.84l.75-5.3a14.8 14.8 0 0 0 3.38-1.97l4.98 2.01a1 1 0 0 0 1.22-.43l4-6.93c.25-.43.14-.97-.24-1.28l-4.22-3.31zM24 31c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-square\">\n <path d=\"M36 8H12c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm0 28H12V12h24v24z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle\">\n <circle cx=\"24\" cy=\"24\" r=\"20\"></circle>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-outline\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-inner-circle\">\n <path d=\"M24 4C12.97 4 4 12.97 4 24s8.97 20 20 20 20-8.97 20-20S35.03 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16zm6-16c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6 6 2.69 6 6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cancel\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm10 27.17L31.17 34 24 26.83 16.83 34 14 31.17 21.17 24 14 16.83 16.83 14 24 21.17 31.17 14 34 16.83 26.83 24 34 31.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-replay\">\n <path d=\"M24 10V2L14 12l10 10v-8c6.63 0 12 5.37 12 12s-5.37 12-12 12-12-5.37-12-12H8c0 8.84 7.16 16 16 16s16-7.16 16-16-7.16-16-16-16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-repeat\">\n <path d=\"M14 14h20v6l8-8-8-8v6H10v12h4v-8zm20 20H14v-6l-8 8 8 8v-6h24V26h-4v8z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-5\">\n <path d=\"M17.689 98l-8.697 8.696 8.697 8.697 2.486-2.485-4.32-4.319h1.302c4.93 0 9.071 1.722 12.424 5.165 3.352 3.443 5.029 7.638 5.029 12.584h3.55c0-2.958-.553-5.73-1.658-8.313-1.104-2.583-2.622-4.841-4.555-6.774-1.932-1.932-4.19-3.45-6.773-4.555-2.584-1.104-5.355-1.657-8.313-1.657H15.5l4.615-4.615zm-8.08 21.659v13.861h11.357v5.008H9.609V143h12.7c.834 0 1.55-.298 2.146-.894.596-.597.895-1.31.895-2.145v-7.781c0-.835-.299-1.55-.895-2.147a2.929 2.929 0 0 0-2.147-.894h-8.227v-5.096H25.35v-4.384z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-10\">\n <path d=\"M42.315 125.63c0-4.997-1.694-9.235-5.08-12.713-3.388-3.479-7.571-5.218-12.552-5.218h-1.315l4.363 4.363-2.51 2.51-8.787-8.786L25.221 97l2.45 2.45-4.662 4.663h1.375c2.988 0 5.788.557 8.397 1.673 2.61 1.116 4.892 2.65 6.844 4.602 1.953 1.953 3.487 4.234 4.602 6.844 1.116 2.61 1.674 5.41 1.674 8.398zM8.183 142v-19.657H3.176V117.8h9.643V142zm13.63 0c-1.156 0-2.127-.393-2.912-1.178-.778-.778-1.168-1.746-1.168-2.902v-16.04c0-1.156.393-2.127 1.178-2.912.779-.779 1.746-1.168 2.902-1.168h7.696c1.156 0 2.126.392 2.911 1.177.779.78 1.168 1.747 1.168 2.903v16.04c0 1.156-.392 2.127-1.177 2.912-.779.779-1.746 1.168-2.902 1.168zm.556-4.636h6.583v-15.02H22.37z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-30\">\n <path d=\"M26.047 97l-8.733 8.732 8.733 8.733 2.496-2.494-4.336-4.338h1.307c4.95 0 9.108 1.73 12.474 5.187 3.367 3.458 5.051 7.668 5.051 12.635h3.565c0-2.97-.556-5.751-1.665-8.346-1.109-2.594-2.633-4.862-4.574-6.802-1.94-1.941-4.208-3.466-6.803-4.575-2.594-1.109-5.375-1.664-8.345-1.664H23.85l4.634-4.634zM2.555 117.531v4.688h10.297v5.25H5.873v4.687h6.979v5.156H2.555V142H13.36c1.061 0 1.95-.395 2.668-1.186.718-.79 1.076-1.772 1.076-2.94v-16.218c0-1.168-.358-2.149-1.076-2.94-.717-.79-1.607-1.185-2.668-1.185zm22.482.14c-1.149 0-2.11.39-2.885 1.165-.78.78-1.172 1.744-1.172 2.893v15.943c0 1.149.388 2.11 1.163 2.885.78.78 1.745 1.172 2.894 1.172h7.649c1.148 0 2.11-.388 2.884-1.163.78-.78 1.17-1.745 1.17-2.894v-15.943c0-1.15-.386-2.111-1.16-2.885-.78-.78-1.746-1.172-2.894-1.172zm.553 4.518h6.545v14.93H25.59z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-5\">\n <path d=\"M29.508 97l-2.431 2.43 4.625 4.625h-1.364c-2.965 0-5.742.554-8.332 1.66-2.589 1.107-4.851 2.629-6.788 4.566-1.937 1.937-3.458 4.2-4.565 6.788-1.107 2.59-1.66 5.367-1.66 8.331h3.557c0-4.957 1.68-9.16 5.04-12.611 3.36-3.45 7.51-5.177 12.451-5.177h1.304l-4.326 4.33 2.49 2.49 8.715-8.716zm-9.783 21.61v13.89h11.382v5.018H19.725V142h12.727a2.93 2.93 0 0 0 2.15-.896 2.93 2.93 0 0 0 .896-2.15v-7.798c0-.837-.299-1.554-.896-2.152a2.93 2.93 0 0 0-2.15-.896h-8.245V123h11.29v-4.392z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-10\">\n <path d=\"M23.119 97l-2.386 2.383 4.538 4.538h-1.339c-2.908 0-5.633.543-8.173 1.63-2.54 1.085-4.76 2.577-6.66 4.478-1.9 1.9-3.392 4.12-4.478 6.66-1.085 2.54-1.629 5.264-1.629 8.172h3.49c0-4.863 1.648-8.986 4.944-12.372 3.297-3.385 7.368-5.078 12.216-5.078h1.279l-4.245 4.247 2.443 2.442 8.55-8.55zm-9.52 21.45v4.42h4.871V142h4.513v-23.55zm18.136 0c-1.125 0-2.066.377-2.824 1.135-.764.764-1.148 1.709-1.148 2.834v15.612c0 1.124.38 2.066 1.139 2.824.764.764 1.708 1.145 2.833 1.145h7.489c1.125 0 2.066-.378 2.824-1.136.764-.764 1.145-1.709 1.145-2.833v-15.612c0-1.125-.378-2.067-1.136-2.825-.764-.764-1.708-1.145-2.833-1.145zm.54 4.42h6.408v14.617h-6.407z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-30\">\n <path d=\"M25.549 97l-2.437 2.434 4.634 4.635H26.38c-2.97 0-5.753.555-8.347 1.664-2.594 1.109-4.861 2.633-6.802 4.574-1.94 1.94-3.465 4.207-4.574 6.802-1.109 2.594-1.664 5.377-1.664 8.347h3.565c0-4.967 1.683-9.178 5.05-12.636 3.366-3.458 7.525-5.187 12.475-5.187h1.307l-4.335 4.338 2.495 2.494 8.732-8.732zm-11.553 20.53v4.689h10.297v5.249h-6.978v4.688h6.978v5.156H13.996V142h10.808c1.06 0 1.948-.395 2.666-1.186.718-.79 1.077-1.771 1.077-2.94v-16.217c0-1.169-.36-2.15-1.077-2.94-.718-.79-1.605-1.186-2.666-1.186zm21.174.168c-1.149 0-2.11.389-2.884 1.163-.78.78-1.172 1.745-1.172 2.894v15.942c0 1.15.388 2.11 1.162 2.885.78.78 1.745 1.17 2.894 1.17h7.649c1.149 0 2.11-.386 2.885-1.16.78-.78 1.17-1.746 1.17-2.895v-15.942c0-1.15-.387-2.11-1.161-2.885-.78-.78-1.745-1.172-2.894-1.172zm.552 4.516h6.542v14.931h-6.542z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 512 512\" id=\"vjs-icon-audio-description\">\n <g fill-rule=\"evenodd\"><path d=\"M227.29 381.351V162.993c50.38-1.017 89.108-3.028 117.631 17.126 27.374 19.342 48.734 56.965 44.89 105.325-4.067 51.155-41.335 94.139-89.776 98.475-24.085 2.155-71.972 0-71.972 0s-.84-1.352-.773-2.568m48.755-54.804c31.43 1.26 53.208-16.633 56.495-45.386 4.403-38.51-21.188-63.552-58.041-60.796v103.612c-.036 1.466.575 2.22 1.546 2.57\"></path><path d=\"M383.78 381.328c13.336 3.71 17.387-11.06 23.215-21.408 12.722-22.571 22.294-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.226 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M425.154 381.328c13.336 3.71 17.384-11.061 23.215-21.408 12.721-22.571 22.291-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.511c-.586 3.874 2.226 7.315 3.866 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M466.26 381.328c13.337 3.71 17.385-11.061 23.216-21.408 12.722-22.571 22.292-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.225 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894M4.477 383.005H72.58l18.573-28.484 64.169-.135s.065 19.413.065 28.62h48.756V160.307h-58.816c-5.653 9.537-140.85 222.697-140.85 222.697zm152.667-145.282v71.158l-40.453-.27 40.453-70.888z\"></path></g>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-next-item\">\n <path d=\"M12 36l17-12-17-12v24zm20-24v24h4V12h-4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-previous-item\">\n <path d=\"M12 12h4v24h-4zm7 12l17 12V12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-shuffle\">\n <path d=\"M21.17 18.34L10.83 8 8 10.83l10.34 10.34 2.83-2.83zM29 8l4.09 4.09L8 37.17 10.83 40l25.09-25.09L40 19V8H29zm.66 18.83l-2.83 2.83 6.26 6.26L29 40h11V29l-4.09 4.09-6.25-6.26z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cast\">\n <path d=\"M42 6H6c-2.21 0-4 1.79-4 4v6h4v-6h36v28H28v4h14c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zM2 36v6h6c0-3.31-2.69-6-6-6zm0-8v4c5.52 0 10 4.48 10 10h4c0-7.73-6.27-14-14-14zm0-8v4c9.94 0 18 8.06 18 18h4c0-12.15-9.85-22-22-22z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-picture-in-picture-enter\">\n <path d=\"M38 22H22v11.99h16V22zm8 16V9.96C46 7.76 44.2 6 42 6H6C3.8 6 2 7.76 2 9.96V38c0 2.2 1.8 4 4 4h36c2.2 0 4-1.8 4-4zm-4 .04H6V9.94h36v28.1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 22 18\" id=\"vjs-icon-picture-in-picture-exit\">\n <path d=\"M18 4H4v10h14V4zm4 12V1.98C22 .88 21.1 0 20 0H2C.9 0 0 .88 0 1.98V16c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 .02H2V1.97h18v14.05z\"></path>\n <path fill=\"none\" d=\"M-1-3h24v24H-1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-facebook\">\n <path d=\"M1343 12v264h-157q-86 0-116 36t-30 108v189h293l-39 296h-254v759H734V905H479V609h255V391q0-186 104-288.5T1115 0q147 0 228 12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-linkedin\">\n <path d=\"M477 625v991H147V625h330zm21-306q1 73-50.5 122T312 490h-2q-82 0-132-49t-50-122q0-74 51.5-122.5T314 148t133 48.5T498 319zm1166 729v568h-329v-530q0-105-40.5-164.5T1168 862q-63 0-105.5 34.5T999 982q-11 30-11 81v553H659q2-399 2-647t-1-296l-1-48h329v144h-2q20-32 41-56t56.5-52 87-43.5T1285 602q171 0 275 113.5t104 332.5z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-twitter\">\n <path d=\"M1684 408q-67 98-162 167 1 14 1 42 0 130-38 259.5T1369.5 1125 1185 1335.5t-258 146-323 54.5q-271 0-496-145 35 4 78 4 225 0 401-138-105-2-188-64.5T285 1033q33 5 61 5 43 0 85-11-112-23-185.5-111.5T172 710v-4q68 38 146 41-66-44-105-115t-39-154q0-88 44-163 121 149 294.5 238.5T884 653q-8-38-8-74 0-134 94.5-228.5T1199 256q140 0 236 102 109-21 205-78-37 115-142 178 93-10 186-50z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-tumblr\">\n <path d=\"M1328 1329l80 237q-23 35-111 66t-177 32q-104 2-190.5-26T787 1564t-95-106-55.5-120-16.5-118V676H452V461q72-26 129-69.5t91-90 58-102 34-99T779 12q1-5 4.5-8.5T791 0h244v424h333v252h-334v518q0 30 6.5 56t22.5 52.5 49.5 41.5 81.5 14q78-2 134-29z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-pinterest\">\n <path d=\"M1664 896q0 209-103 385.5T1281.5 1561 896 1664q-111 0-218-32 59-93 78-164 9-34 54-211 20 39 73 67.5t114 28.5q121 0 216-68.5t147-188.5 52-270q0-114-59.5-214T1180 449t-255-63q-105 0-196 29t-154.5 77-109 110.5-67 129.5T377 866q0 104 40 183t117 111q30 12 38-20 2-7 8-31t8-30q6-23-11-43-51-61-51-151 0-151 104.5-259.5T904 517q151 0 235.5 82t84.5 213q0 170-68.5 289T980 1220q-61 0-98-43.5T859 1072q8-35 26.5-93.5t30-103T927 800q0-50-27-83t-77-33q-62 0-105 57t-43 142q0 73 25 122l-99 418q-17 70-13 177-206-91-333-281T128 896q0-209 103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z\"></path>\n </symbol>\n </defs>\n</svg>";
  9007. /**
  9008. * @file loader.js
  9009. */
  9010. /**
  9011. * The `MediaLoader` is the `Component` that decides which playback technology to load
  9012. * when a player is initialized.
  9013. *
  9014. * @extends Component
  9015. */
  9016. class MediaLoader extends Component {
  9017. /**
  9018. * Create an instance of this class.
  9019. *
  9020. * @param { import('../player').default } player
  9021. * The `Player` that this class should attach to.
  9022. *
  9023. * @param {Object} [options]
  9024. * The key/value store of player options.
  9025. *
  9026. * @param {Function} [ready]
  9027. * The function that is run when this component is ready.
  9028. */
  9029. constructor(player, options, ready) {
  9030. // MediaLoader has no element
  9031. const options_ = merge({
  9032. createEl: false
  9033. }, options);
  9034. super(player, options_, ready);
  9035. // If there are no sources when the player is initialized,
  9036. // load the first supported playback technology.
  9037. if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
  9038. for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
  9039. const techName = toTitleCase(j[i]);
  9040. let tech = Tech.getTech(techName);
  9041. // Support old behavior of techs being registered as components.
  9042. // Remove once that deprecated behavior is removed.
  9043. if (!techName) {
  9044. tech = Component.getComponent(techName);
  9045. }
  9046. // Check if the browser supports this technology
  9047. if (tech && tech.isSupported()) {
  9048. player.loadTech_(techName);
  9049. break;
  9050. }
  9051. }
  9052. } else {
  9053. // Loop through playback technologies (e.g. HTML5) and check for support.
  9054. // Then load the best source.
  9055. // A few assumptions here:
  9056. // All playback technologies respect preload false.
  9057. player.src(options.playerOptions.sources);
  9058. }
  9059. }
  9060. }
  9061. Component.registerComponent('MediaLoader', MediaLoader);
  9062. /**
  9063. * @file clickable-component.js
  9064. */
  9065. /**
  9066. * Component which is clickable or keyboard actionable, but is not a
  9067. * native HTML button.
  9068. *
  9069. * @extends Component
  9070. */
  9071. class ClickableComponent extends Component {
  9072. /**
  9073. * Creates an instance of this class.
  9074. *
  9075. * @param { import('./player').default } player
  9076. * The `Player` that this class should be attached to.
  9077. *
  9078. * @param {Object} [options]
  9079. * The key/value store of component options.
  9080. *
  9081. * @param {function} [options.clickHandler]
  9082. * The function to call when the button is clicked / activated
  9083. *
  9084. * @param {string} [options.controlText]
  9085. * The text to set on the button
  9086. *
  9087. * @param {string} [options.className]
  9088. * A class or space separated list of classes to add the component
  9089. *
  9090. */
  9091. constructor(player, options) {
  9092. super(player, options);
  9093. if (this.options_.controlText) {
  9094. this.controlText(this.options_.controlText);
  9095. }
  9096. this.handleMouseOver_ = e => this.handleMouseOver(e);
  9097. this.handleMouseOut_ = e => this.handleMouseOut(e);
  9098. this.handleClick_ = e => this.handleClick(e);
  9099. this.handleKeyDown_ = e => this.handleKeyDown(e);
  9100. this.emitTapEvents();
  9101. this.enable();
  9102. }
  9103. /**
  9104. * Create the `ClickableComponent`s DOM element.
  9105. *
  9106. * @param {string} [tag=div]
  9107. * The element's node type.
  9108. *
  9109. * @param {Object} [props={}]
  9110. * An object of properties that should be set on the element.
  9111. *
  9112. * @param {Object} [attributes={}]
  9113. * An object of attributes that should be set on the element.
  9114. *
  9115. * @return {Element}
  9116. * The element that gets created.
  9117. */
  9118. createEl(tag = 'div', props = {}, attributes = {}) {
  9119. props = Object.assign({
  9120. className: this.buildCSSClass(),
  9121. tabIndex: 0
  9122. }, props);
  9123. if (tag === 'button') {
  9124. log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
  9125. }
  9126. // Add ARIA attributes for clickable element which is not a native HTML button
  9127. attributes = Object.assign({
  9128. role: 'button'
  9129. }, attributes);
  9130. this.tabIndex_ = props.tabIndex;
  9131. const el = createEl(tag, props, attributes);
  9132. if (!this.player_.options_.experimentalSvgIcons) {
  9133. el.appendChild(createEl('span', {
  9134. className: 'vjs-icon-placeholder'
  9135. }, {
  9136. 'aria-hidden': true
  9137. }));
  9138. }
  9139. this.createControlTextEl(el);
  9140. return el;
  9141. }
  9142. dispose() {
  9143. // remove controlTextEl_ on dispose
  9144. this.controlTextEl_ = null;
  9145. super.dispose();
  9146. }
  9147. /**
  9148. * Create a control text element on this `ClickableComponent`
  9149. *
  9150. * @param {Element} [el]
  9151. * Parent element for the control text.
  9152. *
  9153. * @return {Element}
  9154. * The control text element that gets created.
  9155. */
  9156. createControlTextEl(el) {
  9157. this.controlTextEl_ = createEl('span', {
  9158. className: 'vjs-control-text'
  9159. }, {
  9160. // let the screen reader user know that the text of the element may change
  9161. 'aria-live': 'polite'
  9162. });
  9163. if (el) {
  9164. el.appendChild(this.controlTextEl_);
  9165. }
  9166. this.controlText(this.controlText_, el);
  9167. return this.controlTextEl_;
  9168. }
  9169. /**
  9170. * Get or set the localize text to use for the controls on the `ClickableComponent`.
  9171. *
  9172. * @param {string} [text]
  9173. * Control text for element.
  9174. *
  9175. * @param {Element} [el=this.el()]
  9176. * Element to set the title on.
  9177. *
  9178. * @return {string}
  9179. * - The control text when getting
  9180. */
  9181. controlText(text, el = this.el()) {
  9182. if (text === undefined) {
  9183. return this.controlText_ || 'Need Text';
  9184. }
  9185. const localizedText = this.localize(text);
  9186. /** @protected */
  9187. this.controlText_ = text;
  9188. textContent(this.controlTextEl_, localizedText);
  9189. if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
  9190. // Set title attribute if only an icon is shown
  9191. el.setAttribute('title', localizedText);
  9192. }
  9193. }
  9194. /**
  9195. * Builds the default DOM `className`.
  9196. *
  9197. * @return {string}
  9198. * The DOM `className` for this object.
  9199. */
  9200. buildCSSClass() {
  9201. return `vjs-control vjs-button ${super.buildCSSClass()}`;
  9202. }
  9203. /**
  9204. * Enable this `ClickableComponent`
  9205. */
  9206. enable() {
  9207. if (!this.enabled_) {
  9208. this.enabled_ = true;
  9209. this.removeClass('vjs-disabled');
  9210. this.el_.setAttribute('aria-disabled', 'false');
  9211. if (typeof this.tabIndex_ !== 'undefined') {
  9212. this.el_.setAttribute('tabIndex', this.tabIndex_);
  9213. }
  9214. this.on(['tap', 'click'], this.handleClick_);
  9215. this.on('keydown', this.handleKeyDown_);
  9216. }
  9217. }
  9218. /**
  9219. * Disable this `ClickableComponent`
  9220. */
  9221. disable() {
  9222. this.enabled_ = false;
  9223. this.addClass('vjs-disabled');
  9224. this.el_.setAttribute('aria-disabled', 'true');
  9225. if (typeof this.tabIndex_ !== 'undefined') {
  9226. this.el_.removeAttribute('tabIndex');
  9227. }
  9228. this.off('mouseover', this.handleMouseOver_);
  9229. this.off('mouseout', this.handleMouseOut_);
  9230. this.off(['tap', 'click'], this.handleClick_);
  9231. this.off('keydown', this.handleKeyDown_);
  9232. }
  9233. /**
  9234. * Handles language change in ClickableComponent for the player in components
  9235. *
  9236. *
  9237. */
  9238. handleLanguagechange() {
  9239. this.controlText(this.controlText_);
  9240. }
  9241. /**
  9242. * Event handler that is called when a `ClickableComponent` receives a
  9243. * `click` or `tap` event.
  9244. *
  9245. * @param {Event} event
  9246. * The `tap` or `click` event that caused this function to be called.
  9247. *
  9248. * @listens tap
  9249. * @listens click
  9250. * @abstract
  9251. */
  9252. handleClick(event) {
  9253. if (this.options_.clickHandler) {
  9254. this.options_.clickHandler.call(this, arguments);
  9255. }
  9256. }
  9257. /**
  9258. * Event handler that is called when a `ClickableComponent` receives a
  9259. * `keydown` event.
  9260. *
  9261. * By default, if the key is Space or Enter, it will trigger a `click` event.
  9262. *
  9263. * @param {KeyboardEvent} event
  9264. * The `keydown` event that caused this function to be called.
  9265. *
  9266. * @listens keydown
  9267. */
  9268. handleKeyDown(event) {
  9269. // Support Space or Enter key operation to fire a click event. Also,
  9270. // prevent the event from propagating through the DOM and triggering
  9271. // Player hotkeys.
  9272. if (keycode__default["default"].isEventKey(event, 'Space') || keycode__default["default"].isEventKey(event, 'Enter')) {
  9273. event.preventDefault();
  9274. event.stopPropagation();
  9275. this.trigger('click');
  9276. } else {
  9277. // Pass keypress handling up for unsupported keys
  9278. super.handleKeyDown(event);
  9279. }
  9280. }
  9281. }
  9282. Component.registerComponent('ClickableComponent', ClickableComponent);
  9283. /**
  9284. * @file poster-image.js
  9285. */
  9286. /**
  9287. * A `ClickableComponent` that handles showing the poster image for the player.
  9288. *
  9289. * @extends ClickableComponent
  9290. */
  9291. class PosterImage extends ClickableComponent {
  9292. /**
  9293. * Create an instance of this class.
  9294. *
  9295. * @param { import('./player').default } player
  9296. * The `Player` that this class should attach to.
  9297. *
  9298. * @param {Object} [options]
  9299. * The key/value store of player options.
  9300. */
  9301. constructor(player, options) {
  9302. super(player, options);
  9303. this.update();
  9304. this.update_ = e => this.update(e);
  9305. player.on('posterchange', this.update_);
  9306. }
  9307. /**
  9308. * Clean up and dispose of the `PosterImage`.
  9309. */
  9310. dispose() {
  9311. this.player().off('posterchange', this.update_);
  9312. super.dispose();
  9313. }
  9314. /**
  9315. * Create the `PosterImage`s DOM element.
  9316. *
  9317. * @return {Element}
  9318. * The element that gets created.
  9319. */
  9320. createEl() {
  9321. // The el is an empty div to keep position in the DOM
  9322. // A picture and img el will be inserted when a source is set
  9323. return createEl('div', {
  9324. className: 'vjs-poster'
  9325. });
  9326. }
  9327. /**
  9328. * Get or set the `PosterImage`'s crossOrigin option.
  9329. *
  9330. * @param {string|null} [value]
  9331. * The value to set the crossOrigin to. If an argument is
  9332. * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
  9333. *
  9334. * @return {string|null}
  9335. * - The current crossOrigin value of the `Player` when getting.
  9336. * - undefined when setting
  9337. */
  9338. crossOrigin(value) {
  9339. // `null` can be set to unset a value
  9340. if (typeof value === 'undefined') {
  9341. if (this.$('img')) {
  9342. // If the poster's element exists, give its value
  9343. return this.$('img').crossOrigin;
  9344. } else if (this.player_.tech_ && this.player_.tech_.isReady_) {
  9345. // If not but the tech is ready, query the tech
  9346. return this.player_.crossOrigin();
  9347. }
  9348. // Otherwise check options as the poster is usually set before the state of crossorigin
  9349. // can be retrieved by the getter
  9350. return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
  9351. }
  9352. if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
  9353. this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
  9354. return;
  9355. }
  9356. if (this.$('img')) {
  9357. this.$('img').crossOrigin = value;
  9358. }
  9359. return;
  9360. }
  9361. /**
  9362. * An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
  9363. *
  9364. * @listens Player#posterchange
  9365. *
  9366. * @param {Event} [event]
  9367. * The `Player#posterchange` event that triggered this function.
  9368. */
  9369. update(event) {
  9370. const url = this.player().poster();
  9371. this.setSrc(url);
  9372. // If there's no poster source we should display:none on this component
  9373. // so it's not still clickable or right-clickable
  9374. if (url) {
  9375. this.show();
  9376. } else {
  9377. this.hide();
  9378. }
  9379. }
  9380. /**
  9381. * Set the source of the `PosterImage` depending on the display method. (Re)creates
  9382. * the inner picture and img elementss when needed.
  9383. *
  9384. * @param {string} [url]
  9385. * The URL to the source for the `PosterImage`. If not specified or falsy,
  9386. * any source and ant inner picture/img are removed.
  9387. */
  9388. setSrc(url) {
  9389. if (!url) {
  9390. this.el_.textContent = '';
  9391. return;
  9392. }
  9393. if (!this.$('img')) {
  9394. this.el_.appendChild(createEl('picture', {
  9395. className: 'vjs-poster',
  9396. // Don't want poster to be tabbable.
  9397. tabIndex: -1
  9398. }, {}, createEl('img', {
  9399. loading: 'lazy',
  9400. crossOrigin: this.crossOrigin()
  9401. }, {
  9402. alt: ''
  9403. })));
  9404. }
  9405. this.$('img').src = url;
  9406. }
  9407. /**
  9408. * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
  9409. * {@link ClickableComponent#handleClick} for instances where this will be triggered.
  9410. *
  9411. * @listens tap
  9412. * @listens click
  9413. * @listens keydown
  9414. *
  9415. * @param {Event} event
  9416. + The `click`, `tap` or `keydown` event that caused this function to be called.
  9417. */
  9418. handleClick(event) {
  9419. // We don't want a click to trigger playback when controls are disabled
  9420. if (!this.player_.controls()) {
  9421. return;
  9422. }
  9423. if (this.player_.tech(true)) {
  9424. this.player_.tech(true).focus();
  9425. }
  9426. if (this.player_.paused()) {
  9427. silencePromise(this.player_.play());
  9428. } else {
  9429. this.player_.pause();
  9430. }
  9431. }
  9432. }
  9433. /**
  9434. * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
  9435. * sets the `crossOrigin` property on the `<img>` tag to control the CORS
  9436. * behavior.
  9437. *
  9438. * @param {string|null} [value]
  9439. * The value to set the `PosterImages`'s crossorigin to. If an argument is
  9440. * given, must be one of `anonymous` or `use-credentials`.
  9441. *
  9442. * @return {string|null|undefined}
  9443. * - The current crossorigin value of the `Player` when getting.
  9444. * - undefined when setting
  9445. */
  9446. PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
  9447. Component.registerComponent('PosterImage', PosterImage);
  9448. /**
  9449. * @file text-track-display.js
  9450. */
  9451. const darkGray = '#222';
  9452. const lightGray = '#ccc';
  9453. const fontMap = {
  9454. monospace: 'monospace',
  9455. sansSerif: 'sans-serif',
  9456. serif: 'serif',
  9457. monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
  9458. monospaceSerif: '"Courier New", monospace',
  9459. proportionalSansSerif: 'sans-serif',
  9460. proportionalSerif: 'serif',
  9461. casual: '"Comic Sans MS", Impact, fantasy',
  9462. script: '"Monotype Corsiva", cursive',
  9463. smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
  9464. };
  9465. /**
  9466. * Construct an rgba color from a given hex color code.
  9467. *
  9468. * @param {number} color
  9469. * Hex number for color, like #f0e or #f604e2.
  9470. *
  9471. * @param {number} opacity
  9472. * Value for opacity, 0.0 - 1.0.
  9473. *
  9474. * @return {string}
  9475. * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
  9476. */
  9477. function constructColor(color, opacity) {
  9478. let hex;
  9479. if (color.length === 4) {
  9480. // color looks like "#f0e"
  9481. hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
  9482. } else if (color.length === 7) {
  9483. // color looks like "#f604e2"
  9484. hex = color.slice(1);
  9485. } else {
  9486. throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
  9487. }
  9488. return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
  9489. }
  9490. /**
  9491. * Try to update the style of a DOM element. Some style changes will throw an error,
  9492. * particularly in IE8. Those should be noops.
  9493. *
  9494. * @param {Element} el
  9495. * The DOM element to be styled.
  9496. *
  9497. * @param {string} style
  9498. * The CSS property on the element that should be styled.
  9499. *
  9500. * @param {string} rule
  9501. * The style rule that should be applied to the property.
  9502. *
  9503. * @private
  9504. */
  9505. function tryUpdateStyle(el, style, rule) {
  9506. try {
  9507. el.style[style] = rule;
  9508. } catch (e) {
  9509. // Satisfies linter.
  9510. return;
  9511. }
  9512. }
  9513. /**
  9514. * Converts the CSS top/right/bottom/left property numeric value to string in pixels.
  9515. *
  9516. * @param {number} position
  9517. * The CSS top/right/bottom/left property value.
  9518. *
  9519. * @return {string}
  9520. * The CSS property value that was created, like '10px'.
  9521. *
  9522. * @private
  9523. */
  9524. function getCSSPositionValue(position) {
  9525. return position ? `${position}px` : '';
  9526. }
  9527. /**
  9528. * The component for displaying text track cues.
  9529. *
  9530. * @extends Component
  9531. */
  9532. class TextTrackDisplay extends Component {
  9533. /**
  9534. * Creates an instance of this class.
  9535. *
  9536. * @param { import('../player').default } player
  9537. * The `Player` that this class should be attached to.
  9538. *
  9539. * @param {Object} [options]
  9540. * The key/value store of player options.
  9541. *
  9542. * @param {Function} [ready]
  9543. * The function to call when `TextTrackDisplay` is ready.
  9544. */
  9545. constructor(player, options, ready) {
  9546. super(player, options, ready);
  9547. const updateDisplayTextHandler = e => this.updateDisplay(e);
  9548. const updateDisplayHandler = e => {
  9549. this.updateDisplayOverlay();
  9550. this.updateDisplay(e);
  9551. };
  9552. player.on('loadstart', e => this.toggleDisplay(e));
  9553. player.on('texttrackchange', updateDisplayTextHandler);
  9554. player.on('loadedmetadata', e => {
  9555. this.updateDisplayOverlay();
  9556. this.preselectTrack(e);
  9557. });
  9558. // This used to be called during player init, but was causing an error
  9559. // if a track should show by default and the display hadn't loaded yet.
  9560. // Should probably be moved to an external track loader when we support
  9561. // tracks that don't need a display.
  9562. player.ready(bind_(this, function () {
  9563. if (player.tech_ && player.tech_.featuresNativeTextTracks) {
  9564. this.hide();
  9565. return;
  9566. }
  9567. player.on('fullscreenchange', updateDisplayHandler);
  9568. player.on('playerresize', updateDisplayHandler);
  9569. const screenOrientation = window__default["default"].screen.orientation || window__default["default"];
  9570. const changeOrientationEvent = window__default["default"].screen.orientation ? 'change' : 'orientationchange';
  9571. screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
  9572. player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
  9573. const tracks = this.options_.playerOptions.tracks || [];
  9574. for (let i = 0; i < tracks.length; i++) {
  9575. this.player_.addRemoteTextTrack(tracks[i], true);
  9576. }
  9577. this.preselectTrack();
  9578. }));
  9579. }
  9580. /**
  9581. * Preselect a track following this precedence:
  9582. * - matches the previously selected {@link TextTrack}'s language and kind
  9583. * - matches the previously selected {@link TextTrack}'s language only
  9584. * - is the first default captions track
  9585. * - is the first default descriptions track
  9586. *
  9587. * @listens Player#loadstart
  9588. */
  9589. preselectTrack() {
  9590. const modes = {
  9591. captions: 1,
  9592. subtitles: 1
  9593. };
  9594. const trackList = this.player_.textTracks();
  9595. const userPref = this.player_.cache_.selectedLanguage;
  9596. let firstDesc;
  9597. let firstCaptions;
  9598. let preferredTrack;
  9599. for (let i = 0; i < trackList.length; i++) {
  9600. const track = trackList[i];
  9601. if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
  9602. // Always choose the track that matches both language and kind
  9603. if (track.kind === userPref.kind) {
  9604. preferredTrack = track;
  9605. // or choose the first track that matches language
  9606. } else if (!preferredTrack) {
  9607. preferredTrack = track;
  9608. }
  9609. // clear everything if offTextTrackMenuItem was clicked
  9610. } else if (userPref && !userPref.enabled) {
  9611. preferredTrack = null;
  9612. firstDesc = null;
  9613. firstCaptions = null;
  9614. } else if (track.default) {
  9615. if (track.kind === 'descriptions' && !firstDesc) {
  9616. firstDesc = track;
  9617. } else if (track.kind in modes && !firstCaptions) {
  9618. firstCaptions = track;
  9619. }
  9620. }
  9621. }
  9622. // The preferredTrack matches the user preference and takes
  9623. // precedence over all the other tracks.
  9624. // So, display the preferredTrack before the first default track
  9625. // and the subtitles/captions track before the descriptions track
  9626. if (preferredTrack) {
  9627. preferredTrack.mode = 'showing';
  9628. } else if (firstCaptions) {
  9629. firstCaptions.mode = 'showing';
  9630. } else if (firstDesc) {
  9631. firstDesc.mode = 'showing';
  9632. }
  9633. }
  9634. /**
  9635. * Turn display of {@link TextTrack}'s from the current state into the other state.
  9636. * There are only two states:
  9637. * - 'shown'
  9638. * - 'hidden'
  9639. *
  9640. * @listens Player#loadstart
  9641. */
  9642. toggleDisplay() {
  9643. if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
  9644. this.hide();
  9645. } else {
  9646. this.show();
  9647. }
  9648. }
  9649. /**
  9650. * Create the {@link Component}'s DOM element.
  9651. *
  9652. * @return {Element}
  9653. * The element that was created.
  9654. */
  9655. createEl() {
  9656. return super.createEl('div', {
  9657. className: 'vjs-text-track-display'
  9658. }, {
  9659. 'translate': 'yes',
  9660. 'aria-live': 'off',
  9661. 'aria-atomic': 'true'
  9662. });
  9663. }
  9664. /**
  9665. * Clear all displayed {@link TextTrack}s.
  9666. */
  9667. clearDisplay() {
  9668. if (typeof window__default["default"].WebVTT === 'function') {
  9669. window__default["default"].WebVTT.processCues(window__default["default"], [], this.el_);
  9670. }
  9671. }
  9672. /**
  9673. * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
  9674. * a {@link Player#fullscreenchange} is fired.
  9675. *
  9676. * @listens Player#texttrackchange
  9677. * @listens Player#fullscreenchange
  9678. */
  9679. updateDisplay() {
  9680. const tracks = this.player_.textTracks();
  9681. const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
  9682. this.clearDisplay();
  9683. if (allowMultipleShowingTracks) {
  9684. const showingTracks = [];
  9685. for (let i = 0; i < tracks.length; ++i) {
  9686. const track = tracks[i];
  9687. if (track.mode !== 'showing') {
  9688. continue;
  9689. }
  9690. showingTracks.push(track);
  9691. }
  9692. this.updateForTrack(showingTracks);
  9693. return;
  9694. }
  9695. // Track display prioritization model: if multiple tracks are 'showing',
  9696. // display the first 'subtitles' or 'captions' track which is 'showing',
  9697. // otherwise display the first 'descriptions' track which is 'showing'
  9698. let descriptionsTrack = null;
  9699. let captionsSubtitlesTrack = null;
  9700. let i = tracks.length;
  9701. while (i--) {
  9702. const track = tracks[i];
  9703. if (track.mode === 'showing') {
  9704. if (track.kind === 'descriptions') {
  9705. descriptionsTrack = track;
  9706. } else {
  9707. captionsSubtitlesTrack = track;
  9708. }
  9709. }
  9710. }
  9711. if (captionsSubtitlesTrack) {
  9712. if (this.getAttribute('aria-live') !== 'off') {
  9713. this.setAttribute('aria-live', 'off');
  9714. }
  9715. this.updateForTrack(captionsSubtitlesTrack);
  9716. } else if (descriptionsTrack) {
  9717. if (this.getAttribute('aria-live') !== 'assertive') {
  9718. this.setAttribute('aria-live', 'assertive');
  9719. }
  9720. this.updateForTrack(descriptionsTrack);
  9721. }
  9722. }
  9723. /**
  9724. * Updates the displayed TextTrack to be sure it overlays the video when a either
  9725. * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired.
  9726. */
  9727. updateDisplayOverlay() {
  9728. // inset-inline and inset-block are not supprted on old chrome, but these are
  9729. // only likely to be used on TV devices
  9730. if (!this.player_.videoHeight() || !window__default["default"].CSS.supports('inset-inline: 10px')) {
  9731. return;
  9732. }
  9733. const playerWidth = this.player_.currentWidth();
  9734. const playerHeight = this.player_.currentHeight();
  9735. const playerAspectRatio = playerWidth / playerHeight;
  9736. const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight();
  9737. let insetInlineMatch = 0;
  9738. let insetBlockMatch = 0;
  9739. if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) {
  9740. if (playerAspectRatio > videoAspectRatio) {
  9741. insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2);
  9742. } else {
  9743. insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2);
  9744. }
  9745. }
  9746. tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch));
  9747. tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch));
  9748. }
  9749. /**
  9750. * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
  9751. *
  9752. * @param {TextTrack} track
  9753. * Text track object containing active cues to style.
  9754. */
  9755. updateDisplayState(track) {
  9756. const overrides = this.player_.textTrackSettings.getValues();
  9757. const cues = track.activeCues;
  9758. let i = cues.length;
  9759. while (i--) {
  9760. const cue = cues[i];
  9761. if (!cue) {
  9762. continue;
  9763. }
  9764. const cueDiv = cue.displayState;
  9765. if (overrides.color) {
  9766. cueDiv.firstChild.style.color = overrides.color;
  9767. }
  9768. if (overrides.textOpacity) {
  9769. tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
  9770. }
  9771. if (overrides.backgroundColor) {
  9772. cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
  9773. }
  9774. if (overrides.backgroundOpacity) {
  9775. tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
  9776. }
  9777. if (overrides.windowColor) {
  9778. if (overrides.windowOpacity) {
  9779. tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
  9780. } else {
  9781. cueDiv.style.backgroundColor = overrides.windowColor;
  9782. }
  9783. }
  9784. if (overrides.edgeStyle) {
  9785. if (overrides.edgeStyle === 'dropshadow') {
  9786. cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
  9787. } else if (overrides.edgeStyle === 'raised') {
  9788. cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
  9789. } else if (overrides.edgeStyle === 'depressed') {
  9790. cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
  9791. } else if (overrides.edgeStyle === 'uniform') {
  9792. cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
  9793. }
  9794. }
  9795. if (overrides.fontPercent && overrides.fontPercent !== 1) {
  9796. const fontSize = window__default["default"].parseFloat(cueDiv.style.fontSize);
  9797. cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
  9798. cueDiv.style.height = 'auto';
  9799. cueDiv.style.top = 'auto';
  9800. }
  9801. if (overrides.fontFamily && overrides.fontFamily !== 'default') {
  9802. if (overrides.fontFamily === 'small-caps') {
  9803. cueDiv.firstChild.style.fontVariant = 'small-caps';
  9804. } else {
  9805. cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
  9806. }
  9807. }
  9808. }
  9809. }
  9810. /**
  9811. * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
  9812. *
  9813. * @param {TextTrack|TextTrack[]} tracks
  9814. * Text track object or text track array to be added to the list.
  9815. */
  9816. updateForTrack(tracks) {
  9817. if (!Array.isArray(tracks)) {
  9818. tracks = [tracks];
  9819. }
  9820. if (typeof window__default["default"].WebVTT !== 'function' || tracks.every(track => {
  9821. return !track.activeCues;
  9822. })) {
  9823. return;
  9824. }
  9825. const cues = [];
  9826. // push all active track cues
  9827. for (let i = 0; i < tracks.length; ++i) {
  9828. const track = tracks[i];
  9829. for (let j = 0; j < track.activeCues.length; ++j) {
  9830. cues.push(track.activeCues[j]);
  9831. }
  9832. }
  9833. // removes all cues before it processes new ones
  9834. window__default["default"].WebVTT.processCues(window__default["default"], cues, this.el_);
  9835. // add unique class to each language text track & add settings styling if necessary
  9836. for (let i = 0; i < tracks.length; ++i) {
  9837. const track = tracks[i];
  9838. for (let j = 0; j < track.activeCues.length; ++j) {
  9839. const cueEl = track.activeCues[j].displayState;
  9840. addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
  9841. if (track.language) {
  9842. setAttribute(cueEl, 'lang', track.language);
  9843. }
  9844. }
  9845. if (this.player_.textTrackSettings) {
  9846. this.updateDisplayState(track);
  9847. }
  9848. }
  9849. }
  9850. }
  9851. Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
  9852. /**
  9853. * @file loading-spinner.js
  9854. */
  9855. /**
  9856. * A loading spinner for use during waiting/loading events.
  9857. *
  9858. * @extends Component
  9859. */
  9860. class LoadingSpinner extends Component {
  9861. /**
  9862. * Create the `LoadingSpinner`s DOM element.
  9863. *
  9864. * @return {Element}
  9865. * The dom element that gets created.
  9866. */
  9867. createEl() {
  9868. const isAudio = this.player_.isAudio();
  9869. const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
  9870. const controlText = createEl('span', {
  9871. className: 'vjs-control-text',
  9872. textContent: this.localize('{1} is loading.', [playerType])
  9873. });
  9874. const el = super.createEl('div', {
  9875. className: 'vjs-loading-spinner',
  9876. dir: 'ltr'
  9877. });
  9878. el.appendChild(controlText);
  9879. return el;
  9880. }
  9881. /**
  9882. * Update control text on languagechange
  9883. */
  9884. handleLanguagechange() {
  9885. this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
  9886. }
  9887. }
  9888. Component.registerComponent('LoadingSpinner', LoadingSpinner);
  9889. /**
  9890. * @file button.js
  9891. */
  9892. /**
  9893. * Base class for all buttons.
  9894. *
  9895. * @extends ClickableComponent
  9896. */
  9897. class Button extends ClickableComponent {
  9898. /**
  9899. * Create the `Button`s DOM element.
  9900. *
  9901. * @param {string} [tag="button"]
  9902. * The element's node type. This argument is IGNORED: no matter what
  9903. * is passed, it will always create a `button` element.
  9904. *
  9905. * @param {Object} [props={}]
  9906. * An object of properties that should be set on the element.
  9907. *
  9908. * @param {Object} [attributes={}]
  9909. * An object of attributes that should be set on the element.
  9910. *
  9911. * @return {Element}
  9912. * The element that gets created.
  9913. */
  9914. createEl(tag, props = {}, attributes = {}) {
  9915. tag = 'button';
  9916. props = Object.assign({
  9917. className: this.buildCSSClass()
  9918. }, props);
  9919. // Add attributes for button element
  9920. attributes = Object.assign({
  9921. // Necessary since the default button type is "submit"
  9922. type: 'button'
  9923. }, attributes);
  9924. const el = createEl(tag, props, attributes);
  9925. if (!this.player_.options_.experimentalSvgIcons) {
  9926. el.appendChild(createEl('span', {
  9927. className: 'vjs-icon-placeholder'
  9928. }, {
  9929. 'aria-hidden': true
  9930. }));
  9931. }
  9932. this.createControlTextEl(el);
  9933. return el;
  9934. }
  9935. /**
  9936. * Add a child `Component` inside of this `Button`.
  9937. *
  9938. * @param {string|Component} child
  9939. * The name or instance of a child to add.
  9940. *
  9941. * @param {Object} [options={}]
  9942. * The key/value store of options that will get passed to children of
  9943. * the child.
  9944. *
  9945. * @return {Component}
  9946. * The `Component` that gets added as a child. When using a string the
  9947. * `Component` will get created by this process.
  9948. *
  9949. * @deprecated since version 5
  9950. */
  9951. addChild(child, options = {}) {
  9952. const className = this.constructor.name;
  9953. log.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
  9954. // Avoid the error message generated by ClickableComponent's addChild method
  9955. return Component.prototype.addChild.call(this, child, options);
  9956. }
  9957. /**
  9958. * Enable the `Button` element so that it can be activated or clicked. Use this with
  9959. * {@link Button#disable}.
  9960. */
  9961. enable() {
  9962. super.enable();
  9963. this.el_.removeAttribute('disabled');
  9964. }
  9965. /**
  9966. * Disable the `Button` element so that it cannot be activated or clicked. Use this with
  9967. * {@link Button#enable}.
  9968. */
  9969. disable() {
  9970. super.disable();
  9971. this.el_.setAttribute('disabled', 'disabled');
  9972. }
  9973. /**
  9974. * This gets called when a `Button` has focus and `keydown` is triggered via a key
  9975. * press.
  9976. *
  9977. * @param {KeyboardEvent} event
  9978. * The event that caused this function to get called.
  9979. *
  9980. * @listens keydown
  9981. */
  9982. handleKeyDown(event) {
  9983. // Ignore Space or Enter key operation, which is handled by the browser for
  9984. // a button - though not for its super class, ClickableComponent. Also,
  9985. // prevent the event from propagating through the DOM and triggering Player
  9986. // hotkeys. We do not preventDefault here because we _want_ the browser to
  9987. // handle it.
  9988. if (keycode__default["default"].isEventKey(event, 'Space') || keycode__default["default"].isEventKey(event, 'Enter')) {
  9989. event.stopPropagation();
  9990. return;
  9991. }
  9992. // Pass keypress handling up for unsupported keys
  9993. super.handleKeyDown(event);
  9994. }
  9995. }
  9996. Component.registerComponent('Button', Button);
  9997. /**
  9998. * @file big-play-button.js
  9999. */
  10000. /**
  10001. * The initial play button that shows before the video has played. The hiding of the
  10002. * `BigPlayButton` get done via CSS and `Player` states.
  10003. *
  10004. * @extends Button
  10005. */
  10006. class BigPlayButton extends Button {
  10007. constructor(player, options) {
  10008. super(player, options);
  10009. this.mouseused_ = false;
  10010. this.setIcon('play');
  10011. this.on('mousedown', e => this.handleMouseDown(e));
  10012. }
  10013. /**
  10014. * Builds the default DOM `className`.
  10015. *
  10016. * @return {string}
  10017. * The DOM `className` for this object. Always returns 'vjs-big-play-button'.
  10018. */
  10019. buildCSSClass() {
  10020. return 'vjs-big-play-button';
  10021. }
  10022. /**
  10023. * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
  10024. * for more detailed information on what a click can be.
  10025. *
  10026. * @param {KeyboardEvent|MouseEvent|TouchEvent} event
  10027. * The `keydown`, `tap`, or `click` event that caused this function to be
  10028. * called.
  10029. *
  10030. * @listens tap
  10031. * @listens click
  10032. */
  10033. handleClick(event) {
  10034. const playPromise = this.player_.play();
  10035. // exit early if clicked via the mouse
  10036. if (this.mouseused_ && 'clientX' in event && 'clientY' in event) {
  10037. silencePromise(playPromise);
  10038. if (this.player_.tech(true)) {
  10039. this.player_.tech(true).focus();
  10040. }
  10041. return;
  10042. }
  10043. const cb = this.player_.getChild('controlBar');
  10044. const playToggle = cb && cb.getChild('playToggle');
  10045. if (!playToggle) {
  10046. this.player_.tech(true).focus();
  10047. return;
  10048. }
  10049. const playFocus = () => playToggle.focus();
  10050. if (isPromise(playPromise)) {
  10051. playPromise.then(playFocus, () => {});
  10052. } else {
  10053. this.setTimeout(playFocus, 1);
  10054. }
  10055. }
  10056. /**
  10057. * Event handler that is called when a `BigPlayButton` receives a
  10058. * `keydown` event.
  10059. *
  10060. * @param {KeyboardEvent} event
  10061. * The `keydown` event that caused this function to be called.
  10062. *
  10063. * @listens keydown
  10064. */
  10065. handleKeyDown(event) {
  10066. this.mouseused_ = false;
  10067. super.handleKeyDown(event);
  10068. }
  10069. /**
  10070. * Handle `mousedown` events on the `BigPlayButton`.
  10071. *
  10072. * @param {MouseEvent} event
  10073. * `mousedown` or `touchstart` event that triggered this function
  10074. *
  10075. * @listens mousedown
  10076. */
  10077. handleMouseDown(event) {
  10078. this.mouseused_ = true;
  10079. }
  10080. }
  10081. /**
  10082. * The text that should display over the `BigPlayButton`s controls. Added to for localization.
  10083. *
  10084. * @type {string}
  10085. * @protected
  10086. */
  10087. BigPlayButton.prototype.controlText_ = 'Play Video';
  10088. Component.registerComponent('BigPlayButton', BigPlayButton);
  10089. /**
  10090. * @file close-button.js
  10091. */
  10092. /**
  10093. * The `CloseButton` is a `{@link Button}` that fires a `close` event when
  10094. * it gets clicked.
  10095. *
  10096. * @extends Button
  10097. */
  10098. class CloseButton extends Button {
  10099. /**
  10100. * Creates an instance of the this class.
  10101. *
  10102. * @param { import('./player').default } player
  10103. * The `Player` that this class should be attached to.
  10104. *
  10105. * @param {Object} [options]
  10106. * The key/value store of player options.
  10107. */
  10108. constructor(player, options) {
  10109. super(player, options);
  10110. this.setIcon('cancel');
  10111. this.controlText(options && options.controlText || this.localize('Close'));
  10112. }
  10113. /**
  10114. * Builds the default DOM `className`.
  10115. *
  10116. * @return {string}
  10117. * The DOM `className` for this object.
  10118. */
  10119. buildCSSClass() {
  10120. return `vjs-close-button ${super.buildCSSClass()}`;
  10121. }
  10122. /**
  10123. * This gets called when a `CloseButton` gets clicked. See
  10124. * {@link ClickableComponent#handleClick} for more information on when
  10125. * this will be triggered
  10126. *
  10127. * @param {Event} event
  10128. * The `keydown`, `tap`, or `click` event that caused this function to be
  10129. * called.
  10130. *
  10131. * @listens tap
  10132. * @listens click
  10133. * @fires CloseButton#close
  10134. */
  10135. handleClick(event) {
  10136. /**
  10137. * Triggered when the a `CloseButton` is clicked.
  10138. *
  10139. * @event CloseButton#close
  10140. * @type {Event}
  10141. *
  10142. * @property {boolean} [bubbles=false]
  10143. * set to false so that the close event does not
  10144. * bubble up to parents if there is no listener
  10145. */
  10146. this.trigger({
  10147. type: 'close',
  10148. bubbles: false
  10149. });
  10150. }
  10151. /**
  10152. * Event handler that is called when a `CloseButton` receives a
  10153. * `keydown` event.
  10154. *
  10155. * By default, if the key is Esc, it will trigger a `click` event.
  10156. *
  10157. * @param {KeyboardEvent} event
  10158. * The `keydown` event that caused this function to be called.
  10159. *
  10160. * @listens keydown
  10161. */
  10162. handleKeyDown(event) {
  10163. // Esc button will trigger `click` event
  10164. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  10165. event.preventDefault();
  10166. event.stopPropagation();
  10167. this.trigger('click');
  10168. } else {
  10169. // Pass keypress handling up for unsupported keys
  10170. super.handleKeyDown(event);
  10171. }
  10172. }
  10173. }
  10174. Component.registerComponent('CloseButton', CloseButton);
  10175. /**
  10176. * @file play-toggle.js
  10177. */
  10178. /**
  10179. * Button to toggle between play and pause.
  10180. *
  10181. * @extends Button
  10182. */
  10183. class PlayToggle extends Button {
  10184. /**
  10185. * Creates an instance of this class.
  10186. *
  10187. * @param { import('./player').default } player
  10188. * The `Player` that this class should be attached to.
  10189. *
  10190. * @param {Object} [options={}]
  10191. * The key/value store of player options.
  10192. */
  10193. constructor(player, options = {}) {
  10194. super(player, options);
  10195. // show or hide replay icon
  10196. options.replay = options.replay === undefined || options.replay;
  10197. this.setIcon('play');
  10198. this.on(player, 'play', e => this.handlePlay(e));
  10199. this.on(player, 'pause', e => this.handlePause(e));
  10200. if (options.replay) {
  10201. this.on(player, 'ended', e => this.handleEnded(e));
  10202. }
  10203. }
  10204. /**
  10205. * Builds the default DOM `className`.
  10206. *
  10207. * @return {string}
  10208. * The DOM `className` for this object.
  10209. */
  10210. buildCSSClass() {
  10211. return `vjs-play-control ${super.buildCSSClass()}`;
  10212. }
  10213. /**
  10214. * This gets called when an `PlayToggle` is "clicked". See
  10215. * {@link ClickableComponent} for more detailed information on what a click can be.
  10216. *
  10217. * @param {Event} [event]
  10218. * The `keydown`, `tap`, or `click` event that caused this function to be
  10219. * called.
  10220. *
  10221. * @listens tap
  10222. * @listens click
  10223. */
  10224. handleClick(event) {
  10225. if (this.player_.paused()) {
  10226. silencePromise(this.player_.play());
  10227. } else {
  10228. this.player_.pause();
  10229. }
  10230. }
  10231. /**
  10232. * This gets called once after the video has ended and the user seeks so that
  10233. * we can change the replay button back to a play button.
  10234. *
  10235. * @param {Event} [event]
  10236. * The event that caused this function to run.
  10237. *
  10238. * @listens Player#seeked
  10239. */
  10240. handleSeeked(event) {
  10241. this.removeClass('vjs-ended');
  10242. if (this.player_.paused()) {
  10243. this.handlePause(event);
  10244. } else {
  10245. this.handlePlay(event);
  10246. }
  10247. }
  10248. /**
  10249. * Add the vjs-playing class to the element so it can change appearance.
  10250. *
  10251. * @param {Event} [event]
  10252. * The event that caused this function to run.
  10253. *
  10254. * @listens Player#play
  10255. */
  10256. handlePlay(event) {
  10257. this.removeClass('vjs-ended', 'vjs-paused');
  10258. this.addClass('vjs-playing');
  10259. // change the button text to "Pause"
  10260. this.setIcon('pause');
  10261. this.controlText('Pause');
  10262. }
  10263. /**
  10264. * Add the vjs-paused class to the element so it can change appearance.
  10265. *
  10266. * @param {Event} [event]
  10267. * The event that caused this function to run.
  10268. *
  10269. * @listens Player#pause
  10270. */
  10271. handlePause(event) {
  10272. this.removeClass('vjs-playing');
  10273. this.addClass('vjs-paused');
  10274. // change the button text to "Play"
  10275. this.setIcon('play');
  10276. this.controlText('Play');
  10277. }
  10278. /**
  10279. * Add the vjs-ended class to the element so it can change appearance
  10280. *
  10281. * @param {Event} [event]
  10282. * The event that caused this function to run.
  10283. *
  10284. * @listens Player#ended
  10285. */
  10286. handleEnded(event) {
  10287. this.removeClass('vjs-playing');
  10288. this.addClass('vjs-ended');
  10289. // change the button text to "Replay"
  10290. this.setIcon('replay');
  10291. this.controlText('Replay');
  10292. // on the next seek remove the replay button
  10293. this.one(this.player_, 'seeked', e => this.handleSeeked(e));
  10294. }
  10295. }
  10296. /**
  10297. * The text that should display over the `PlayToggle`s controls. Added for localization.
  10298. *
  10299. * @type {string}
  10300. * @protected
  10301. */
  10302. PlayToggle.prototype.controlText_ = 'Play';
  10303. Component.registerComponent('PlayToggle', PlayToggle);
  10304. /**
  10305. * @file time-display.js
  10306. */
  10307. /**
  10308. * Displays time information about the video
  10309. *
  10310. * @extends Component
  10311. */
  10312. class TimeDisplay extends Component {
  10313. /**
  10314. * Creates an instance of this class.
  10315. *
  10316. * @param { import('../../player').default } player
  10317. * The `Player` that this class should be attached to.
  10318. *
  10319. * @param {Object} [options]
  10320. * The key/value store of player options.
  10321. */
  10322. constructor(player, options) {
  10323. super(player, options);
  10324. this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e));
  10325. this.updateTextNode_();
  10326. }
  10327. /**
  10328. * Create the `Component`'s DOM element
  10329. *
  10330. * @return {Element}
  10331. * The element that was created.
  10332. */
  10333. createEl() {
  10334. const className = this.buildCSSClass();
  10335. const el = super.createEl('div', {
  10336. className: `${className} vjs-time-control vjs-control`
  10337. });
  10338. const span = createEl('span', {
  10339. className: 'vjs-control-text',
  10340. textContent: `${this.localize(this.labelText_)}\u00a0`
  10341. }, {
  10342. role: 'presentation'
  10343. });
  10344. el.appendChild(span);
  10345. this.contentEl_ = createEl('span', {
  10346. className: `${className}-display`
  10347. }, {
  10348. // span elements have no implicit role, but some screen readers (notably VoiceOver)
  10349. // treat them as a break between items in the DOM when using arrow keys
  10350. // (or left-to-right swipes on iOS) to read contents of a page. Using
  10351. // role='presentation' causes VoiceOver to NOT treat this span as a break.
  10352. role: 'presentation'
  10353. });
  10354. el.appendChild(this.contentEl_);
  10355. return el;
  10356. }
  10357. dispose() {
  10358. this.contentEl_ = null;
  10359. this.textNode_ = null;
  10360. super.dispose();
  10361. }
  10362. /**
  10363. * Updates the displayed time according to the `updateContent` function which is defined in the child class.
  10364. *
  10365. * @param {Event} [event]
  10366. * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called.
  10367. */
  10368. update(event) {
  10369. if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') {
  10370. return;
  10371. }
  10372. this.updateContent(event);
  10373. }
  10374. /**
  10375. * Updates the time display text node with a new time
  10376. *
  10377. * @param {number} [time=0] the time to update to
  10378. *
  10379. * @private
  10380. */
  10381. updateTextNode_(time = 0) {
  10382. time = formatTime(time);
  10383. if (this.formattedTime_ === time) {
  10384. return;
  10385. }
  10386. this.formattedTime_ = time;
  10387. this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
  10388. if (!this.contentEl_) {
  10389. return;
  10390. }
  10391. let oldNode = this.textNode_;
  10392. if (oldNode && this.contentEl_.firstChild !== oldNode) {
  10393. oldNode = null;
  10394. 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.');
  10395. }
  10396. this.textNode_ = document__default["default"].createTextNode(this.formattedTime_);
  10397. if (!this.textNode_) {
  10398. return;
  10399. }
  10400. if (oldNode) {
  10401. this.contentEl_.replaceChild(this.textNode_, oldNode);
  10402. } else {
  10403. this.contentEl_.appendChild(this.textNode_);
  10404. }
  10405. });
  10406. }
  10407. /**
  10408. * To be filled out in the child class, should update the displayed time
  10409. * in accordance with the fact that the current time has changed.
  10410. *
  10411. * @param {Event} [event]
  10412. * The `timeupdate` event that caused this to run.
  10413. *
  10414. * @listens Player#timeupdate
  10415. */
  10416. updateContent(event) {}
  10417. }
  10418. /**
  10419. * The text that is added to the `TimeDisplay` for screen reader users.
  10420. *
  10421. * @type {string}
  10422. * @private
  10423. */
  10424. TimeDisplay.prototype.labelText_ = 'Time';
  10425. /**
  10426. * The text that should display over the `TimeDisplay`s controls. Added to for localization.
  10427. *
  10428. * @type {string}
  10429. * @protected
  10430. *
  10431. * @deprecated in v7; controlText_ is not used in non-active display Components
  10432. */
  10433. TimeDisplay.prototype.controlText_ = 'Time';
  10434. Component.registerComponent('TimeDisplay', TimeDisplay);
  10435. /**
  10436. * @file current-time-display.js
  10437. */
  10438. /**
  10439. * Displays the current time
  10440. *
  10441. * @extends Component
  10442. */
  10443. class CurrentTimeDisplay extends TimeDisplay {
  10444. /**
  10445. * Builds the default DOM `className`.
  10446. *
  10447. * @return {string}
  10448. * The DOM `className` for this object.
  10449. */
  10450. buildCSSClass() {
  10451. return 'vjs-current-time';
  10452. }
  10453. /**
  10454. * Update current time display
  10455. *
  10456. * @param {Event} [event]
  10457. * The `timeupdate` event that caused this function to run.
  10458. *
  10459. * @listens Player#timeupdate
  10460. */
  10461. updateContent(event) {
  10462. // Allows for smooth scrubbing, when player can't keep up.
  10463. let time;
  10464. if (this.player_.ended()) {
  10465. time = this.player_.duration();
  10466. } else {
  10467. time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
  10468. }
  10469. this.updateTextNode_(time);
  10470. }
  10471. }
  10472. /**
  10473. * The text that is added to the `CurrentTimeDisplay` for screen reader users.
  10474. *
  10475. * @type {string}
  10476. * @private
  10477. */
  10478. CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
  10479. /**
  10480. * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
  10481. *
  10482. * @type {string}
  10483. * @protected
  10484. *
  10485. * @deprecated in v7; controlText_ is not used in non-active display Components
  10486. */
  10487. CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
  10488. Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
  10489. /**
  10490. * @file duration-display.js
  10491. */
  10492. /**
  10493. * Displays the duration
  10494. *
  10495. * @extends Component
  10496. */
  10497. class DurationDisplay extends TimeDisplay {
  10498. /**
  10499. * Creates an instance of this class.
  10500. *
  10501. * @param { import('../../player').default } player
  10502. * The `Player` that this class should be attached to.
  10503. *
  10504. * @param {Object} [options]
  10505. * The key/value store of player options.
  10506. */
  10507. constructor(player, options) {
  10508. super(player, options);
  10509. const updateContent = e => this.updateContent(e);
  10510. // we do not want to/need to throttle duration changes,
  10511. // as they should always display the changed duration as
  10512. // it has changed
  10513. this.on(player, 'durationchange', updateContent);
  10514. // Listen to loadstart because the player duration is reset when a new media element is loaded,
  10515. // but the durationchange on the user agent will not fire.
  10516. // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
  10517. this.on(player, 'loadstart', updateContent);
  10518. // Also listen for timeupdate (in the parent) and loadedmetadata because removing those
  10519. // listeners could have broken dependent applications/libraries. These
  10520. // can likely be removed for 7.0.
  10521. this.on(player, 'loadedmetadata', updateContent);
  10522. }
  10523. /**
  10524. * Builds the default DOM `className`.
  10525. *
  10526. * @return {string}
  10527. * The DOM `className` for this object.
  10528. */
  10529. buildCSSClass() {
  10530. return 'vjs-duration';
  10531. }
  10532. /**
  10533. * Update duration time display.
  10534. *
  10535. * @param {Event} [event]
  10536. * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
  10537. * this function to be called.
  10538. *
  10539. * @listens Player#durationchange
  10540. * @listens Player#timeupdate
  10541. * @listens Player#loadedmetadata
  10542. */
  10543. updateContent(event) {
  10544. const duration = this.player_.duration();
  10545. this.updateTextNode_(duration);
  10546. }
  10547. }
  10548. /**
  10549. * The text that is added to the `DurationDisplay` for screen reader users.
  10550. *
  10551. * @type {string}
  10552. * @private
  10553. */
  10554. DurationDisplay.prototype.labelText_ = 'Duration';
  10555. /**
  10556. * The text that should display over the `DurationDisplay`s controls. Added to for localization.
  10557. *
  10558. * @type {string}
  10559. * @protected
  10560. *
  10561. * @deprecated in v7; controlText_ is not used in non-active display Components
  10562. */
  10563. DurationDisplay.prototype.controlText_ = 'Duration';
  10564. Component.registerComponent('DurationDisplay', DurationDisplay);
  10565. /**
  10566. * @file time-divider.js
  10567. */
  10568. /**
  10569. * The separator between the current time and duration.
  10570. * Can be hidden if it's not needed in the design.
  10571. *
  10572. * @extends Component
  10573. */
  10574. class TimeDivider extends Component {
  10575. /**
  10576. * Create the component's DOM element
  10577. *
  10578. * @return {Element}
  10579. * The element that was created.
  10580. */
  10581. createEl() {
  10582. const el = super.createEl('div', {
  10583. className: 'vjs-time-control vjs-time-divider'
  10584. }, {
  10585. // this element and its contents can be hidden from assistive techs since
  10586. // it is made extraneous by the announcement of the control text
  10587. // for the current time and duration displays
  10588. 'aria-hidden': true
  10589. });
  10590. const div = super.createEl('div');
  10591. const span = super.createEl('span', {
  10592. textContent: '/'
  10593. });
  10594. div.appendChild(span);
  10595. el.appendChild(div);
  10596. return el;
  10597. }
  10598. }
  10599. Component.registerComponent('TimeDivider', TimeDivider);
  10600. /**
  10601. * @file remaining-time-display.js
  10602. */
  10603. /**
  10604. * Displays the time left in the video
  10605. *
  10606. * @extends Component
  10607. */
  10608. class RemainingTimeDisplay extends TimeDisplay {
  10609. /**
  10610. * Creates an instance of this class.
  10611. *
  10612. * @param { import('../../player').default } player
  10613. * The `Player` that this class should be attached to.
  10614. *
  10615. * @param {Object} [options]
  10616. * The key/value store of player options.
  10617. */
  10618. constructor(player, options) {
  10619. super(player, options);
  10620. this.on(player, 'durationchange', e => this.updateContent(e));
  10621. }
  10622. /**
  10623. * Builds the default DOM `className`.
  10624. *
  10625. * @return {string}
  10626. * The DOM `className` for this object.
  10627. */
  10628. buildCSSClass() {
  10629. return 'vjs-remaining-time';
  10630. }
  10631. /**
  10632. * Create the `Component`'s DOM element with the "minus" character prepend to the time
  10633. *
  10634. * @return {Element}
  10635. * The element that was created.
  10636. */
  10637. createEl() {
  10638. const el = super.createEl();
  10639. if (this.options_.displayNegative !== false) {
  10640. el.insertBefore(createEl('span', {}, {
  10641. 'aria-hidden': true
  10642. }, '-'), this.contentEl_);
  10643. }
  10644. return el;
  10645. }
  10646. /**
  10647. * Update remaining time display.
  10648. *
  10649. * @param {Event} [event]
  10650. * The `timeupdate` or `durationchange` event that caused this to run.
  10651. *
  10652. * @listens Player#timeupdate
  10653. * @listens Player#durationchange
  10654. */
  10655. updateContent(event) {
  10656. if (typeof this.player_.duration() !== 'number') {
  10657. return;
  10658. }
  10659. let time;
  10660. // @deprecated We should only use remainingTimeDisplay
  10661. // as of video.js 7
  10662. if (this.player_.ended()) {
  10663. time = 0;
  10664. } else if (this.player_.remainingTimeDisplay) {
  10665. time = this.player_.remainingTimeDisplay();
  10666. } else {
  10667. time = this.player_.remainingTime();
  10668. }
  10669. this.updateTextNode_(time);
  10670. }
  10671. }
  10672. /**
  10673. * The text that is added to the `RemainingTimeDisplay` for screen reader users.
  10674. *
  10675. * @type {string}
  10676. * @private
  10677. */
  10678. RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
  10679. /**
  10680. * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
  10681. *
  10682. * @type {string}
  10683. * @protected
  10684. *
  10685. * @deprecated in v7; controlText_ is not used in non-active display Components
  10686. */
  10687. RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
  10688. Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
  10689. /**
  10690. * @file live-display.js
  10691. */
  10692. // TODO - Future make it click to snap to live
  10693. /**
  10694. * Displays the live indicator when duration is Infinity.
  10695. *
  10696. * @extends Component
  10697. */
  10698. class LiveDisplay extends Component {
  10699. /**
  10700. * Creates an instance of this class.
  10701. *
  10702. * @param { import('./player').default } player
  10703. * The `Player` that this class should be attached to.
  10704. *
  10705. * @param {Object} [options]
  10706. * The key/value store of player options.
  10707. */
  10708. constructor(player, options) {
  10709. super(player, options);
  10710. this.updateShowing();
  10711. this.on(this.player(), 'durationchange', e => this.updateShowing(e));
  10712. }
  10713. /**
  10714. * Create the `Component`'s DOM element
  10715. *
  10716. * @return {Element}
  10717. * The element that was created.
  10718. */
  10719. createEl() {
  10720. const el = super.createEl('div', {
  10721. className: 'vjs-live-control vjs-control'
  10722. });
  10723. this.contentEl_ = createEl('div', {
  10724. className: 'vjs-live-display'
  10725. }, {
  10726. 'aria-live': 'off'
  10727. });
  10728. this.contentEl_.appendChild(createEl('span', {
  10729. className: 'vjs-control-text',
  10730. textContent: `${this.localize('Stream Type')}\u00a0`
  10731. }));
  10732. this.contentEl_.appendChild(document__default["default"].createTextNode(this.localize('LIVE')));
  10733. el.appendChild(this.contentEl_);
  10734. return el;
  10735. }
  10736. dispose() {
  10737. this.contentEl_ = null;
  10738. super.dispose();
  10739. }
  10740. /**
  10741. * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
  10742. * it accordingly
  10743. *
  10744. * @param {Event} [event]
  10745. * The {@link Player#durationchange} event that caused this function to run.
  10746. *
  10747. * @listens Player#durationchange
  10748. */
  10749. updateShowing(event) {
  10750. if (this.player().duration() === Infinity) {
  10751. this.show();
  10752. } else {
  10753. this.hide();
  10754. }
  10755. }
  10756. }
  10757. Component.registerComponent('LiveDisplay', LiveDisplay);
  10758. /**
  10759. * @file seek-to-live.js
  10760. */
  10761. /**
  10762. * Displays the live indicator when duration is Infinity.
  10763. *
  10764. * @extends Component
  10765. */
  10766. class SeekToLive extends Button {
  10767. /**
  10768. * Creates an instance of this class.
  10769. *
  10770. * @param { import('./player').default } player
  10771. * The `Player` that this class should be attached to.
  10772. *
  10773. * @param {Object} [options]
  10774. * The key/value store of player options.
  10775. */
  10776. constructor(player, options) {
  10777. super(player, options);
  10778. this.updateLiveEdgeStatus();
  10779. if (this.player_.liveTracker) {
  10780. this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
  10781. this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
  10782. }
  10783. }
  10784. /**
  10785. * Create the `Component`'s DOM element
  10786. *
  10787. * @return {Element}
  10788. * The element that was created.
  10789. */
  10790. createEl() {
  10791. const el = super.createEl('button', {
  10792. className: 'vjs-seek-to-live-control vjs-control'
  10793. });
  10794. this.setIcon('circle', el);
  10795. this.textEl_ = createEl('span', {
  10796. className: 'vjs-seek-to-live-text',
  10797. textContent: this.localize('LIVE')
  10798. }, {
  10799. 'aria-hidden': 'true'
  10800. });
  10801. el.appendChild(this.textEl_);
  10802. return el;
  10803. }
  10804. /**
  10805. * Update the state of this button if we are at the live edge
  10806. * or not
  10807. */
  10808. updateLiveEdgeStatus() {
  10809. // default to live edge
  10810. if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
  10811. this.setAttribute('aria-disabled', true);
  10812. this.addClass('vjs-at-live-edge');
  10813. this.controlText('Seek to live, currently playing live');
  10814. } else {
  10815. this.setAttribute('aria-disabled', false);
  10816. this.removeClass('vjs-at-live-edge');
  10817. this.controlText('Seek to live, currently behind live');
  10818. }
  10819. }
  10820. /**
  10821. * On click bring us as near to the live point as possible.
  10822. * This requires that we wait for the next `live-seekable-change`
  10823. * event which will happen every segment length seconds.
  10824. */
  10825. handleClick() {
  10826. this.player_.liveTracker.seekToLiveEdge();
  10827. }
  10828. /**
  10829. * Dispose of the element and stop tracking
  10830. */
  10831. dispose() {
  10832. if (this.player_.liveTracker) {
  10833. this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
  10834. }
  10835. this.textEl_ = null;
  10836. super.dispose();
  10837. }
  10838. }
  10839. /**
  10840. * The text that should display over the `SeekToLive`s control. Added for localization.
  10841. *
  10842. * @type {string}
  10843. * @protected
  10844. */
  10845. SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
  10846. Component.registerComponent('SeekToLive', SeekToLive);
  10847. /**
  10848. * @file num.js
  10849. * @module num
  10850. */
  10851. /**
  10852. * Keep a number between a min and a max value
  10853. *
  10854. * @param {number} number
  10855. * The number to clamp
  10856. *
  10857. * @param {number} min
  10858. * The minimum value
  10859. * @param {number} max
  10860. * The maximum value
  10861. *
  10862. * @return {number}
  10863. * the clamped number
  10864. */
  10865. function clamp(number, min, max) {
  10866. number = Number(number);
  10867. return Math.min(max, Math.max(min, isNaN(number) ? min : number));
  10868. }
  10869. var Num = /*#__PURE__*/Object.freeze({
  10870. __proto__: null,
  10871. clamp: clamp
  10872. });
  10873. /**
  10874. * @file slider.js
  10875. */
  10876. /**
  10877. * The base functionality for a slider. Can be vertical or horizontal.
  10878. * For instance the volume bar or the seek bar on a video is a slider.
  10879. *
  10880. * @extends Component
  10881. */
  10882. class Slider extends Component {
  10883. /**
  10884. * Create an instance of this class
  10885. *
  10886. * @param { import('../player').default } player
  10887. * The `Player` that this class should be attached to.
  10888. *
  10889. * @param {Object} [options]
  10890. * The key/value store of player options.
  10891. */
  10892. constructor(player, options) {
  10893. super(player, options);
  10894. this.handleMouseDown_ = e => this.handleMouseDown(e);
  10895. this.handleMouseUp_ = e => this.handleMouseUp(e);
  10896. this.handleKeyDown_ = e => this.handleKeyDown(e);
  10897. this.handleClick_ = e => this.handleClick(e);
  10898. this.handleMouseMove_ = e => this.handleMouseMove(e);
  10899. this.update_ = e => this.update(e);
  10900. // Set property names to bar to match with the child Slider class is looking for
  10901. this.bar = this.getChild(this.options_.barName);
  10902. // Set a horizontal or vertical class on the slider depending on the slider type
  10903. this.vertical(!!this.options_.vertical);
  10904. this.enable();
  10905. }
  10906. /**
  10907. * Are controls are currently enabled for this slider or not.
  10908. *
  10909. * @return {boolean}
  10910. * true if controls are enabled, false otherwise
  10911. */
  10912. enabled() {
  10913. return this.enabled_;
  10914. }
  10915. /**
  10916. * Enable controls for this slider if they are disabled
  10917. */
  10918. enable() {
  10919. if (this.enabled()) {
  10920. return;
  10921. }
  10922. this.on('mousedown', this.handleMouseDown_);
  10923. this.on('touchstart', this.handleMouseDown_);
  10924. this.on('keydown', this.handleKeyDown_);
  10925. this.on('click', this.handleClick_);
  10926. // TODO: deprecated, controlsvisible does not seem to be fired
  10927. this.on(this.player_, 'controlsvisible', this.update);
  10928. if (this.playerEvent) {
  10929. this.on(this.player_, this.playerEvent, this.update);
  10930. }
  10931. this.removeClass('disabled');
  10932. this.setAttribute('tabindex', 0);
  10933. this.enabled_ = true;
  10934. }
  10935. /**
  10936. * Disable controls for this slider if they are enabled
  10937. */
  10938. disable() {
  10939. if (!this.enabled()) {
  10940. return;
  10941. }
  10942. const doc = this.bar.el_.ownerDocument;
  10943. this.off('mousedown', this.handleMouseDown_);
  10944. this.off('touchstart', this.handleMouseDown_);
  10945. this.off('keydown', this.handleKeyDown_);
  10946. this.off('click', this.handleClick_);
  10947. this.off(this.player_, 'controlsvisible', this.update_);
  10948. this.off(doc, 'mousemove', this.handleMouseMove_);
  10949. this.off(doc, 'mouseup', this.handleMouseUp_);
  10950. this.off(doc, 'touchmove', this.handleMouseMove_);
  10951. this.off(doc, 'touchend', this.handleMouseUp_);
  10952. this.removeAttribute('tabindex');
  10953. this.addClass('disabled');
  10954. if (this.playerEvent) {
  10955. this.off(this.player_, this.playerEvent, this.update);
  10956. }
  10957. this.enabled_ = false;
  10958. }
  10959. /**
  10960. * Create the `Slider`s DOM element.
  10961. *
  10962. * @param {string} type
  10963. * Type of element to create.
  10964. *
  10965. * @param {Object} [props={}]
  10966. * List of properties in Object form.
  10967. *
  10968. * @param {Object} [attributes={}]
  10969. * list of attributes in Object form.
  10970. *
  10971. * @return {Element}
  10972. * The element that gets created.
  10973. */
  10974. createEl(type, props = {}, attributes = {}) {
  10975. // Add the slider element class to all sub classes
  10976. props.className = props.className + ' vjs-slider';
  10977. props = Object.assign({
  10978. tabIndex: 0
  10979. }, props);
  10980. attributes = Object.assign({
  10981. 'role': 'slider',
  10982. 'aria-valuenow': 0,
  10983. 'aria-valuemin': 0,
  10984. 'aria-valuemax': 100
  10985. }, attributes);
  10986. return super.createEl(type, props, attributes);
  10987. }
  10988. /**
  10989. * Handle `mousedown` or `touchstart` events on the `Slider`.
  10990. *
  10991. * @param {MouseEvent} event
  10992. * `mousedown` or `touchstart` event that triggered this function
  10993. *
  10994. * @listens mousedown
  10995. * @listens touchstart
  10996. * @fires Slider#slideractive
  10997. */
  10998. handleMouseDown(event) {
  10999. const doc = this.bar.el_.ownerDocument;
  11000. if (event.type === 'mousedown') {
  11001. event.preventDefault();
  11002. }
  11003. // Do not call preventDefault() on touchstart in Chrome
  11004. // to avoid console warnings. Use a 'touch-action: none' style
  11005. // instead to prevent unintended scrolling.
  11006. // https://developers.google.com/web/updates/2017/01/scrolling-intervention
  11007. if (event.type === 'touchstart' && !IS_CHROME) {
  11008. event.preventDefault();
  11009. }
  11010. blockTextSelection();
  11011. this.addClass('vjs-sliding');
  11012. /**
  11013. * Triggered when the slider is in an active state
  11014. *
  11015. * @event Slider#slideractive
  11016. * @type {MouseEvent}
  11017. */
  11018. this.trigger('slideractive');
  11019. this.on(doc, 'mousemove', this.handleMouseMove_);
  11020. this.on(doc, 'mouseup', this.handleMouseUp_);
  11021. this.on(doc, 'touchmove', this.handleMouseMove_);
  11022. this.on(doc, 'touchend', this.handleMouseUp_);
  11023. this.handleMouseMove(event, true);
  11024. }
  11025. /**
  11026. * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
  11027. * The `mousemove` and `touchmove` events will only only trigger this function during
  11028. * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
  11029. * {@link Slider#handleMouseUp}.
  11030. *
  11031. * @param {MouseEvent} event
  11032. * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
  11033. * this function
  11034. * @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.
  11035. *
  11036. * @listens mousemove
  11037. * @listens touchmove
  11038. */
  11039. handleMouseMove(event) {}
  11040. /**
  11041. * Handle `mouseup` or `touchend` events on the `Slider`.
  11042. *
  11043. * @param {MouseEvent} event
  11044. * `mouseup` or `touchend` event that triggered this function.
  11045. *
  11046. * @listens touchend
  11047. * @listens mouseup
  11048. * @fires Slider#sliderinactive
  11049. */
  11050. handleMouseUp(event) {
  11051. const doc = this.bar.el_.ownerDocument;
  11052. unblockTextSelection();
  11053. this.removeClass('vjs-sliding');
  11054. /**
  11055. * Triggered when the slider is no longer in an active state.
  11056. *
  11057. * @event Slider#sliderinactive
  11058. * @type {Event}
  11059. */
  11060. this.trigger('sliderinactive');
  11061. this.off(doc, 'mousemove', this.handleMouseMove_);
  11062. this.off(doc, 'mouseup', this.handleMouseUp_);
  11063. this.off(doc, 'touchmove', this.handleMouseMove_);
  11064. this.off(doc, 'touchend', this.handleMouseUp_);
  11065. this.update();
  11066. }
  11067. /**
  11068. * Update the progress bar of the `Slider`.
  11069. *
  11070. * @return {number}
  11071. * The percentage of progress the progress bar represents as a
  11072. * number from 0 to 1.
  11073. */
  11074. update() {
  11075. // In VolumeBar init we have a setTimeout for update that pops and update
  11076. // to the end of the execution stack. The player is destroyed before then
  11077. // update will cause an error
  11078. // If there's no bar...
  11079. if (!this.el_ || !this.bar) {
  11080. return;
  11081. }
  11082. // clamp progress between 0 and 1
  11083. // and only round to four decimal places, as we round to two below
  11084. const progress = this.getProgress();
  11085. if (progress === this.progress_) {
  11086. return progress;
  11087. }
  11088. this.progress_ = progress;
  11089. this.requestNamedAnimationFrame('Slider#update', () => {
  11090. // Set the new bar width or height
  11091. const sizeKey = this.vertical() ? 'height' : 'width';
  11092. // Convert to a percentage for css value
  11093. this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
  11094. });
  11095. return progress;
  11096. }
  11097. /**
  11098. * Get the percentage of the bar that should be filled
  11099. * but clamped and rounded.
  11100. *
  11101. * @return {number}
  11102. * percentage filled that the slider is
  11103. */
  11104. getProgress() {
  11105. return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
  11106. }
  11107. /**
  11108. * Calculate distance for slider
  11109. *
  11110. * @param {Event} event
  11111. * The event that caused this function to run.
  11112. *
  11113. * @return {number}
  11114. * The current position of the Slider.
  11115. * - position.x for vertical `Slider`s
  11116. * - position.y for horizontal `Slider`s
  11117. */
  11118. calculateDistance(event) {
  11119. const position = getPointerPosition(this.el_, event);
  11120. if (this.vertical()) {
  11121. return position.y;
  11122. }
  11123. return position.x;
  11124. }
  11125. /**
  11126. * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
  11127. * arrow keys. This function will only be called when the slider has focus. See
  11128. * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
  11129. *
  11130. * @param {KeyboardEvent} event
  11131. * the `keydown` event that caused this function to run.
  11132. *
  11133. * @listens keydown
  11134. */
  11135. handleKeyDown(event) {
  11136. // Left and Down Arrows
  11137. if (keycode__default["default"].isEventKey(event, 'Left') || keycode__default["default"].isEventKey(event, 'Down')) {
  11138. event.preventDefault();
  11139. event.stopPropagation();
  11140. this.stepBack();
  11141. // Up and Right Arrows
  11142. } else if (keycode__default["default"].isEventKey(event, 'Right') || keycode__default["default"].isEventKey(event, 'Up')) {
  11143. event.preventDefault();
  11144. event.stopPropagation();
  11145. this.stepForward();
  11146. } else {
  11147. // Pass keydown handling up for unsupported keys
  11148. super.handleKeyDown(event);
  11149. }
  11150. }
  11151. /**
  11152. * Listener for click events on slider, used to prevent clicks
  11153. * from bubbling up to parent elements like button menus.
  11154. *
  11155. * @param {Object} event
  11156. * Event that caused this object to run
  11157. */
  11158. handleClick(event) {
  11159. event.stopPropagation();
  11160. event.preventDefault();
  11161. }
  11162. /**
  11163. * Get/set if slider is horizontal for vertical
  11164. *
  11165. * @param {boolean} [bool]
  11166. * - true if slider is vertical,
  11167. * - false is horizontal
  11168. *
  11169. * @return {boolean}
  11170. * - true if slider is vertical, and getting
  11171. * - false if the slider is horizontal, and getting
  11172. */
  11173. vertical(bool) {
  11174. if (bool === undefined) {
  11175. return this.vertical_ || false;
  11176. }
  11177. this.vertical_ = !!bool;
  11178. if (this.vertical_) {
  11179. this.addClass('vjs-slider-vertical');
  11180. } else {
  11181. this.addClass('vjs-slider-horizontal');
  11182. }
  11183. }
  11184. }
  11185. Component.registerComponent('Slider', Slider);
  11186. /**
  11187. * @file load-progress-bar.js
  11188. */
  11189. // get the percent width of a time compared to the total end
  11190. const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
  11191. /**
  11192. * Shows loading progress
  11193. *
  11194. * @extends Component
  11195. */
  11196. class LoadProgressBar extends Component {
  11197. /**
  11198. * Creates an instance of this class.
  11199. *
  11200. * @param { import('../../player').default } player
  11201. * The `Player` that this class should be attached to.
  11202. *
  11203. * @param {Object} [options]
  11204. * The key/value store of player options.
  11205. */
  11206. constructor(player, options) {
  11207. super(player, options);
  11208. this.partEls_ = [];
  11209. this.on(player, 'progress', e => this.update(e));
  11210. }
  11211. /**
  11212. * Create the `Component`'s DOM element
  11213. *
  11214. * @return {Element}
  11215. * The element that was created.
  11216. */
  11217. createEl() {
  11218. const el = super.createEl('div', {
  11219. className: 'vjs-load-progress'
  11220. });
  11221. const wrapper = createEl('span', {
  11222. className: 'vjs-control-text'
  11223. });
  11224. const loadedText = createEl('span', {
  11225. textContent: this.localize('Loaded')
  11226. });
  11227. const separator = document__default["default"].createTextNode(': ');
  11228. this.percentageEl_ = createEl('span', {
  11229. className: 'vjs-control-text-loaded-percentage',
  11230. textContent: '0%'
  11231. });
  11232. el.appendChild(wrapper);
  11233. wrapper.appendChild(loadedText);
  11234. wrapper.appendChild(separator);
  11235. wrapper.appendChild(this.percentageEl_);
  11236. return el;
  11237. }
  11238. dispose() {
  11239. this.partEls_ = null;
  11240. this.percentageEl_ = null;
  11241. super.dispose();
  11242. }
  11243. /**
  11244. * Update progress bar
  11245. *
  11246. * @param {Event} [event]
  11247. * The `progress` event that caused this function to run.
  11248. *
  11249. * @listens Player#progress
  11250. */
  11251. update(event) {
  11252. this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
  11253. const liveTracker = this.player_.liveTracker;
  11254. const buffered = this.player_.buffered();
  11255. const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
  11256. const bufferedEnd = this.player_.bufferedEnd();
  11257. const children = this.partEls_;
  11258. const percent = percentify(bufferedEnd, duration);
  11259. if (this.percent_ !== percent) {
  11260. // update the width of the progress bar
  11261. this.el_.style.width = percent;
  11262. // update the control-text
  11263. textContent(this.percentageEl_, percent);
  11264. this.percent_ = percent;
  11265. }
  11266. // add child elements to represent the individual buffered time ranges
  11267. for (let i = 0; i < buffered.length; i++) {
  11268. const start = buffered.start(i);
  11269. const end = buffered.end(i);
  11270. let part = children[i];
  11271. if (!part) {
  11272. part = this.el_.appendChild(createEl());
  11273. children[i] = part;
  11274. }
  11275. // only update if changed
  11276. if (part.dataset.start === start && part.dataset.end === end) {
  11277. continue;
  11278. }
  11279. part.dataset.start = start;
  11280. part.dataset.end = end;
  11281. // set the percent based on the width of the progress bar (bufferedEnd)
  11282. part.style.left = percentify(start, bufferedEnd);
  11283. part.style.width = percentify(end - start, bufferedEnd);
  11284. }
  11285. // remove unused buffered range elements
  11286. for (let i = children.length; i > buffered.length; i--) {
  11287. this.el_.removeChild(children[i - 1]);
  11288. }
  11289. children.length = buffered.length;
  11290. });
  11291. }
  11292. }
  11293. Component.registerComponent('LoadProgressBar', LoadProgressBar);
  11294. /**
  11295. * @file time-tooltip.js
  11296. */
  11297. /**
  11298. * Time tooltips display a time above the progress bar.
  11299. *
  11300. * @extends Component
  11301. */
  11302. class TimeTooltip extends Component {
  11303. /**
  11304. * Creates an instance of this class.
  11305. *
  11306. * @param { import('../../player').default } player
  11307. * The {@link Player} that this class should be attached to.
  11308. *
  11309. * @param {Object} [options]
  11310. * The key/value store of player options.
  11311. */
  11312. constructor(player, options) {
  11313. super(player, options);
  11314. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  11315. }
  11316. /**
  11317. * Create the time tooltip DOM element
  11318. *
  11319. * @return {Element}
  11320. * The element that was created.
  11321. */
  11322. createEl() {
  11323. return super.createEl('div', {
  11324. className: 'vjs-time-tooltip'
  11325. }, {
  11326. 'aria-hidden': 'true'
  11327. });
  11328. }
  11329. /**
  11330. * Updates the position of the time tooltip relative to the `SeekBar`.
  11331. *
  11332. * @param {Object} seekBarRect
  11333. * The `ClientRect` for the {@link SeekBar} element.
  11334. *
  11335. * @param {number} seekBarPoint
  11336. * A number from 0 to 1, representing a horizontal reference point
  11337. * from the left edge of the {@link SeekBar}
  11338. */
  11339. update(seekBarRect, seekBarPoint, content) {
  11340. const tooltipRect = findPosition(this.el_);
  11341. const playerRect = getBoundingClientRect(this.player_.el());
  11342. const seekBarPointPx = seekBarRect.width * seekBarPoint;
  11343. // do nothing if either rect isn't available
  11344. // for example, if the player isn't in the DOM for testing
  11345. if (!playerRect || !tooltipRect) {
  11346. return;
  11347. }
  11348. // This is the space left of the `seekBarPoint` available within the bounds
  11349. // of the player. We calculate any gap between the left edge of the player
  11350. // and the left edge of the `SeekBar` and add the number of pixels in the
  11351. // `SeekBar` before hitting the `seekBarPoint`
  11352. const spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
  11353. // This is the space right of the `seekBarPoint` available within the bounds
  11354. // of the player. We calculate the number of pixels from the `seekBarPoint`
  11355. // to the right edge of the `SeekBar` and add to that any gap between the
  11356. // right edge of the `SeekBar` and the player.
  11357. const spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
  11358. // This is the number of pixels by which the tooltip will need to be pulled
  11359. // further to the right to center it over the `seekBarPoint`.
  11360. let pullTooltipBy = tooltipRect.width / 2;
  11361. // Adjust the `pullTooltipBy` distance to the left or right depending on
  11362. // the results of the space calculations above.
  11363. if (spaceLeftOfPoint < pullTooltipBy) {
  11364. pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
  11365. } else if (spaceRightOfPoint < pullTooltipBy) {
  11366. pullTooltipBy = spaceRightOfPoint;
  11367. }
  11368. // Due to the imprecision of decimal/ratio based calculations and varying
  11369. // rounding behaviors, there are cases where the spacing adjustment is off
  11370. // by a pixel or two. This adds insurance to these calculations.
  11371. if (pullTooltipBy < 0) {
  11372. pullTooltipBy = 0;
  11373. } else if (pullTooltipBy > tooltipRect.width) {
  11374. pullTooltipBy = tooltipRect.width;
  11375. }
  11376. // prevent small width fluctuations within 0.4px from
  11377. // changing the value below.
  11378. // This really helps for live to prevent the play
  11379. // progress time tooltip from jittering
  11380. pullTooltipBy = Math.round(pullTooltipBy);
  11381. this.el_.style.right = `-${pullTooltipBy}px`;
  11382. this.write(content);
  11383. }
  11384. /**
  11385. * Write the time to the tooltip DOM element.
  11386. *
  11387. * @param {string} content
  11388. * The formatted time for the tooltip.
  11389. */
  11390. write(content) {
  11391. textContent(this.el_, content);
  11392. }
  11393. /**
  11394. * Updates the position of the time tooltip relative to the `SeekBar`.
  11395. *
  11396. * @param {Object} seekBarRect
  11397. * The `ClientRect` for the {@link SeekBar} element.
  11398. *
  11399. * @param {number} seekBarPoint
  11400. * A number from 0 to 1, representing a horizontal reference point
  11401. * from the left edge of the {@link SeekBar}
  11402. *
  11403. * @param {number} time
  11404. * The time to update the tooltip to, not used during live playback
  11405. *
  11406. * @param {Function} cb
  11407. * A function that will be called during the request animation frame
  11408. * for tooltips that need to do additional animations from the default
  11409. */
  11410. updateTime(seekBarRect, seekBarPoint, time, cb) {
  11411. this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
  11412. let content;
  11413. const duration = this.player_.duration();
  11414. if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
  11415. const liveWindow = this.player_.liveTracker.liveWindow();
  11416. const secondsBehind = liveWindow - seekBarPoint * liveWindow;
  11417. content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
  11418. } else {
  11419. content = formatTime(time, duration);
  11420. }
  11421. this.update(seekBarRect, seekBarPoint, content);
  11422. if (cb) {
  11423. cb();
  11424. }
  11425. });
  11426. }
  11427. }
  11428. Component.registerComponent('TimeTooltip', TimeTooltip);
  11429. /**
  11430. * @file play-progress-bar.js
  11431. */
  11432. /**
  11433. * Used by {@link SeekBar} to display media playback progress as part of the
  11434. * {@link ProgressControl}.
  11435. *
  11436. * @extends Component
  11437. */
  11438. class PlayProgressBar extends Component {
  11439. /**
  11440. * Creates an instance of this class.
  11441. *
  11442. * @param { import('../../player').default } player
  11443. * The {@link Player} that this class should be attached to.
  11444. *
  11445. * @param {Object} [options]
  11446. * The key/value store of player options.
  11447. */
  11448. constructor(player, options) {
  11449. super(player, options);
  11450. this.setIcon('circle');
  11451. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  11452. }
  11453. /**
  11454. * Create the the DOM element for this class.
  11455. *
  11456. * @return {Element}
  11457. * The element that was created.
  11458. */
  11459. createEl() {
  11460. return super.createEl('div', {
  11461. className: 'vjs-play-progress vjs-slider-bar'
  11462. }, {
  11463. 'aria-hidden': 'true'
  11464. });
  11465. }
  11466. /**
  11467. * Enqueues updates to its own DOM as well as the DOM of its
  11468. * {@link TimeTooltip} child.
  11469. *
  11470. * @param {Object} seekBarRect
  11471. * The `ClientRect` for the {@link SeekBar} element.
  11472. *
  11473. * @param {number} seekBarPoint
  11474. * A number from 0 to 1, representing a horizontal reference point
  11475. * from the left edge of the {@link SeekBar}
  11476. */
  11477. update(seekBarRect, seekBarPoint) {
  11478. const timeTooltip = this.getChild('timeTooltip');
  11479. if (!timeTooltip) {
  11480. return;
  11481. }
  11482. const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
  11483. timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
  11484. }
  11485. }
  11486. /**
  11487. * Default options for {@link PlayProgressBar}.
  11488. *
  11489. * @type {Object}
  11490. * @private
  11491. */
  11492. PlayProgressBar.prototype.options_ = {
  11493. children: []
  11494. };
  11495. // Time tooltips should not be added to a player on mobile devices
  11496. if (!IS_IOS && !IS_ANDROID) {
  11497. PlayProgressBar.prototype.options_.children.push('timeTooltip');
  11498. }
  11499. Component.registerComponent('PlayProgressBar', PlayProgressBar);
  11500. /**
  11501. * @file mouse-time-display.js
  11502. */
  11503. /**
  11504. * The {@link MouseTimeDisplay} component tracks mouse movement over the
  11505. * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
  11506. * indicating the time which is represented by a given point in the
  11507. * {@link ProgressControl}.
  11508. *
  11509. * @extends Component
  11510. */
  11511. class MouseTimeDisplay extends Component {
  11512. /**
  11513. * Creates an instance of this class.
  11514. *
  11515. * @param { import('../../player').default } player
  11516. * The {@link Player} that this class should be attached to.
  11517. *
  11518. * @param {Object} [options]
  11519. * The key/value store of player options.
  11520. */
  11521. constructor(player, options) {
  11522. super(player, options);
  11523. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  11524. }
  11525. /**
  11526. * Create the DOM element for this class.
  11527. *
  11528. * @return {Element}
  11529. * The element that was created.
  11530. */
  11531. createEl() {
  11532. return super.createEl('div', {
  11533. className: 'vjs-mouse-display'
  11534. });
  11535. }
  11536. /**
  11537. * Enqueues updates to its own DOM as well as the DOM of its
  11538. * {@link TimeTooltip} child.
  11539. *
  11540. * @param {Object} seekBarRect
  11541. * The `ClientRect` for the {@link SeekBar} element.
  11542. *
  11543. * @param {number} seekBarPoint
  11544. * A number from 0 to 1, representing a horizontal reference point
  11545. * from the left edge of the {@link SeekBar}
  11546. */
  11547. update(seekBarRect, seekBarPoint) {
  11548. const time = seekBarPoint * this.player_.duration();
  11549. this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
  11550. this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
  11551. });
  11552. }
  11553. }
  11554. /**
  11555. * Default options for `MouseTimeDisplay`
  11556. *
  11557. * @type {Object}
  11558. * @private
  11559. */
  11560. MouseTimeDisplay.prototype.options_ = {
  11561. children: ['timeTooltip']
  11562. };
  11563. Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
  11564. /**
  11565. * @file seek-bar.js
  11566. */
  11567. // The number of seconds the `step*` functions move the timeline.
  11568. const STEP_SECONDS = 5;
  11569. // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
  11570. const PAGE_KEY_MULTIPLIER = 12;
  11571. /**
  11572. * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
  11573. * as its `bar`.
  11574. *
  11575. * @extends Slider
  11576. */
  11577. class SeekBar extends Slider {
  11578. /**
  11579. * Creates an instance of this class.
  11580. *
  11581. * @param { import('../../player').default } player
  11582. * The `Player` that this class should be attached to.
  11583. *
  11584. * @param {Object} [options]
  11585. * The key/value store of player options.
  11586. */
  11587. constructor(player, options) {
  11588. super(player, options);
  11589. this.setEventHandlers_();
  11590. }
  11591. /**
  11592. * Sets the event handlers
  11593. *
  11594. * @private
  11595. */
  11596. setEventHandlers_() {
  11597. this.update_ = bind_(this, this.update);
  11598. this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
  11599. this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
  11600. if (this.player_.liveTracker) {
  11601. this.on(this.player_.liveTracker, 'liveedgechange', this.update);
  11602. }
  11603. // when playing, let's ensure we smoothly update the play progress bar
  11604. // via an interval
  11605. this.updateInterval = null;
  11606. this.enableIntervalHandler_ = e => this.enableInterval_(e);
  11607. this.disableIntervalHandler_ = e => this.disableInterval_(e);
  11608. this.on(this.player_, ['playing'], this.enableIntervalHandler_);
  11609. this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
  11610. // we don't need to update the play progress if the document is hidden,
  11611. // also, this causes the CPU to spike and eventually crash the page on IE11.
  11612. if ('hidden' in document__default["default"] && 'visibilityState' in document__default["default"]) {
  11613. this.on(document__default["default"], 'visibilitychange', this.toggleVisibility_);
  11614. }
  11615. }
  11616. toggleVisibility_(e) {
  11617. if (document__default["default"].visibilityState === 'hidden') {
  11618. this.cancelNamedAnimationFrame('SeekBar#update');
  11619. this.cancelNamedAnimationFrame('Slider#update');
  11620. this.disableInterval_(e);
  11621. } else {
  11622. if (!this.player_.ended() && !this.player_.paused()) {
  11623. this.enableInterval_();
  11624. }
  11625. // we just switched back to the page and someone may be looking, so, update ASAP
  11626. this.update();
  11627. }
  11628. }
  11629. enableInterval_() {
  11630. if (this.updateInterval) {
  11631. return;
  11632. }
  11633. this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
  11634. }
  11635. disableInterval_(e) {
  11636. if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
  11637. return;
  11638. }
  11639. if (!this.updateInterval) {
  11640. return;
  11641. }
  11642. this.clearInterval(this.updateInterval);
  11643. this.updateInterval = null;
  11644. }
  11645. /**
  11646. * Create the `Component`'s DOM element
  11647. *
  11648. * @return {Element}
  11649. * The element that was created.
  11650. */
  11651. createEl() {
  11652. return super.createEl('div', {
  11653. className: 'vjs-progress-holder'
  11654. }, {
  11655. 'aria-label': this.localize('Progress Bar')
  11656. });
  11657. }
  11658. /**
  11659. * This function updates the play progress bar and accessibility
  11660. * attributes to whatever is passed in.
  11661. *
  11662. * @param {Event} [event]
  11663. * The `timeupdate` or `ended` event that caused this to run.
  11664. *
  11665. * @listens Player#timeupdate
  11666. *
  11667. * @return {number}
  11668. * The current percent at a number from 0-1
  11669. */
  11670. update(event) {
  11671. // ignore updates while the tab is hidden
  11672. if (document__default["default"].visibilityState === 'hidden') {
  11673. return;
  11674. }
  11675. const percent = super.update();
  11676. this.requestNamedAnimationFrame('SeekBar#update', () => {
  11677. const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
  11678. const liveTracker = this.player_.liveTracker;
  11679. let duration = this.player_.duration();
  11680. if (liveTracker && liveTracker.isLive()) {
  11681. duration = this.player_.liveTracker.liveCurrentTime();
  11682. }
  11683. if (this.percent_ !== percent) {
  11684. // machine readable value of progress bar (percentage complete)
  11685. this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
  11686. this.percent_ = percent;
  11687. }
  11688. if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
  11689. // human readable value of progress bar (time complete)
  11690. this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
  11691. this.currentTime_ = currentTime;
  11692. this.duration_ = duration;
  11693. }
  11694. // update the progress bar time tooltip with the current time
  11695. if (this.bar) {
  11696. this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
  11697. }
  11698. });
  11699. return percent;
  11700. }
  11701. /**
  11702. * Prevent liveThreshold from causing seeks to seem like they
  11703. * are not happening from a user perspective.
  11704. *
  11705. * @param {number} ct
  11706. * current time to seek to
  11707. */
  11708. userSeek_(ct) {
  11709. if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
  11710. this.player_.liveTracker.nextSeekedFromUser();
  11711. }
  11712. this.player_.currentTime(ct);
  11713. }
  11714. /**
  11715. * Get the value of current time but allows for smooth scrubbing,
  11716. * when player can't keep up.
  11717. *
  11718. * @return {number}
  11719. * The current time value to display
  11720. *
  11721. * @private
  11722. */
  11723. getCurrentTime_() {
  11724. return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
  11725. }
  11726. /**
  11727. * Get the percentage of media played so far.
  11728. *
  11729. * @return {number}
  11730. * The percentage of media played so far (0 to 1).
  11731. */
  11732. getPercent() {
  11733. const currentTime = this.getCurrentTime_();
  11734. let percent;
  11735. const liveTracker = this.player_.liveTracker;
  11736. if (liveTracker && liveTracker.isLive()) {
  11737. percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
  11738. // prevent the percent from changing at the live edge
  11739. if (liveTracker.atLiveEdge()) {
  11740. percent = 1;
  11741. }
  11742. } else {
  11743. percent = currentTime / this.player_.duration();
  11744. }
  11745. return percent;
  11746. }
  11747. /**
  11748. * Handle mouse down on seek bar
  11749. *
  11750. * @param {MouseEvent} event
  11751. * The `mousedown` event that caused this to run.
  11752. *
  11753. * @listens mousedown
  11754. */
  11755. handleMouseDown(event) {
  11756. if (!isSingleLeftClick(event)) {
  11757. return;
  11758. }
  11759. // Stop event propagation to prevent double fire in progress-control.js
  11760. event.stopPropagation();
  11761. this.videoWasPlaying = !this.player_.paused();
  11762. this.player_.pause();
  11763. super.handleMouseDown(event);
  11764. }
  11765. /**
  11766. * Handle mouse move on seek bar
  11767. *
  11768. * @param {MouseEvent} event
  11769. * The `mousemove` event that caused this to run.
  11770. * @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
  11771. *
  11772. * @listens mousemove
  11773. */
  11774. handleMouseMove(event, mouseDown = false) {
  11775. if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) {
  11776. return;
  11777. }
  11778. if (!mouseDown && !this.player_.scrubbing()) {
  11779. this.player_.scrubbing(true);
  11780. }
  11781. let newTime;
  11782. const distance = this.calculateDistance(event);
  11783. const liveTracker = this.player_.liveTracker;
  11784. if (!liveTracker || !liveTracker.isLive()) {
  11785. newTime = distance * this.player_.duration();
  11786. // Don't let video end while scrubbing.
  11787. if (newTime === this.player_.duration()) {
  11788. newTime = newTime - 0.1;
  11789. }
  11790. } else {
  11791. if (distance >= 0.99) {
  11792. liveTracker.seekToLiveEdge();
  11793. return;
  11794. }
  11795. const seekableStart = liveTracker.seekableStart();
  11796. const seekableEnd = liveTracker.liveCurrentTime();
  11797. newTime = seekableStart + distance * liveTracker.liveWindow();
  11798. // Don't let video end while scrubbing.
  11799. if (newTime >= seekableEnd) {
  11800. newTime = seekableEnd;
  11801. }
  11802. // Compensate for precision differences so that currentTime is not less
  11803. // than seekable start
  11804. if (newTime <= seekableStart) {
  11805. newTime = seekableStart + 0.1;
  11806. }
  11807. // On android seekableEnd can be Infinity sometimes,
  11808. // this will cause newTime to be Infinity, which is
  11809. // not a valid currentTime.
  11810. if (newTime === Infinity) {
  11811. return;
  11812. }
  11813. }
  11814. // Set new time (tell player to seek to new time)
  11815. this.userSeek_(newTime);
  11816. if (this.player_.options_.enableSmoothSeeking) {
  11817. this.update();
  11818. }
  11819. }
  11820. enable() {
  11821. super.enable();
  11822. const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
  11823. if (!mouseTimeDisplay) {
  11824. return;
  11825. }
  11826. mouseTimeDisplay.show();
  11827. }
  11828. disable() {
  11829. super.disable();
  11830. const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
  11831. if (!mouseTimeDisplay) {
  11832. return;
  11833. }
  11834. mouseTimeDisplay.hide();
  11835. }
  11836. /**
  11837. * Handle mouse up on seek bar
  11838. *
  11839. * @param {MouseEvent} event
  11840. * The `mouseup` event that caused this to run.
  11841. *
  11842. * @listens mouseup
  11843. */
  11844. handleMouseUp(event) {
  11845. super.handleMouseUp(event);
  11846. // Stop event propagation to prevent double fire in progress-control.js
  11847. if (event) {
  11848. event.stopPropagation();
  11849. }
  11850. this.player_.scrubbing(false);
  11851. /**
  11852. * Trigger timeupdate because we're done seeking and the time has changed.
  11853. * This is particularly useful for if the player is paused to time the time displays.
  11854. *
  11855. * @event Tech#timeupdate
  11856. * @type {Event}
  11857. */
  11858. this.player_.trigger({
  11859. type: 'timeupdate',
  11860. target: this,
  11861. manuallyTriggered: true
  11862. });
  11863. if (this.videoWasPlaying) {
  11864. silencePromise(this.player_.play());
  11865. } else {
  11866. // We're done seeking and the time has changed.
  11867. // If the player is paused, make sure we display the correct time on the seek bar.
  11868. this.update_();
  11869. }
  11870. }
  11871. /**
  11872. * Move more quickly fast forward for keyboard-only users
  11873. */
  11874. stepForward() {
  11875. this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
  11876. }
  11877. /**
  11878. * Move more quickly rewind for keyboard-only users
  11879. */
  11880. stepBack() {
  11881. this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
  11882. }
  11883. /**
  11884. * Toggles the playback state of the player
  11885. * This gets called when enter or space is used on the seekbar
  11886. *
  11887. * @param {KeyboardEvent} event
  11888. * The `keydown` event that caused this function to be called
  11889. *
  11890. */
  11891. handleAction(event) {
  11892. if (this.player_.paused()) {
  11893. this.player_.play();
  11894. } else {
  11895. this.player_.pause();
  11896. }
  11897. }
  11898. /**
  11899. * Called when this SeekBar has focus and a key gets pressed down.
  11900. * Supports the following keys:
  11901. *
  11902. * Space or Enter key fire a click event
  11903. * Home key moves to start of the timeline
  11904. * End key moves to end of the timeline
  11905. * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
  11906. * PageDown key moves back a larger step than ArrowDown
  11907. * PageUp key moves forward a large step
  11908. *
  11909. * @param {KeyboardEvent} event
  11910. * The `keydown` event that caused this function to be called.
  11911. *
  11912. * @listens keydown
  11913. */
  11914. handleKeyDown(event) {
  11915. const liveTracker = this.player_.liveTracker;
  11916. if (keycode__default["default"].isEventKey(event, 'Space') || keycode__default["default"].isEventKey(event, 'Enter')) {
  11917. event.preventDefault();
  11918. event.stopPropagation();
  11919. this.handleAction(event);
  11920. } else if (keycode__default["default"].isEventKey(event, 'Home')) {
  11921. event.preventDefault();
  11922. event.stopPropagation();
  11923. this.userSeek_(0);
  11924. } else if (keycode__default["default"].isEventKey(event, 'End')) {
  11925. event.preventDefault();
  11926. event.stopPropagation();
  11927. if (liveTracker && liveTracker.isLive()) {
  11928. this.userSeek_(liveTracker.liveCurrentTime());
  11929. } else {
  11930. this.userSeek_(this.player_.duration());
  11931. }
  11932. } else if (/^[0-9]$/.test(keycode__default["default"](event))) {
  11933. event.preventDefault();
  11934. event.stopPropagation();
  11935. const gotoFraction = (keycode__default["default"].codes[keycode__default["default"](event)] - keycode__default["default"].codes['0']) * 10.0 / 100.0;
  11936. if (liveTracker && liveTracker.isLive()) {
  11937. this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
  11938. } else {
  11939. this.userSeek_(this.player_.duration() * gotoFraction);
  11940. }
  11941. } else if (keycode__default["default"].isEventKey(event, 'PgDn')) {
  11942. event.preventDefault();
  11943. event.stopPropagation();
  11944. this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
  11945. } else if (keycode__default["default"].isEventKey(event, 'PgUp')) {
  11946. event.preventDefault();
  11947. event.stopPropagation();
  11948. this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
  11949. } else {
  11950. // Pass keydown handling up for unsupported keys
  11951. super.handleKeyDown(event);
  11952. }
  11953. }
  11954. dispose() {
  11955. this.disableInterval_();
  11956. this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
  11957. if (this.player_.liveTracker) {
  11958. this.off(this.player_.liveTracker, 'liveedgechange', this.update);
  11959. }
  11960. this.off(this.player_, ['playing'], this.enableIntervalHandler_);
  11961. this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
  11962. // we don't need to update the play progress if the document is hidden,
  11963. // also, this causes the CPU to spike and eventually crash the page on IE11.
  11964. if ('hidden' in document__default["default"] && 'visibilityState' in document__default["default"]) {
  11965. this.off(document__default["default"], 'visibilitychange', this.toggleVisibility_);
  11966. }
  11967. super.dispose();
  11968. }
  11969. }
  11970. /**
  11971. * Default options for the `SeekBar`
  11972. *
  11973. * @type {Object}
  11974. * @private
  11975. */
  11976. SeekBar.prototype.options_ = {
  11977. children: ['loadProgressBar', 'playProgressBar'],
  11978. barName: 'playProgressBar'
  11979. };
  11980. // MouseTimeDisplay tooltips should not be added to a player on mobile devices
  11981. if (!IS_IOS && !IS_ANDROID) {
  11982. SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
  11983. }
  11984. Component.registerComponent('SeekBar', SeekBar);
  11985. /**
  11986. * @file progress-control.js
  11987. */
  11988. /**
  11989. * The Progress Control component contains the seek bar, load progress,
  11990. * and play progress.
  11991. *
  11992. * @extends Component
  11993. */
  11994. class ProgressControl extends Component {
  11995. /**
  11996. * Creates an instance of this class.
  11997. *
  11998. * @param { import('../../player').default } player
  11999. * The `Player` that this class should be attached to.
  12000. *
  12001. * @param {Object} [options]
  12002. * The key/value store of player options.
  12003. */
  12004. constructor(player, options) {
  12005. super(player, options);
  12006. this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
  12007. this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
  12008. this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
  12009. this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
  12010. this.enable();
  12011. }
  12012. /**
  12013. * Create the `Component`'s DOM element
  12014. *
  12015. * @return {Element}
  12016. * The element that was created.
  12017. */
  12018. createEl() {
  12019. return super.createEl('div', {
  12020. className: 'vjs-progress-control vjs-control'
  12021. });
  12022. }
  12023. /**
  12024. * When the mouse moves over the `ProgressControl`, the pointer position
  12025. * gets passed down to the `MouseTimeDisplay` component.
  12026. *
  12027. * @param {Event} event
  12028. * The `mousemove` event that caused this function to run.
  12029. *
  12030. * @listen mousemove
  12031. */
  12032. handleMouseMove(event) {
  12033. const seekBar = this.getChild('seekBar');
  12034. if (!seekBar) {
  12035. return;
  12036. }
  12037. const playProgressBar = seekBar.getChild('playProgressBar');
  12038. const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
  12039. if (!playProgressBar && !mouseTimeDisplay) {
  12040. return;
  12041. }
  12042. const seekBarEl = seekBar.el();
  12043. const seekBarRect = findPosition(seekBarEl);
  12044. let seekBarPoint = getPointerPosition(seekBarEl, event).x;
  12045. // The default skin has a gap on either side of the `SeekBar`. This means
  12046. // that it's possible to trigger this behavior outside the boundaries of
  12047. // the `SeekBar`. This ensures we stay within it at all times.
  12048. seekBarPoint = clamp(seekBarPoint, 0, 1);
  12049. if (mouseTimeDisplay) {
  12050. mouseTimeDisplay.update(seekBarRect, seekBarPoint);
  12051. }
  12052. if (playProgressBar) {
  12053. playProgressBar.update(seekBarRect, seekBar.getProgress());
  12054. }
  12055. }
  12056. /**
  12057. * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
  12058. *
  12059. * @method ProgressControl#throttledHandleMouseSeek
  12060. * @param {Event} event
  12061. * The `mousemove` event that caused this function to run.
  12062. *
  12063. * @listen mousemove
  12064. * @listen touchmove
  12065. */
  12066. /**
  12067. * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
  12068. *
  12069. * @param {Event} event
  12070. * `mousedown` or `touchstart` event that triggered this function
  12071. *
  12072. * @listens mousemove
  12073. * @listens touchmove
  12074. */
  12075. handleMouseSeek(event) {
  12076. const seekBar = this.getChild('seekBar');
  12077. if (seekBar) {
  12078. seekBar.handleMouseMove(event);
  12079. }
  12080. }
  12081. /**
  12082. * Are controls are currently enabled for this progress control.
  12083. *
  12084. * @return {boolean}
  12085. * true if controls are enabled, false otherwise
  12086. */
  12087. enabled() {
  12088. return this.enabled_;
  12089. }
  12090. /**
  12091. * Disable all controls on the progress control and its children
  12092. */
  12093. disable() {
  12094. this.children().forEach(child => child.disable && child.disable());
  12095. if (!this.enabled()) {
  12096. return;
  12097. }
  12098. this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
  12099. this.off(this.el_, 'mousemove', this.handleMouseMove);
  12100. this.removeListenersAddedOnMousedownAndTouchstart();
  12101. this.addClass('disabled');
  12102. this.enabled_ = false;
  12103. // Restore normal playback state if controls are disabled while scrubbing
  12104. if (this.player_.scrubbing()) {
  12105. const seekBar = this.getChild('seekBar');
  12106. this.player_.scrubbing(false);
  12107. if (seekBar.videoWasPlaying) {
  12108. silencePromise(this.player_.play());
  12109. }
  12110. }
  12111. }
  12112. /**
  12113. * Enable all controls on the progress control and its children
  12114. */
  12115. enable() {
  12116. this.children().forEach(child => child.enable && child.enable());
  12117. if (this.enabled()) {
  12118. return;
  12119. }
  12120. this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
  12121. this.on(this.el_, 'mousemove', this.handleMouseMove);
  12122. this.removeClass('disabled');
  12123. this.enabled_ = true;
  12124. }
  12125. /**
  12126. * Cleanup listeners after the user finishes interacting with the progress controls
  12127. */
  12128. removeListenersAddedOnMousedownAndTouchstart() {
  12129. const doc = this.el_.ownerDocument;
  12130. this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
  12131. this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
  12132. this.off(doc, 'mouseup', this.handleMouseUpHandler_);
  12133. this.off(doc, 'touchend', this.handleMouseUpHandler_);
  12134. }
  12135. /**
  12136. * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
  12137. *
  12138. * @param {Event} event
  12139. * `mousedown` or `touchstart` event that triggered this function
  12140. *
  12141. * @listens mousedown
  12142. * @listens touchstart
  12143. */
  12144. handleMouseDown(event) {
  12145. const doc = this.el_.ownerDocument;
  12146. const seekBar = this.getChild('seekBar');
  12147. if (seekBar) {
  12148. seekBar.handleMouseDown(event);
  12149. }
  12150. this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
  12151. this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
  12152. this.on(doc, 'mouseup', this.handleMouseUpHandler_);
  12153. this.on(doc, 'touchend', this.handleMouseUpHandler_);
  12154. }
  12155. /**
  12156. * Handle `mouseup` or `touchend` events on the `ProgressControl`.
  12157. *
  12158. * @param {Event} event
  12159. * `mouseup` or `touchend` event that triggered this function.
  12160. *
  12161. * @listens touchend
  12162. * @listens mouseup
  12163. */
  12164. handleMouseUp(event) {
  12165. const seekBar = this.getChild('seekBar');
  12166. if (seekBar) {
  12167. seekBar.handleMouseUp(event);
  12168. }
  12169. this.removeListenersAddedOnMousedownAndTouchstart();
  12170. }
  12171. }
  12172. /**
  12173. * Default options for `ProgressControl`
  12174. *
  12175. * @type {Object}
  12176. * @private
  12177. */
  12178. ProgressControl.prototype.options_ = {
  12179. children: ['seekBar']
  12180. };
  12181. Component.registerComponent('ProgressControl', ProgressControl);
  12182. /**
  12183. * @file picture-in-picture-toggle.js
  12184. */
  12185. /**
  12186. * Toggle Picture-in-Picture mode
  12187. *
  12188. * @extends Button
  12189. */
  12190. class PictureInPictureToggle extends Button {
  12191. /**
  12192. * Creates an instance of this class.
  12193. *
  12194. * @param { import('./player').default } player
  12195. * The `Player` that this class should be attached to.
  12196. *
  12197. * @param {Object} [options]
  12198. * The key/value store of player options.
  12199. *
  12200. * @listens Player#enterpictureinpicture
  12201. * @listens Player#leavepictureinpicture
  12202. */
  12203. constructor(player, options) {
  12204. super(player, options);
  12205. this.setIcon('picture-in-picture-enter');
  12206. this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
  12207. this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
  12208. this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange());
  12209. // TODO: Deactivate button on player emptied event.
  12210. this.disable();
  12211. }
  12212. /**
  12213. * Builds the default DOM `className`.
  12214. *
  12215. * @return {string}
  12216. * The DOM `className` for this object.
  12217. */
  12218. buildCSSClass() {
  12219. return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`;
  12220. }
  12221. /**
  12222. * Displays or hides the button depending on the audio mode detection.
  12223. * Exits picture-in-picture if it is enabled when switching to audio mode.
  12224. */
  12225. handlePictureInPictureAudioModeChange() {
  12226. // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
  12227. const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio';
  12228. const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode();
  12229. if (!isAudioMode) {
  12230. this.show();
  12231. return;
  12232. }
  12233. if (this.player_.isInPictureInPicture()) {
  12234. this.player_.exitPictureInPicture();
  12235. }
  12236. this.hide();
  12237. }
  12238. /**
  12239. * Enables or disables button based on availability of a Picture-In-Picture mode.
  12240. *
  12241. * Enabled if
  12242. * - `player.options().enableDocumentPictureInPicture` is true and
  12243. * window.documentPictureInPicture is available; or
  12244. * - `player.disablePictureInPicture()` is false and
  12245. * element.requestPictureInPicture is available
  12246. */
  12247. handlePictureInPictureEnabledChange() {
  12248. if (document__default["default"].pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window__default["default"]) {
  12249. this.enable();
  12250. } else {
  12251. this.disable();
  12252. }
  12253. }
  12254. /**
  12255. * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
  12256. *
  12257. * @param {Event} [event]
  12258. * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
  12259. * called.
  12260. *
  12261. * @listens Player#enterpictureinpicture
  12262. * @listens Player#leavepictureinpicture
  12263. */
  12264. handlePictureInPictureChange(event) {
  12265. if (this.player_.isInPictureInPicture()) {
  12266. this.setIcon('picture-in-picture-exit');
  12267. this.controlText('Exit Picture-in-Picture');
  12268. } else {
  12269. this.setIcon('picture-in-picture-enter');
  12270. this.controlText('Picture-in-Picture');
  12271. }
  12272. this.handlePictureInPictureEnabledChange();
  12273. }
  12274. /**
  12275. * This gets called when an `PictureInPictureToggle` is "clicked". See
  12276. * {@link ClickableComponent} for more detailed information on what a click can be.
  12277. *
  12278. * @param {Event} [event]
  12279. * The `keydown`, `tap`, or `click` event that caused this function to be
  12280. * called.
  12281. *
  12282. * @listens tap
  12283. * @listens click
  12284. */
  12285. handleClick(event) {
  12286. if (!this.player_.isInPictureInPicture()) {
  12287. this.player_.requestPictureInPicture();
  12288. } else {
  12289. this.player_.exitPictureInPicture();
  12290. }
  12291. }
  12292. /**
  12293. * Show the `Component`s element if it is hidden by removing the
  12294. * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API.
  12295. */
  12296. show() {
  12297. // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox.
  12298. if (typeof document__default["default"].exitPictureInPicture !== 'function') {
  12299. return;
  12300. }
  12301. super.show();
  12302. }
  12303. }
  12304. /**
  12305. * The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
  12306. *
  12307. * @type {string}
  12308. * @protected
  12309. */
  12310. PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
  12311. Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
  12312. /**
  12313. * @file fullscreen-toggle.js
  12314. */
  12315. /**
  12316. * Toggle fullscreen video
  12317. *
  12318. * @extends Button
  12319. */
  12320. class FullscreenToggle extends Button {
  12321. /**
  12322. * Creates an instance of this class.
  12323. *
  12324. * @param { import('./player').default } player
  12325. * The `Player` that this class should be attached to.
  12326. *
  12327. * @param {Object} [options]
  12328. * The key/value store of player options.
  12329. */
  12330. constructor(player, options) {
  12331. super(player, options);
  12332. this.setIcon('fullscreen-enter');
  12333. this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
  12334. if (document__default["default"][player.fsApi_.fullscreenEnabled] === false) {
  12335. this.disable();
  12336. }
  12337. }
  12338. /**
  12339. * Builds the default DOM `className`.
  12340. *
  12341. * @return {string}
  12342. * The DOM `className` for this object.
  12343. */
  12344. buildCSSClass() {
  12345. return `vjs-fullscreen-control ${super.buildCSSClass()}`;
  12346. }
  12347. /**
  12348. * Handles fullscreenchange on the player and change control text accordingly.
  12349. *
  12350. * @param {Event} [event]
  12351. * The {@link Player#fullscreenchange} event that caused this function to be
  12352. * called.
  12353. *
  12354. * @listens Player#fullscreenchange
  12355. */
  12356. handleFullscreenChange(event) {
  12357. if (this.player_.isFullscreen()) {
  12358. this.controlText('Exit Fullscreen');
  12359. this.setIcon('fullscreen-exit');
  12360. } else {
  12361. this.controlText('Fullscreen');
  12362. this.setIcon('fullscreen-enter');
  12363. }
  12364. }
  12365. /**
  12366. * This gets called when an `FullscreenToggle` is "clicked". See
  12367. * {@link ClickableComponent} for more detailed information on what a click can be.
  12368. *
  12369. * @param {Event} [event]
  12370. * The `keydown`, `tap`, or `click` event that caused this function to be
  12371. * called.
  12372. *
  12373. * @listens tap
  12374. * @listens click
  12375. */
  12376. handleClick(event) {
  12377. if (!this.player_.isFullscreen()) {
  12378. this.player_.requestFullscreen();
  12379. } else {
  12380. this.player_.exitFullscreen();
  12381. }
  12382. }
  12383. }
  12384. /**
  12385. * The text that should display over the `FullscreenToggle`s controls. Added for localization.
  12386. *
  12387. * @type {string}
  12388. * @protected
  12389. */
  12390. FullscreenToggle.prototype.controlText_ = 'Fullscreen';
  12391. Component.registerComponent('FullscreenToggle', FullscreenToggle);
  12392. /**
  12393. * Check if volume control is supported and if it isn't hide the
  12394. * `Component` that was passed using the `vjs-hidden` class.
  12395. *
  12396. * @param { import('../../component').default } self
  12397. * The component that should be hidden if volume is unsupported
  12398. *
  12399. * @param { import('../../player').default } player
  12400. * A reference to the player
  12401. *
  12402. * @private
  12403. */
  12404. const checkVolumeSupport = function (self, player) {
  12405. // hide volume controls when they're not supported by the current tech
  12406. if (player.tech_ && !player.tech_.featuresVolumeControl) {
  12407. self.addClass('vjs-hidden');
  12408. }
  12409. self.on(player, 'loadstart', function () {
  12410. if (!player.tech_.featuresVolumeControl) {
  12411. self.addClass('vjs-hidden');
  12412. } else {
  12413. self.removeClass('vjs-hidden');
  12414. }
  12415. });
  12416. };
  12417. /**
  12418. * @file volume-level.js
  12419. */
  12420. /**
  12421. * Shows volume level
  12422. *
  12423. * @extends Component
  12424. */
  12425. class VolumeLevel extends Component {
  12426. /**
  12427. * Create the `Component`'s DOM element
  12428. *
  12429. * @return {Element}
  12430. * The element that was created.
  12431. */
  12432. createEl() {
  12433. const el = super.createEl('div', {
  12434. className: 'vjs-volume-level'
  12435. });
  12436. this.setIcon('circle', el);
  12437. el.appendChild(super.createEl('span', {
  12438. className: 'vjs-control-text'
  12439. }));
  12440. return el;
  12441. }
  12442. }
  12443. Component.registerComponent('VolumeLevel', VolumeLevel);
  12444. /**
  12445. * @file volume-level-tooltip.js
  12446. */
  12447. /**
  12448. * Volume level tooltips display a volume above or side by side the volume bar.
  12449. *
  12450. * @extends Component
  12451. */
  12452. class VolumeLevelTooltip extends Component {
  12453. /**
  12454. * Creates an instance of this class.
  12455. *
  12456. * @param { import('../../player').default } player
  12457. * The {@link Player} that this class should be attached to.
  12458. *
  12459. * @param {Object} [options]
  12460. * The key/value store of player options.
  12461. */
  12462. constructor(player, options) {
  12463. super(player, options);
  12464. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  12465. }
  12466. /**
  12467. * Create the volume tooltip DOM element
  12468. *
  12469. * @return {Element}
  12470. * The element that was created.
  12471. */
  12472. createEl() {
  12473. return super.createEl('div', {
  12474. className: 'vjs-volume-tooltip'
  12475. }, {
  12476. 'aria-hidden': 'true'
  12477. });
  12478. }
  12479. /**
  12480. * Updates the position of the tooltip relative to the `VolumeBar` and
  12481. * its content text.
  12482. *
  12483. * @param {Object} rangeBarRect
  12484. * The `ClientRect` for the {@link VolumeBar} element.
  12485. *
  12486. * @param {number} rangeBarPoint
  12487. * A number from 0 to 1, representing a horizontal/vertical reference point
  12488. * from the left edge of the {@link VolumeBar}
  12489. *
  12490. * @param {boolean} vertical
  12491. * Referees to the Volume control position
  12492. * in the control bar{@link VolumeControl}
  12493. *
  12494. */
  12495. update(rangeBarRect, rangeBarPoint, vertical, content) {
  12496. if (!vertical) {
  12497. const tooltipRect = getBoundingClientRect(this.el_);
  12498. const playerRect = getBoundingClientRect(this.player_.el());
  12499. const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
  12500. if (!playerRect || !tooltipRect) {
  12501. return;
  12502. }
  12503. const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
  12504. const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
  12505. let pullTooltipBy = tooltipRect.width / 2;
  12506. if (spaceLeftOfPoint < pullTooltipBy) {
  12507. pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
  12508. } else if (spaceRightOfPoint < pullTooltipBy) {
  12509. pullTooltipBy = spaceRightOfPoint;
  12510. }
  12511. if (pullTooltipBy < 0) {
  12512. pullTooltipBy = 0;
  12513. } else if (pullTooltipBy > tooltipRect.width) {
  12514. pullTooltipBy = tooltipRect.width;
  12515. }
  12516. this.el_.style.right = `-${pullTooltipBy}px`;
  12517. }
  12518. this.write(`${content}%`);
  12519. }
  12520. /**
  12521. * Write the volume to the tooltip DOM element.
  12522. *
  12523. * @param {string} content
  12524. * The formatted volume for the tooltip.
  12525. */
  12526. write(content) {
  12527. textContent(this.el_, content);
  12528. }
  12529. /**
  12530. * Updates the position of the volume tooltip relative to the `VolumeBar`.
  12531. *
  12532. * @param {Object} rangeBarRect
  12533. * The `ClientRect` for the {@link VolumeBar} element.
  12534. *
  12535. * @param {number} rangeBarPoint
  12536. * A number from 0 to 1, representing a horizontal/vertical reference point
  12537. * from the left edge of the {@link VolumeBar}
  12538. *
  12539. * @param {boolean} vertical
  12540. * Referees to the Volume control position
  12541. * in the control bar{@link VolumeControl}
  12542. *
  12543. * @param {number} volume
  12544. * The volume level to update the tooltip to
  12545. *
  12546. * @param {Function} cb
  12547. * A function that will be called during the request animation frame
  12548. * for tooltips that need to do additional animations from the default
  12549. */
  12550. updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
  12551. this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
  12552. this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
  12553. if (cb) {
  12554. cb();
  12555. }
  12556. });
  12557. }
  12558. }
  12559. Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
  12560. /**
  12561. * @file mouse-volume-level-display.js
  12562. */
  12563. /**
  12564. * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
  12565. * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
  12566. * indicating the volume level which is represented by a given point in the
  12567. * {@link VolumeBar}.
  12568. *
  12569. * @extends Component
  12570. */
  12571. class MouseVolumeLevelDisplay extends Component {
  12572. /**
  12573. * Creates an instance of this class.
  12574. *
  12575. * @param { import('../../player').default } player
  12576. * The {@link Player} that this class should be attached to.
  12577. *
  12578. * @param {Object} [options]
  12579. * The key/value store of player options.
  12580. */
  12581. constructor(player, options) {
  12582. super(player, options);
  12583. this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
  12584. }
  12585. /**
  12586. * Create the DOM element for this class.
  12587. *
  12588. * @return {Element}
  12589. * The element that was created.
  12590. */
  12591. createEl() {
  12592. return super.createEl('div', {
  12593. className: 'vjs-mouse-display'
  12594. });
  12595. }
  12596. /**
  12597. * Enquires updates to its own DOM as well as the DOM of its
  12598. * {@link VolumeLevelTooltip} child.
  12599. *
  12600. * @param {Object} rangeBarRect
  12601. * The `ClientRect` for the {@link VolumeBar} element.
  12602. *
  12603. * @param {number} rangeBarPoint
  12604. * A number from 0 to 1, representing a horizontal/vertical reference point
  12605. * from the left edge of the {@link VolumeBar}
  12606. *
  12607. * @param {boolean} vertical
  12608. * Referees to the Volume control position
  12609. * in the control bar{@link VolumeControl}
  12610. *
  12611. */
  12612. update(rangeBarRect, rangeBarPoint, vertical) {
  12613. const volume = 100 * rangeBarPoint;
  12614. this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
  12615. if (vertical) {
  12616. this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
  12617. } else {
  12618. this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
  12619. }
  12620. });
  12621. }
  12622. }
  12623. /**
  12624. * Default options for `MouseVolumeLevelDisplay`
  12625. *
  12626. * @type {Object}
  12627. * @private
  12628. */
  12629. MouseVolumeLevelDisplay.prototype.options_ = {
  12630. children: ['volumeLevelTooltip']
  12631. };
  12632. Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
  12633. /**
  12634. * @file volume-bar.js
  12635. */
  12636. /**
  12637. * The bar that contains the volume level and can be clicked on to adjust the level
  12638. *
  12639. * @extends Slider
  12640. */
  12641. class VolumeBar extends Slider {
  12642. /**
  12643. * Creates an instance of this class.
  12644. *
  12645. * @param { import('../../player').default } player
  12646. * The `Player` that this class should be attached to.
  12647. *
  12648. * @param {Object} [options]
  12649. * The key/value store of player options.
  12650. */
  12651. constructor(player, options) {
  12652. super(player, options);
  12653. this.on('slideractive', e => this.updateLastVolume_(e));
  12654. this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
  12655. player.ready(() => this.updateARIAAttributes());
  12656. }
  12657. /**
  12658. * Create the `Component`'s DOM element
  12659. *
  12660. * @return {Element}
  12661. * The element that was created.
  12662. */
  12663. createEl() {
  12664. return super.createEl('div', {
  12665. className: 'vjs-volume-bar vjs-slider-bar'
  12666. }, {
  12667. 'aria-label': this.localize('Volume Level'),
  12668. 'aria-live': 'polite'
  12669. });
  12670. }
  12671. /**
  12672. * Handle mouse down on volume bar
  12673. *
  12674. * @param {Event} event
  12675. * The `mousedown` event that caused this to run.
  12676. *
  12677. * @listens mousedown
  12678. */
  12679. handleMouseDown(event) {
  12680. if (!isSingleLeftClick(event)) {
  12681. return;
  12682. }
  12683. super.handleMouseDown(event);
  12684. }
  12685. /**
  12686. * Handle movement events on the {@link VolumeMenuButton}.
  12687. *
  12688. * @param {Event} event
  12689. * The event that caused this function to run.
  12690. *
  12691. * @listens mousemove
  12692. */
  12693. handleMouseMove(event) {
  12694. const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
  12695. if (mouseVolumeLevelDisplay) {
  12696. const volumeBarEl = this.el();
  12697. const volumeBarRect = getBoundingClientRect(volumeBarEl);
  12698. const vertical = this.vertical();
  12699. let volumeBarPoint = getPointerPosition(volumeBarEl, event);
  12700. volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
  12701. // The default skin has a gap on either side of the `VolumeBar`. This means
  12702. // that it's possible to trigger this behavior outside the boundaries of
  12703. // the `VolumeBar`. This ensures we stay within it at all times.
  12704. volumeBarPoint = clamp(volumeBarPoint, 0, 1);
  12705. mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
  12706. }
  12707. if (!isSingleLeftClick(event)) {
  12708. return;
  12709. }
  12710. this.checkMuted();
  12711. this.player_.volume(this.calculateDistance(event));
  12712. }
  12713. /**
  12714. * If the player is muted unmute it.
  12715. */
  12716. checkMuted() {
  12717. if (this.player_.muted()) {
  12718. this.player_.muted(false);
  12719. }
  12720. }
  12721. /**
  12722. * Get percent of volume level
  12723. *
  12724. * @return {number}
  12725. * Volume level percent as a decimal number.
  12726. */
  12727. getPercent() {
  12728. if (this.player_.muted()) {
  12729. return 0;
  12730. }
  12731. return this.player_.volume();
  12732. }
  12733. /**
  12734. * Increase volume level for keyboard users
  12735. */
  12736. stepForward() {
  12737. this.checkMuted();
  12738. this.player_.volume(this.player_.volume() + 0.1);
  12739. }
  12740. /**
  12741. * Decrease volume level for keyboard users
  12742. */
  12743. stepBack() {
  12744. this.checkMuted();
  12745. this.player_.volume(this.player_.volume() - 0.1);
  12746. }
  12747. /**
  12748. * Update ARIA accessibility attributes
  12749. *
  12750. * @param {Event} [event]
  12751. * The `volumechange` event that caused this function to run.
  12752. *
  12753. * @listens Player#volumechange
  12754. */
  12755. updateARIAAttributes(event) {
  12756. const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
  12757. this.el_.setAttribute('aria-valuenow', ariaValue);
  12758. this.el_.setAttribute('aria-valuetext', ariaValue + '%');
  12759. }
  12760. /**
  12761. * Returns the current value of the player volume as a percentage
  12762. *
  12763. * @private
  12764. */
  12765. volumeAsPercentage_() {
  12766. return Math.round(this.player_.volume() * 100);
  12767. }
  12768. /**
  12769. * When user starts dragging the VolumeBar, store the volume and listen for
  12770. * the end of the drag. When the drag ends, if the volume was set to zero,
  12771. * set lastVolume to the stored volume.
  12772. *
  12773. * @listens slideractive
  12774. * @private
  12775. */
  12776. updateLastVolume_() {
  12777. const volumeBeforeDrag = this.player_.volume();
  12778. this.one('sliderinactive', () => {
  12779. if (this.player_.volume() === 0) {
  12780. this.player_.lastVolume_(volumeBeforeDrag);
  12781. }
  12782. });
  12783. }
  12784. }
  12785. /**
  12786. * Default options for the `VolumeBar`
  12787. *
  12788. * @type {Object}
  12789. * @private
  12790. */
  12791. VolumeBar.prototype.options_ = {
  12792. children: ['volumeLevel'],
  12793. barName: 'volumeLevel'
  12794. };
  12795. // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
  12796. if (!IS_IOS && !IS_ANDROID) {
  12797. VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
  12798. }
  12799. /**
  12800. * Call the update event for this Slider when this event happens on the player.
  12801. *
  12802. * @type {string}
  12803. */
  12804. VolumeBar.prototype.playerEvent = 'volumechange';
  12805. Component.registerComponent('VolumeBar', VolumeBar);
  12806. /**
  12807. * @file volume-control.js
  12808. */
  12809. /**
  12810. * The component for controlling the volume level
  12811. *
  12812. * @extends Component
  12813. */
  12814. class VolumeControl extends Component {
  12815. /**
  12816. * Creates an instance of this class.
  12817. *
  12818. * @param { import('../../player').default } player
  12819. * The `Player` that this class should be attached to.
  12820. *
  12821. * @param {Object} [options={}]
  12822. * The key/value store of player options.
  12823. */
  12824. constructor(player, options = {}) {
  12825. options.vertical = options.vertical || false;
  12826. // Pass the vertical option down to the VolumeBar if
  12827. // the VolumeBar is turned on.
  12828. if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
  12829. options.volumeBar = options.volumeBar || {};
  12830. options.volumeBar.vertical = options.vertical;
  12831. }
  12832. super(player, options);
  12833. // hide this control if volume support is missing
  12834. checkVolumeSupport(this, player);
  12835. this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
  12836. this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
  12837. this.on('mousedown', e => this.handleMouseDown(e));
  12838. this.on('touchstart', e => this.handleMouseDown(e));
  12839. this.on('mousemove', e => this.handleMouseMove(e));
  12840. // while the slider is active (the mouse has been pressed down and
  12841. // is dragging) or in focus we do not want to hide the VolumeBar
  12842. this.on(this.volumeBar, ['focus', 'slideractive'], () => {
  12843. this.volumeBar.addClass('vjs-slider-active');
  12844. this.addClass('vjs-slider-active');
  12845. this.trigger('slideractive');
  12846. });
  12847. this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
  12848. this.volumeBar.removeClass('vjs-slider-active');
  12849. this.removeClass('vjs-slider-active');
  12850. this.trigger('sliderinactive');
  12851. });
  12852. }
  12853. /**
  12854. * Create the `Component`'s DOM element
  12855. *
  12856. * @return {Element}
  12857. * The element that was created.
  12858. */
  12859. createEl() {
  12860. let orientationClass = 'vjs-volume-horizontal';
  12861. if (this.options_.vertical) {
  12862. orientationClass = 'vjs-volume-vertical';
  12863. }
  12864. return super.createEl('div', {
  12865. className: `vjs-volume-control vjs-control ${orientationClass}`
  12866. });
  12867. }
  12868. /**
  12869. * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
  12870. *
  12871. * @param {Event} event
  12872. * `mousedown` or `touchstart` event that triggered this function
  12873. *
  12874. * @listens mousedown
  12875. * @listens touchstart
  12876. */
  12877. handleMouseDown(event) {
  12878. const doc = this.el_.ownerDocument;
  12879. this.on(doc, 'mousemove', this.throttledHandleMouseMove);
  12880. this.on(doc, 'touchmove', this.throttledHandleMouseMove);
  12881. this.on(doc, 'mouseup', this.handleMouseUpHandler_);
  12882. this.on(doc, 'touchend', this.handleMouseUpHandler_);
  12883. }
  12884. /**
  12885. * Handle `mouseup` or `touchend` events on the `VolumeControl`.
  12886. *
  12887. * @param {Event} event
  12888. * `mouseup` or `touchend` event that triggered this function.
  12889. *
  12890. * @listens touchend
  12891. * @listens mouseup
  12892. */
  12893. handleMouseUp(event) {
  12894. const doc = this.el_.ownerDocument;
  12895. this.off(doc, 'mousemove', this.throttledHandleMouseMove);
  12896. this.off(doc, 'touchmove', this.throttledHandleMouseMove);
  12897. this.off(doc, 'mouseup', this.handleMouseUpHandler_);
  12898. this.off(doc, 'touchend', this.handleMouseUpHandler_);
  12899. }
  12900. /**
  12901. * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
  12902. *
  12903. * @param {Event} event
  12904. * `mousedown` or `touchstart` event that triggered this function
  12905. *
  12906. * @listens mousedown
  12907. * @listens touchstart
  12908. */
  12909. handleMouseMove(event) {
  12910. this.volumeBar.handleMouseMove(event);
  12911. }
  12912. }
  12913. /**
  12914. * Default options for the `VolumeControl`
  12915. *
  12916. * @type {Object}
  12917. * @private
  12918. */
  12919. VolumeControl.prototype.options_ = {
  12920. children: ['volumeBar']
  12921. };
  12922. Component.registerComponent('VolumeControl', VolumeControl);
  12923. /**
  12924. * Check if muting volume is supported and if it isn't hide the mute toggle
  12925. * button.
  12926. *
  12927. * @param { import('../../component').default } self
  12928. * A reference to the mute toggle button
  12929. *
  12930. * @param { import('../../player').default } player
  12931. * A reference to the player
  12932. *
  12933. * @private
  12934. */
  12935. const checkMuteSupport = function (self, player) {
  12936. // hide mute toggle button if it's not supported by the current tech
  12937. if (player.tech_ && !player.tech_.featuresMuteControl) {
  12938. self.addClass('vjs-hidden');
  12939. }
  12940. self.on(player, 'loadstart', function () {
  12941. if (!player.tech_.featuresMuteControl) {
  12942. self.addClass('vjs-hidden');
  12943. } else {
  12944. self.removeClass('vjs-hidden');
  12945. }
  12946. });
  12947. };
  12948. /**
  12949. * @file mute-toggle.js
  12950. */
  12951. /**
  12952. * A button component for muting the audio.
  12953. *
  12954. * @extends Button
  12955. */
  12956. class MuteToggle extends Button {
  12957. /**
  12958. * Creates an instance of this class.
  12959. *
  12960. * @param { import('./player').default } player
  12961. * The `Player` that this class should be attached to.
  12962. *
  12963. * @param {Object} [options]
  12964. * The key/value store of player options.
  12965. */
  12966. constructor(player, options) {
  12967. super(player, options);
  12968. // hide this control if volume support is missing
  12969. checkMuteSupport(this, player);
  12970. this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
  12971. }
  12972. /**
  12973. * Builds the default DOM `className`.
  12974. *
  12975. * @return {string}
  12976. * The DOM `className` for this object.
  12977. */
  12978. buildCSSClass() {
  12979. return `vjs-mute-control ${super.buildCSSClass()}`;
  12980. }
  12981. /**
  12982. * This gets called when an `MuteToggle` is "clicked". See
  12983. * {@link ClickableComponent} for more detailed information on what a click can be.
  12984. *
  12985. * @param {Event} [event]
  12986. * The `keydown`, `tap`, or `click` event that caused this function to be
  12987. * called.
  12988. *
  12989. * @listens tap
  12990. * @listens click
  12991. */
  12992. handleClick(event) {
  12993. const vol = this.player_.volume();
  12994. const lastVolume = this.player_.lastVolume_();
  12995. if (vol === 0) {
  12996. const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
  12997. this.player_.volume(volumeToSet);
  12998. this.player_.muted(false);
  12999. } else {
  13000. this.player_.muted(this.player_.muted() ? false : true);
  13001. }
  13002. }
  13003. /**
  13004. * Update the `MuteToggle` button based on the state of `volume` and `muted`
  13005. * on the player.
  13006. *
  13007. * @param {Event} [event]
  13008. * The {@link Player#loadstart} event if this function was called
  13009. * through an event.
  13010. *
  13011. * @listens Player#loadstart
  13012. * @listens Player#volumechange
  13013. */
  13014. update(event) {
  13015. this.updateIcon_();
  13016. this.updateControlText_();
  13017. }
  13018. /**
  13019. * Update the appearance of the `MuteToggle` icon.
  13020. *
  13021. * Possible states (given `level` variable below):
  13022. * - 0: crossed out
  13023. * - 1: zero bars of volume
  13024. * - 2: one bar of volume
  13025. * - 3: two bars of volume
  13026. *
  13027. * @private
  13028. */
  13029. updateIcon_() {
  13030. const vol = this.player_.volume();
  13031. let level = 3;
  13032. this.setIcon('volume-high');
  13033. // in iOS when a player is loaded with muted attribute
  13034. // and volume is changed with a native mute button
  13035. // we want to make sure muted state is updated
  13036. if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
  13037. this.player_.muted(this.player_.tech_.el_.muted);
  13038. }
  13039. if (vol === 0 || this.player_.muted()) {
  13040. this.setIcon('volume-mute');
  13041. level = 0;
  13042. } else if (vol < 0.33) {
  13043. this.setIcon('volume-low');
  13044. level = 1;
  13045. } else if (vol < 0.67) {
  13046. this.setIcon('volume-medium');
  13047. level = 2;
  13048. }
  13049. removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
  13050. addClass(this.el_, `vjs-vol-${level}`);
  13051. }
  13052. /**
  13053. * If `muted` has changed on the player, update the control text
  13054. * (`title` attribute on `vjs-mute-control` element and content of
  13055. * `vjs-control-text` element).
  13056. *
  13057. * @private
  13058. */
  13059. updateControlText_() {
  13060. const soundOff = this.player_.muted() || this.player_.volume() === 0;
  13061. const text = soundOff ? 'Unmute' : 'Mute';
  13062. if (this.controlText() !== text) {
  13063. this.controlText(text);
  13064. }
  13065. }
  13066. }
  13067. /**
  13068. * The text that should display over the `MuteToggle`s controls. Added for localization.
  13069. *
  13070. * @type {string}
  13071. * @protected
  13072. */
  13073. MuteToggle.prototype.controlText_ = 'Mute';
  13074. Component.registerComponent('MuteToggle', MuteToggle);
  13075. /**
  13076. * @file volume-control.js
  13077. */
  13078. /**
  13079. * A Component to contain the MuteToggle and VolumeControl so that
  13080. * they can work together.
  13081. *
  13082. * @extends Component
  13083. */
  13084. class VolumePanel extends Component {
  13085. /**
  13086. * Creates an instance of this class.
  13087. *
  13088. * @param { import('./player').default } player
  13089. * The `Player` that this class should be attached to.
  13090. *
  13091. * @param {Object} [options={}]
  13092. * The key/value store of player options.
  13093. */
  13094. constructor(player, options = {}) {
  13095. if (typeof options.inline !== 'undefined') {
  13096. options.inline = options.inline;
  13097. } else {
  13098. options.inline = true;
  13099. }
  13100. // pass the inline option down to the VolumeControl as vertical if
  13101. // the VolumeControl is on.
  13102. if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
  13103. options.volumeControl = options.volumeControl || {};
  13104. options.volumeControl.vertical = !options.inline;
  13105. }
  13106. super(player, options);
  13107. // this handler is used by mouse handler methods below
  13108. this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
  13109. this.on(player, ['loadstart'], e => this.volumePanelState_(e));
  13110. this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
  13111. this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
  13112. this.on('keydown', e => this.handleKeyPress(e));
  13113. this.on('mouseover', e => this.handleMouseOver(e));
  13114. this.on('mouseout', e => this.handleMouseOut(e));
  13115. // while the slider is active (the mouse has been pressed down and
  13116. // is dragging) we do not want to hide the VolumeBar
  13117. this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
  13118. this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
  13119. }
  13120. /**
  13121. * Add vjs-slider-active class to the VolumePanel
  13122. *
  13123. * @listens VolumeControl#slideractive
  13124. * @private
  13125. */
  13126. sliderActive_() {
  13127. this.addClass('vjs-slider-active');
  13128. }
  13129. /**
  13130. * Removes vjs-slider-active class to the VolumePanel
  13131. *
  13132. * @listens VolumeControl#sliderinactive
  13133. * @private
  13134. */
  13135. sliderInactive_() {
  13136. this.removeClass('vjs-slider-active');
  13137. }
  13138. /**
  13139. * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
  13140. * depending on MuteToggle and VolumeControl state
  13141. *
  13142. * @listens Player#loadstart
  13143. * @private
  13144. */
  13145. volumePanelState_() {
  13146. // hide volume panel if neither volume control or mute toggle
  13147. // are displayed
  13148. if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
  13149. this.addClass('vjs-hidden');
  13150. }
  13151. // if only mute toggle is visible we don't want
  13152. // volume panel expanding when hovered or active
  13153. if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
  13154. this.addClass('vjs-mute-toggle-only');
  13155. }
  13156. }
  13157. /**
  13158. * Create the `Component`'s DOM element
  13159. *
  13160. * @return {Element}
  13161. * The element that was created.
  13162. */
  13163. createEl() {
  13164. let orientationClass = 'vjs-volume-panel-horizontal';
  13165. if (!this.options_.inline) {
  13166. orientationClass = 'vjs-volume-panel-vertical';
  13167. }
  13168. return super.createEl('div', {
  13169. className: `vjs-volume-panel vjs-control ${orientationClass}`
  13170. });
  13171. }
  13172. /**
  13173. * Dispose of the `volume-panel` and all child components.
  13174. */
  13175. dispose() {
  13176. this.handleMouseOut();
  13177. super.dispose();
  13178. }
  13179. /**
  13180. * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
  13181. * the volume panel and sets focus on `MuteToggle`.
  13182. *
  13183. * @param {Event} event
  13184. * The `keyup` event that caused this function to be called.
  13185. *
  13186. * @listens keyup
  13187. */
  13188. handleVolumeControlKeyUp(event) {
  13189. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  13190. this.muteToggle.focus();
  13191. }
  13192. }
  13193. /**
  13194. * This gets called when a `VolumePanel` gains hover via a `mouseover` event.
  13195. * Turns on listening for `mouseover` event. When they happen it
  13196. * calls `this.handleMouseOver`.
  13197. *
  13198. * @param {Event} event
  13199. * The `mouseover` event that caused this function to be called.
  13200. *
  13201. * @listens mouseover
  13202. */
  13203. handleMouseOver(event) {
  13204. this.addClass('vjs-hover');
  13205. on(document__default["default"], 'keyup', this.handleKeyPressHandler_);
  13206. }
  13207. /**
  13208. * This gets called when a `VolumePanel` gains hover via a `mouseout` event.
  13209. * Turns on listening for `mouseout` event. When they happen it
  13210. * calls `this.handleMouseOut`.
  13211. *
  13212. * @param {Event} event
  13213. * The `mouseout` event that caused this function to be called.
  13214. *
  13215. * @listens mouseout
  13216. */
  13217. handleMouseOut(event) {
  13218. this.removeClass('vjs-hover');
  13219. off(document__default["default"], 'keyup', this.handleKeyPressHandler_);
  13220. }
  13221. /**
  13222. * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
  13223. * looking for ESC, which hides the `VolumeControl`.
  13224. *
  13225. * @param {Event} event
  13226. * The keypress that triggered this event.
  13227. *
  13228. * @listens keydown | keyup
  13229. */
  13230. handleKeyPress(event) {
  13231. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  13232. this.handleMouseOut();
  13233. }
  13234. }
  13235. }
  13236. /**
  13237. * Default options for the `VolumeControl`
  13238. *
  13239. * @type {Object}
  13240. * @private
  13241. */
  13242. VolumePanel.prototype.options_ = {
  13243. children: ['muteToggle', 'volumeControl']
  13244. };
  13245. Component.registerComponent('VolumePanel', VolumePanel);
  13246. /**
  13247. * Button to skip forward a configurable amount of time
  13248. * through a video. Renders in the control bar.
  13249. *
  13250. * e.g. options: {controlBar: {skipButtons: forward: 5}}
  13251. *
  13252. * @extends Button
  13253. */
  13254. class SkipForward extends Button {
  13255. constructor(player, options) {
  13256. super(player, options);
  13257. this.validOptions = [5, 10, 30];
  13258. this.skipTime = this.getSkipForwardTime();
  13259. if (this.skipTime && this.validOptions.includes(this.skipTime)) {
  13260. this.setIcon(`forward-${this.skipTime}`);
  13261. this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
  13262. this.show();
  13263. } else {
  13264. this.hide();
  13265. }
  13266. }
  13267. getSkipForwardTime() {
  13268. const playerOptions = this.options_.playerOptions;
  13269. return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
  13270. }
  13271. buildCSSClass() {
  13272. return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
  13273. }
  13274. /**
  13275. * On click, skips forward in the duration/seekable range by a configurable amount of seconds.
  13276. * If the time left in the duration/seekable range is less than the configured 'skip forward' time,
  13277. * skips to end of duration/seekable range.
  13278. *
  13279. * Handle a click on a `SkipForward` button
  13280. *
  13281. * @param {EventTarget~Event} event
  13282. * The `click` event that caused this function
  13283. * to be called
  13284. */
  13285. handleClick(event) {
  13286. if (isNaN(this.player_.duration())) {
  13287. return;
  13288. }
  13289. const currentVideoTime = this.player_.currentTime();
  13290. const liveTracker = this.player_.liveTracker;
  13291. const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
  13292. let newTime;
  13293. if (currentVideoTime + this.skipTime <= duration) {
  13294. newTime = currentVideoTime + this.skipTime;
  13295. } else {
  13296. newTime = duration;
  13297. }
  13298. this.player_.currentTime(newTime);
  13299. }
  13300. /**
  13301. * Update control text on languagechange
  13302. */
  13303. handleLanguagechange() {
  13304. this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
  13305. }
  13306. }
  13307. SkipForward.prototype.controlText_ = 'Skip Forward';
  13308. Component.registerComponent('SkipForward', SkipForward);
  13309. /**
  13310. * Button to skip backward a configurable amount of time
  13311. * through a video. Renders in the control bar.
  13312. *
  13313. * * e.g. options: {controlBar: {skipButtons: backward: 5}}
  13314. *
  13315. * @extends Button
  13316. */
  13317. class SkipBackward extends Button {
  13318. constructor(player, options) {
  13319. super(player, options);
  13320. this.validOptions = [5, 10, 30];
  13321. this.skipTime = this.getSkipBackwardTime();
  13322. if (this.skipTime && this.validOptions.includes(this.skipTime)) {
  13323. this.setIcon(`replay-${this.skipTime}`);
  13324. this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
  13325. this.show();
  13326. } else {
  13327. this.hide();
  13328. }
  13329. }
  13330. getSkipBackwardTime() {
  13331. const playerOptions = this.options_.playerOptions;
  13332. return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
  13333. }
  13334. buildCSSClass() {
  13335. return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
  13336. }
  13337. /**
  13338. * On click, skips backward in the video by a configurable amount of seconds.
  13339. * If the current time in the video is less than the configured 'skip backward' time,
  13340. * skips to beginning of video or seekable range.
  13341. *
  13342. * Handle a click on a `SkipBackward` button
  13343. *
  13344. * @param {EventTarget~Event} event
  13345. * The `click` event that caused this function
  13346. * to be called
  13347. */
  13348. handleClick(event) {
  13349. const currentVideoTime = this.player_.currentTime();
  13350. const liveTracker = this.player_.liveTracker;
  13351. const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
  13352. let newTime;
  13353. if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
  13354. newTime = seekableStart;
  13355. } else if (currentVideoTime >= this.skipTime) {
  13356. newTime = currentVideoTime - this.skipTime;
  13357. } else {
  13358. newTime = 0;
  13359. }
  13360. this.player_.currentTime(newTime);
  13361. }
  13362. /**
  13363. * Update control text on languagechange
  13364. */
  13365. handleLanguagechange() {
  13366. this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
  13367. }
  13368. }
  13369. SkipBackward.prototype.controlText_ = 'Skip Backward';
  13370. Component.registerComponent('SkipBackward', SkipBackward);
  13371. /**
  13372. * @file menu.js
  13373. */
  13374. /**
  13375. * The Menu component is used to build popup menus, including subtitle and
  13376. * captions selection menus.
  13377. *
  13378. * @extends Component
  13379. */
  13380. class Menu extends Component {
  13381. /**
  13382. * Create an instance of this class.
  13383. *
  13384. * @param { import('../player').default } player
  13385. * the player that this component should attach to
  13386. *
  13387. * @param {Object} [options]
  13388. * Object of option names and values
  13389. *
  13390. */
  13391. constructor(player, options) {
  13392. super(player, options);
  13393. if (options) {
  13394. this.menuButton_ = options.menuButton;
  13395. }
  13396. this.focusedChild_ = -1;
  13397. this.on('keydown', e => this.handleKeyDown(e));
  13398. // All the menu item instances share the same blur handler provided by the menu container.
  13399. this.boundHandleBlur_ = e => this.handleBlur(e);
  13400. this.boundHandleTapClick_ = e => this.handleTapClick(e);
  13401. }
  13402. /**
  13403. * Add event listeners to the {@link MenuItem}.
  13404. *
  13405. * @param {Object} component
  13406. * The instance of the `MenuItem` to add listeners to.
  13407. *
  13408. */
  13409. addEventListenerForItem(component) {
  13410. if (!(component instanceof Component)) {
  13411. return;
  13412. }
  13413. this.on(component, 'blur', this.boundHandleBlur_);
  13414. this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
  13415. }
  13416. /**
  13417. * Remove event listeners from the {@link MenuItem}.
  13418. *
  13419. * @param {Object} component
  13420. * The instance of the `MenuItem` to remove listeners.
  13421. *
  13422. */
  13423. removeEventListenerForItem(component) {
  13424. if (!(component instanceof Component)) {
  13425. return;
  13426. }
  13427. this.off(component, 'blur', this.boundHandleBlur_);
  13428. this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
  13429. }
  13430. /**
  13431. * This method will be called indirectly when the component has been added
  13432. * before the component adds to the new menu instance by `addItem`.
  13433. * In this case, the original menu instance will remove the component
  13434. * by calling `removeChild`.
  13435. *
  13436. * @param {Object} component
  13437. * The instance of the `MenuItem`
  13438. */
  13439. removeChild(component) {
  13440. if (typeof component === 'string') {
  13441. component = this.getChild(component);
  13442. }
  13443. this.removeEventListenerForItem(component);
  13444. super.removeChild(component);
  13445. }
  13446. /**
  13447. * Add a {@link MenuItem} to the menu.
  13448. *
  13449. * @param {Object|string} component
  13450. * The name or instance of the `MenuItem` to add.
  13451. *
  13452. */
  13453. addItem(component) {
  13454. const childComponent = this.addChild(component);
  13455. if (childComponent) {
  13456. this.addEventListenerForItem(childComponent);
  13457. }
  13458. }
  13459. /**
  13460. * Create the `Menu`s DOM element.
  13461. *
  13462. * @return {Element}
  13463. * the element that was created
  13464. */
  13465. createEl() {
  13466. const contentElType = this.options_.contentElType || 'ul';
  13467. this.contentEl_ = createEl(contentElType, {
  13468. className: 'vjs-menu-content'
  13469. });
  13470. this.contentEl_.setAttribute('role', 'menu');
  13471. const el = super.createEl('div', {
  13472. append: this.contentEl_,
  13473. className: 'vjs-menu'
  13474. });
  13475. el.appendChild(this.contentEl_);
  13476. // Prevent clicks from bubbling up. Needed for Menu Buttons,
  13477. // where a click on the parent is significant
  13478. on(el, 'click', function (event) {
  13479. event.preventDefault();
  13480. event.stopImmediatePropagation();
  13481. });
  13482. return el;
  13483. }
  13484. dispose() {
  13485. this.contentEl_ = null;
  13486. this.boundHandleBlur_ = null;
  13487. this.boundHandleTapClick_ = null;
  13488. super.dispose();
  13489. }
  13490. /**
  13491. * Called when a `MenuItem` loses focus.
  13492. *
  13493. * @param {Event} event
  13494. * The `blur` event that caused this function to be called.
  13495. *
  13496. * @listens blur
  13497. */
  13498. handleBlur(event) {
  13499. const relatedTarget = event.relatedTarget || document__default["default"].activeElement;
  13500. // Close menu popup when a user clicks outside the menu
  13501. if (!this.children().some(element => {
  13502. return element.el() === relatedTarget;
  13503. })) {
  13504. const btn = this.menuButton_;
  13505. if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
  13506. btn.unpressButton();
  13507. }
  13508. }
  13509. }
  13510. /**
  13511. * Called when a `MenuItem` gets clicked or tapped.
  13512. *
  13513. * @param {Event} event
  13514. * The `click` or `tap` event that caused this function to be called.
  13515. *
  13516. * @listens click,tap
  13517. */
  13518. handleTapClick(event) {
  13519. // Unpress the associated MenuButton, and move focus back to it
  13520. if (this.menuButton_) {
  13521. this.menuButton_.unpressButton();
  13522. const childComponents = this.children();
  13523. if (!Array.isArray(childComponents)) {
  13524. return;
  13525. }
  13526. const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
  13527. if (!foundComponent) {
  13528. return;
  13529. }
  13530. // don't focus menu button if item is a caption settings item
  13531. // because focus will move elsewhere
  13532. if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
  13533. this.menuButton_.focus();
  13534. }
  13535. }
  13536. }
  13537. /**
  13538. * Handle a `keydown` event on this menu. This listener is added in the constructor.
  13539. *
  13540. * @param {KeyboardEvent} event
  13541. * A `keydown` event that happened on the menu.
  13542. *
  13543. * @listens keydown
  13544. */
  13545. handleKeyDown(event) {
  13546. // Left and Down Arrows
  13547. if (keycode__default["default"].isEventKey(event, 'Left') || keycode__default["default"].isEventKey(event, 'Down')) {
  13548. event.preventDefault();
  13549. event.stopPropagation();
  13550. this.stepForward();
  13551. // Up and Right Arrows
  13552. } else if (keycode__default["default"].isEventKey(event, 'Right') || keycode__default["default"].isEventKey(event, 'Up')) {
  13553. event.preventDefault();
  13554. event.stopPropagation();
  13555. this.stepBack();
  13556. }
  13557. }
  13558. /**
  13559. * Move to next (lower) menu item for keyboard users.
  13560. */
  13561. stepForward() {
  13562. let stepChild = 0;
  13563. if (this.focusedChild_ !== undefined) {
  13564. stepChild = this.focusedChild_ + 1;
  13565. }
  13566. this.focus(stepChild);
  13567. }
  13568. /**
  13569. * Move to previous (higher) menu item for keyboard users.
  13570. */
  13571. stepBack() {
  13572. let stepChild = 0;
  13573. if (this.focusedChild_ !== undefined) {
  13574. stepChild = this.focusedChild_ - 1;
  13575. }
  13576. this.focus(stepChild);
  13577. }
  13578. /**
  13579. * Set focus on a {@link MenuItem} in the `Menu`.
  13580. *
  13581. * @param {Object|string} [item=0]
  13582. * Index of child item set focus on.
  13583. */
  13584. focus(item = 0) {
  13585. const children = this.children().slice();
  13586. const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
  13587. if (haveTitle) {
  13588. children.shift();
  13589. }
  13590. if (children.length > 0) {
  13591. if (item < 0) {
  13592. item = 0;
  13593. } else if (item >= children.length) {
  13594. item = children.length - 1;
  13595. }
  13596. this.focusedChild_ = item;
  13597. children[item].el_.focus();
  13598. }
  13599. }
  13600. }
  13601. Component.registerComponent('Menu', Menu);
  13602. /**
  13603. * @file menu-button.js
  13604. */
  13605. /**
  13606. * A `MenuButton` class for any popup {@link Menu}.
  13607. *
  13608. * @extends Component
  13609. */
  13610. class MenuButton extends Component {
  13611. /**
  13612. * Creates an instance of this class.
  13613. *
  13614. * @param { import('../player').default } player
  13615. * The `Player` that this class should be attached to.
  13616. *
  13617. * @param {Object} [options={}]
  13618. * The key/value store of player options.
  13619. */
  13620. constructor(player, options = {}) {
  13621. super(player, options);
  13622. this.menuButton_ = new Button(player, options);
  13623. this.menuButton_.controlText(this.controlText_);
  13624. this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
  13625. // Add buildCSSClass values to the button, not the wrapper
  13626. const buttonClass = Button.prototype.buildCSSClass();
  13627. this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
  13628. this.menuButton_.removeClass('vjs-control');
  13629. this.addChild(this.menuButton_);
  13630. this.update();
  13631. this.enabled_ = true;
  13632. const handleClick = e => this.handleClick(e);
  13633. this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
  13634. this.on(this.menuButton_, 'tap', handleClick);
  13635. this.on(this.menuButton_, 'click', handleClick);
  13636. this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
  13637. this.on(this.menuButton_, 'mouseenter', () => {
  13638. this.addClass('vjs-hover');
  13639. this.menu.show();
  13640. on(document__default["default"], 'keyup', this.handleMenuKeyUp_);
  13641. });
  13642. this.on('mouseleave', e => this.handleMouseLeave(e));
  13643. this.on('keydown', e => this.handleSubmenuKeyDown(e));
  13644. }
  13645. /**
  13646. * Update the menu based on the current state of its items.
  13647. */
  13648. update() {
  13649. const menu = this.createMenu();
  13650. if (this.menu) {
  13651. this.menu.dispose();
  13652. this.removeChild(this.menu);
  13653. }
  13654. this.menu = menu;
  13655. this.addChild(menu);
  13656. /**
  13657. * Track the state of the menu button
  13658. *
  13659. * @type {Boolean}
  13660. * @private
  13661. */
  13662. this.buttonPressed_ = false;
  13663. this.menuButton_.el_.setAttribute('aria-expanded', 'false');
  13664. if (this.items && this.items.length <= this.hideThreshold_) {
  13665. this.hide();
  13666. this.menu.contentEl_.removeAttribute('role');
  13667. } else {
  13668. this.show();
  13669. this.menu.contentEl_.setAttribute('role', 'menu');
  13670. }
  13671. }
  13672. /**
  13673. * Create the menu and add all items to it.
  13674. *
  13675. * @return {Menu}
  13676. * The constructed menu
  13677. */
  13678. createMenu() {
  13679. const menu = new Menu(this.player_, {
  13680. menuButton: this
  13681. });
  13682. /**
  13683. * Hide the menu if the number of items is less than or equal to this threshold. This defaults
  13684. * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
  13685. * it here because every time we run `createMenu` we need to reset the value.
  13686. *
  13687. * @protected
  13688. * @type {Number}
  13689. */
  13690. this.hideThreshold_ = 0;
  13691. // Add a title list item to the top
  13692. if (this.options_.title) {
  13693. const titleEl = createEl('li', {
  13694. className: 'vjs-menu-title',
  13695. textContent: toTitleCase(this.options_.title),
  13696. tabIndex: -1
  13697. });
  13698. const titleComponent = new Component(this.player_, {
  13699. el: titleEl
  13700. });
  13701. menu.addItem(titleComponent);
  13702. }
  13703. this.items = this.createItems();
  13704. if (this.items) {
  13705. // Add menu items to the menu
  13706. for (let i = 0; i < this.items.length; i++) {
  13707. menu.addItem(this.items[i]);
  13708. }
  13709. }
  13710. return menu;
  13711. }
  13712. /**
  13713. * Create the list of menu items. Specific to each subclass.
  13714. *
  13715. * @abstract
  13716. */
  13717. createItems() {}
  13718. /**
  13719. * Create the `MenuButtons`s DOM element.
  13720. *
  13721. * @return {Element}
  13722. * The element that gets created.
  13723. */
  13724. createEl() {
  13725. return super.createEl('div', {
  13726. className: this.buildWrapperCSSClass()
  13727. }, {});
  13728. }
  13729. /**
  13730. * Overwrites the `setIcon` method from `Component`.
  13731. * In this case, we want the icon to be appended to the menuButton.
  13732. *
  13733. * @param {string} name
  13734. * The icon name to be added.
  13735. */
  13736. setIcon(name) {
  13737. super.setIcon(name, this.menuButton_.el_);
  13738. }
  13739. /**
  13740. * Allow sub components to stack CSS class names for the wrapper element
  13741. *
  13742. * @return {string}
  13743. * The constructed wrapper DOM `className`
  13744. */
  13745. buildWrapperCSSClass() {
  13746. let menuButtonClass = 'vjs-menu-button';
  13747. // If the inline option is passed, we want to use different styles altogether.
  13748. if (this.options_.inline === true) {
  13749. menuButtonClass += '-inline';
  13750. } else {
  13751. menuButtonClass += '-popup';
  13752. }
  13753. // TODO: Fix the CSS so that this isn't necessary
  13754. const buttonClass = Button.prototype.buildCSSClass();
  13755. return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
  13756. }
  13757. /**
  13758. * Builds the default DOM `className`.
  13759. *
  13760. * @return {string}
  13761. * The DOM `className` for this object.
  13762. */
  13763. buildCSSClass() {
  13764. let menuButtonClass = 'vjs-menu-button';
  13765. // If the inline option is passed, we want to use different styles altogether.
  13766. if (this.options_.inline === true) {
  13767. menuButtonClass += '-inline';
  13768. } else {
  13769. menuButtonClass += '-popup';
  13770. }
  13771. return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
  13772. }
  13773. /**
  13774. * Get or set the localized control text that will be used for accessibility.
  13775. *
  13776. * > NOTE: This will come from the internal `menuButton_` element.
  13777. *
  13778. * @param {string} [text]
  13779. * Control text for element.
  13780. *
  13781. * @param {Element} [el=this.menuButton_.el()]
  13782. * Element to set the title on.
  13783. *
  13784. * @return {string}
  13785. * - The control text when getting
  13786. */
  13787. controlText(text, el = this.menuButton_.el()) {
  13788. return this.menuButton_.controlText(text, el);
  13789. }
  13790. /**
  13791. * Dispose of the `menu-button` and all child components.
  13792. */
  13793. dispose() {
  13794. this.handleMouseLeave();
  13795. super.dispose();
  13796. }
  13797. /**
  13798. * Handle a click on a `MenuButton`.
  13799. * See {@link ClickableComponent#handleClick} for instances where this is called.
  13800. *
  13801. * @param {Event} event
  13802. * The `keydown`, `tap`, or `click` event that caused this function to be
  13803. * called.
  13804. *
  13805. * @listens tap
  13806. * @listens click
  13807. */
  13808. handleClick(event) {
  13809. if (this.buttonPressed_) {
  13810. this.unpressButton();
  13811. } else {
  13812. this.pressButton();
  13813. }
  13814. }
  13815. /**
  13816. * Handle `mouseleave` for `MenuButton`.
  13817. *
  13818. * @param {Event} event
  13819. * The `mouseleave` event that caused this function to be called.
  13820. *
  13821. * @listens mouseleave
  13822. */
  13823. handleMouseLeave(event) {
  13824. this.removeClass('vjs-hover');
  13825. off(document__default["default"], 'keyup', this.handleMenuKeyUp_);
  13826. }
  13827. /**
  13828. * Set the focus to the actual button, not to this element
  13829. */
  13830. focus() {
  13831. this.menuButton_.focus();
  13832. }
  13833. /**
  13834. * Remove the focus from the actual button, not this element
  13835. */
  13836. blur() {
  13837. this.menuButton_.blur();
  13838. }
  13839. /**
  13840. * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
  13841. * {@link ClickableComponent#handleKeyDown} for instances where this is called.
  13842. *
  13843. * @param {Event} event
  13844. * The `keydown` event that caused this function to be called.
  13845. *
  13846. * @listens keydown
  13847. */
  13848. handleKeyDown(event) {
  13849. // Escape or Tab unpress the 'button'
  13850. if (keycode__default["default"].isEventKey(event, 'Esc') || keycode__default["default"].isEventKey(event, 'Tab')) {
  13851. if (this.buttonPressed_) {
  13852. this.unpressButton();
  13853. }
  13854. // Don't preventDefault for Tab key - we still want to lose focus
  13855. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  13856. event.preventDefault();
  13857. // Set focus back to the menu button's button
  13858. this.menuButton_.focus();
  13859. }
  13860. // Up Arrow or Down Arrow also 'press' the button to open the menu
  13861. } else if (keycode__default["default"].isEventKey(event, 'Up') || keycode__default["default"].isEventKey(event, 'Down')) {
  13862. if (!this.buttonPressed_) {
  13863. event.preventDefault();
  13864. this.pressButton();
  13865. }
  13866. }
  13867. }
  13868. /**
  13869. * Handle a `keyup` event on a `MenuButton`. The listener for this is added in
  13870. * the constructor.
  13871. *
  13872. * @param {Event} event
  13873. * Key press event
  13874. *
  13875. * @listens keyup
  13876. */
  13877. handleMenuKeyUp(event) {
  13878. // Escape hides popup menu
  13879. if (keycode__default["default"].isEventKey(event, 'Esc') || keycode__default["default"].isEventKey(event, 'Tab')) {
  13880. this.removeClass('vjs-hover');
  13881. }
  13882. }
  13883. /**
  13884. * This method name now delegates to `handleSubmenuKeyDown`. This means
  13885. * anyone calling `handleSubmenuKeyPress` will not see their method calls
  13886. * stop working.
  13887. *
  13888. * @param {Event} event
  13889. * The event that caused this function to be called.
  13890. */
  13891. handleSubmenuKeyPress(event) {
  13892. this.handleSubmenuKeyDown(event);
  13893. }
  13894. /**
  13895. * Handle a `keydown` event on a sub-menu. The listener for this is added in
  13896. * the constructor.
  13897. *
  13898. * @param {Event} event
  13899. * Key press event
  13900. *
  13901. * @listens keydown
  13902. */
  13903. handleSubmenuKeyDown(event) {
  13904. // Escape or Tab unpress the 'button'
  13905. if (keycode__default["default"].isEventKey(event, 'Esc') || keycode__default["default"].isEventKey(event, 'Tab')) {
  13906. if (this.buttonPressed_) {
  13907. this.unpressButton();
  13908. }
  13909. // Don't preventDefault for Tab key - we still want to lose focus
  13910. if (!keycode__default["default"].isEventKey(event, 'Tab')) {
  13911. event.preventDefault();
  13912. // Set focus back to the menu button's button
  13913. this.menuButton_.focus();
  13914. }
  13915. }
  13916. }
  13917. /**
  13918. * Put the current `MenuButton` into a pressed state.
  13919. */
  13920. pressButton() {
  13921. if (this.enabled_) {
  13922. this.buttonPressed_ = true;
  13923. this.menu.show();
  13924. this.menu.lockShowing();
  13925. this.menuButton_.el_.setAttribute('aria-expanded', 'true');
  13926. // set the focus into the submenu, except on iOS where it is resulting in
  13927. // undesired scrolling behavior when the player is in an iframe
  13928. if (IS_IOS && isInFrame()) {
  13929. // Return early so that the menu isn't focused
  13930. return;
  13931. }
  13932. this.menu.focus();
  13933. }
  13934. }
  13935. /**
  13936. * Take the current `MenuButton` out of a pressed state.
  13937. */
  13938. unpressButton() {
  13939. if (this.enabled_) {
  13940. this.buttonPressed_ = false;
  13941. this.menu.unlockShowing();
  13942. this.menu.hide();
  13943. this.menuButton_.el_.setAttribute('aria-expanded', 'false');
  13944. }
  13945. }
  13946. /**
  13947. * Disable the `MenuButton`. Don't allow it to be clicked.
  13948. */
  13949. disable() {
  13950. this.unpressButton();
  13951. this.enabled_ = false;
  13952. this.addClass('vjs-disabled');
  13953. this.menuButton_.disable();
  13954. }
  13955. /**
  13956. * Enable the `MenuButton`. Allow it to be clicked.
  13957. */
  13958. enable() {
  13959. this.enabled_ = true;
  13960. this.removeClass('vjs-disabled');
  13961. this.menuButton_.enable();
  13962. }
  13963. }
  13964. Component.registerComponent('MenuButton', MenuButton);
  13965. /**
  13966. * @file track-button.js
  13967. */
  13968. /**
  13969. * The base class for buttons that toggle specific track types (e.g. subtitles).
  13970. *
  13971. * @extends MenuButton
  13972. */
  13973. class TrackButton extends MenuButton {
  13974. /**
  13975. * Creates an instance of this class.
  13976. *
  13977. * @param { import('./player').default } player
  13978. * The `Player` that this class should be attached to.
  13979. *
  13980. * @param {Object} [options]
  13981. * The key/value store of player options.
  13982. */
  13983. constructor(player, options) {
  13984. const tracks = options.tracks;
  13985. super(player, options);
  13986. if (this.items.length <= 1) {
  13987. this.hide();
  13988. }
  13989. if (!tracks) {
  13990. return;
  13991. }
  13992. const updateHandler = bind_(this, this.update);
  13993. tracks.addEventListener('removetrack', updateHandler);
  13994. tracks.addEventListener('addtrack', updateHandler);
  13995. tracks.addEventListener('labelchange', updateHandler);
  13996. this.player_.on('ready', updateHandler);
  13997. this.player_.on('dispose', function () {
  13998. tracks.removeEventListener('removetrack', updateHandler);
  13999. tracks.removeEventListener('addtrack', updateHandler);
  14000. tracks.removeEventListener('labelchange', updateHandler);
  14001. });
  14002. }
  14003. }
  14004. Component.registerComponent('TrackButton', TrackButton);
  14005. /**
  14006. * @file menu-keys.js
  14007. */
  14008. /**
  14009. * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`)
  14010. * Note that 'Enter' and 'Space' are not included here (otherwise they would
  14011. * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable)
  14012. *
  14013. * @typedef MenuKeys
  14014. * @array
  14015. */
  14016. const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left'];
  14017. /**
  14018. * @file menu-item.js
  14019. */
  14020. /**
  14021. * The component for a menu item. `<li>`
  14022. *
  14023. * @extends ClickableComponent
  14024. */
  14025. class MenuItem extends ClickableComponent {
  14026. /**
  14027. * Creates an instance of the this class.
  14028. *
  14029. * @param { import('../player').default } player
  14030. * The `Player` that this class should be attached to.
  14031. *
  14032. * @param {Object} [options={}]
  14033. * The key/value store of player options.
  14034. *
  14035. */
  14036. constructor(player, options) {
  14037. super(player, options);
  14038. this.selectable = options.selectable;
  14039. this.isSelected_ = options.selected || false;
  14040. this.multiSelectable = options.multiSelectable;
  14041. this.selected(this.isSelected_);
  14042. if (this.selectable) {
  14043. if (this.multiSelectable) {
  14044. this.el_.setAttribute('role', 'menuitemcheckbox');
  14045. } else {
  14046. this.el_.setAttribute('role', 'menuitemradio');
  14047. }
  14048. } else {
  14049. this.el_.setAttribute('role', 'menuitem');
  14050. }
  14051. }
  14052. /**
  14053. * Create the `MenuItem's DOM element
  14054. *
  14055. * @param {string} [type=li]
  14056. * Element's node type, not actually used, always set to `li`.
  14057. *
  14058. * @param {Object} [props={}]
  14059. * An object of properties that should be set on the element
  14060. *
  14061. * @param {Object} [attrs={}]
  14062. * An object of attributes that should be set on the element
  14063. *
  14064. * @return {Element}
  14065. * The element that gets created.
  14066. */
  14067. createEl(type, props, attrs) {
  14068. // The control is textual, not just an icon
  14069. this.nonIconControl = true;
  14070. const el = super.createEl('li', Object.assign({
  14071. className: 'vjs-menu-item',
  14072. tabIndex: -1
  14073. }, props), attrs);
  14074. // swap icon with menu item text.
  14075. const menuItemEl = createEl('span', {
  14076. className: 'vjs-menu-item-text',
  14077. textContent: this.localize(this.options_.label)
  14078. });
  14079. // If using SVG icons, the element with vjs-icon-placeholder will be added separately.
  14080. if (this.player_.options_.experimentalSvgIcons) {
  14081. el.appendChild(menuItemEl);
  14082. } else {
  14083. el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder'));
  14084. }
  14085. return el;
  14086. }
  14087. /**
  14088. * Ignore keys which are used by the menu, but pass any other ones up. See
  14089. * {@link ClickableComponent#handleKeyDown} for instances where this is called.
  14090. *
  14091. * @param {KeyboardEvent} event
  14092. * The `keydown` event that caused this function to be called.
  14093. *
  14094. * @listens keydown
  14095. */
  14096. handleKeyDown(event) {
  14097. if (!MenuKeys.some(key => keycode__default["default"].isEventKey(event, key))) {
  14098. // Pass keydown handling up for unused keys
  14099. super.handleKeyDown(event);
  14100. }
  14101. }
  14102. /**
  14103. * Any click on a `MenuItem` puts it into the selected state.
  14104. * See {@link ClickableComponent#handleClick} for instances where this is called.
  14105. *
  14106. * @param {Event} event
  14107. * The `keydown`, `tap`, or `click` event that caused this function to be
  14108. * called.
  14109. *
  14110. * @listens tap
  14111. * @listens click
  14112. */
  14113. handleClick(event) {
  14114. this.selected(true);
  14115. }
  14116. /**
  14117. * Set the state for this menu item as selected or not.
  14118. *
  14119. * @param {boolean} selected
  14120. * if the menu item is selected or not
  14121. */
  14122. selected(selected) {
  14123. if (this.selectable) {
  14124. if (selected) {
  14125. this.addClass('vjs-selected');
  14126. this.el_.setAttribute('aria-checked', 'true');
  14127. // aria-checked isn't fully supported by browsers/screen readers,
  14128. // so indicate selected state to screen reader in the control text.
  14129. this.controlText(', selected');
  14130. this.isSelected_ = true;
  14131. } else {
  14132. this.removeClass('vjs-selected');
  14133. this.el_.setAttribute('aria-checked', 'false');
  14134. // Indicate un-selected state to screen reader
  14135. this.controlText('');
  14136. this.isSelected_ = false;
  14137. }
  14138. }
  14139. }
  14140. }
  14141. Component.registerComponent('MenuItem', MenuItem);
  14142. /**
  14143. * @file text-track-menu-item.js
  14144. */
  14145. /**
  14146. * The specific menu item type for selecting a language within a text track kind
  14147. *
  14148. * @extends MenuItem
  14149. */
  14150. class TextTrackMenuItem extends MenuItem {
  14151. /**
  14152. * Creates an instance of this class.
  14153. *
  14154. * @param { import('../../player').default } player
  14155. * The `Player` that this class should be attached to.
  14156. *
  14157. * @param {Object} [options]
  14158. * The key/value store of player options.
  14159. */
  14160. constructor(player, options) {
  14161. const track = options.track;
  14162. const tracks = player.textTracks();
  14163. // Modify options for parent MenuItem class's init.
  14164. options.label = track.label || track.language || 'Unknown';
  14165. options.selected = track.mode === 'showing';
  14166. super(player, options);
  14167. this.track = track;
  14168. // Determine the relevant kind(s) of tracks for this component and filter
  14169. // out empty kinds.
  14170. this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
  14171. const changeHandler = (...args) => {
  14172. this.handleTracksChange.apply(this, args);
  14173. };
  14174. const selectedLanguageChangeHandler = (...args) => {
  14175. this.handleSelectedLanguageChange.apply(this, args);
  14176. };
  14177. player.on(['loadstart', 'texttrackchange'], changeHandler);
  14178. tracks.addEventListener('change', changeHandler);
  14179. tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
  14180. this.on('dispose', function () {
  14181. player.off(['loadstart', 'texttrackchange'], changeHandler);
  14182. tracks.removeEventListener('change', changeHandler);
  14183. tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
  14184. });
  14185. // iOS7 doesn't dispatch change events to TextTrackLists when an
  14186. // associated track's mode changes. Without something like
  14187. // Object.observe() (also not present on iOS7), it's not
  14188. // possible to detect changes to the mode attribute and polyfill
  14189. // the change event. As a poor substitute, we manually dispatch
  14190. // change events whenever the controls modify the mode.
  14191. if (tracks.onchange === undefined) {
  14192. let event;
  14193. this.on(['tap', 'click'], function () {
  14194. if (typeof window__default["default"].Event !== 'object') {
  14195. // Android 2.3 throws an Illegal Constructor error for window.Event
  14196. try {
  14197. event = new window__default["default"].Event('change');
  14198. } catch (err) {
  14199. // continue regardless of error
  14200. }
  14201. }
  14202. if (!event) {
  14203. event = document__default["default"].createEvent('Event');
  14204. event.initEvent('change', true, true);
  14205. }
  14206. tracks.dispatchEvent(event);
  14207. });
  14208. }
  14209. // set the default state based on current tracks
  14210. this.handleTracksChange();
  14211. }
  14212. /**
  14213. * This gets called when an `TextTrackMenuItem` is "clicked". See
  14214. * {@link ClickableComponent} for more detailed information on what a click can be.
  14215. *
  14216. * @param {Event} event
  14217. * The `keydown`, `tap`, or `click` event that caused this function to be
  14218. * called.
  14219. *
  14220. * @listens tap
  14221. * @listens click
  14222. */
  14223. handleClick(event) {
  14224. const referenceTrack = this.track;
  14225. const tracks = this.player_.textTracks();
  14226. super.handleClick(event);
  14227. if (!tracks) {
  14228. return;
  14229. }
  14230. for (let i = 0; i < tracks.length; i++) {
  14231. const track = tracks[i];
  14232. // If the track from the text tracks list is not of the right kind,
  14233. // skip it. We do not want to affect tracks of incompatible kind(s).
  14234. if (this.kinds.indexOf(track.kind) === -1) {
  14235. continue;
  14236. }
  14237. // If this text track is the component's track and it is not showing,
  14238. // set it to showing.
  14239. if (track === referenceTrack) {
  14240. if (track.mode !== 'showing') {
  14241. track.mode = 'showing';
  14242. }
  14243. // If this text track is not the component's track and it is not
  14244. // disabled, set it to disabled.
  14245. } else if (track.mode !== 'disabled') {
  14246. track.mode = 'disabled';
  14247. }
  14248. }
  14249. }
  14250. /**
  14251. * Handle text track list change
  14252. *
  14253. * @param {Event} event
  14254. * The `change` event that caused this function to be called.
  14255. *
  14256. * @listens TextTrackList#change
  14257. */
  14258. handleTracksChange(event) {
  14259. const shouldBeSelected = this.track.mode === 'showing';
  14260. // Prevent redundant selected() calls because they may cause
  14261. // screen readers to read the appended control text unnecessarily
  14262. if (shouldBeSelected !== this.isSelected_) {
  14263. this.selected(shouldBeSelected);
  14264. }
  14265. }
  14266. handleSelectedLanguageChange(event) {
  14267. if (this.track.mode === 'showing') {
  14268. const selectedLanguage = this.player_.cache_.selectedLanguage;
  14269. // Don't replace the kind of track across the same language
  14270. if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
  14271. return;
  14272. }
  14273. this.player_.cache_.selectedLanguage = {
  14274. enabled: true,
  14275. language: this.track.language,
  14276. kind: this.track.kind
  14277. };
  14278. }
  14279. }
  14280. dispose() {
  14281. // remove reference to track object on dispose
  14282. this.track = null;
  14283. super.dispose();
  14284. }
  14285. }
  14286. Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
  14287. /**
  14288. * @file off-text-track-menu-item.js
  14289. */
  14290. /**
  14291. * A special menu item for turning off a specific type of text track
  14292. *
  14293. * @extends TextTrackMenuItem
  14294. */
  14295. class OffTextTrackMenuItem extends TextTrackMenuItem {
  14296. /**
  14297. * Creates an instance of this class.
  14298. *
  14299. * @param { import('../../player').default } player
  14300. * The `Player` that this class should be attached to.
  14301. *
  14302. * @param {Object} [options]
  14303. * The key/value store of player options.
  14304. */
  14305. constructor(player, options) {
  14306. // Create pseudo track info
  14307. // Requires options['kind']
  14308. options.track = {
  14309. player,
  14310. // it is no longer necessary to store `kind` or `kinds` on the track itself
  14311. // since they are now stored in the `kinds` property of all instances of
  14312. // TextTrackMenuItem, but this will remain for backwards compatibility
  14313. kind: options.kind,
  14314. kinds: options.kinds,
  14315. default: false,
  14316. mode: 'disabled'
  14317. };
  14318. if (!options.kinds) {
  14319. options.kinds = [options.kind];
  14320. }
  14321. if (options.label) {
  14322. options.track.label = options.label;
  14323. } else {
  14324. options.track.label = options.kinds.join(' and ') + ' off';
  14325. }
  14326. // MenuItem is selectable
  14327. options.selectable = true;
  14328. // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
  14329. options.multiSelectable = false;
  14330. super(player, options);
  14331. }
  14332. /**
  14333. * Handle text track change
  14334. *
  14335. * @param {Event} event
  14336. * The event that caused this function to run
  14337. */
  14338. handleTracksChange(event) {
  14339. const tracks = this.player().textTracks();
  14340. let shouldBeSelected = true;
  14341. for (let i = 0, l = tracks.length; i < l; i++) {
  14342. const track = tracks[i];
  14343. if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
  14344. shouldBeSelected = false;
  14345. break;
  14346. }
  14347. }
  14348. // Prevent redundant selected() calls because they may cause
  14349. // screen readers to read the appended control text unnecessarily
  14350. if (shouldBeSelected !== this.isSelected_) {
  14351. this.selected(shouldBeSelected);
  14352. }
  14353. }
  14354. handleSelectedLanguageChange(event) {
  14355. const tracks = this.player().textTracks();
  14356. let allHidden = true;
  14357. for (let i = 0, l = tracks.length; i < l; i++) {
  14358. const track = tracks[i];
  14359. if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
  14360. allHidden = false;
  14361. break;
  14362. }
  14363. }
  14364. if (allHidden) {
  14365. this.player_.cache_.selectedLanguage = {
  14366. enabled: false
  14367. };
  14368. }
  14369. }
  14370. /**
  14371. * Update control text and label on languagechange
  14372. */
  14373. handleLanguagechange() {
  14374. this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
  14375. super.handleLanguagechange();
  14376. }
  14377. }
  14378. Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
  14379. /**
  14380. * @file text-track-button.js
  14381. */
  14382. /**
  14383. * The base class for buttons that toggle specific text track types (e.g. subtitles)
  14384. *
  14385. * @extends MenuButton
  14386. */
  14387. class TextTrackButton extends TrackButton {
  14388. /**
  14389. * Creates an instance of this class.
  14390. *
  14391. * @param { import('../../player').default } player
  14392. * The `Player` that this class should be attached to.
  14393. *
  14394. * @param {Object} [options={}]
  14395. * The key/value store of player options.
  14396. */
  14397. constructor(player, options = {}) {
  14398. options.tracks = player.textTracks();
  14399. super(player, options);
  14400. }
  14401. /**
  14402. * Create a menu item for each text track
  14403. *
  14404. * @param {TextTrackMenuItem[]} [items=[]]
  14405. * Existing array of items to use during creation
  14406. *
  14407. * @return {TextTrackMenuItem[]}
  14408. * Array of menu items that were created
  14409. */
  14410. createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
  14411. // Label is an override for the [track] off label
  14412. // USed to localise captions/subtitles
  14413. let label;
  14414. if (this.label_) {
  14415. label = `${this.label_} off`;
  14416. }
  14417. // Add an OFF menu item to turn all tracks off
  14418. items.push(new OffTextTrackMenuItem(this.player_, {
  14419. kinds: this.kinds_,
  14420. kind: this.kind_,
  14421. label
  14422. }));
  14423. this.hideThreshold_ += 1;
  14424. const tracks = this.player_.textTracks();
  14425. if (!Array.isArray(this.kinds_)) {
  14426. this.kinds_ = [this.kind_];
  14427. }
  14428. for (let i = 0; i < tracks.length; i++) {
  14429. const track = tracks[i];
  14430. // only add tracks that are of an appropriate kind and have a label
  14431. if (this.kinds_.indexOf(track.kind) > -1) {
  14432. const item = new TrackMenuItem(this.player_, {
  14433. track,
  14434. kinds: this.kinds_,
  14435. kind: this.kind_,
  14436. // MenuItem is selectable
  14437. selectable: true,
  14438. // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
  14439. multiSelectable: false
  14440. });
  14441. item.addClass(`vjs-${track.kind}-menu-item`);
  14442. items.push(item);
  14443. }
  14444. }
  14445. return items;
  14446. }
  14447. }
  14448. Component.registerComponent('TextTrackButton', TextTrackButton);
  14449. /**
  14450. * @file chapters-track-menu-item.js
  14451. */
  14452. /**
  14453. * The chapter track menu item
  14454. *
  14455. * @extends MenuItem
  14456. */
  14457. class ChaptersTrackMenuItem extends MenuItem {
  14458. /**
  14459. * Creates an instance of this class.
  14460. *
  14461. * @param { import('../../player').default } player
  14462. * The `Player` that this class should be attached to.
  14463. *
  14464. * @param {Object} [options]
  14465. * The key/value store of player options.
  14466. */
  14467. constructor(player, options) {
  14468. const track = options.track;
  14469. const cue = options.cue;
  14470. const currentTime = player.currentTime();
  14471. // Modify options for parent MenuItem class's init.
  14472. options.selectable = true;
  14473. options.multiSelectable = false;
  14474. options.label = cue.text;
  14475. options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
  14476. super(player, options);
  14477. this.track = track;
  14478. this.cue = cue;
  14479. }
  14480. /**
  14481. * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
  14482. * {@link ClickableComponent} for more detailed information on what a click can be.
  14483. *
  14484. * @param {Event} [event]
  14485. * The `keydown`, `tap`, or `click` event that caused this function to be
  14486. * called.
  14487. *
  14488. * @listens tap
  14489. * @listens click
  14490. */
  14491. handleClick(event) {
  14492. super.handleClick();
  14493. this.player_.currentTime(this.cue.startTime);
  14494. }
  14495. }
  14496. Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
  14497. /**
  14498. * @file chapters-button.js
  14499. */
  14500. /**
  14501. * The button component for toggling and selecting chapters
  14502. * Chapters act much differently than other text tracks
  14503. * Cues are navigation vs. other tracks of alternative languages
  14504. *
  14505. * @extends TextTrackButton
  14506. */
  14507. class ChaptersButton extends TextTrackButton {
  14508. /**
  14509. * Creates an instance of this class.
  14510. *
  14511. * @param { import('../../player').default } player
  14512. * The `Player` that this class should be attached to.
  14513. *
  14514. * @param {Object} [options]
  14515. * The key/value store of player options.
  14516. *
  14517. * @param {Function} [ready]
  14518. * The function to call when this function is ready.
  14519. */
  14520. constructor(player, options, ready) {
  14521. super(player, options, ready);
  14522. this.setIcon('chapters');
  14523. this.selectCurrentItem_ = () => {
  14524. this.items.forEach(item => {
  14525. item.selected(this.track_.activeCues[0] === item.cue);
  14526. });
  14527. };
  14528. }
  14529. /**
  14530. * Builds the default DOM `className`.
  14531. *
  14532. * @return {string}
  14533. * The DOM `className` for this object.
  14534. */
  14535. buildCSSClass() {
  14536. return `vjs-chapters-button ${super.buildCSSClass()}`;
  14537. }
  14538. buildWrapperCSSClass() {
  14539. return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
  14540. }
  14541. /**
  14542. * Update the menu based on the current state of its items.
  14543. *
  14544. * @param {Event} [event]
  14545. * An event that triggered this function to run.
  14546. *
  14547. * @listens TextTrackList#addtrack
  14548. * @listens TextTrackList#removetrack
  14549. * @listens TextTrackList#change
  14550. */
  14551. update(event) {
  14552. if (event && event.track && event.track.kind !== 'chapters') {
  14553. return;
  14554. }
  14555. const track = this.findChaptersTrack();
  14556. if (track !== this.track_) {
  14557. this.setTrack(track);
  14558. super.update();
  14559. } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
  14560. // Update the menu initially or if the number of cues has changed since set
  14561. super.update();
  14562. }
  14563. }
  14564. /**
  14565. * Set the currently selected track for the chapters button.
  14566. *
  14567. * @param {TextTrack} track
  14568. * The new track to select. Nothing will change if this is the currently selected
  14569. * track.
  14570. */
  14571. setTrack(track) {
  14572. if (this.track_ === track) {
  14573. return;
  14574. }
  14575. if (!this.updateHandler_) {
  14576. this.updateHandler_ = this.update.bind(this);
  14577. }
  14578. // here this.track_ refers to the old track instance
  14579. if (this.track_) {
  14580. const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
  14581. if (remoteTextTrackEl) {
  14582. remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
  14583. }
  14584. this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
  14585. this.track_ = null;
  14586. }
  14587. this.track_ = track;
  14588. // here this.track_ refers to the new track instance
  14589. if (this.track_) {
  14590. this.track_.mode = 'hidden';
  14591. const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
  14592. if (remoteTextTrackEl) {
  14593. remoteTextTrackEl.addEventListener('load', this.updateHandler_);
  14594. }
  14595. this.track_.addEventListener('cuechange', this.selectCurrentItem_);
  14596. }
  14597. }
  14598. /**
  14599. * Find the track object that is currently in use by this ChaptersButton
  14600. *
  14601. * @return {TextTrack|undefined}
  14602. * The current track or undefined if none was found.
  14603. */
  14604. findChaptersTrack() {
  14605. const tracks = this.player_.textTracks() || [];
  14606. for (let i = tracks.length - 1; i >= 0; i--) {
  14607. // We will always choose the last track as our chaptersTrack
  14608. const track = tracks[i];
  14609. if (track.kind === this.kind_) {
  14610. return track;
  14611. }
  14612. }
  14613. }
  14614. /**
  14615. * Get the caption for the ChaptersButton based on the track label. This will also
  14616. * use the current tracks localized kind as a fallback if a label does not exist.
  14617. *
  14618. * @return {string}
  14619. * The tracks current label or the localized track kind.
  14620. */
  14621. getMenuCaption() {
  14622. if (this.track_ && this.track_.label) {
  14623. return this.track_.label;
  14624. }
  14625. return this.localize(toTitleCase(this.kind_));
  14626. }
  14627. /**
  14628. * Create menu from chapter track
  14629. *
  14630. * @return { import('../../menu/menu').default }
  14631. * New menu for the chapter buttons
  14632. */
  14633. createMenu() {
  14634. this.options_.title = this.getMenuCaption();
  14635. return super.createMenu();
  14636. }
  14637. /**
  14638. * Create a menu item for each text track
  14639. *
  14640. * @return { import('./text-track-menu-item').default[] }
  14641. * Array of menu items
  14642. */
  14643. createItems() {
  14644. const items = [];
  14645. if (!this.track_) {
  14646. return items;
  14647. }
  14648. const cues = this.track_.cues;
  14649. if (!cues) {
  14650. return items;
  14651. }
  14652. for (let i = 0, l = cues.length; i < l; i++) {
  14653. const cue = cues[i];
  14654. const mi = new ChaptersTrackMenuItem(this.player_, {
  14655. track: this.track_,
  14656. cue
  14657. });
  14658. items.push(mi);
  14659. }
  14660. return items;
  14661. }
  14662. }
  14663. /**
  14664. * `kind` of TextTrack to look for to associate it with this menu.
  14665. *
  14666. * @type {string}
  14667. * @private
  14668. */
  14669. ChaptersButton.prototype.kind_ = 'chapters';
  14670. /**
  14671. * The text that should display over the `ChaptersButton`s controls. Added for localization.
  14672. *
  14673. * @type {string}
  14674. * @protected
  14675. */
  14676. ChaptersButton.prototype.controlText_ = 'Chapters';
  14677. Component.registerComponent('ChaptersButton', ChaptersButton);
  14678. /**
  14679. * @file descriptions-button.js
  14680. */
  14681. /**
  14682. * The button component for toggling and selecting descriptions
  14683. *
  14684. * @extends TextTrackButton
  14685. */
  14686. class DescriptionsButton extends TextTrackButton {
  14687. /**
  14688. * Creates an instance of this class.
  14689. *
  14690. * @param { import('../../player').default } player
  14691. * The `Player` that this class should be attached to.
  14692. *
  14693. * @param {Object} [options]
  14694. * The key/value store of player options.
  14695. *
  14696. * @param {Function} [ready]
  14697. * The function to call when this component is ready.
  14698. */
  14699. constructor(player, options, ready) {
  14700. super(player, options, ready);
  14701. this.setIcon('audio-description');
  14702. const tracks = player.textTracks();
  14703. const changeHandler = bind_(this, this.handleTracksChange);
  14704. tracks.addEventListener('change', changeHandler);
  14705. this.on('dispose', function () {
  14706. tracks.removeEventListener('change', changeHandler);
  14707. });
  14708. }
  14709. /**
  14710. * Handle text track change
  14711. *
  14712. * @param {Event} event
  14713. * The event that caused this function to run
  14714. *
  14715. * @listens TextTrackList#change
  14716. */
  14717. handleTracksChange(event) {
  14718. const tracks = this.player().textTracks();
  14719. let disabled = false;
  14720. // Check whether a track of a different kind is showing
  14721. for (let i = 0, l = tracks.length; i < l; i++) {
  14722. const track = tracks[i];
  14723. if (track.kind !== this.kind_ && track.mode === 'showing') {
  14724. disabled = true;
  14725. break;
  14726. }
  14727. }
  14728. // If another track is showing, disable this menu button
  14729. if (disabled) {
  14730. this.disable();
  14731. } else {
  14732. this.enable();
  14733. }
  14734. }
  14735. /**
  14736. * Builds the default DOM `className`.
  14737. *
  14738. * @return {string}
  14739. * The DOM `className` for this object.
  14740. */
  14741. buildCSSClass() {
  14742. return `vjs-descriptions-button ${super.buildCSSClass()}`;
  14743. }
  14744. buildWrapperCSSClass() {
  14745. return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
  14746. }
  14747. }
  14748. /**
  14749. * `kind` of TextTrack to look for to associate it with this menu.
  14750. *
  14751. * @type {string}
  14752. * @private
  14753. */
  14754. DescriptionsButton.prototype.kind_ = 'descriptions';
  14755. /**
  14756. * The text that should display over the `DescriptionsButton`s controls. Added for localization.
  14757. *
  14758. * @type {string}
  14759. * @protected
  14760. */
  14761. DescriptionsButton.prototype.controlText_ = 'Descriptions';
  14762. Component.registerComponent('DescriptionsButton', DescriptionsButton);
  14763. /**
  14764. * @file subtitles-button.js
  14765. */
  14766. /**
  14767. * The button component for toggling and selecting subtitles
  14768. *
  14769. * @extends TextTrackButton
  14770. */
  14771. class SubtitlesButton extends TextTrackButton {
  14772. /**
  14773. * Creates an instance of this class.
  14774. *
  14775. * @param { import('../../player').default } player
  14776. * The `Player` that this class should be attached to.
  14777. *
  14778. * @param {Object} [options]
  14779. * The key/value store of player options.
  14780. *
  14781. * @param {Function} [ready]
  14782. * The function to call when this component is ready.
  14783. */
  14784. constructor(player, options, ready) {
  14785. super(player, options, ready);
  14786. this.setIcon('subtitles');
  14787. }
  14788. /**
  14789. * Builds the default DOM `className`.
  14790. *
  14791. * @return {string}
  14792. * The DOM `className` for this object.
  14793. */
  14794. buildCSSClass() {
  14795. return `vjs-subtitles-button ${super.buildCSSClass()}`;
  14796. }
  14797. buildWrapperCSSClass() {
  14798. return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
  14799. }
  14800. }
  14801. /**
  14802. * `kind` of TextTrack to look for to associate it with this menu.
  14803. *
  14804. * @type {string}
  14805. * @private
  14806. */
  14807. SubtitlesButton.prototype.kind_ = 'subtitles';
  14808. /**
  14809. * The text that should display over the `SubtitlesButton`s controls. Added for localization.
  14810. *
  14811. * @type {string}
  14812. * @protected
  14813. */
  14814. SubtitlesButton.prototype.controlText_ = 'Subtitles';
  14815. Component.registerComponent('SubtitlesButton', SubtitlesButton);
  14816. /**
  14817. * @file caption-settings-menu-item.js
  14818. */
  14819. /**
  14820. * The menu item for caption track settings menu
  14821. *
  14822. * @extends TextTrackMenuItem
  14823. */
  14824. class CaptionSettingsMenuItem extends TextTrackMenuItem {
  14825. /**
  14826. * Creates an instance of this class.
  14827. *
  14828. * @param { import('../../player').default } player
  14829. * The `Player` that this class should be attached to.
  14830. *
  14831. * @param {Object} [options]
  14832. * The key/value store of player options.
  14833. */
  14834. constructor(player, options) {
  14835. options.track = {
  14836. player,
  14837. kind: options.kind,
  14838. label: options.kind + ' settings',
  14839. selectable: false,
  14840. default: false,
  14841. mode: 'disabled'
  14842. };
  14843. // CaptionSettingsMenuItem has no concept of 'selected'
  14844. options.selectable = false;
  14845. options.name = 'CaptionSettingsMenuItem';
  14846. super(player, options);
  14847. this.addClass('vjs-texttrack-settings');
  14848. this.controlText(', opens ' + options.kind + ' settings dialog');
  14849. }
  14850. /**
  14851. * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
  14852. * {@link ClickableComponent} for more detailed information on what a click can be.
  14853. *
  14854. * @param {Event} [event]
  14855. * The `keydown`, `tap`, or `click` event that caused this function to be
  14856. * called.
  14857. *
  14858. * @listens tap
  14859. * @listens click
  14860. */
  14861. handleClick(event) {
  14862. this.player().getChild('textTrackSettings').open();
  14863. }
  14864. /**
  14865. * Update control text and label on languagechange
  14866. */
  14867. handleLanguagechange() {
  14868. this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
  14869. super.handleLanguagechange();
  14870. }
  14871. }
  14872. Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
  14873. /**
  14874. * @file captions-button.js
  14875. */
  14876. /**
  14877. * The button component for toggling and selecting captions
  14878. *
  14879. * @extends TextTrackButton
  14880. */
  14881. class CaptionsButton extends TextTrackButton {
  14882. /**
  14883. * Creates an instance of this class.
  14884. *
  14885. * @param { import('../../player').default } player
  14886. * The `Player` that this class should be attached to.
  14887. *
  14888. * @param {Object} [options]
  14889. * The key/value store of player options.
  14890. *
  14891. * @param {Function} [ready]
  14892. * The function to call when this component is ready.
  14893. */
  14894. constructor(player, options, ready) {
  14895. super(player, options, ready);
  14896. this.setIcon('captions');
  14897. }
  14898. /**
  14899. * Builds the default DOM `className`.
  14900. *
  14901. * @return {string}
  14902. * The DOM `className` for this object.
  14903. */
  14904. buildCSSClass() {
  14905. return `vjs-captions-button ${super.buildCSSClass()}`;
  14906. }
  14907. buildWrapperCSSClass() {
  14908. return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
  14909. }
  14910. /**
  14911. * Create caption menu items
  14912. *
  14913. * @return {CaptionSettingsMenuItem[]}
  14914. * The array of current menu items.
  14915. */
  14916. createItems() {
  14917. const items = [];
  14918. if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
  14919. items.push(new CaptionSettingsMenuItem(this.player_, {
  14920. kind: this.kind_
  14921. }));
  14922. this.hideThreshold_ += 1;
  14923. }
  14924. return super.createItems(items);
  14925. }
  14926. }
  14927. /**
  14928. * `kind` of TextTrack to look for to associate it with this menu.
  14929. *
  14930. * @type {string}
  14931. * @private
  14932. */
  14933. CaptionsButton.prototype.kind_ = 'captions';
  14934. /**
  14935. * The text that should display over the `CaptionsButton`s controls. Added for localization.
  14936. *
  14937. * @type {string}
  14938. * @protected
  14939. */
  14940. CaptionsButton.prototype.controlText_ = 'Captions';
  14941. Component.registerComponent('CaptionsButton', CaptionsButton);
  14942. /**
  14943. * @file subs-caps-menu-item.js
  14944. */
  14945. /**
  14946. * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
  14947. * in the SubsCapsMenu.
  14948. *
  14949. * @extends TextTrackMenuItem
  14950. */
  14951. class SubsCapsMenuItem extends TextTrackMenuItem {
  14952. createEl(type, props, attrs) {
  14953. const el = super.createEl(type, props, attrs);
  14954. const parentSpan = el.querySelector('.vjs-menu-item-text');
  14955. if (this.options_.track.kind === 'captions') {
  14956. if (this.player_.options_.experimentalSvgIcons) {
  14957. this.setIcon('captions', el);
  14958. } else {
  14959. parentSpan.appendChild(createEl('span', {
  14960. className: 'vjs-icon-placeholder'
  14961. }, {
  14962. 'aria-hidden': true
  14963. }));
  14964. }
  14965. parentSpan.appendChild(createEl('span', {
  14966. className: 'vjs-control-text',
  14967. // space added as the text will visually flow with the
  14968. // label
  14969. textContent: ` ${this.localize('Captions')}`
  14970. }));
  14971. }
  14972. return el;
  14973. }
  14974. }
  14975. Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
  14976. /**
  14977. * @file sub-caps-button.js
  14978. */
  14979. /**
  14980. * The button component for toggling and selecting captions and/or subtitles
  14981. *
  14982. * @extends TextTrackButton
  14983. */
  14984. class SubsCapsButton extends TextTrackButton {
  14985. /**
  14986. * Creates an instance of this class.
  14987. *
  14988. * @param { import('../../player').default } player
  14989. * The `Player` that this class should be attached to.
  14990. *
  14991. * @param {Object} [options]
  14992. * The key/value store of player options.
  14993. *
  14994. * @param {Function} [ready]
  14995. * The function to call when this component is ready.
  14996. */
  14997. constructor(player, options = {}) {
  14998. super(player, options);
  14999. // Although North America uses "captions" in most cases for
  15000. // "captions and subtitles" other locales use "subtitles"
  15001. this.label_ = 'subtitles';
  15002. this.setIcon('subtitles');
  15003. if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
  15004. this.label_ = 'captions';
  15005. this.setIcon('captions');
  15006. }
  15007. this.menuButton_.controlText(toTitleCase(this.label_));
  15008. }
  15009. /**
  15010. * Builds the default DOM `className`.
  15011. *
  15012. * @return {string}
  15013. * The DOM `className` for this object.
  15014. */
  15015. buildCSSClass() {
  15016. return `vjs-subs-caps-button ${super.buildCSSClass()}`;
  15017. }
  15018. buildWrapperCSSClass() {
  15019. return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
  15020. }
  15021. /**
  15022. * Create caption/subtitles menu items
  15023. *
  15024. * @return {CaptionSettingsMenuItem[]}
  15025. * The array of current menu items.
  15026. */
  15027. createItems() {
  15028. let items = [];
  15029. if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
  15030. items.push(new CaptionSettingsMenuItem(this.player_, {
  15031. kind: this.label_
  15032. }));
  15033. this.hideThreshold_ += 1;
  15034. }
  15035. items = super.createItems(items, SubsCapsMenuItem);
  15036. return items;
  15037. }
  15038. }
  15039. /**
  15040. * `kind`s of TextTrack to look for to associate it with this menu.
  15041. *
  15042. * @type {array}
  15043. * @private
  15044. */
  15045. SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
  15046. /**
  15047. * The text that should display over the `SubsCapsButton`s controls.
  15048. *
  15049. *
  15050. * @type {string}
  15051. * @protected
  15052. */
  15053. SubsCapsButton.prototype.controlText_ = 'Subtitles';
  15054. Component.registerComponent('SubsCapsButton', SubsCapsButton);
  15055. /**
  15056. * @file audio-track-menu-item.js
  15057. */
  15058. /**
  15059. * An {@link AudioTrack} {@link MenuItem}
  15060. *
  15061. * @extends MenuItem
  15062. */
  15063. class AudioTrackMenuItem extends MenuItem {
  15064. /**
  15065. * Creates an instance of this class.
  15066. *
  15067. * @param { import('../../player').default } player
  15068. * The `Player` that this class should be attached to.
  15069. *
  15070. * @param {Object} [options]
  15071. * The key/value store of player options.
  15072. */
  15073. constructor(player, options) {
  15074. const track = options.track;
  15075. const tracks = player.audioTracks();
  15076. // Modify options for parent MenuItem class's init.
  15077. options.label = track.label || track.language || 'Unknown';
  15078. options.selected = track.enabled;
  15079. super(player, options);
  15080. this.track = track;
  15081. this.addClass(`vjs-${track.kind}-menu-item`);
  15082. const changeHandler = (...args) => {
  15083. this.handleTracksChange.apply(this, args);
  15084. };
  15085. tracks.addEventListener('change', changeHandler);
  15086. this.on('dispose', () => {
  15087. tracks.removeEventListener('change', changeHandler);
  15088. });
  15089. }
  15090. createEl(type, props, attrs) {
  15091. const el = super.createEl(type, props, attrs);
  15092. const parentSpan = el.querySelector('.vjs-menu-item-text');
  15093. if (['main-desc', 'description'].indexOf(this.options_.track.kind) >= 0) {
  15094. parentSpan.appendChild(createEl('span', {
  15095. className: 'vjs-icon-placeholder'
  15096. }, {
  15097. 'aria-hidden': true
  15098. }));
  15099. parentSpan.appendChild(createEl('span', {
  15100. className: 'vjs-control-text',
  15101. textContent: ' ' + this.localize('Descriptions')
  15102. }));
  15103. }
  15104. return el;
  15105. }
  15106. /**
  15107. * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
  15108. * for more detailed information on what a click can be.
  15109. *
  15110. * @param {Event} [event]
  15111. * The `keydown`, `tap`, or `click` event that caused this function to be
  15112. * called.
  15113. *
  15114. * @listens tap
  15115. * @listens click
  15116. */
  15117. handleClick(event) {
  15118. super.handleClick(event);
  15119. // the audio track list will automatically toggle other tracks
  15120. // off for us.
  15121. this.track.enabled = true;
  15122. // when native audio tracks are used, we want to make sure that other tracks are turned off
  15123. if (this.player_.tech_.featuresNativeAudioTracks) {
  15124. const tracks = this.player_.audioTracks();
  15125. for (let i = 0; i < tracks.length; i++) {
  15126. const track = tracks[i];
  15127. // skip the current track since we enabled it above
  15128. if (track === this.track) {
  15129. continue;
  15130. }
  15131. track.enabled = track === this.track;
  15132. }
  15133. }
  15134. }
  15135. /**
  15136. * Handle any {@link AudioTrack} change.
  15137. *
  15138. * @param {Event} [event]
  15139. * The {@link AudioTrackList#change} event that caused this to run.
  15140. *
  15141. * @listens AudioTrackList#change
  15142. */
  15143. handleTracksChange(event) {
  15144. this.selected(this.track.enabled);
  15145. }
  15146. }
  15147. Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
  15148. /**
  15149. * @file audio-track-button.js
  15150. */
  15151. /**
  15152. * The base class for buttons that toggle specific {@link AudioTrack} types.
  15153. *
  15154. * @extends TrackButton
  15155. */
  15156. class AudioTrackButton extends TrackButton {
  15157. /**
  15158. * Creates an instance of this class.
  15159. *
  15160. * @param {Player} player
  15161. * The `Player` that this class should be attached to.
  15162. *
  15163. * @param {Object} [options={}]
  15164. * The key/value store of player options.
  15165. */
  15166. constructor(player, options = {}) {
  15167. options.tracks = player.audioTracks();
  15168. super(player, options);
  15169. this.setIcon('audio');
  15170. }
  15171. /**
  15172. * Builds the default DOM `className`.
  15173. *
  15174. * @return {string}
  15175. * The DOM `className` for this object.
  15176. */
  15177. buildCSSClass() {
  15178. return `vjs-audio-button ${super.buildCSSClass()}`;
  15179. }
  15180. buildWrapperCSSClass() {
  15181. return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
  15182. }
  15183. /**
  15184. * Create a menu item for each audio track
  15185. *
  15186. * @param {AudioTrackMenuItem[]} [items=[]]
  15187. * An array of existing menu items to use.
  15188. *
  15189. * @return {AudioTrackMenuItem[]}
  15190. * An array of menu items
  15191. */
  15192. createItems(items = []) {
  15193. // if there's only one audio track, there no point in showing it
  15194. this.hideThreshold_ = 1;
  15195. const tracks = this.player_.audioTracks();
  15196. for (let i = 0; i < tracks.length; i++) {
  15197. const track = tracks[i];
  15198. items.push(new AudioTrackMenuItem(this.player_, {
  15199. track,
  15200. // MenuItem is selectable
  15201. selectable: true,
  15202. // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
  15203. multiSelectable: false
  15204. }));
  15205. }
  15206. return items;
  15207. }
  15208. }
  15209. /**
  15210. * The text that should display over the `AudioTrackButton`s controls. Added for localization.
  15211. *
  15212. * @type {string}
  15213. * @protected
  15214. */
  15215. AudioTrackButton.prototype.controlText_ = 'Audio Track';
  15216. Component.registerComponent('AudioTrackButton', AudioTrackButton);
  15217. /**
  15218. * @file playback-rate-menu-item.js
  15219. */
  15220. /**
  15221. * The specific menu item type for selecting a playback rate.
  15222. *
  15223. * @extends MenuItem
  15224. */
  15225. class PlaybackRateMenuItem extends MenuItem {
  15226. /**
  15227. * Creates an instance of this class.
  15228. *
  15229. * @param { import('../../player').default } player
  15230. * The `Player` that this class should be attached to.
  15231. *
  15232. * @param {Object} [options]
  15233. * The key/value store of player options.
  15234. */
  15235. constructor(player, options) {
  15236. const label = options.rate;
  15237. const rate = parseFloat(label, 10);
  15238. // Modify options for parent MenuItem class's init.
  15239. options.label = label;
  15240. options.selected = rate === player.playbackRate();
  15241. options.selectable = true;
  15242. options.multiSelectable = false;
  15243. super(player, options);
  15244. this.label = label;
  15245. this.rate = rate;
  15246. this.on(player, 'ratechange', e => this.update(e));
  15247. }
  15248. /**
  15249. * This gets called when an `PlaybackRateMenuItem` is "clicked". See
  15250. * {@link ClickableComponent} for more detailed information on what a click can be.
  15251. *
  15252. * @param {Event} [event]
  15253. * The `keydown`, `tap`, or `click` event that caused this function to be
  15254. * called.
  15255. *
  15256. * @listens tap
  15257. * @listens click
  15258. */
  15259. handleClick(event) {
  15260. super.handleClick();
  15261. this.player().playbackRate(this.rate);
  15262. }
  15263. /**
  15264. * Update the PlaybackRateMenuItem when the playbackrate changes.
  15265. *
  15266. * @param {Event} [event]
  15267. * The `ratechange` event that caused this function to run.
  15268. *
  15269. * @listens Player#ratechange
  15270. */
  15271. update(event) {
  15272. this.selected(this.player().playbackRate() === this.rate);
  15273. }
  15274. }
  15275. /**
  15276. * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
  15277. *
  15278. * @type {string}
  15279. * @private
  15280. */
  15281. PlaybackRateMenuItem.prototype.contentElType = 'button';
  15282. Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
  15283. /**
  15284. * @file playback-rate-menu-button.js
  15285. */
  15286. /**
  15287. * The component for controlling the playback rate.
  15288. *
  15289. * @extends MenuButton
  15290. */
  15291. class PlaybackRateMenuButton extends MenuButton {
  15292. /**
  15293. * Creates an instance of this class.
  15294. *
  15295. * @param { import('../../player').default } player
  15296. * The `Player` that this class should be attached to.
  15297. *
  15298. * @param {Object} [options]
  15299. * The key/value store of player options.
  15300. */
  15301. constructor(player, options) {
  15302. super(player, options);
  15303. this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
  15304. this.updateVisibility();
  15305. this.updateLabel();
  15306. this.on(player, 'loadstart', e => this.updateVisibility(e));
  15307. this.on(player, 'ratechange', e => this.updateLabel(e));
  15308. this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
  15309. }
  15310. /**
  15311. * Create the `Component`'s DOM element
  15312. *
  15313. * @return {Element}
  15314. * The element that was created.
  15315. */
  15316. createEl() {
  15317. const el = super.createEl();
  15318. this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
  15319. this.labelEl_ = createEl('div', {
  15320. className: 'vjs-playback-rate-value',
  15321. id: this.labelElId_,
  15322. textContent: '1x'
  15323. });
  15324. el.appendChild(this.labelEl_);
  15325. return el;
  15326. }
  15327. dispose() {
  15328. this.labelEl_ = null;
  15329. super.dispose();
  15330. }
  15331. /**
  15332. * Builds the default DOM `className`.
  15333. *
  15334. * @return {string}
  15335. * The DOM `className` for this object.
  15336. */
  15337. buildCSSClass() {
  15338. return `vjs-playback-rate ${super.buildCSSClass()}`;
  15339. }
  15340. buildWrapperCSSClass() {
  15341. return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
  15342. }
  15343. /**
  15344. * Create the list of menu items. Specific to each subclass.
  15345. *
  15346. */
  15347. createItems() {
  15348. const rates = this.playbackRates();
  15349. const items = [];
  15350. for (let i = rates.length - 1; i >= 0; i--) {
  15351. items.push(new PlaybackRateMenuItem(this.player(), {
  15352. rate: rates[i] + 'x'
  15353. }));
  15354. }
  15355. return items;
  15356. }
  15357. /**
  15358. * On playbackrateschange, update the menu to account for the new items.
  15359. *
  15360. * @listens Player#playbackrateschange
  15361. */
  15362. handlePlaybackRateschange(event) {
  15363. this.update();
  15364. }
  15365. /**
  15366. * Get possible playback rates
  15367. *
  15368. * @return {Array}
  15369. * All possible playback rates
  15370. */
  15371. playbackRates() {
  15372. const player = this.player();
  15373. return player.playbackRates && player.playbackRates() || [];
  15374. }
  15375. /**
  15376. * Get whether playback rates is supported by the tech
  15377. * and an array of playback rates exists
  15378. *
  15379. * @return {boolean}
  15380. * Whether changing playback rate is supported
  15381. */
  15382. playbackRateSupported() {
  15383. return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
  15384. }
  15385. /**
  15386. * Hide playback rate controls when they're no playback rate options to select
  15387. *
  15388. * @param {Event} [event]
  15389. * The event that caused this function to run.
  15390. *
  15391. * @listens Player#loadstart
  15392. */
  15393. updateVisibility(event) {
  15394. if (this.playbackRateSupported()) {
  15395. this.removeClass('vjs-hidden');
  15396. } else {
  15397. this.addClass('vjs-hidden');
  15398. }
  15399. }
  15400. /**
  15401. * Update button label when rate changed
  15402. *
  15403. * @param {Event} [event]
  15404. * The event that caused this function to run.
  15405. *
  15406. * @listens Player#ratechange
  15407. */
  15408. updateLabel(event) {
  15409. if (this.playbackRateSupported()) {
  15410. this.labelEl_.textContent = this.player().playbackRate() + 'x';
  15411. }
  15412. }
  15413. }
  15414. /**
  15415. * The text that should display over the `PlaybackRateMenuButton`s controls.
  15416. *
  15417. * Added for localization.
  15418. *
  15419. * @type {string}
  15420. * @protected
  15421. */
  15422. PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
  15423. Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
  15424. /**
  15425. * @file spacer.js
  15426. */
  15427. /**
  15428. * Just an empty spacer element that can be used as an append point for plugins, etc.
  15429. * Also can be used to create space between elements when necessary.
  15430. *
  15431. * @extends Component
  15432. */
  15433. class Spacer extends Component {
  15434. /**
  15435. * Builds the default DOM `className`.
  15436. *
  15437. * @return {string}
  15438. * The DOM `className` for this object.
  15439. */
  15440. buildCSSClass() {
  15441. return `vjs-spacer ${super.buildCSSClass()}`;
  15442. }
  15443. /**
  15444. * Create the `Component`'s DOM element
  15445. *
  15446. * @return {Element}
  15447. * The element that was created.
  15448. */
  15449. createEl(tag = 'div', props = {}, attributes = {}) {
  15450. if (!props.className) {
  15451. props.className = this.buildCSSClass();
  15452. }
  15453. return super.createEl(tag, props, attributes);
  15454. }
  15455. }
  15456. Component.registerComponent('Spacer', Spacer);
  15457. /**
  15458. * @file custom-control-spacer.js
  15459. */
  15460. /**
  15461. * Spacer specifically meant to be used as an insertion point for new plugins, etc.
  15462. *
  15463. * @extends Spacer
  15464. */
  15465. class CustomControlSpacer extends Spacer {
  15466. /**
  15467. * Builds the default DOM `className`.
  15468. *
  15469. * @return {string}
  15470. * The DOM `className` for this object.
  15471. */
  15472. buildCSSClass() {
  15473. return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
  15474. }
  15475. /**
  15476. * Create the `Component`'s DOM element
  15477. *
  15478. * @return {Element}
  15479. * The element that was created.
  15480. */
  15481. createEl() {
  15482. return super.createEl('div', {
  15483. className: this.buildCSSClass(),
  15484. // No-flex/table-cell mode requires there be some content
  15485. // in the cell to fill the remaining space of the table.
  15486. textContent: '\u00a0'
  15487. });
  15488. }
  15489. }
  15490. Component.registerComponent('CustomControlSpacer', CustomControlSpacer);
  15491. /**
  15492. * @file control-bar.js
  15493. */
  15494. /**
  15495. * Container of main controls.
  15496. *
  15497. * @extends Component
  15498. */
  15499. class ControlBar extends Component {
  15500. /**
  15501. * Create the `Component`'s DOM element
  15502. *
  15503. * @return {Element}
  15504. * The element that was created.
  15505. */
  15506. createEl() {
  15507. return super.createEl('div', {
  15508. className: 'vjs-control-bar',
  15509. dir: 'ltr'
  15510. });
  15511. }
  15512. }
  15513. /**
  15514. * Default options for `ControlBar`
  15515. *
  15516. * @type {Object}
  15517. * @private
  15518. */
  15519. ControlBar.prototype.options_ = {
  15520. children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
  15521. };
  15522. Component.registerComponent('ControlBar', ControlBar);
  15523. /**
  15524. * @file error-display.js
  15525. */
  15526. /**
  15527. * A display that indicates an error has occurred. This means that the video
  15528. * is unplayable.
  15529. *
  15530. * @extends ModalDialog
  15531. */
  15532. class ErrorDisplay extends ModalDialog {
  15533. /**
  15534. * Creates an instance of this class.
  15535. *
  15536. * @param { import('./player').default } player
  15537. * The `Player` that this class should be attached to.
  15538. *
  15539. * @param {Object} [options]
  15540. * The key/value store of player options.
  15541. */
  15542. constructor(player, options) {
  15543. super(player, options);
  15544. this.on(player, 'error', e => {
  15545. this.close();
  15546. this.open(e);
  15547. });
  15548. }
  15549. /**
  15550. * Builds the default DOM `className`.
  15551. *
  15552. * @return {string}
  15553. * The DOM `className` for this object.
  15554. *
  15555. * @deprecated Since version 5.
  15556. */
  15557. buildCSSClass() {
  15558. return `vjs-error-display ${super.buildCSSClass()}`;
  15559. }
  15560. /**
  15561. * Gets the localized error message based on the `Player`s error.
  15562. *
  15563. * @return {string}
  15564. * The `Player`s error message localized or an empty string.
  15565. */
  15566. content() {
  15567. const error = this.player().error();
  15568. return error ? this.localize(error.message) : '';
  15569. }
  15570. }
  15571. /**
  15572. * The default options for an `ErrorDisplay`.
  15573. *
  15574. * @private
  15575. */
  15576. ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
  15577. pauseOnOpen: false,
  15578. fillAlways: true,
  15579. temporary: false,
  15580. uncloseable: true
  15581. });
  15582. Component.registerComponent('ErrorDisplay', ErrorDisplay);
  15583. /**
  15584. * @file text-track-settings.js
  15585. */
  15586. const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
  15587. const COLOR_BLACK = ['#000', 'Black'];
  15588. const COLOR_BLUE = ['#00F', 'Blue'];
  15589. const COLOR_CYAN = ['#0FF', 'Cyan'];
  15590. const COLOR_GREEN = ['#0F0', 'Green'];
  15591. const COLOR_MAGENTA = ['#F0F', 'Magenta'];
  15592. const COLOR_RED = ['#F00', 'Red'];
  15593. const COLOR_WHITE = ['#FFF', 'White'];
  15594. const COLOR_YELLOW = ['#FF0', 'Yellow'];
  15595. const OPACITY_OPAQUE = ['1', 'Opaque'];
  15596. const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
  15597. const OPACITY_TRANS = ['0', 'Transparent'];
  15598. // Configuration for the various <select> elements in the DOM of this component.
  15599. //
  15600. // Possible keys include:
  15601. //
  15602. // `default`:
  15603. // The default option index. Only needs to be provided if not zero.
  15604. // `parser`:
  15605. // A function which is used to parse the value from the selected option in
  15606. // a customized way.
  15607. // `selector`:
  15608. // The selector used to find the associated <select> element.
  15609. const selectConfigs = {
  15610. backgroundColor: {
  15611. selector: '.vjs-bg-color > select',
  15612. id: 'captions-background-color-%s',
  15613. label: 'Color',
  15614. options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
  15615. },
  15616. backgroundOpacity: {
  15617. selector: '.vjs-bg-opacity > select',
  15618. id: 'captions-background-opacity-%s',
  15619. label: 'Opacity',
  15620. options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS]
  15621. },
  15622. color: {
  15623. selector: '.vjs-text-color > select',
  15624. id: 'captions-foreground-color-%s',
  15625. label: 'Color',
  15626. options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
  15627. },
  15628. edgeStyle: {
  15629. selector: '.vjs-edge-style > select',
  15630. id: '%s',
  15631. label: 'Text Edge Style',
  15632. options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']]
  15633. },
  15634. fontFamily: {
  15635. selector: '.vjs-font-family > select',
  15636. id: 'captions-font-family-%s',
  15637. label: 'Font Family',
  15638. options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
  15639. },
  15640. fontPercent: {
  15641. selector: '.vjs-font-percent > select',
  15642. id: 'captions-font-size-%s',
  15643. label: 'Font Size',
  15644. 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%']],
  15645. default: 2,
  15646. parser: v => v === '1.00' ? null : Number(v)
  15647. },
  15648. textOpacity: {
  15649. selector: '.vjs-text-opacity > select',
  15650. id: 'captions-foreground-opacity-%s',
  15651. label: 'Opacity',
  15652. options: [OPACITY_OPAQUE, OPACITY_SEMI]
  15653. },
  15654. // Options for this object are defined below.
  15655. windowColor: {
  15656. selector: '.vjs-window-color > select',
  15657. id: 'captions-window-color-%s',
  15658. label: 'Color'
  15659. },
  15660. // Options for this object are defined below.
  15661. windowOpacity: {
  15662. selector: '.vjs-window-opacity > select',
  15663. id: 'captions-window-opacity-%s',
  15664. label: 'Opacity',
  15665. options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE]
  15666. }
  15667. };
  15668. selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
  15669. /**
  15670. * Get the actual value of an option.
  15671. *
  15672. * @param {string} value
  15673. * The value to get
  15674. *
  15675. * @param {Function} [parser]
  15676. * Optional function to adjust the value.
  15677. *
  15678. * @return {*}
  15679. * - Will be `undefined` if no value exists
  15680. * - Will be `undefined` if the given value is "none".
  15681. * - Will be the actual value otherwise.
  15682. *
  15683. * @private
  15684. */
  15685. function parseOptionValue(value, parser) {
  15686. if (parser) {
  15687. value = parser(value);
  15688. }
  15689. if (value && value !== 'none') {
  15690. return value;
  15691. }
  15692. }
  15693. /**
  15694. * Gets the value of the selected <option> element within a <select> element.
  15695. *
  15696. * @param {Element} el
  15697. * the element to look in
  15698. *
  15699. * @param {Function} [parser]
  15700. * Optional function to adjust the value.
  15701. *
  15702. * @return {*}
  15703. * - Will be `undefined` if no value exists
  15704. * - Will be `undefined` if the given value is "none".
  15705. * - Will be the actual value otherwise.
  15706. *
  15707. * @private
  15708. */
  15709. function getSelectedOptionValue(el, parser) {
  15710. const value = el.options[el.options.selectedIndex].value;
  15711. return parseOptionValue(value, parser);
  15712. }
  15713. /**
  15714. * Sets the selected <option> element within a <select> element based on a
  15715. * given value.
  15716. *
  15717. * @param {Element} el
  15718. * The element to look in.
  15719. *
  15720. * @param {string} value
  15721. * the property to look on.
  15722. *
  15723. * @param {Function} [parser]
  15724. * Optional function to adjust the value before comparing.
  15725. *
  15726. * @private
  15727. */
  15728. function setSelectedOption(el, value, parser) {
  15729. if (!value) {
  15730. return;
  15731. }
  15732. for (let i = 0; i < el.options.length; i++) {
  15733. if (parseOptionValue(el.options[i].value, parser) === value) {
  15734. el.selectedIndex = i;
  15735. break;
  15736. }
  15737. }
  15738. }
  15739. /**
  15740. * Manipulate Text Tracks settings.
  15741. *
  15742. * @extends ModalDialog
  15743. */
  15744. class TextTrackSettings extends ModalDialog {
  15745. /**
  15746. * Creates an instance of this class.
  15747. *
  15748. * @param { import('../player').default } player
  15749. * The `Player` that this class should be attached to.
  15750. *
  15751. * @param {Object} [options]
  15752. * The key/value store of player options.
  15753. */
  15754. constructor(player, options) {
  15755. options.temporary = false;
  15756. super(player, options);
  15757. this.updateDisplay = this.updateDisplay.bind(this);
  15758. // fill the modal and pretend we have opened it
  15759. this.fill();
  15760. this.hasBeenOpened_ = this.hasBeenFilled_ = true;
  15761. this.endDialog = createEl('p', {
  15762. className: 'vjs-control-text',
  15763. textContent: this.localize('End of dialog window.')
  15764. });
  15765. this.el().appendChild(this.endDialog);
  15766. this.setDefaults();
  15767. // Grab `persistTextTrackSettings` from the player options if not passed in child options
  15768. if (options.persistTextTrackSettings === undefined) {
  15769. this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
  15770. }
  15771. this.on(this.$('.vjs-done-button'), 'click', () => {
  15772. this.saveSettings();
  15773. this.close();
  15774. });
  15775. this.on(this.$('.vjs-default-button'), 'click', () => {
  15776. this.setDefaults();
  15777. this.updateDisplay();
  15778. });
  15779. each(selectConfigs, config => {
  15780. this.on(this.$(config.selector), 'change', this.updateDisplay);
  15781. });
  15782. if (this.options_.persistTextTrackSettings) {
  15783. this.restoreSettings();
  15784. }
  15785. }
  15786. dispose() {
  15787. this.endDialog = null;
  15788. super.dispose();
  15789. }
  15790. /**
  15791. * Create a <select> element with configured options.
  15792. *
  15793. * @param {string} key
  15794. * Configuration key to use during creation.
  15795. *
  15796. * @param {string} [legendId]
  15797. * Id of associated <legend>.
  15798. *
  15799. * @param {string} [type=label]
  15800. * Type of labelling element, `label` or `legend`
  15801. *
  15802. * @return {string}
  15803. * An HTML string.
  15804. *
  15805. * @private
  15806. */
  15807. createElSelect_(key, legendId = '', type = 'label') {
  15808. const config = selectConfigs[key];
  15809. const id = config.id.replace('%s', this.id_);
  15810. const selectLabelledbyIds = [legendId, id].join(' ').trim();
  15811. const guid = `vjs_select_${newGUID()}`;
  15812. return [`<${type} id="${id}"${type === 'label' ? ` for="${guid}" class="vjs-label"` : ''}>`, this.localize(config.label), `</${type}>`, `<select aria-labelledby="${selectLabelledbyIds}" id="${guid}">`].concat(config.options.map(o => {
  15813. const optionId = id + '-' + o[1].replace(/\W+/g, '');
  15814. return [`<option id="${optionId}" value="${o[0]}" `, `aria-labelledby="${selectLabelledbyIds} ${optionId}">`, this.localize(o[1]), '</option>'].join('');
  15815. })).concat('</select>').join('');
  15816. }
  15817. /**
  15818. * Create foreground color element for the component
  15819. *
  15820. * @return {string}
  15821. * An HTML string.
  15822. *
  15823. * @private
  15824. */
  15825. createElFgColor_() {
  15826. const legendId = `captions-text-legend-${this.id_}`;
  15827. 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('');
  15828. }
  15829. /**
  15830. * Create background color element for the component
  15831. *
  15832. * @return {string}
  15833. * An HTML string.
  15834. *
  15835. * @private
  15836. */
  15837. createElBgColor_() {
  15838. const legendId = `captions-background-${this.id_}`;
  15839. 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('');
  15840. }
  15841. /**
  15842. * Create window color element for the component
  15843. *
  15844. * @return {string}
  15845. * An HTML string.
  15846. *
  15847. * @private
  15848. */
  15849. createElWinColor_() {
  15850. const legendId = `captions-window-${this.id_}`;
  15851. 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('');
  15852. }
  15853. /**
  15854. * Create color elements for the component
  15855. *
  15856. * @return {Element}
  15857. * The element that was created
  15858. *
  15859. * @private
  15860. */
  15861. createElColors_() {
  15862. return createEl('div', {
  15863. className: 'vjs-track-settings-colors',
  15864. innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('')
  15865. });
  15866. }
  15867. /**
  15868. * Create font elements for the component
  15869. *
  15870. * @return {Element}
  15871. * The element that was created.
  15872. *
  15873. * @private
  15874. */
  15875. createElFont_() {
  15876. return createEl('div', {
  15877. className: 'vjs-track-settings-font',
  15878. 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('')
  15879. });
  15880. }
  15881. /**
  15882. * Create controls for the component
  15883. *
  15884. * @return {Element}
  15885. * The element that was created.
  15886. *
  15887. * @private
  15888. */
  15889. createElControls_() {
  15890. const defaultsDescription = this.localize('restore all settings to the default values');
  15891. return createEl('div', {
  15892. className: 'vjs-track-settings-controls',
  15893. 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('')
  15894. });
  15895. }
  15896. content() {
  15897. return [this.createElColors_(), this.createElFont_(), this.createElControls_()];
  15898. }
  15899. label() {
  15900. return this.localize('Caption Settings Dialog');
  15901. }
  15902. description() {
  15903. return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
  15904. }
  15905. buildCSSClass() {
  15906. return super.buildCSSClass() + ' vjs-text-track-settings';
  15907. }
  15908. /**
  15909. * Gets an object of text track settings (or null).
  15910. *
  15911. * @return {Object}
  15912. * An object with config values parsed from the DOM or localStorage.
  15913. */
  15914. getValues() {
  15915. return reduce(selectConfigs, (accum, config, key) => {
  15916. const value = getSelectedOptionValue(this.$(config.selector), config.parser);
  15917. if (value !== undefined) {
  15918. accum[key] = value;
  15919. }
  15920. return accum;
  15921. }, {});
  15922. }
  15923. /**
  15924. * Sets text track settings from an object of values.
  15925. *
  15926. * @param {Object} values
  15927. * An object with config values parsed from the DOM or localStorage.
  15928. */
  15929. setValues(values) {
  15930. each(selectConfigs, (config, key) => {
  15931. setSelectedOption(this.$(config.selector), values[key], config.parser);
  15932. });
  15933. }
  15934. /**
  15935. * Sets all `<select>` elements to their default values.
  15936. */
  15937. setDefaults() {
  15938. each(selectConfigs, config => {
  15939. const index = config.hasOwnProperty('default') ? config.default : 0;
  15940. this.$(config.selector).selectedIndex = index;
  15941. });
  15942. }
  15943. /**
  15944. * Restore texttrack settings from localStorage
  15945. */
  15946. restoreSettings() {
  15947. let values;
  15948. try {
  15949. values = JSON.parse(window__default["default"].localStorage.getItem(LOCAL_STORAGE_KEY));
  15950. } catch (err) {
  15951. log.warn(err);
  15952. }
  15953. if (values) {
  15954. this.setValues(values);
  15955. }
  15956. }
  15957. /**
  15958. * Save text track settings to localStorage
  15959. */
  15960. saveSettings() {
  15961. if (!this.options_.persistTextTrackSettings) {
  15962. return;
  15963. }
  15964. const values = this.getValues();
  15965. try {
  15966. if (Object.keys(values).length) {
  15967. window__default["default"].localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values));
  15968. } else {
  15969. window__default["default"].localStorage.removeItem(LOCAL_STORAGE_KEY);
  15970. }
  15971. } catch (err) {
  15972. log.warn(err);
  15973. }
  15974. }
  15975. /**
  15976. * Update display of text track settings
  15977. */
  15978. updateDisplay() {
  15979. const ttDisplay = this.player_.getChild('textTrackDisplay');
  15980. if (ttDisplay) {
  15981. ttDisplay.updateDisplay();
  15982. }
  15983. }
  15984. /**
  15985. * conditionally blur the element and refocus the captions button
  15986. *
  15987. * @private
  15988. */
  15989. conditionalBlur_() {
  15990. this.previouslyActiveEl_ = null;
  15991. const cb = this.player_.controlBar;
  15992. const subsCapsBtn = cb && cb.subsCapsButton;
  15993. const ccBtn = cb && cb.captionsButton;
  15994. if (subsCapsBtn) {
  15995. subsCapsBtn.focus();
  15996. } else if (ccBtn) {
  15997. ccBtn.focus();
  15998. }
  15999. }
  16000. /**
  16001. * Repopulate dialog with new localizations on languagechange
  16002. */
  16003. handleLanguagechange() {
  16004. this.fill();
  16005. }
  16006. }
  16007. Component.registerComponent('TextTrackSettings', TextTrackSettings);
  16008. /**
  16009. * @file resize-manager.js
  16010. */
  16011. /**
  16012. * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
  16013. *
  16014. * 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}.
  16015. *
  16016. * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
  16017. * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
  16018. *
  16019. * @example <caption>How to disable the resize manager</caption>
  16020. * const player = videojs('#vid', {
  16021. * resizeManager: false
  16022. * });
  16023. *
  16024. * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
  16025. *
  16026. * @extends Component
  16027. */
  16028. class ResizeManager extends Component {
  16029. /**
  16030. * Create the ResizeManager.
  16031. *
  16032. * @param {Object} player
  16033. * The `Player` that this class should be attached to.
  16034. *
  16035. * @param {Object} [options]
  16036. * The key/value store of ResizeManager options.
  16037. *
  16038. * @param {Object} [options.ResizeObserver]
  16039. * A polyfill for ResizeObserver can be passed in here.
  16040. * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
  16041. */
  16042. constructor(player, options) {
  16043. let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window__default["default"].ResizeObserver;
  16044. // if `null` was passed, we want to disable the ResizeObserver
  16045. if (options.ResizeObserver === null) {
  16046. RESIZE_OBSERVER_AVAILABLE = false;
  16047. }
  16048. // Only create an element when ResizeObserver isn't available
  16049. const options_ = merge({
  16050. createEl: !RESIZE_OBSERVER_AVAILABLE,
  16051. reportTouchActivity: false
  16052. }, options);
  16053. super(player, options_);
  16054. this.ResizeObserver = options.ResizeObserver || window__default["default"].ResizeObserver;
  16055. this.loadListener_ = null;
  16056. this.resizeObserver_ = null;
  16057. this.debouncedHandler_ = debounce(() => {
  16058. this.resizeHandler();
  16059. }, 100, false, this);
  16060. if (RESIZE_OBSERVER_AVAILABLE) {
  16061. this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
  16062. this.resizeObserver_.observe(player.el());
  16063. } else {
  16064. this.loadListener_ = () => {
  16065. if (!this.el_ || !this.el_.contentWindow) {
  16066. return;
  16067. }
  16068. const debouncedHandler_ = this.debouncedHandler_;
  16069. let unloadListener_ = this.unloadListener_ = function () {
  16070. off(this, 'resize', debouncedHandler_);
  16071. off(this, 'unload', unloadListener_);
  16072. unloadListener_ = null;
  16073. };
  16074. // safari and edge can unload the iframe before resizemanager dispose
  16075. // we have to dispose of event handlers correctly before that happens
  16076. on(this.el_.contentWindow, 'unload', unloadListener_);
  16077. on(this.el_.contentWindow, 'resize', debouncedHandler_);
  16078. };
  16079. this.one('load', this.loadListener_);
  16080. }
  16081. }
  16082. createEl() {
  16083. return super.createEl('iframe', {
  16084. className: 'vjs-resize-manager',
  16085. tabIndex: -1,
  16086. title: this.localize('No content')
  16087. }, {
  16088. 'aria-hidden': 'true'
  16089. });
  16090. }
  16091. /**
  16092. * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
  16093. *
  16094. * @fires Player#playerresize
  16095. */
  16096. resizeHandler() {
  16097. /**
  16098. * Called when the player size has changed
  16099. *
  16100. * @event Player#playerresize
  16101. * @type {Event}
  16102. */
  16103. // make sure player is still around to trigger
  16104. // prevents this from causing an error after dispose
  16105. if (!this.player_ || !this.player_.trigger) {
  16106. return;
  16107. }
  16108. this.player_.trigger('playerresize');
  16109. }
  16110. dispose() {
  16111. if (this.debouncedHandler_) {
  16112. this.debouncedHandler_.cancel();
  16113. }
  16114. if (this.resizeObserver_) {
  16115. if (this.player_.el()) {
  16116. this.resizeObserver_.unobserve(this.player_.el());
  16117. }
  16118. this.resizeObserver_.disconnect();
  16119. }
  16120. if (this.loadListener_) {
  16121. this.off('load', this.loadListener_);
  16122. }
  16123. if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
  16124. this.unloadListener_.call(this.el_.contentWindow);
  16125. }
  16126. this.ResizeObserver = null;
  16127. this.resizeObserver = null;
  16128. this.debouncedHandler_ = null;
  16129. this.loadListener_ = null;
  16130. super.dispose();
  16131. }
  16132. }
  16133. Component.registerComponent('ResizeManager', ResizeManager);
  16134. const defaults = {
  16135. trackingThreshold: 20,
  16136. liveTolerance: 15
  16137. };
  16138. /*
  16139. track when we are at the live edge, and other helpers for live playback */
  16140. /**
  16141. * A class for checking live current time and determining when the player
  16142. * is at or behind the live edge.
  16143. */
  16144. class LiveTracker extends Component {
  16145. /**
  16146. * Creates an instance of this class.
  16147. *
  16148. * @param { import('./player').default } player
  16149. * The `Player` that this class should be attached to.
  16150. *
  16151. * @param {Object} [options]
  16152. * The key/value store of player options.
  16153. *
  16154. * @param {number} [options.trackingThreshold=20]
  16155. * Number of seconds of live window (seekableEnd - seekableStart) that
  16156. * media needs to have before the liveui will be shown.
  16157. *
  16158. * @param {number} [options.liveTolerance=15]
  16159. * Number of seconds behind live that we have to be
  16160. * before we will be considered non-live. Note that this will only
  16161. * be used when playing at the live edge. This allows large seekable end
  16162. * changes to not effect whether we are live or not.
  16163. */
  16164. constructor(player, options) {
  16165. // LiveTracker does not need an element
  16166. const options_ = merge(defaults, options, {
  16167. createEl: false
  16168. });
  16169. super(player, options_);
  16170. this.trackLiveHandler_ = () => this.trackLive_();
  16171. this.handlePlay_ = e => this.handlePlay(e);
  16172. this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
  16173. this.handleSeeked_ = e => this.handleSeeked(e);
  16174. this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
  16175. this.reset_();
  16176. this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
  16177. // we should try to toggle tracking on canplay as native playback engines, like Safari
  16178. // may not have the proper values for things like seekableEnd until then
  16179. this.on(this.player_, 'canplay', () => this.toggleTracking());
  16180. }
  16181. /**
  16182. * all the functionality for tracking when seek end changes
  16183. * and for tracking how far past seek end we should be
  16184. */
  16185. trackLive_() {
  16186. const seekable = this.player_.seekable();
  16187. // skip undefined seekable
  16188. if (!seekable || !seekable.length) {
  16189. return;
  16190. }
  16191. const newTime = Number(window__default["default"].performance.now().toFixed(4));
  16192. const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
  16193. this.lastTime_ = newTime;
  16194. this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
  16195. const liveCurrentTime = this.liveCurrentTime();
  16196. const currentTime = this.player_.currentTime();
  16197. // we are behind live if any are true
  16198. // 1. the player is paused
  16199. // 2. the user seeked to a location 2 seconds away from live
  16200. // 3. the difference between live and current time is greater
  16201. // liveTolerance which defaults to 15s
  16202. let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
  16203. // we cannot be behind if
  16204. // 1. until we have not seen a timeupdate yet
  16205. // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
  16206. if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
  16207. isBehind = false;
  16208. }
  16209. if (isBehind !== this.behindLiveEdge_) {
  16210. this.behindLiveEdge_ = isBehind;
  16211. this.trigger('liveedgechange');
  16212. }
  16213. }
  16214. /**
  16215. * handle a durationchange event on the player
  16216. * and start/stop tracking accordingly.
  16217. */
  16218. handleDurationchange() {
  16219. this.toggleTracking();
  16220. }
  16221. /**
  16222. * start/stop tracking
  16223. */
  16224. toggleTracking() {
  16225. if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
  16226. if (this.player_.options_.liveui) {
  16227. this.player_.addClass('vjs-liveui');
  16228. }
  16229. this.startTracking();
  16230. } else {
  16231. this.player_.removeClass('vjs-liveui');
  16232. this.stopTracking();
  16233. }
  16234. }
  16235. /**
  16236. * start tracking live playback
  16237. */
  16238. startTracking() {
  16239. if (this.isTracking()) {
  16240. return;
  16241. }
  16242. // If we haven't seen a timeupdate, we need to check whether playback
  16243. // began before this component started tracking. This can happen commonly
  16244. // when using autoplay.
  16245. if (!this.timeupdateSeen_) {
  16246. this.timeupdateSeen_ = this.player_.hasStarted();
  16247. }
  16248. this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
  16249. this.trackLive_();
  16250. this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
  16251. if (!this.timeupdateSeen_) {
  16252. this.one(this.player_, 'play', this.handlePlay_);
  16253. this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
  16254. } else {
  16255. this.on(this.player_, 'seeked', this.handleSeeked_);
  16256. }
  16257. }
  16258. /**
  16259. * handle the first timeupdate on the player if it wasn't already playing
  16260. * when live tracker started tracking.
  16261. */
  16262. handleFirstTimeupdate() {
  16263. this.timeupdateSeen_ = true;
  16264. this.on(this.player_, 'seeked', this.handleSeeked_);
  16265. }
  16266. /**
  16267. * Keep track of what time a seek starts, and listen for seeked
  16268. * to find where a seek ends.
  16269. */
  16270. handleSeeked() {
  16271. const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
  16272. this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
  16273. this.nextSeekedFromUser_ = false;
  16274. this.trackLive_();
  16275. }
  16276. /**
  16277. * handle the first play on the player, and make sure that we seek
  16278. * right to the live edge.
  16279. */
  16280. handlePlay() {
  16281. this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
  16282. }
  16283. /**
  16284. * Stop tracking, and set all internal variables to
  16285. * their initial value.
  16286. */
  16287. reset_() {
  16288. this.lastTime_ = -1;
  16289. this.pastSeekEnd_ = 0;
  16290. this.lastSeekEnd_ = -1;
  16291. this.behindLiveEdge_ = true;
  16292. this.timeupdateSeen_ = false;
  16293. this.seekedBehindLive_ = false;
  16294. this.nextSeekedFromUser_ = false;
  16295. this.clearInterval(this.trackingInterval_);
  16296. this.trackingInterval_ = null;
  16297. this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
  16298. this.off(this.player_, 'seeked', this.handleSeeked_);
  16299. this.off(this.player_, 'play', this.handlePlay_);
  16300. this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
  16301. this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
  16302. }
  16303. /**
  16304. * The next seeked event is from the user. Meaning that any seek
  16305. * > 2s behind live will be considered behind live for real and
  16306. * liveTolerance will be ignored.
  16307. */
  16308. nextSeekedFromUser() {
  16309. this.nextSeekedFromUser_ = true;
  16310. }
  16311. /**
  16312. * stop tracking live playback
  16313. */
  16314. stopTracking() {
  16315. if (!this.isTracking()) {
  16316. return;
  16317. }
  16318. this.reset_();
  16319. this.trigger('liveedgechange');
  16320. }
  16321. /**
  16322. * A helper to get the player seekable end
  16323. * so that we don't have to null check everywhere
  16324. *
  16325. * @return {number}
  16326. * The furthest seekable end or Infinity.
  16327. */
  16328. seekableEnd() {
  16329. const seekable = this.player_.seekable();
  16330. const seekableEnds = [];
  16331. let i = seekable ? seekable.length : 0;
  16332. while (i--) {
  16333. seekableEnds.push(seekable.end(i));
  16334. }
  16335. // grab the furthest seekable end after sorting, or if there are none
  16336. // default to Infinity
  16337. return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
  16338. }
  16339. /**
  16340. * A helper to get the player seekable start
  16341. * so that we don't have to null check everywhere
  16342. *
  16343. * @return {number}
  16344. * The earliest seekable start or 0.
  16345. */
  16346. seekableStart() {
  16347. const seekable = this.player_.seekable();
  16348. const seekableStarts = [];
  16349. let i = seekable ? seekable.length : 0;
  16350. while (i--) {
  16351. seekableStarts.push(seekable.start(i));
  16352. }
  16353. // grab the first seekable start after sorting, or if there are none
  16354. // default to 0
  16355. return seekableStarts.length ? seekableStarts.sort()[0] : 0;
  16356. }
  16357. /**
  16358. * Get the live time window aka
  16359. * the amount of time between seekable start and
  16360. * live current time.
  16361. *
  16362. * @return {number}
  16363. * The amount of seconds that are seekable in
  16364. * the live video.
  16365. */
  16366. liveWindow() {
  16367. const liveCurrentTime = this.liveCurrentTime();
  16368. // if liveCurrenTime is Infinity then we don't have a liveWindow at all
  16369. if (liveCurrentTime === Infinity) {
  16370. return 0;
  16371. }
  16372. return liveCurrentTime - this.seekableStart();
  16373. }
  16374. /**
  16375. * Determines if the player is live, only checks if this component
  16376. * is tracking live playback or not
  16377. *
  16378. * @return {boolean}
  16379. * Whether liveTracker is tracking
  16380. */
  16381. isLive() {
  16382. return this.isTracking();
  16383. }
  16384. /**
  16385. * Determines if currentTime is at the live edge and won't fall behind
  16386. * on each seekableendchange
  16387. *
  16388. * @return {boolean}
  16389. * Whether playback is at the live edge
  16390. */
  16391. atLiveEdge() {
  16392. return !this.behindLiveEdge();
  16393. }
  16394. /**
  16395. * get what we expect the live current time to be
  16396. *
  16397. * @return {number}
  16398. * The expected live current time
  16399. */
  16400. liveCurrentTime() {
  16401. return this.pastSeekEnd() + this.seekableEnd();
  16402. }
  16403. /**
  16404. * The number of seconds that have occurred after seekable end
  16405. * changed. This will be reset to 0 once seekable end changes.
  16406. *
  16407. * @return {number}
  16408. * Seconds past the current seekable end
  16409. */
  16410. pastSeekEnd() {
  16411. const seekableEnd = this.seekableEnd();
  16412. if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
  16413. this.pastSeekEnd_ = 0;
  16414. }
  16415. this.lastSeekEnd_ = seekableEnd;
  16416. return this.pastSeekEnd_;
  16417. }
  16418. /**
  16419. * If we are currently behind the live edge, aka currentTime will be
  16420. * behind on a seekableendchange
  16421. *
  16422. * @return {boolean}
  16423. * If we are behind the live edge
  16424. */
  16425. behindLiveEdge() {
  16426. return this.behindLiveEdge_;
  16427. }
  16428. /**
  16429. * Whether live tracker is currently tracking or not.
  16430. */
  16431. isTracking() {
  16432. return typeof this.trackingInterval_ === 'number';
  16433. }
  16434. /**
  16435. * Seek to the live edge if we are behind the live edge
  16436. */
  16437. seekToLiveEdge() {
  16438. this.seekedBehindLive_ = false;
  16439. if (this.atLiveEdge()) {
  16440. return;
  16441. }
  16442. this.nextSeekedFromUser_ = false;
  16443. this.player_.currentTime(this.liveCurrentTime());
  16444. }
  16445. /**
  16446. * Dispose of liveTracker
  16447. */
  16448. dispose() {
  16449. this.stopTracking();
  16450. super.dispose();
  16451. }
  16452. }
  16453. Component.registerComponent('LiveTracker', LiveTracker);
  16454. /**
  16455. * Displays an element over the player which contains an optional title and
  16456. * description for the current content.
  16457. *
  16458. * Much of the code for this component originated in the now obsolete
  16459. * videojs-dock plugin: https://github.com/brightcove/videojs-dock/
  16460. *
  16461. * @extends Component
  16462. */
  16463. class TitleBar extends Component {
  16464. constructor(player, options) {
  16465. super(player, options);
  16466. this.on('statechanged', e => this.updateDom_());
  16467. this.updateDom_();
  16468. }
  16469. /**
  16470. * Create the `TitleBar`'s DOM element
  16471. *
  16472. * @return {Element}
  16473. * The element that was created.
  16474. */
  16475. createEl() {
  16476. this.els = {
  16477. title: createEl('div', {
  16478. className: 'vjs-title-bar-title',
  16479. id: `vjs-title-bar-title-${newGUID()}`
  16480. }),
  16481. description: createEl('div', {
  16482. className: 'vjs-title-bar-description',
  16483. id: `vjs-title-bar-description-${newGUID()}`
  16484. })
  16485. };
  16486. return createEl('div', {
  16487. className: 'vjs-title-bar'
  16488. }, {}, values(this.els));
  16489. }
  16490. /**
  16491. * Updates the DOM based on the component's state object.
  16492. */
  16493. updateDom_() {
  16494. const tech = this.player_.tech_;
  16495. const techEl = tech && tech.el_;
  16496. const techAriaAttrs = {
  16497. title: 'aria-labelledby',
  16498. description: 'aria-describedby'
  16499. };
  16500. ['title', 'description'].forEach(k => {
  16501. const value = this.state[k];
  16502. const el = this.els[k];
  16503. const techAriaAttr = techAriaAttrs[k];
  16504. emptyEl(el);
  16505. if (value) {
  16506. textContent(el, value);
  16507. }
  16508. // If there is a tech element available, update its ARIA attributes
  16509. // according to whether a title and/or description have been provided.
  16510. if (techEl) {
  16511. techEl.removeAttribute(techAriaAttr);
  16512. if (value) {
  16513. techEl.setAttribute(techAriaAttr, el.id);
  16514. }
  16515. }
  16516. });
  16517. if (this.state.title || this.state.description) {
  16518. this.show();
  16519. } else {
  16520. this.hide();
  16521. }
  16522. }
  16523. /**
  16524. * Update the contents of the title bar component with new title and
  16525. * description text.
  16526. *
  16527. * If both title and description are missing, the title bar will be hidden.
  16528. *
  16529. * If either title or description are present, the title bar will be visible.
  16530. *
  16531. * NOTE: Any previously set value will be preserved. To unset a previously
  16532. * set value, you must pass an empty string or null.
  16533. *
  16534. * For example:
  16535. *
  16536. * ```
  16537. * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
  16538. * update({description: 'bar2'}) // title: 'foo', description: 'bar2'
  16539. * update({title: ''}) // title: '', description: 'bar2'
  16540. * update({title: 'foo', description: null}) // title: 'foo', description: null
  16541. * ```
  16542. *
  16543. * @param {Object} [options={}]
  16544. * An options object. When empty, the title bar will be hidden.
  16545. *
  16546. * @param {string} [options.title]
  16547. * A title to display in the title bar.
  16548. *
  16549. * @param {string} [options.description]
  16550. * A description to display in the title bar.
  16551. */
  16552. update(options) {
  16553. this.setState(options);
  16554. }
  16555. /**
  16556. * Dispose the component.
  16557. */
  16558. dispose() {
  16559. const tech = this.player_.tech_;
  16560. const techEl = tech && tech.el_;
  16561. if (techEl) {
  16562. techEl.removeAttribute('aria-labelledby');
  16563. techEl.removeAttribute('aria-describedby');
  16564. }
  16565. super.dispose();
  16566. this.els = null;
  16567. }
  16568. }
  16569. Component.registerComponent('TitleBar', TitleBar);
  16570. /**
  16571. * This function is used to fire a sourceset when there is something
  16572. * similar to `mediaEl.load()` being called. It will try to find the source via
  16573. * the `src` attribute and then the `<source>` elements. It will then fire `sourceset`
  16574. * with the source that was found or empty string if we cannot know. If it cannot
  16575. * find a source then `sourceset` will not be fired.
  16576. *
  16577. * @param { import('./html5').default } tech
  16578. * The tech object that sourceset was setup on
  16579. *
  16580. * @return {boolean}
  16581. * returns false if the sourceset was not fired and true otherwise.
  16582. */
  16583. const sourcesetLoad = tech => {
  16584. const el = tech.el();
  16585. // if `el.src` is set, that source will be loaded.
  16586. if (el.hasAttribute('src')) {
  16587. tech.triggerSourceset(el.src);
  16588. return true;
  16589. }
  16590. /**
  16591. * Since there isn't a src property on the media element, source elements will be used for
  16592. * implementing the source selection algorithm. This happens asynchronously and
  16593. * for most cases were there is more than one source we cannot tell what source will
  16594. * be loaded, without re-implementing the source selection algorithm. At this time we are not
  16595. * going to do that. There are three special cases that we do handle here though:
  16596. *
  16597. * 1. If there are no sources, do not fire `sourceset`.
  16598. * 2. If there is only one `<source>` with a `src` property/attribute that is our `src`
  16599. * 3. If there is more than one `<source>` but all of them have the same `src` url.
  16600. * That will be our src.
  16601. */
  16602. const sources = tech.$$('source');
  16603. const srcUrls = [];
  16604. let src = '';
  16605. // if there are no sources, do not fire sourceset
  16606. if (!sources.length) {
  16607. return false;
  16608. }
  16609. // only count valid/non-duplicate source elements
  16610. for (let i = 0; i < sources.length; i++) {
  16611. const url = sources[i].src;
  16612. if (url && srcUrls.indexOf(url) === -1) {
  16613. srcUrls.push(url);
  16614. }
  16615. }
  16616. // there were no valid sources
  16617. if (!srcUrls.length) {
  16618. return false;
  16619. }
  16620. // there is only one valid source element url
  16621. // use that
  16622. if (srcUrls.length === 1) {
  16623. src = srcUrls[0];
  16624. }
  16625. tech.triggerSourceset(src);
  16626. return true;
  16627. };
  16628. /**
  16629. * our implementation of an `innerHTML` descriptor for browsers
  16630. * that do not have one.
  16631. */
  16632. const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
  16633. get() {
  16634. return this.cloneNode(true).innerHTML;
  16635. },
  16636. set(v) {
  16637. // make a dummy node to use innerHTML on
  16638. const dummy = document__default["default"].createElement(this.nodeName.toLowerCase());
  16639. // set innerHTML to the value provided
  16640. dummy.innerHTML = v;
  16641. // make a document fragment to hold the nodes from dummy
  16642. const docFrag = document__default["default"].createDocumentFragment();
  16643. // copy all of the nodes created by the innerHTML on dummy
  16644. // to the document fragment
  16645. while (dummy.childNodes.length) {
  16646. docFrag.appendChild(dummy.childNodes[0]);
  16647. }
  16648. // remove content
  16649. this.innerText = '';
  16650. // now we add all of that html in one by appending the
  16651. // document fragment. This is how innerHTML does it.
  16652. window__default["default"].Element.prototype.appendChild.call(this, docFrag);
  16653. // then return the result that innerHTML's setter would
  16654. return this.innerHTML;
  16655. }
  16656. });
  16657. /**
  16658. * Get a property descriptor given a list of priorities and the
  16659. * property to get.
  16660. */
  16661. const getDescriptor = (priority, prop) => {
  16662. let descriptor = {};
  16663. for (let i = 0; i < priority.length; i++) {
  16664. descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
  16665. if (descriptor && descriptor.set && descriptor.get) {
  16666. break;
  16667. }
  16668. }
  16669. descriptor.enumerable = true;
  16670. descriptor.configurable = true;
  16671. return descriptor;
  16672. };
  16673. const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window__default["default"].HTMLMediaElement.prototype, window__default["default"].Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
  16674. /**
  16675. * Patches browser internal functions so that we can tell synchronously
  16676. * if a `<source>` was appended to the media element. For some reason this
  16677. * causes a `sourceset` if the the media element is ready and has no source.
  16678. * This happens when:
  16679. * - The page has just loaded and the media element does not have a source.
  16680. * - The media element was emptied of all sources, then `load()` was called.
  16681. *
  16682. * It does this by patching the following functions/properties when they are supported:
  16683. *
  16684. * - `append()` - can be used to add a `<source>` element to the media element
  16685. * - `appendChild()` - can be used to add a `<source>` element to the media element
  16686. * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element
  16687. * - `innerHTML` - can be used to add a `<source>` element to the media element
  16688. *
  16689. * @param {Html5} tech
  16690. * The tech object that sourceset is being setup on.
  16691. */
  16692. const firstSourceWatch = function (tech) {
  16693. const el = tech.el();
  16694. // make sure firstSourceWatch isn't setup twice.
  16695. if (el.resetSourceWatch_) {
  16696. return;
  16697. }
  16698. const old = {};
  16699. const innerDescriptor = getInnerHTMLDescriptor(tech);
  16700. const appendWrapper = appendFn => (...args) => {
  16701. const retval = appendFn.apply(el, args);
  16702. sourcesetLoad(tech);
  16703. return retval;
  16704. };
  16705. ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
  16706. if (!el[k]) {
  16707. return;
  16708. }
  16709. // store the old function
  16710. old[k] = el[k];
  16711. // call the old function with a sourceset if a source
  16712. // was loaded
  16713. el[k] = appendWrapper(old[k]);
  16714. });
  16715. Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, {
  16716. set: appendWrapper(innerDescriptor.set)
  16717. }));
  16718. el.resetSourceWatch_ = () => {
  16719. el.resetSourceWatch_ = null;
  16720. Object.keys(old).forEach(k => {
  16721. el[k] = old[k];
  16722. });
  16723. Object.defineProperty(el, 'innerHTML', innerDescriptor);
  16724. };
  16725. // on the first sourceset, we need to revert our changes
  16726. tech.one('sourceset', el.resetSourceWatch_);
  16727. };
  16728. /**
  16729. * our implementation of a `src` descriptor for browsers
  16730. * that do not have one
  16731. */
  16732. const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
  16733. get() {
  16734. if (this.hasAttribute('src')) {
  16735. return getAbsoluteURL(window__default["default"].Element.prototype.getAttribute.call(this, 'src'));
  16736. }
  16737. return '';
  16738. },
  16739. set(v) {
  16740. window__default["default"].Element.prototype.setAttribute.call(this, 'src', v);
  16741. return v;
  16742. }
  16743. });
  16744. const getSrcDescriptor = tech => getDescriptor([tech.el(), window__default["default"].HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
  16745. /**
  16746. * setup `sourceset` handling on the `Html5` tech. This function
  16747. * patches the following element properties/functions:
  16748. *
  16749. * - `src` - to determine when `src` is set
  16750. * - `setAttribute()` - to determine when `src` is set
  16751. * - `load()` - this re-triggers the source selection algorithm, and can
  16752. * cause a sourceset.
  16753. *
  16754. * If there is no source when we are adding `sourceset` support or during a `load()`
  16755. * we also patch the functions listed in `firstSourceWatch`.
  16756. *
  16757. * @param {Html5} tech
  16758. * The tech to patch
  16759. */
  16760. const setupSourceset = function (tech) {
  16761. if (!tech.featuresSourceset) {
  16762. return;
  16763. }
  16764. const el = tech.el();
  16765. // make sure sourceset isn't setup twice.
  16766. if (el.resetSourceset_) {
  16767. return;
  16768. }
  16769. const srcDescriptor = getSrcDescriptor(tech);
  16770. const oldSetAttribute = el.setAttribute;
  16771. const oldLoad = el.load;
  16772. Object.defineProperty(el, 'src', merge(srcDescriptor, {
  16773. set: v => {
  16774. const retval = srcDescriptor.set.call(el, v);
  16775. // we use the getter here to get the actual value set on src
  16776. tech.triggerSourceset(el.src);
  16777. return retval;
  16778. }
  16779. }));
  16780. el.setAttribute = (n, v) => {
  16781. const retval = oldSetAttribute.call(el, n, v);
  16782. if (/src/i.test(n)) {
  16783. tech.triggerSourceset(el.src);
  16784. }
  16785. return retval;
  16786. };
  16787. el.load = () => {
  16788. const retval = oldLoad.call(el);
  16789. // if load was called, but there was no source to fire
  16790. // sourceset on. We have to watch for a source append
  16791. // as that can trigger a `sourceset` when the media element
  16792. // has no source
  16793. if (!sourcesetLoad(tech)) {
  16794. tech.triggerSourceset('');
  16795. firstSourceWatch(tech);
  16796. }
  16797. return retval;
  16798. };
  16799. if (el.currentSrc) {
  16800. tech.triggerSourceset(el.currentSrc);
  16801. } else if (!sourcesetLoad(tech)) {
  16802. firstSourceWatch(tech);
  16803. }
  16804. el.resetSourceset_ = () => {
  16805. el.resetSourceset_ = null;
  16806. el.load = oldLoad;
  16807. el.setAttribute = oldSetAttribute;
  16808. Object.defineProperty(el, 'src', srcDescriptor);
  16809. if (el.resetSourceWatch_) {
  16810. el.resetSourceWatch_();
  16811. }
  16812. };
  16813. };
  16814. /**
  16815. * @file html5.js
  16816. */
  16817. /**
  16818. * HTML5 Media Controller - Wrapper for HTML5 Media API
  16819. *
  16820. * @mixes Tech~SourceHandlerAdditions
  16821. * @extends Tech
  16822. */
  16823. class Html5 extends Tech {
  16824. /**
  16825. * Create an instance of this Tech.
  16826. *
  16827. * @param {Object} [options]
  16828. * The key/value store of player options.
  16829. *
  16830. * @param {Function} [ready]
  16831. * Callback function to call when the `HTML5` Tech is ready.
  16832. */
  16833. constructor(options, ready) {
  16834. super(options, ready);
  16835. const source = options.source;
  16836. let crossoriginTracks = false;
  16837. this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
  16838. // Set the source if one is provided
  16839. // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
  16840. // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
  16841. // anyway so the error gets fired.
  16842. if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
  16843. this.setSource(source);
  16844. } else {
  16845. this.handleLateInit_(this.el_);
  16846. }
  16847. // setup sourceset after late sourceset/init
  16848. if (options.enableSourceset) {
  16849. this.setupSourcesetHandling_();
  16850. }
  16851. this.isScrubbing_ = false;
  16852. if (this.el_.hasChildNodes()) {
  16853. const nodes = this.el_.childNodes;
  16854. let nodesLength = nodes.length;
  16855. const removeNodes = [];
  16856. while (nodesLength--) {
  16857. const node = nodes[nodesLength];
  16858. const nodeName = node.nodeName.toLowerCase();
  16859. if (nodeName === 'track') {
  16860. if (!this.featuresNativeTextTracks) {
  16861. // Empty video tag tracks so the built-in player doesn't use them also.
  16862. // This may not be fast enough to stop HTML5 browsers from reading the tags
  16863. // so we'll need to turn off any default tracks if we're manually doing
  16864. // captions and subtitles. videoElement.textTracks
  16865. removeNodes.push(node);
  16866. } else {
  16867. // store HTMLTrackElement and TextTrack to remote list
  16868. this.remoteTextTrackEls().addTrackElement_(node);
  16869. this.remoteTextTracks().addTrack(node.track);
  16870. this.textTracks().addTrack(node.track);
  16871. if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
  16872. crossoriginTracks = true;
  16873. }
  16874. }
  16875. }
  16876. }
  16877. for (let i = 0; i < removeNodes.length; i++) {
  16878. this.el_.removeChild(removeNodes[i]);
  16879. }
  16880. }
  16881. this.proxyNativeTracks_();
  16882. if (this.featuresNativeTextTracks && crossoriginTracks) {
  16883. 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.');
  16884. }
  16885. // prevent iOS Safari from disabling metadata text tracks during native playback
  16886. this.restoreMetadataTracksInIOSNativePlayer_();
  16887. // Determine if native controls should be used
  16888. // Our goal should be to get the custom controls on mobile solid everywhere
  16889. // so we can remove this all together. Right now this will block custom
  16890. // controls on touch enabled laptops like the Chrome Pixel
  16891. if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
  16892. this.setControls(true);
  16893. }
  16894. // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
  16895. // into a `fullscreenchange` event
  16896. this.proxyWebkitFullscreen_();
  16897. this.triggerReady();
  16898. }
  16899. /**
  16900. * Dispose of `HTML5` media element and remove all tracks.
  16901. */
  16902. dispose() {
  16903. if (this.el_ && this.el_.resetSourceset_) {
  16904. this.el_.resetSourceset_();
  16905. }
  16906. Html5.disposeMediaElement(this.el_);
  16907. this.options_ = null;
  16908. // tech will handle clearing of the emulated track list
  16909. super.dispose();
  16910. }
  16911. /**
  16912. * Modify the media element so that we can detect when
  16913. * the source is changed. Fires `sourceset` just after the source has changed
  16914. */
  16915. setupSourcesetHandling_() {
  16916. setupSourceset(this);
  16917. }
  16918. /**
  16919. * When a captions track is enabled in the iOS Safari native player, all other
  16920. * tracks are disabled (including metadata tracks), which nulls all of their
  16921. * associated cue points. This will restore metadata tracks to their pre-fullscreen
  16922. * state in those cases so that cue points are not needlessly lost.
  16923. *
  16924. * @private
  16925. */
  16926. restoreMetadataTracksInIOSNativePlayer_() {
  16927. const textTracks = this.textTracks();
  16928. let metadataTracksPreFullscreenState;
  16929. // captures a snapshot of every metadata track's current state
  16930. const takeMetadataTrackSnapshot = () => {
  16931. metadataTracksPreFullscreenState = [];
  16932. for (let i = 0; i < textTracks.length; i++) {
  16933. const track = textTracks[i];
  16934. if (track.kind === 'metadata') {
  16935. metadataTracksPreFullscreenState.push({
  16936. track,
  16937. storedMode: track.mode
  16938. });
  16939. }
  16940. }
  16941. };
  16942. // snapshot each metadata track's initial state, and update the snapshot
  16943. // each time there is a track 'change' event
  16944. takeMetadataTrackSnapshot();
  16945. textTracks.addEventListener('change', takeMetadataTrackSnapshot);
  16946. this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
  16947. const restoreTrackMode = () => {
  16948. for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
  16949. const storedTrack = metadataTracksPreFullscreenState[i];
  16950. if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
  16951. storedTrack.track.mode = storedTrack.storedMode;
  16952. }
  16953. }
  16954. // we only want this handler to be executed on the first 'change' event
  16955. textTracks.removeEventListener('change', restoreTrackMode);
  16956. };
  16957. // when we enter fullscreen playback, stop updating the snapshot and
  16958. // restore all track modes to their pre-fullscreen state
  16959. this.on('webkitbeginfullscreen', () => {
  16960. textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
  16961. // remove the listener before adding it just in case it wasn't previously removed
  16962. textTracks.removeEventListener('change', restoreTrackMode);
  16963. textTracks.addEventListener('change', restoreTrackMode);
  16964. });
  16965. // start updating the snapshot again after leaving fullscreen
  16966. this.on('webkitendfullscreen', () => {
  16967. // remove the listener before adding it just in case it wasn't previously removed
  16968. textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
  16969. textTracks.addEventListener('change', takeMetadataTrackSnapshot);
  16970. // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
  16971. textTracks.removeEventListener('change', restoreTrackMode);
  16972. });
  16973. }
  16974. /**
  16975. * Attempt to force override of tracks for the given type
  16976. *
  16977. * @param {string} type - Track type to override, possible values include 'Audio',
  16978. * 'Video', and 'Text'.
  16979. * @param {boolean} override - If set to true native audio/video will be overridden,
  16980. * otherwise native audio/video will potentially be used.
  16981. * @private
  16982. */
  16983. overrideNative_(type, override) {
  16984. // If there is no behavioral change don't add/remove listeners
  16985. if (override !== this[`featuresNative${type}Tracks`]) {
  16986. return;
  16987. }
  16988. const lowerCaseType = type.toLowerCase();
  16989. if (this[`${lowerCaseType}TracksListeners_`]) {
  16990. Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
  16991. const elTracks = this.el()[`${lowerCaseType}Tracks`];
  16992. elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
  16993. });
  16994. }
  16995. this[`featuresNative${type}Tracks`] = !override;
  16996. this[`${lowerCaseType}TracksListeners_`] = null;
  16997. this.proxyNativeTracksForType_(lowerCaseType);
  16998. }
  16999. /**
  17000. * Attempt to force override of native audio tracks.
  17001. *
  17002. * @param {boolean} override - If set to true native audio will be overridden,
  17003. * otherwise native audio will potentially be used.
  17004. */
  17005. overrideNativeAudioTracks(override) {
  17006. this.overrideNative_('Audio', override);
  17007. }
  17008. /**
  17009. * Attempt to force override of native video tracks.
  17010. *
  17011. * @param {boolean} override - If set to true native video will be overridden,
  17012. * otherwise native video will potentially be used.
  17013. */
  17014. overrideNativeVideoTracks(override) {
  17015. this.overrideNative_('Video', override);
  17016. }
  17017. /**
  17018. * Proxy native track list events for the given type to our track
  17019. * lists if the browser we are playing in supports that type of track list.
  17020. *
  17021. * @param {string} name - Track type; values include 'audio', 'video', and 'text'
  17022. * @private
  17023. */
  17024. proxyNativeTracksForType_(name) {
  17025. const props = NORMAL[name];
  17026. const elTracks = this.el()[props.getterName];
  17027. const techTracks = this[props.getterName]();
  17028. if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
  17029. return;
  17030. }
  17031. const listeners = {
  17032. change: e => {
  17033. const event = {
  17034. type: 'change',
  17035. target: techTracks,
  17036. currentTarget: techTracks,
  17037. srcElement: techTracks
  17038. };
  17039. techTracks.trigger(event);
  17040. // if we are a text track change event, we should also notify the
  17041. // remote text track list. This can potentially cause a false positive
  17042. // if we were to get a change event on a non-remote track and
  17043. // we triggered the event on the remote text track list which doesn't
  17044. // contain that track. However, best practices mean looping through the
  17045. // list of tracks and searching for the appropriate mode value, so,
  17046. // this shouldn't pose an issue
  17047. if (name === 'text') {
  17048. this[REMOTE.remoteText.getterName]().trigger(event);
  17049. }
  17050. },
  17051. addtrack(e) {
  17052. techTracks.addTrack(e.track);
  17053. },
  17054. removetrack(e) {
  17055. techTracks.removeTrack(e.track);
  17056. }
  17057. };
  17058. const removeOldTracks = function () {
  17059. const removeTracks = [];
  17060. for (let i = 0; i < techTracks.length; i++) {
  17061. let found = false;
  17062. for (let j = 0; j < elTracks.length; j++) {
  17063. if (elTracks[j] === techTracks[i]) {
  17064. found = true;
  17065. break;
  17066. }
  17067. }
  17068. if (!found) {
  17069. removeTracks.push(techTracks[i]);
  17070. }
  17071. }
  17072. while (removeTracks.length) {
  17073. techTracks.removeTrack(removeTracks.shift());
  17074. }
  17075. };
  17076. this[props.getterName + 'Listeners_'] = listeners;
  17077. Object.keys(listeners).forEach(eventName => {
  17078. const listener = listeners[eventName];
  17079. elTracks.addEventListener(eventName, listener);
  17080. this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
  17081. });
  17082. // Remove (native) tracks that are not used anymore
  17083. this.on('loadstart', removeOldTracks);
  17084. this.on('dispose', e => this.off('loadstart', removeOldTracks));
  17085. }
  17086. /**
  17087. * Proxy all native track list events to our track lists if the browser we are playing
  17088. * in supports that type of track list.
  17089. *
  17090. * @private
  17091. */
  17092. proxyNativeTracks_() {
  17093. NORMAL.names.forEach(name => {
  17094. this.proxyNativeTracksForType_(name);
  17095. });
  17096. }
  17097. /**
  17098. * Create the `Html5` Tech's DOM element.
  17099. *
  17100. * @return {Element}
  17101. * The element that gets created.
  17102. */
  17103. createEl() {
  17104. let el = this.options_.tag;
  17105. // Check if this browser supports moving the element into the box.
  17106. // On the iPhone video will break if you move the element,
  17107. // So we have to create a brand new element.
  17108. // If we ingested the player div, we do not need to move the media element.
  17109. if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
  17110. // If the original tag is still there, clone and remove it.
  17111. if (el) {
  17112. const clone = el.cloneNode(true);
  17113. if (el.parentNode) {
  17114. el.parentNode.insertBefore(clone, el);
  17115. }
  17116. Html5.disposeMediaElement(el);
  17117. el = clone;
  17118. } else {
  17119. el = document__default["default"].createElement('video');
  17120. // determine if native controls should be used
  17121. const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
  17122. const attributes = merge({}, tagAttributes);
  17123. if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
  17124. delete attributes.controls;
  17125. }
  17126. setAttributes(el, Object.assign(attributes, {
  17127. id: this.options_.techId,
  17128. class: 'vjs-tech'
  17129. }));
  17130. }
  17131. el.playerId = this.options_.playerId;
  17132. }
  17133. if (typeof this.options_.preload !== 'undefined') {
  17134. setAttribute(el, 'preload', this.options_.preload);
  17135. }
  17136. if (this.options_.disablePictureInPicture !== undefined) {
  17137. el.disablePictureInPicture = this.options_.disablePictureInPicture;
  17138. }
  17139. // Update specific tag settings, in case they were overridden
  17140. // `autoplay` has to be *last* so that `muted` and `playsinline` are present
  17141. // when iOS/Safari or other browsers attempt to autoplay.
  17142. const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
  17143. for (let i = 0; i < settingsAttrs.length; i++) {
  17144. const attr = settingsAttrs[i];
  17145. const value = this.options_[attr];
  17146. if (typeof value !== 'undefined') {
  17147. if (value) {
  17148. setAttribute(el, attr, attr);
  17149. } else {
  17150. removeAttribute(el, attr);
  17151. }
  17152. el[attr] = value;
  17153. }
  17154. }
  17155. return el;
  17156. }
  17157. /**
  17158. * This will be triggered if the loadstart event has already fired, before videojs was
  17159. * ready. Two known examples of when this can happen are:
  17160. * 1. If we're loading the playback object after it has started loading
  17161. * 2. The media is already playing the (often with autoplay on) then
  17162. *
  17163. * This function will fire another loadstart so that videojs can catchup.
  17164. *
  17165. * @fires Tech#loadstart
  17166. *
  17167. * @return {undefined}
  17168. * returns nothing.
  17169. */
  17170. handleLateInit_(el) {
  17171. if (el.networkState === 0 || el.networkState === 3) {
  17172. // The video element hasn't started loading the source yet
  17173. // or didn't find a source
  17174. return;
  17175. }
  17176. if (el.readyState === 0) {
  17177. // NetworkState is set synchronously BUT loadstart is fired at the
  17178. // end of the current stack, usually before setInterval(fn, 0).
  17179. // So at this point we know loadstart may have already fired or is
  17180. // about to fire, and either way the player hasn't seen it yet.
  17181. // We don't want to fire loadstart prematurely here and cause a
  17182. // double loadstart so we'll wait and see if it happens between now
  17183. // and the next loop, and fire it if not.
  17184. // HOWEVER, we also want to make sure it fires before loadedmetadata
  17185. // which could also happen between now and the next loop, so we'll
  17186. // watch for that also.
  17187. let loadstartFired = false;
  17188. const setLoadstartFired = function () {
  17189. loadstartFired = true;
  17190. };
  17191. this.on('loadstart', setLoadstartFired);
  17192. const triggerLoadstart = function () {
  17193. // We did miss the original loadstart. Make sure the player
  17194. // sees loadstart before loadedmetadata
  17195. if (!loadstartFired) {
  17196. this.trigger('loadstart');
  17197. }
  17198. };
  17199. this.on('loadedmetadata', triggerLoadstart);
  17200. this.ready(function () {
  17201. this.off('loadstart', setLoadstartFired);
  17202. this.off('loadedmetadata', triggerLoadstart);
  17203. if (!loadstartFired) {
  17204. // We did miss the original native loadstart. Fire it now.
  17205. this.trigger('loadstart');
  17206. }
  17207. });
  17208. return;
  17209. }
  17210. // From here on we know that loadstart already fired and we missed it.
  17211. // The other readyState events aren't as much of a problem if we double
  17212. // them, so not going to go to as much trouble as loadstart to prevent
  17213. // that unless we find reason to.
  17214. const eventsToTrigger = ['loadstart'];
  17215. // loadedmetadata: newly equal to HAVE_METADATA (1) or greater
  17216. eventsToTrigger.push('loadedmetadata');
  17217. // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
  17218. if (el.readyState >= 2) {
  17219. eventsToTrigger.push('loadeddata');
  17220. }
  17221. // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
  17222. if (el.readyState >= 3) {
  17223. eventsToTrigger.push('canplay');
  17224. }
  17225. // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
  17226. if (el.readyState >= 4) {
  17227. eventsToTrigger.push('canplaythrough');
  17228. }
  17229. // We still need to give the player time to add event listeners
  17230. this.ready(function () {
  17231. eventsToTrigger.forEach(function (type) {
  17232. this.trigger(type);
  17233. }, this);
  17234. });
  17235. }
  17236. /**
  17237. * Set whether we are scrubbing or not.
  17238. * This is used to decide whether we should use `fastSeek` or not.
  17239. * `fastSeek` is used to provide trick play on Safari browsers.
  17240. *
  17241. * @param {boolean} isScrubbing
  17242. * - true for we are currently scrubbing
  17243. * - false for we are no longer scrubbing
  17244. */
  17245. setScrubbing(isScrubbing) {
  17246. this.isScrubbing_ = isScrubbing;
  17247. }
  17248. /**
  17249. * Get whether we are scrubbing or not.
  17250. *
  17251. * @return {boolean} isScrubbing
  17252. * - true for we are currently scrubbing
  17253. * - false for we are no longer scrubbing
  17254. */
  17255. scrubbing() {
  17256. return this.isScrubbing_;
  17257. }
  17258. /**
  17259. * Set current time for the `HTML5` tech.
  17260. *
  17261. * @param {number} seconds
  17262. * Set the current time of the media to this.
  17263. */
  17264. setCurrentTime(seconds) {
  17265. try {
  17266. if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
  17267. this.el_.fastSeek(seconds);
  17268. } else {
  17269. this.el_.currentTime = seconds;
  17270. }
  17271. } catch (e) {
  17272. log(e, 'Video is not ready. (Video.js)');
  17273. // this.warning(VideoJS.warnings.videoNotReady);
  17274. }
  17275. }
  17276. /**
  17277. * Get the current duration of the HTML5 media element.
  17278. *
  17279. * @return {number}
  17280. * The duration of the media or 0 if there is no duration.
  17281. */
  17282. duration() {
  17283. // Android Chrome will report duration as Infinity for VOD HLS until after
  17284. // playback has started, which triggers the live display erroneously.
  17285. // Return NaN if playback has not started and trigger a durationupdate once
  17286. // the duration can be reliably known.
  17287. if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
  17288. // Wait for the first `timeupdate` with currentTime > 0 - there may be
  17289. // several with 0
  17290. const checkProgress = () => {
  17291. if (this.el_.currentTime > 0) {
  17292. // Trigger durationchange for genuinely live video
  17293. if (this.el_.duration === Infinity) {
  17294. this.trigger('durationchange');
  17295. }
  17296. this.off('timeupdate', checkProgress);
  17297. }
  17298. };
  17299. this.on('timeupdate', checkProgress);
  17300. return NaN;
  17301. }
  17302. return this.el_.duration || NaN;
  17303. }
  17304. /**
  17305. * Get the current width of the HTML5 media element.
  17306. *
  17307. * @return {number}
  17308. * The width of the HTML5 media element.
  17309. */
  17310. width() {
  17311. return this.el_.offsetWidth;
  17312. }
  17313. /**
  17314. * Get the current height of the HTML5 media element.
  17315. *
  17316. * @return {number}
  17317. * The height of the HTML5 media element.
  17318. */
  17319. height() {
  17320. return this.el_.offsetHeight;
  17321. }
  17322. /**
  17323. * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
  17324. * `fullscreenchange` event.
  17325. *
  17326. * @private
  17327. * @fires fullscreenchange
  17328. * @listens webkitendfullscreen
  17329. * @listens webkitbeginfullscreen
  17330. * @listens webkitbeginfullscreen
  17331. */
  17332. proxyWebkitFullscreen_() {
  17333. if (!('webkitDisplayingFullscreen' in this.el_)) {
  17334. return;
  17335. }
  17336. const endFn = function () {
  17337. this.trigger('fullscreenchange', {
  17338. isFullscreen: false
  17339. });
  17340. // Safari will sometimes set controls on the videoelement when existing fullscreen.
  17341. if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
  17342. this.el_.controls = false;
  17343. }
  17344. };
  17345. const beginFn = function () {
  17346. if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
  17347. this.one('webkitendfullscreen', endFn);
  17348. this.trigger('fullscreenchange', {
  17349. isFullscreen: true,
  17350. // set a flag in case another tech triggers fullscreenchange
  17351. nativeIOSFullscreen: true
  17352. });
  17353. }
  17354. };
  17355. this.on('webkitbeginfullscreen', beginFn);
  17356. this.on('dispose', () => {
  17357. this.off('webkitbeginfullscreen', beginFn);
  17358. this.off('webkitendfullscreen', endFn);
  17359. });
  17360. }
  17361. /**
  17362. * Check if fullscreen is supported on the video el.
  17363. *
  17364. * @return {boolean}
  17365. * - True if fullscreen is supported.
  17366. * - False if fullscreen is not supported.
  17367. */
  17368. supportsFullScreen() {
  17369. return typeof this.el_.webkitEnterFullScreen === 'function';
  17370. }
  17371. /**
  17372. * Request that the `HTML5` Tech enter fullscreen.
  17373. */
  17374. enterFullScreen() {
  17375. const video = this.el_;
  17376. if (video.paused && video.networkState <= video.HAVE_METADATA) {
  17377. // attempt to prime the video element for programmatic access
  17378. // this isn't necessary on the desktop but shouldn't hurt
  17379. silencePromise(this.el_.play());
  17380. // playing and pausing synchronously during the transition to fullscreen
  17381. // can get iOS ~6.1 devices into a play/pause loop
  17382. this.setTimeout(function () {
  17383. video.pause();
  17384. try {
  17385. video.webkitEnterFullScreen();
  17386. } catch (e) {
  17387. this.trigger('fullscreenerror', e);
  17388. }
  17389. }, 0);
  17390. } else {
  17391. try {
  17392. video.webkitEnterFullScreen();
  17393. } catch (e) {
  17394. this.trigger('fullscreenerror', e);
  17395. }
  17396. }
  17397. }
  17398. /**
  17399. * Request that the `HTML5` Tech exit fullscreen.
  17400. */
  17401. exitFullScreen() {
  17402. if (!this.el_.webkitDisplayingFullscreen) {
  17403. this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
  17404. return;
  17405. }
  17406. this.el_.webkitExitFullScreen();
  17407. }
  17408. /**
  17409. * Create a floating video window always on top of other windows so that users may
  17410. * continue consuming media while they interact with other content sites, or
  17411. * applications on their device.
  17412. *
  17413. * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
  17414. *
  17415. * @return {Promise}
  17416. * A promise with a Picture-in-Picture window.
  17417. */
  17418. requestPictureInPicture() {
  17419. return this.el_.requestPictureInPicture();
  17420. }
  17421. /**
  17422. * Native requestVideoFrameCallback if supported by browser/tech, or fallback
  17423. * Don't use rVCF on Safari when DRM is playing, as it doesn't fire
  17424. * Needs to be checked later than the constructor
  17425. * This will be a false positive for clear sources loaded after a Fairplay source
  17426. *
  17427. * @param {function} cb function to call
  17428. * @return {number} id of request
  17429. */
  17430. requestVideoFrameCallback(cb) {
  17431. if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
  17432. return this.el_.requestVideoFrameCallback(cb);
  17433. }
  17434. return super.requestVideoFrameCallback(cb);
  17435. }
  17436. /**
  17437. * Native or fallback requestVideoFrameCallback
  17438. *
  17439. * @param {number} id request id to cancel
  17440. */
  17441. cancelVideoFrameCallback(id) {
  17442. if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
  17443. this.el_.cancelVideoFrameCallback(id);
  17444. } else {
  17445. super.cancelVideoFrameCallback(id);
  17446. }
  17447. }
  17448. /**
  17449. * A getter/setter for the `Html5` Tech's source object.
  17450. * > Note: Please use {@link Html5#setSource}
  17451. *
  17452. * @param {Tech~SourceObject} [src]
  17453. * The source object you want to set on the `HTML5` techs element.
  17454. *
  17455. * @return {Tech~SourceObject|undefined}
  17456. * - The current source object when a source is not passed in.
  17457. * - undefined when setting
  17458. *
  17459. * @deprecated Since version 5.
  17460. */
  17461. src(src) {
  17462. if (src === undefined) {
  17463. return this.el_.src;
  17464. }
  17465. // Setting src through `src` instead of `setSrc` will be deprecated
  17466. this.setSrc(src);
  17467. }
  17468. /**
  17469. * Reset the tech by removing all sources and then calling
  17470. * {@link Html5.resetMediaElement}.
  17471. */
  17472. reset() {
  17473. Html5.resetMediaElement(this.el_);
  17474. }
  17475. /**
  17476. * Get the current source on the `HTML5` Tech. Falls back to returning the source from
  17477. * the HTML5 media element.
  17478. *
  17479. * @return {Tech~SourceObject}
  17480. * The current source object from the HTML5 tech. With a fallback to the
  17481. * elements source.
  17482. */
  17483. currentSrc() {
  17484. if (this.currentSource_) {
  17485. return this.currentSource_.src;
  17486. }
  17487. return this.el_.currentSrc;
  17488. }
  17489. /**
  17490. * Set controls attribute for the HTML5 media Element.
  17491. *
  17492. * @param {string} val
  17493. * Value to set the controls attribute to
  17494. */
  17495. setControls(val) {
  17496. this.el_.controls = !!val;
  17497. }
  17498. /**
  17499. * Create and returns a remote {@link TextTrack} object.
  17500. *
  17501. * @param {string} kind
  17502. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
  17503. *
  17504. * @param {string} [label]
  17505. * Label to identify the text track
  17506. *
  17507. * @param {string} [language]
  17508. * Two letter language abbreviation
  17509. *
  17510. * @return {TextTrack}
  17511. * The TextTrack that gets created.
  17512. */
  17513. addTextTrack(kind, label, language) {
  17514. if (!this.featuresNativeTextTracks) {
  17515. return super.addTextTrack(kind, label, language);
  17516. }
  17517. return this.el_.addTextTrack(kind, label, language);
  17518. }
  17519. /**
  17520. * Creates either native TextTrack or an emulated TextTrack depending
  17521. * on the value of `featuresNativeTextTracks`
  17522. *
  17523. * @param {Object} options
  17524. * The object should contain the options to initialize the TextTrack with.
  17525. *
  17526. * @param {string} [options.kind]
  17527. * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
  17528. *
  17529. * @param {string} [options.label]
  17530. * Label to identify the text track
  17531. *
  17532. * @param {string} [options.language]
  17533. * Two letter language abbreviation.
  17534. *
  17535. * @param {boolean} [options.default]
  17536. * Default this track to on.
  17537. *
  17538. * @param {string} [options.id]
  17539. * The internal id to assign this track.
  17540. *
  17541. * @param {string} [options.src]
  17542. * A source url for the track.
  17543. *
  17544. * @return {HTMLTrackElement}
  17545. * The track element that gets created.
  17546. */
  17547. createRemoteTextTrack(options) {
  17548. if (!this.featuresNativeTextTracks) {
  17549. return super.createRemoteTextTrack(options);
  17550. }
  17551. const htmlTrackElement = document__default["default"].createElement('track');
  17552. if (options.kind) {
  17553. htmlTrackElement.kind = options.kind;
  17554. }
  17555. if (options.label) {
  17556. htmlTrackElement.label = options.label;
  17557. }
  17558. if (options.language || options.srclang) {
  17559. htmlTrackElement.srclang = options.language || options.srclang;
  17560. }
  17561. if (options.default) {
  17562. htmlTrackElement.default = options.default;
  17563. }
  17564. if (options.id) {
  17565. htmlTrackElement.id = options.id;
  17566. }
  17567. if (options.src) {
  17568. htmlTrackElement.src = options.src;
  17569. }
  17570. return htmlTrackElement;
  17571. }
  17572. /**
  17573. * Creates a remote text track object and returns an html track element.
  17574. *
  17575. * @param {Object} options The object should contain values for
  17576. * kind, language, label, and src (location of the WebVTT file)
  17577. * @param {boolean} [manualCleanup=false] if set to true, the TextTrack
  17578. * will not be removed from the TextTrackList and HtmlTrackElementList
  17579. * after a source change
  17580. * @return {HTMLTrackElement} An Html Track Element.
  17581. * This can be an emulated {@link HTMLTrackElement} or a native one.
  17582. *
  17583. */
  17584. addRemoteTextTrack(options, manualCleanup) {
  17585. const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
  17586. if (this.featuresNativeTextTracks) {
  17587. this.el().appendChild(htmlTrackElement);
  17588. }
  17589. return htmlTrackElement;
  17590. }
  17591. /**
  17592. * Remove remote `TextTrack` from `TextTrackList` object
  17593. *
  17594. * @param {TextTrack} track
  17595. * `TextTrack` object to remove
  17596. */
  17597. removeRemoteTextTrack(track) {
  17598. super.removeRemoteTextTrack(track);
  17599. if (this.featuresNativeTextTracks) {
  17600. const tracks = this.$$('track');
  17601. let i = tracks.length;
  17602. while (i--) {
  17603. if (track === tracks[i] || track === tracks[i].track) {
  17604. this.el().removeChild(tracks[i]);
  17605. }
  17606. }
  17607. }
  17608. }
  17609. /**
  17610. * Gets available media playback quality metrics as specified by the W3C's Media
  17611. * Playback Quality API.
  17612. *
  17613. * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
  17614. *
  17615. * @return {Object}
  17616. * An object with supported media playback quality metrics
  17617. */
  17618. getVideoPlaybackQuality() {
  17619. if (typeof this.el().getVideoPlaybackQuality === 'function') {
  17620. return this.el().getVideoPlaybackQuality();
  17621. }
  17622. const videoPlaybackQuality = {};
  17623. if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
  17624. videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
  17625. videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
  17626. }
  17627. if (window__default["default"].performance) {
  17628. videoPlaybackQuality.creationTime = window__default["default"].performance.now();
  17629. }
  17630. return videoPlaybackQuality;
  17631. }
  17632. }
  17633. /* HTML5 Support Testing ---------------------------------------------------- */
  17634. /**
  17635. * Element for testing browser HTML5 media capabilities
  17636. *
  17637. * @type {Element}
  17638. * @constant
  17639. * @private
  17640. */
  17641. defineLazyProperty(Html5, 'TEST_VID', function () {
  17642. if (!isReal()) {
  17643. return;
  17644. }
  17645. const video = document__default["default"].createElement('video');
  17646. const track = document__default["default"].createElement('track');
  17647. track.kind = 'captions';
  17648. track.srclang = 'en';
  17649. track.label = 'English';
  17650. video.appendChild(track);
  17651. return video;
  17652. });
  17653. /**
  17654. * Check if HTML5 media is supported by this browser/device.
  17655. *
  17656. * @return {boolean}
  17657. * - True if HTML5 media is supported.
  17658. * - False if HTML5 media is not supported.
  17659. */
  17660. Html5.isSupported = function () {
  17661. // IE with no Media Player is a LIAR! (#984)
  17662. try {
  17663. Html5.TEST_VID.volume = 0.5;
  17664. } catch (e) {
  17665. return false;
  17666. }
  17667. return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
  17668. };
  17669. /**
  17670. * Check if the tech can support the given type
  17671. *
  17672. * @param {string} type
  17673. * The mimetype to check
  17674. * @return {string} 'probably', 'maybe', or '' (empty string)
  17675. */
  17676. Html5.canPlayType = function (type) {
  17677. return Html5.TEST_VID.canPlayType(type);
  17678. };
  17679. /**
  17680. * Check if the tech can support the given source
  17681. *
  17682. * @param {Object} srcObj
  17683. * The source object
  17684. * @param {Object} options
  17685. * The options passed to the tech
  17686. * @return {string} 'probably', 'maybe', or '' (empty string)
  17687. */
  17688. Html5.canPlaySource = function (srcObj, options) {
  17689. return Html5.canPlayType(srcObj.type);
  17690. };
  17691. /**
  17692. * Check if the volume can be changed in this browser/device.
  17693. * Volume cannot be changed in a lot of mobile devices.
  17694. * Specifically, it can't be changed from 1 on iOS.
  17695. *
  17696. * @return {boolean}
  17697. * - True if volume can be controlled
  17698. * - False otherwise
  17699. */
  17700. Html5.canControlVolume = function () {
  17701. // IE will error if Windows Media Player not installed #3315
  17702. try {
  17703. const volume = Html5.TEST_VID.volume;
  17704. Html5.TEST_VID.volume = volume / 2 + 0.1;
  17705. const canControl = volume !== Html5.TEST_VID.volume;
  17706. // With the introduction of iOS 15, there are cases where the volume is read as
  17707. // changed but reverts back to its original state at the start of the next tick.
  17708. // To determine whether volume can be controlled on iOS,
  17709. // a timeout is set and the volume is checked asynchronously.
  17710. // Since `features` doesn't currently work asynchronously, the value is manually set.
  17711. if (canControl && IS_IOS) {
  17712. window__default["default"].setTimeout(() => {
  17713. if (Html5 && Html5.prototype) {
  17714. Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
  17715. }
  17716. });
  17717. // default iOS to false, which will be updated in the timeout above.
  17718. return false;
  17719. }
  17720. return canControl;
  17721. } catch (e) {
  17722. return false;
  17723. }
  17724. };
  17725. /**
  17726. * Check if the volume can be muted in this browser/device.
  17727. * Some devices, e.g. iOS, don't allow changing volume
  17728. * but permits muting/unmuting.
  17729. *
  17730. * @return {boolean}
  17731. * - True if volume can be muted
  17732. * - False otherwise
  17733. */
  17734. Html5.canMuteVolume = function () {
  17735. try {
  17736. const muted = Html5.TEST_VID.muted;
  17737. // in some versions of iOS muted property doesn't always
  17738. // work, so we want to set both property and attribute
  17739. Html5.TEST_VID.muted = !muted;
  17740. if (Html5.TEST_VID.muted) {
  17741. setAttribute(Html5.TEST_VID, 'muted', 'muted');
  17742. } else {
  17743. removeAttribute(Html5.TEST_VID, 'muted', 'muted');
  17744. }
  17745. return muted !== Html5.TEST_VID.muted;
  17746. } catch (e) {
  17747. return false;
  17748. }
  17749. };
  17750. /**
  17751. * Check if the playback rate can be changed in this browser/device.
  17752. *
  17753. * @return {boolean}
  17754. * - True if playback rate can be controlled
  17755. * - False otherwise
  17756. */
  17757. Html5.canControlPlaybackRate = function () {
  17758. // Playback rate API is implemented in Android Chrome, but doesn't do anything
  17759. // https://github.com/videojs/video.js/issues/3180
  17760. if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
  17761. return false;
  17762. }
  17763. // IE will error if Windows Media Player not installed #3315
  17764. try {
  17765. const playbackRate = Html5.TEST_VID.playbackRate;
  17766. Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
  17767. return playbackRate !== Html5.TEST_VID.playbackRate;
  17768. } catch (e) {
  17769. return false;
  17770. }
  17771. };
  17772. /**
  17773. * Check if we can override a video/audio elements attributes, with
  17774. * Object.defineProperty.
  17775. *
  17776. * @return {boolean}
  17777. * - True if builtin attributes can be overridden
  17778. * - False otherwise
  17779. */
  17780. Html5.canOverrideAttributes = function () {
  17781. // if we cannot overwrite the src/innerHTML property, there is no support
  17782. // iOS 7 safari for instance cannot do this.
  17783. try {
  17784. const noop = () => {};
  17785. Object.defineProperty(document__default["default"].createElement('video'), 'src', {
  17786. get: noop,
  17787. set: noop
  17788. });
  17789. Object.defineProperty(document__default["default"].createElement('audio'), 'src', {
  17790. get: noop,
  17791. set: noop
  17792. });
  17793. Object.defineProperty(document__default["default"].createElement('video'), 'innerHTML', {
  17794. get: noop,
  17795. set: noop
  17796. });
  17797. Object.defineProperty(document__default["default"].createElement('audio'), 'innerHTML', {
  17798. get: noop,
  17799. set: noop
  17800. });
  17801. } catch (e) {
  17802. return false;
  17803. }
  17804. return true;
  17805. };
  17806. /**
  17807. * Check to see if native `TextTrack`s are supported by this browser/device.
  17808. *
  17809. * @return {boolean}
  17810. * - True if native `TextTrack`s are supported.
  17811. * - False otherwise
  17812. */
  17813. Html5.supportsNativeTextTracks = function () {
  17814. return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
  17815. };
  17816. /**
  17817. * Check to see if native `VideoTrack`s are supported by this browser/device
  17818. *
  17819. * @return {boolean}
  17820. * - True if native `VideoTrack`s are supported.
  17821. * - False otherwise
  17822. */
  17823. Html5.supportsNativeVideoTracks = function () {
  17824. return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
  17825. };
  17826. /**
  17827. * Check to see if native `AudioTrack`s are supported by this browser/device
  17828. *
  17829. * @return {boolean}
  17830. * - True if native `AudioTrack`s are supported.
  17831. * - False otherwise
  17832. */
  17833. Html5.supportsNativeAudioTracks = function () {
  17834. return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
  17835. };
  17836. /**
  17837. * An array of events available on the Html5 tech.
  17838. *
  17839. * @private
  17840. * @type {Array}
  17841. */
  17842. 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'];
  17843. /**
  17844. * Boolean indicating whether the `Tech` supports volume control.
  17845. *
  17846. * @type {boolean}
  17847. * @default {@link Html5.canControlVolume}
  17848. */
  17849. /**
  17850. * Boolean indicating whether the `Tech` supports muting volume.
  17851. *
  17852. * @type {boolean}
  17853. * @default {@link Html5.canMuteVolume}
  17854. */
  17855. /**
  17856. * Boolean indicating whether the `Tech` supports changing the speed at which the media
  17857. * plays. Examples:
  17858. * - Set player to play 2x (twice) as fast
  17859. * - Set player to play 0.5x (half) as fast
  17860. *
  17861. * @type {boolean}
  17862. * @default {@link Html5.canControlPlaybackRate}
  17863. */
  17864. /**
  17865. * Boolean indicating whether the `Tech` supports the `sourceset` event.
  17866. *
  17867. * @type {boolean}
  17868. * @default
  17869. */
  17870. /**
  17871. * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
  17872. *
  17873. * @type {boolean}
  17874. * @default {@link Html5.supportsNativeTextTracks}
  17875. */
  17876. /**
  17877. * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
  17878. *
  17879. * @type {boolean}
  17880. * @default {@link Html5.supportsNativeVideoTracks}
  17881. */
  17882. /**
  17883. * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
  17884. *
  17885. * @type {boolean}
  17886. * @default {@link Html5.supportsNativeAudioTracks}
  17887. */
  17888. [['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
  17889. defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
  17890. });
  17891. Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
  17892. /**
  17893. * Boolean indicating whether the `HTML5` tech currently supports the media element
  17894. * moving in the DOM. iOS breaks if you move the media element, so this is set this to
  17895. * false there. Everywhere else this should be true.
  17896. *
  17897. * @type {boolean}
  17898. * @default
  17899. */
  17900. Html5.prototype.movingMediaElementInDOM = !IS_IOS;
  17901. // TODO: Previous comment: No longer appears to be used. Can probably be removed.
  17902. // Is this true?
  17903. /**
  17904. * Boolean indicating whether the `HTML5` tech currently supports automatic media resize
  17905. * when going into fullscreen.
  17906. *
  17907. * @type {boolean}
  17908. * @default
  17909. */
  17910. Html5.prototype.featuresFullscreenResize = true;
  17911. /**
  17912. * Boolean indicating whether the `HTML5` tech currently supports the progress event.
  17913. * If this is false, manual `progress` events will be triggered instead.
  17914. *
  17915. * @type {boolean}
  17916. * @default
  17917. */
  17918. Html5.prototype.featuresProgressEvents = true;
  17919. /**
  17920. * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
  17921. * If this is false, manual `timeupdate` events will be triggered instead.
  17922. *
  17923. * @default
  17924. */
  17925. Html5.prototype.featuresTimeupdateEvents = true;
  17926. /**
  17927. * Whether the HTML5 el supports `requestVideoFrameCallback`
  17928. *
  17929. * @type {boolean}
  17930. */
  17931. Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
  17932. Html5.disposeMediaElement = function (el) {
  17933. if (!el) {
  17934. return;
  17935. }
  17936. if (el.parentNode) {
  17937. el.parentNode.removeChild(el);
  17938. }
  17939. // remove any child track or source nodes to prevent their loading
  17940. while (el.hasChildNodes()) {
  17941. el.removeChild(el.firstChild);
  17942. }
  17943. // remove any src reference. not setting `src=''` because that causes a warning
  17944. // in firefox
  17945. el.removeAttribute('src');
  17946. // force the media element to update its loading state by calling load()
  17947. // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
  17948. if (typeof el.load === 'function') {
  17949. // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
  17950. (function () {
  17951. try {
  17952. el.load();
  17953. } catch (e) {
  17954. // not supported
  17955. }
  17956. })();
  17957. }
  17958. };
  17959. Html5.resetMediaElement = function (el) {
  17960. if (!el) {
  17961. return;
  17962. }
  17963. const sources = el.querySelectorAll('source');
  17964. let i = sources.length;
  17965. while (i--) {
  17966. el.removeChild(sources[i]);
  17967. }
  17968. // remove any src reference.
  17969. // not setting `src=''` because that throws an error
  17970. el.removeAttribute('src');
  17971. if (typeof el.load === 'function') {
  17972. // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
  17973. (function () {
  17974. try {
  17975. el.load();
  17976. } catch (e) {
  17977. // satisfy linter
  17978. }
  17979. })();
  17980. }
  17981. };
  17982. /* Native HTML5 element property wrapping ----------------------------------- */
  17983. // Wrap native boolean attributes with getters that check both property and attribute
  17984. // The list is as followed:
  17985. // muted, defaultMuted, autoplay, controls, loop, playsinline
  17986. [
  17987. /**
  17988. * Get the value of `muted` from the media element. `muted` indicates
  17989. * that the volume for the media should be set to silent. This does not actually change
  17990. * the `volume` attribute.
  17991. *
  17992. * @method Html5#muted
  17993. * @return {boolean}
  17994. * - True if the value of `volume` should be ignored and the audio set to silent.
  17995. * - False if the value of `volume` should be used.
  17996. *
  17997. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
  17998. */
  17999. 'muted',
  18000. /**
  18001. * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
  18002. * whether the media should start muted or not. Only changes the default state of the
  18003. * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
  18004. * current state.
  18005. *
  18006. * @method Html5#defaultMuted
  18007. * @return {boolean}
  18008. * - The value of `defaultMuted` from the media element.
  18009. * - True indicates that the media should start muted.
  18010. * - False indicates that the media should not start muted
  18011. *
  18012. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
  18013. */
  18014. 'defaultMuted',
  18015. /**
  18016. * Get the value of `autoplay` from the media element. `autoplay` indicates
  18017. * that the media should start to play as soon as the page is ready.
  18018. *
  18019. * @method Html5#autoplay
  18020. * @return {boolean}
  18021. * - The value of `autoplay` from the media element.
  18022. * - True indicates that the media should start as soon as the page loads.
  18023. * - False indicates that the media should not start as soon as the page loads.
  18024. *
  18025. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
  18026. */
  18027. 'autoplay',
  18028. /**
  18029. * Get the value of `controls` from the media element. `controls` indicates
  18030. * whether the native media controls should be shown or hidden.
  18031. *
  18032. * @method Html5#controls
  18033. * @return {boolean}
  18034. * - The value of `controls` from the media element.
  18035. * - True indicates that native controls should be showing.
  18036. * - False indicates that native controls should be hidden.
  18037. *
  18038. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
  18039. */
  18040. 'controls',
  18041. /**
  18042. * Get the value of `loop` from the media element. `loop` indicates
  18043. * that the media should return to the start of the media and continue playing once
  18044. * it reaches the end.
  18045. *
  18046. * @method Html5#loop
  18047. * @return {boolean}
  18048. * - The value of `loop` from the media element.
  18049. * - True indicates that playback should seek back to start once
  18050. * the end of a media is reached.
  18051. * - False indicates that playback should not loop back to the start when the
  18052. * end of the media is reached.
  18053. *
  18054. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
  18055. */
  18056. 'loop',
  18057. /**
  18058. * Get the value of `playsinline` from the media element. `playsinline` indicates
  18059. * to the browser that non-fullscreen playback is preferred when fullscreen
  18060. * playback is the native default, such as in iOS Safari.
  18061. *
  18062. * @method Html5#playsinline
  18063. * @return {boolean}
  18064. * - The value of `playsinline` from the media element.
  18065. * - True indicates that the media should play inline.
  18066. * - False indicates that the media should not play inline.
  18067. *
  18068. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
  18069. */
  18070. 'playsinline'].forEach(function (prop) {
  18071. Html5.prototype[prop] = function () {
  18072. return this.el_[prop] || this.el_.hasAttribute(prop);
  18073. };
  18074. });
  18075. // Wrap native boolean attributes with setters that set both property and attribute
  18076. // The list is as followed:
  18077. // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
  18078. // setControls is special-cased above
  18079. [
  18080. /**
  18081. * Set the value of `muted` on the media element. `muted` indicates that the current
  18082. * audio level should be silent.
  18083. *
  18084. * @method Html5#setMuted
  18085. * @param {boolean} muted
  18086. * - True if the audio should be set to silent
  18087. * - False otherwise
  18088. *
  18089. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
  18090. */
  18091. 'muted',
  18092. /**
  18093. * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
  18094. * audio level should be silent, but will only effect the muted level on initial playback..
  18095. *
  18096. * @method Html5.prototype.setDefaultMuted
  18097. * @param {boolean} defaultMuted
  18098. * - True if the audio should be set to silent
  18099. * - False otherwise
  18100. *
  18101. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
  18102. */
  18103. 'defaultMuted',
  18104. /**
  18105. * Set the value of `autoplay` on the media element. `autoplay` indicates
  18106. * that the media should start to play as soon as the page is ready.
  18107. *
  18108. * @method Html5#setAutoplay
  18109. * @param {boolean} autoplay
  18110. * - True indicates that the media should start as soon as the page loads.
  18111. * - False indicates that the media should not start as soon as the page loads.
  18112. *
  18113. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
  18114. */
  18115. 'autoplay',
  18116. /**
  18117. * Set the value of `loop` on the media element. `loop` indicates
  18118. * that the media should return to the start of the media and continue playing once
  18119. * it reaches the end.
  18120. *
  18121. * @method Html5#setLoop
  18122. * @param {boolean} loop
  18123. * - True indicates that playback should seek back to start once
  18124. * the end of a media is reached.
  18125. * - False indicates that playback should not loop back to the start when the
  18126. * end of the media is reached.
  18127. *
  18128. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
  18129. */
  18130. 'loop',
  18131. /**
  18132. * Set the value of `playsinline` from the media element. `playsinline` indicates
  18133. * to the browser that non-fullscreen playback is preferred when fullscreen
  18134. * playback is the native default, such as in iOS Safari.
  18135. *
  18136. * @method Html5#setPlaysinline
  18137. * @param {boolean} playsinline
  18138. * - True indicates that the media should play inline.
  18139. * - False indicates that the media should not play inline.
  18140. *
  18141. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
  18142. */
  18143. 'playsinline'].forEach(function (prop) {
  18144. Html5.prototype['set' + toTitleCase(prop)] = function (v) {
  18145. this.el_[prop] = v;
  18146. if (v) {
  18147. this.el_.setAttribute(prop, prop);
  18148. } else {
  18149. this.el_.removeAttribute(prop);
  18150. }
  18151. };
  18152. });
  18153. // Wrap native properties with a getter
  18154. // The list is as followed
  18155. // paused, currentTime, buffered, volume, poster, preload, error, seeking
  18156. // seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
  18157. // played, networkState, readyState, videoWidth, videoHeight, crossOrigin
  18158. [
  18159. /**
  18160. * Get the value of `paused` from the media element. `paused` indicates whether the media element
  18161. * is currently paused or not.
  18162. *
  18163. * @method Html5#paused
  18164. * @return {boolean}
  18165. * The value of `paused` from the media element.
  18166. *
  18167. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
  18168. */
  18169. 'paused',
  18170. /**
  18171. * Get the value of `currentTime` from the media element. `currentTime` indicates
  18172. * the current second that the media is at in playback.
  18173. *
  18174. * @method Html5#currentTime
  18175. * @return {number}
  18176. * The value of `currentTime` from the media element.
  18177. *
  18178. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
  18179. */
  18180. 'currentTime',
  18181. /**
  18182. * Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
  18183. * object that represents the parts of the media that are already downloaded and
  18184. * available for playback.
  18185. *
  18186. * @method Html5#buffered
  18187. * @return {TimeRange}
  18188. * The value of `buffered` from the media element.
  18189. *
  18190. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
  18191. */
  18192. 'buffered',
  18193. /**
  18194. * Get the value of `volume` from the media element. `volume` indicates
  18195. * the current playback volume of audio for a media. `volume` will be a value from 0
  18196. * (silent) to 1 (loudest and default).
  18197. *
  18198. * @method Html5#volume
  18199. * @return {number}
  18200. * The value of `volume` from the media element. Value will be between 0-1.
  18201. *
  18202. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
  18203. */
  18204. 'volume',
  18205. /**
  18206. * Get the value of `poster` from the media element. `poster` indicates
  18207. * that the url of an image file that can/will be shown when no media data is available.
  18208. *
  18209. * @method Html5#poster
  18210. * @return {string}
  18211. * The value of `poster` from the media element. Value will be a url to an
  18212. * image.
  18213. *
  18214. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
  18215. */
  18216. 'poster',
  18217. /**
  18218. * Get the value of `preload` from the media element. `preload` indicates
  18219. * what should download before the media is interacted with. It can have the following
  18220. * values:
  18221. * - none: nothing should be downloaded
  18222. * - metadata: poster and the first few frames of the media may be downloaded to get
  18223. * media dimensions and other metadata
  18224. * - auto: allow the media and metadata for the media to be downloaded before
  18225. * interaction
  18226. *
  18227. * @method Html5#preload
  18228. * @return {string}
  18229. * The value of `preload` from the media element. Will be 'none', 'metadata',
  18230. * or 'auto'.
  18231. *
  18232. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
  18233. */
  18234. 'preload',
  18235. /**
  18236. * Get the value of the `error` from the media element. `error` indicates any
  18237. * MediaError that may have occurred during playback. If error returns null there is no
  18238. * current error.
  18239. *
  18240. * @method Html5#error
  18241. * @return {MediaError|null}
  18242. * The value of `error` from the media element. Will be `MediaError` if there
  18243. * is a current error and null otherwise.
  18244. *
  18245. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
  18246. */
  18247. 'error',
  18248. /**
  18249. * Get the value of `seeking` from the media element. `seeking` indicates whether the
  18250. * media is currently seeking to a new position or not.
  18251. *
  18252. * @method Html5#seeking
  18253. * @return {boolean}
  18254. * - The value of `seeking` from the media element.
  18255. * - True indicates that the media is currently seeking to a new position.
  18256. * - False indicates that the media is not seeking to a new position at this time.
  18257. *
  18258. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
  18259. */
  18260. 'seeking',
  18261. /**
  18262. * Get the value of `seekable` from the media element. `seekable` returns a
  18263. * `TimeRange` object indicating ranges of time that can currently be `seeked` to.
  18264. *
  18265. * @method Html5#seekable
  18266. * @return {TimeRange}
  18267. * The value of `seekable` from the media element. A `TimeRange` object
  18268. * indicating the current ranges of time that can be seeked to.
  18269. *
  18270. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
  18271. */
  18272. 'seekable',
  18273. /**
  18274. * Get the value of `ended` from the media element. `ended` indicates whether
  18275. * the media has reached the end or not.
  18276. *
  18277. * @method Html5#ended
  18278. * @return {boolean}
  18279. * - The value of `ended` from the media element.
  18280. * - True indicates that the media has ended.
  18281. * - False indicates that the media has not ended.
  18282. *
  18283. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
  18284. */
  18285. 'ended',
  18286. /**
  18287. * Get the value of `playbackRate` from the media element. `playbackRate` indicates
  18288. * the rate at which the media is currently playing back. Examples:
  18289. * - if playbackRate is set to 2, media will play twice as fast.
  18290. * - if playbackRate is set to 0.5, media will play half as fast.
  18291. *
  18292. * @method Html5#playbackRate
  18293. * @return {number}
  18294. * The value of `playbackRate` from the media element. A number indicating
  18295. * the current playback speed of the media, where 1 is normal speed.
  18296. *
  18297. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
  18298. */
  18299. 'playbackRate',
  18300. /**
  18301. * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
  18302. * the rate at which the media is currently playing back. This value will not indicate the current
  18303. * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
  18304. *
  18305. * Examples:
  18306. * - if defaultPlaybackRate is set to 2, media will play twice as fast.
  18307. * - if defaultPlaybackRate is set to 0.5, media will play half as fast.
  18308. *
  18309. * @method Html5.prototype.defaultPlaybackRate
  18310. * @return {number}
  18311. * The value of `defaultPlaybackRate` from the media element. A number indicating
  18312. * the current playback speed of the media, where 1 is normal speed.
  18313. *
  18314. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
  18315. */
  18316. 'defaultPlaybackRate',
  18317. /**
  18318. * Get the value of 'disablePictureInPicture' from the video element.
  18319. *
  18320. * @method Html5#disablePictureInPicture
  18321. * @return {boolean} value
  18322. * - The value of `disablePictureInPicture` from the video element.
  18323. * - True indicates that the video can't be played in Picture-In-Picture mode
  18324. * - False indicates that the video can be played in Picture-In-Picture mode
  18325. *
  18326. * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
  18327. */
  18328. 'disablePictureInPicture',
  18329. /**
  18330. * Get the value of `played` from the media element. `played` returns a `TimeRange`
  18331. * object representing points in the media timeline that have been played.
  18332. *
  18333. * @method Html5#played
  18334. * @return {TimeRange}
  18335. * The value of `played` from the media element. A `TimeRange` object indicating
  18336. * the ranges of time that have been played.
  18337. *
  18338. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
  18339. */
  18340. 'played',
  18341. /**
  18342. * Get the value of `networkState` from the media element. `networkState` indicates
  18343. * the current network state. It returns an enumeration from the following list:
  18344. * - 0: NETWORK_EMPTY
  18345. * - 1: NETWORK_IDLE
  18346. * - 2: NETWORK_LOADING
  18347. * - 3: NETWORK_NO_SOURCE
  18348. *
  18349. * @method Html5#networkState
  18350. * @return {number}
  18351. * The value of `networkState` from the media element. This will be a number
  18352. * from the list in the description.
  18353. *
  18354. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
  18355. */
  18356. 'networkState',
  18357. /**
  18358. * Get the value of `readyState` from the media element. `readyState` indicates
  18359. * the current state of the media element. It returns an enumeration from the
  18360. * following list:
  18361. * - 0: HAVE_NOTHING
  18362. * - 1: HAVE_METADATA
  18363. * - 2: HAVE_CURRENT_DATA
  18364. * - 3: HAVE_FUTURE_DATA
  18365. * - 4: HAVE_ENOUGH_DATA
  18366. *
  18367. * @method Html5#readyState
  18368. * @return {number}
  18369. * The value of `readyState` from the media element. This will be a number
  18370. * from the list in the description.
  18371. *
  18372. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
  18373. */
  18374. 'readyState',
  18375. /**
  18376. * Get the value of `videoWidth` from the video element. `videoWidth` indicates
  18377. * the current width of the video in css pixels.
  18378. *
  18379. * @method Html5#videoWidth
  18380. * @return {number}
  18381. * The value of `videoWidth` from the video element. This will be a number
  18382. * in css pixels.
  18383. *
  18384. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
  18385. */
  18386. 'videoWidth',
  18387. /**
  18388. * Get the value of `videoHeight` from the video element. `videoHeight` indicates
  18389. * the current height of the video in css pixels.
  18390. *
  18391. * @method Html5#videoHeight
  18392. * @return {number}
  18393. * The value of `videoHeight` from the video element. This will be a number
  18394. * in css pixels.
  18395. *
  18396. * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
  18397. */
  18398. 'videoHeight',
  18399. /**
  18400. * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
  18401. * to the browser that should sent the cookies along with the requests for the
  18402. * different assets/playlists
  18403. *
  18404. * @method Html5#crossOrigin
  18405. * @return {string}
  18406. * - anonymous indicates that the media should not sent cookies.
  18407. * - use-credentials indicates that the media should sent cookies along the requests.
  18408. *
  18409. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
  18410. */
  18411. 'crossOrigin'].forEach(function (prop) {
  18412. Html5.prototype[prop] = function () {
  18413. return this.el_[prop];
  18414. };
  18415. });
  18416. // Wrap native properties with a setter in this format:
  18417. // set + toTitleCase(name)
  18418. // The list is as follows:
  18419. // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
  18420. // setDisablePictureInPicture, setCrossOrigin
  18421. [
  18422. /**
  18423. * Set the value of `volume` on the media element. `volume` indicates the current
  18424. * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
  18425. * so on.
  18426. *
  18427. * @method Html5#setVolume
  18428. * @param {number} percentAsDecimal
  18429. * The volume percent as a decimal. Valid range is from 0-1.
  18430. *
  18431. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
  18432. */
  18433. 'volume',
  18434. /**
  18435. * Set the value of `src` on the media element. `src` indicates the current
  18436. * {@link Tech~SourceObject} for the media.
  18437. *
  18438. * @method Html5#setSrc
  18439. * @param {Tech~SourceObject} src
  18440. * The source object to set as the current source.
  18441. *
  18442. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
  18443. */
  18444. 'src',
  18445. /**
  18446. * Set the value of `poster` on the media element. `poster` is the url to
  18447. * an image file that can/will be shown when no media data is available.
  18448. *
  18449. * @method Html5#setPoster
  18450. * @param {string} poster
  18451. * The url to an image that should be used as the `poster` for the media
  18452. * element.
  18453. *
  18454. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
  18455. */
  18456. 'poster',
  18457. /**
  18458. * Set the value of `preload` on the media element. `preload` indicates
  18459. * what should download before the media is interacted with. It can have the following
  18460. * values:
  18461. * - none: nothing should be downloaded
  18462. * - metadata: poster and the first few frames of the media may be downloaded to get
  18463. * media dimensions and other metadata
  18464. * - auto: allow the media and metadata for the media to be downloaded before
  18465. * interaction
  18466. *
  18467. * @method Html5#setPreload
  18468. * @param {string} preload
  18469. * The value of `preload` to set on the media element. Must be 'none', 'metadata',
  18470. * or 'auto'.
  18471. *
  18472. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
  18473. */
  18474. 'preload',
  18475. /**
  18476. * Set the value of `playbackRate` on the media element. `playbackRate` indicates
  18477. * the rate at which the media should play back. Examples:
  18478. * - if playbackRate is set to 2, media will play twice as fast.
  18479. * - if playbackRate is set to 0.5, media will play half as fast.
  18480. *
  18481. * @method Html5#setPlaybackRate
  18482. * @return {number}
  18483. * The value of `playbackRate` from the media element. A number indicating
  18484. * the current playback speed of the media, where 1 is normal speed.
  18485. *
  18486. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
  18487. */
  18488. 'playbackRate',
  18489. /**
  18490. * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
  18491. * the rate at which the media should play back upon initial startup. Changing this value
  18492. * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
  18493. *
  18494. * Example Values:
  18495. * - if playbackRate is set to 2, media will play twice as fast.
  18496. * - if playbackRate is set to 0.5, media will play half as fast.
  18497. *
  18498. * @method Html5.prototype.setDefaultPlaybackRate
  18499. * @return {number}
  18500. * The value of `defaultPlaybackRate` from the media element. A number indicating
  18501. * the current playback speed of the media, where 1 is normal speed.
  18502. *
  18503. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
  18504. */
  18505. 'defaultPlaybackRate',
  18506. /**
  18507. * Prevents the browser from suggesting a Picture-in-Picture context menu
  18508. * or to request Picture-in-Picture automatically in some cases.
  18509. *
  18510. * @method Html5#setDisablePictureInPicture
  18511. * @param {boolean} value
  18512. * The true value will disable Picture-in-Picture mode.
  18513. *
  18514. * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
  18515. */
  18516. 'disablePictureInPicture',
  18517. /**
  18518. * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
  18519. * to the browser that should sent the cookies along with the requests for the
  18520. * different assets/playlists
  18521. *
  18522. * @method Html5#setCrossOrigin
  18523. * @param {string} crossOrigin
  18524. * - anonymous indicates that the media should not sent cookies.
  18525. * - use-credentials indicates that the media should sent cookies along the requests.
  18526. *
  18527. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
  18528. */
  18529. 'crossOrigin'].forEach(function (prop) {
  18530. Html5.prototype['set' + toTitleCase(prop)] = function (v) {
  18531. this.el_[prop] = v;
  18532. };
  18533. });
  18534. // wrap native functions with a function
  18535. // The list is as follows:
  18536. // pause, load, play
  18537. [
  18538. /**
  18539. * A wrapper around the media elements `pause` function. This will call the `HTML5`
  18540. * media elements `pause` function.
  18541. *
  18542. * @method Html5#pause
  18543. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
  18544. */
  18545. 'pause',
  18546. /**
  18547. * A wrapper around the media elements `load` function. This will call the `HTML5`s
  18548. * media element `load` function.
  18549. *
  18550. * @method Html5#load
  18551. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
  18552. */
  18553. 'load',
  18554. /**
  18555. * A wrapper around the media elements `play` function. This will call the `HTML5`s
  18556. * media element `play` function.
  18557. *
  18558. * @method Html5#play
  18559. * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
  18560. */
  18561. 'play'].forEach(function (prop) {
  18562. Html5.prototype[prop] = function () {
  18563. return this.el_[prop]();
  18564. };
  18565. });
  18566. Tech.withSourceHandlers(Html5);
  18567. /**
  18568. * Native source handler for Html5, simply passes the source to the media element.
  18569. *
  18570. * @property {Tech~SourceObject} source
  18571. * The source object
  18572. *
  18573. * @property {Html5} tech
  18574. * The instance of the HTML5 tech.
  18575. */
  18576. Html5.nativeSourceHandler = {};
  18577. /**
  18578. * Check if the media element can play the given mime type.
  18579. *
  18580. * @param {string} type
  18581. * The mimetype to check
  18582. *
  18583. * @return {string}
  18584. * 'probably', 'maybe', or '' (empty string)
  18585. */
  18586. Html5.nativeSourceHandler.canPlayType = function (type) {
  18587. // IE without MediaPlayer throws an error (#519)
  18588. try {
  18589. return Html5.TEST_VID.canPlayType(type);
  18590. } catch (e) {
  18591. return '';
  18592. }
  18593. };
  18594. /**
  18595. * Check if the media element can handle a source natively.
  18596. *
  18597. * @param {Tech~SourceObject} source
  18598. * The source object
  18599. *
  18600. * @param {Object} [options]
  18601. * Options to be passed to the tech.
  18602. *
  18603. * @return {string}
  18604. * 'probably', 'maybe', or '' (empty string).
  18605. */
  18606. Html5.nativeSourceHandler.canHandleSource = function (source, options) {
  18607. // If a type was provided we should rely on that
  18608. if (source.type) {
  18609. return Html5.nativeSourceHandler.canPlayType(source.type);
  18610. // If no type, fall back to checking 'video/[EXTENSION]'
  18611. } else if (source.src) {
  18612. const ext = getFileExtension(source.src);
  18613. return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
  18614. }
  18615. return '';
  18616. };
  18617. /**
  18618. * Pass the source to the native media element.
  18619. *
  18620. * @param {Tech~SourceObject} source
  18621. * The source object
  18622. *
  18623. * @param {Html5} tech
  18624. * The instance of the Html5 tech
  18625. *
  18626. * @param {Object} [options]
  18627. * The options to pass to the source
  18628. */
  18629. Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
  18630. tech.setSrc(source.src);
  18631. };
  18632. /**
  18633. * A noop for the native dispose function, as cleanup is not needed.
  18634. */
  18635. Html5.nativeSourceHandler.dispose = function () {};
  18636. // Register the native source handler
  18637. Html5.registerSourceHandler(Html5.nativeSourceHandler);
  18638. Tech.registerTech('Html5', Html5);
  18639. /**
  18640. * @file player.js
  18641. */
  18642. // The following tech events are simply re-triggered
  18643. // on the player when they happen
  18644. const TECH_EVENTS_RETRIGGER = [
  18645. /**
  18646. * Fired while the user agent is downloading media data.
  18647. *
  18648. * @event Player#progress
  18649. * @type {Event}
  18650. */
  18651. /**
  18652. * Retrigger the `progress` event that was triggered by the {@link Tech}.
  18653. *
  18654. * @private
  18655. * @method Player#handleTechProgress_
  18656. * @fires Player#progress
  18657. * @listens Tech#progress
  18658. */
  18659. 'progress',
  18660. /**
  18661. * Fires when the loading of an audio/video is aborted.
  18662. *
  18663. * @event Player#abort
  18664. * @type {Event}
  18665. */
  18666. /**
  18667. * Retrigger the `abort` event that was triggered by the {@link Tech}.
  18668. *
  18669. * @private
  18670. * @method Player#handleTechAbort_
  18671. * @fires Player#abort
  18672. * @listens Tech#abort
  18673. */
  18674. 'abort',
  18675. /**
  18676. * Fires when the browser is intentionally not getting media data.
  18677. *
  18678. * @event Player#suspend
  18679. * @type {Event}
  18680. */
  18681. /**
  18682. * Retrigger the `suspend` event that was triggered by the {@link Tech}.
  18683. *
  18684. * @private
  18685. * @method Player#handleTechSuspend_
  18686. * @fires Player#suspend
  18687. * @listens Tech#suspend
  18688. */
  18689. 'suspend',
  18690. /**
  18691. * Fires when the current playlist is empty.
  18692. *
  18693. * @event Player#emptied
  18694. * @type {Event}
  18695. */
  18696. /**
  18697. * Retrigger the `emptied` event that was triggered by the {@link Tech}.
  18698. *
  18699. * @private
  18700. * @method Player#handleTechEmptied_
  18701. * @fires Player#emptied
  18702. * @listens Tech#emptied
  18703. */
  18704. 'emptied',
  18705. /**
  18706. * Fires when the browser is trying to get media data, but data is not available.
  18707. *
  18708. * @event Player#stalled
  18709. * @type {Event}
  18710. */
  18711. /**
  18712. * Retrigger the `stalled` event that was triggered by the {@link Tech}.
  18713. *
  18714. * @private
  18715. * @method Player#handleTechStalled_
  18716. * @fires Player#stalled
  18717. * @listens Tech#stalled
  18718. */
  18719. 'stalled',
  18720. /**
  18721. * Fires when the browser has loaded meta data for the audio/video.
  18722. *
  18723. * @event Player#loadedmetadata
  18724. * @type {Event}
  18725. */
  18726. /**
  18727. * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
  18728. *
  18729. * @private
  18730. * @method Player#handleTechLoadedmetadata_
  18731. * @fires Player#loadedmetadata
  18732. * @listens Tech#loadedmetadata
  18733. */
  18734. 'loadedmetadata',
  18735. /**
  18736. * Fires when the browser has loaded the current frame of the audio/video.
  18737. *
  18738. * @event Player#loadeddata
  18739. * @type {event}
  18740. */
  18741. /**
  18742. * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
  18743. *
  18744. * @private
  18745. * @method Player#handleTechLoaddeddata_
  18746. * @fires Player#loadeddata
  18747. * @listens Tech#loadeddata
  18748. */
  18749. 'loadeddata',
  18750. /**
  18751. * Fires when the current playback position has changed.
  18752. *
  18753. * @event Player#timeupdate
  18754. * @type {event}
  18755. */
  18756. /**
  18757. * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
  18758. *
  18759. * @private
  18760. * @method Player#handleTechTimeUpdate_
  18761. * @fires Player#timeupdate
  18762. * @listens Tech#timeupdate
  18763. */
  18764. 'timeupdate',
  18765. /**
  18766. * Fires when the video's intrinsic dimensions change
  18767. *
  18768. * @event Player#resize
  18769. * @type {event}
  18770. */
  18771. /**
  18772. * Retrigger the `resize` event that was triggered by the {@link Tech}.
  18773. *
  18774. * @private
  18775. * @method Player#handleTechResize_
  18776. * @fires Player#resize
  18777. * @listens Tech#resize
  18778. */
  18779. 'resize',
  18780. /**
  18781. * Fires when the volume has been changed
  18782. *
  18783. * @event Player#volumechange
  18784. * @type {event}
  18785. */
  18786. /**
  18787. * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
  18788. *
  18789. * @private
  18790. * @method Player#handleTechVolumechange_
  18791. * @fires Player#volumechange
  18792. * @listens Tech#volumechange
  18793. */
  18794. 'volumechange',
  18795. /**
  18796. * Fires when the text track has been changed
  18797. *
  18798. * @event Player#texttrackchange
  18799. * @type {event}
  18800. */
  18801. /**
  18802. * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
  18803. *
  18804. * @private
  18805. * @method Player#handleTechTexttrackchange_
  18806. * @fires Player#texttrackchange
  18807. * @listens Tech#texttrackchange
  18808. */
  18809. 'texttrackchange'];
  18810. // events to queue when playback rate is zero
  18811. // this is a hash for the sole purpose of mapping non-camel-cased event names
  18812. // to camel-cased function names
  18813. const TECH_EVENTS_QUEUE = {
  18814. canplay: 'CanPlay',
  18815. canplaythrough: 'CanPlayThrough',
  18816. playing: 'Playing',
  18817. seeked: 'Seeked'
  18818. };
  18819. const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
  18820. const BREAKPOINT_CLASSES = {};
  18821. // grep: vjs-layout-tiny
  18822. // grep: vjs-layout-x-small
  18823. // grep: vjs-layout-small
  18824. // grep: vjs-layout-medium
  18825. // grep: vjs-layout-large
  18826. // grep: vjs-layout-x-large
  18827. // grep: vjs-layout-huge
  18828. BREAKPOINT_ORDER.forEach(k => {
  18829. const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
  18830. BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
  18831. });
  18832. const DEFAULT_BREAKPOINTS = {
  18833. tiny: 210,
  18834. xsmall: 320,
  18835. small: 425,
  18836. medium: 768,
  18837. large: 1440,
  18838. xlarge: 2560,
  18839. huge: Infinity
  18840. };
  18841. /**
  18842. * An instance of the `Player` class is created when any of the Video.js setup methods
  18843. * are used to initialize a video.
  18844. *
  18845. * After an instance has been created it can be accessed globally in three ways:
  18846. * 1. By calling `videojs.getPlayer('example_video_1');`
  18847. * 2. By calling `videojs('example_video_1');` (not recommended)
  18848. * 2. By using it directly via `videojs.players.example_video_1;`
  18849. *
  18850. * @extends Component
  18851. * @global
  18852. */
  18853. class Player extends Component {
  18854. /**
  18855. * Create an instance of this class.
  18856. *
  18857. * @param {Element} tag
  18858. * The original video DOM element used for configuring options.
  18859. *
  18860. * @param {Object} [options]
  18861. * Object of option names and values.
  18862. *
  18863. * @param {Function} [ready]
  18864. * Ready callback function.
  18865. */
  18866. constructor(tag, options, ready) {
  18867. // Make sure tag ID exists
  18868. // also here.. probably better
  18869. tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
  18870. // Set Options
  18871. // The options argument overrides options set in the video tag
  18872. // which overrides globally set options.
  18873. // This latter part coincides with the load order
  18874. // (tag must exist before Player)
  18875. options = Object.assign(Player.getTagSettings(tag), options);
  18876. // Delay the initialization of children because we need to set up
  18877. // player properties first, and can't use `this` before `super()`
  18878. options.initChildren = false;
  18879. // Same with creating the element
  18880. options.createEl = false;
  18881. // don't auto mixin the evented mixin
  18882. options.evented = false;
  18883. // we don't want the player to report touch activity on itself
  18884. // see enableTouchActivity in Component
  18885. options.reportTouchActivity = false;
  18886. // If language is not set, get the closest lang attribute
  18887. if (!options.language) {
  18888. const closest = tag.closest('[lang]');
  18889. if (closest) {
  18890. options.language = closest.getAttribute('lang');
  18891. }
  18892. }
  18893. // Run base component initializing with new options
  18894. super(null, options, ready);
  18895. // Create bound methods for document listeners.
  18896. this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
  18897. this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
  18898. this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
  18899. this.boundApplyInitTime_ = e => this.applyInitTime_(e);
  18900. this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
  18901. this.boundHandleTechClick_ = e => this.handleTechClick_(e);
  18902. this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
  18903. this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
  18904. this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
  18905. this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
  18906. this.boundHandleTechTap_ = e => this.handleTechTap_(e);
  18907. // default isFullscreen_ to false
  18908. this.isFullscreen_ = false;
  18909. // create logger
  18910. this.log = createLogger(this.id_);
  18911. // Hold our own reference to fullscreen api so it can be mocked in tests
  18912. this.fsApi_ = FullscreenApi;
  18913. // Tracks when a tech changes the poster
  18914. this.isPosterFromTech_ = false;
  18915. // Holds callback info that gets queued when playback rate is zero
  18916. // and a seek is happening
  18917. this.queuedCallbacks_ = [];
  18918. // Turn off API access because we're loading a new tech that might load asynchronously
  18919. this.isReady_ = false;
  18920. // Init state hasStarted_
  18921. this.hasStarted_ = false;
  18922. // Init state userActive_
  18923. this.userActive_ = false;
  18924. // Init debugEnabled_
  18925. this.debugEnabled_ = false;
  18926. // Init state audioOnlyMode_
  18927. this.audioOnlyMode_ = false;
  18928. // Init state audioPosterMode_
  18929. this.audioPosterMode_ = false;
  18930. // Init state audioOnlyCache_
  18931. this.audioOnlyCache_ = {
  18932. playerHeight: null,
  18933. hiddenChildren: []
  18934. };
  18935. // if the global option object was accidentally blown away by
  18936. // someone, bail early with an informative error
  18937. if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
  18938. throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
  18939. }
  18940. // Store the original tag used to set options
  18941. this.tag = tag;
  18942. // Store the tag attributes used to restore html5 element
  18943. this.tagAttributes = tag && getAttributes(tag);
  18944. // Update current language
  18945. this.language(this.options_.language);
  18946. // Update Supported Languages
  18947. if (options.languages) {
  18948. // Normalise player option languages to lowercase
  18949. const languagesToLower = {};
  18950. Object.getOwnPropertyNames(options.languages).forEach(function (name) {
  18951. languagesToLower[name.toLowerCase()] = options.languages[name];
  18952. });
  18953. this.languages_ = languagesToLower;
  18954. } else {
  18955. this.languages_ = Player.prototype.options_.languages;
  18956. }
  18957. this.resetCache_();
  18958. // Set poster
  18959. /** @type string */
  18960. this.poster_ = options.poster || '';
  18961. // Set controls
  18962. /** @type {boolean} */
  18963. this.controls_ = !!options.controls;
  18964. // Original tag settings stored in options
  18965. // now remove immediately so native controls don't flash.
  18966. // May be turned back on by HTML5 tech if nativeControlsForTouch is true
  18967. tag.controls = false;
  18968. tag.removeAttribute('controls');
  18969. this.changingSrc_ = false;
  18970. this.playCallbacks_ = [];
  18971. this.playTerminatedQueue_ = [];
  18972. // the attribute overrides the option
  18973. if (tag.hasAttribute('autoplay')) {
  18974. this.autoplay(true);
  18975. } else {
  18976. // otherwise use the setter to validate and
  18977. // set the correct value.
  18978. this.autoplay(this.options_.autoplay);
  18979. }
  18980. // check plugins
  18981. if (options.plugins) {
  18982. Object.keys(options.plugins).forEach(name => {
  18983. if (typeof this[name] !== 'function') {
  18984. throw new Error(`plugin "${name}" does not exist`);
  18985. }
  18986. });
  18987. }
  18988. /*
  18989. * Store the internal state of scrubbing
  18990. *
  18991. * @private
  18992. * @return {Boolean} True if the user is scrubbing
  18993. */
  18994. this.scrubbing_ = false;
  18995. this.el_ = this.createEl();
  18996. // Make this an evented object and use `el_` as its event bus.
  18997. evented(this, {
  18998. eventBusKey: 'el_'
  18999. });
  19000. // listen to document and player fullscreenchange handlers so we receive those events
  19001. // before a user can receive them so we can update isFullscreen appropriately.
  19002. // make sure that we listen to fullscreenchange events before everything else to make sure that
  19003. // our isFullscreen method is updated properly for internal components as well as external.
  19004. if (this.fsApi_.requestFullscreen) {
  19005. on(document__default["default"], this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
  19006. this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
  19007. }
  19008. if (this.fluid_) {
  19009. this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
  19010. }
  19011. // We also want to pass the original player options to each component and plugin
  19012. // as well so they don't need to reach back into the player for options later.
  19013. // We also need to do another copy of this.options_ so we don't end up with
  19014. // an infinite loop.
  19015. const playerOptionsCopy = merge(this.options_);
  19016. // Load plugins
  19017. if (options.plugins) {
  19018. Object.keys(options.plugins).forEach(name => {
  19019. this[name](options.plugins[name]);
  19020. });
  19021. }
  19022. // Enable debug mode to fire debugon event for all plugins.
  19023. if (options.debug) {
  19024. this.debug(true);
  19025. }
  19026. this.options_.playerOptions = playerOptionsCopy;
  19027. this.middleware_ = [];
  19028. this.playbackRates(options.playbackRates);
  19029. if (options.experimentalSvgIcons) {
  19030. // Add SVG Sprite to the DOM
  19031. const parser = new window__default["default"].DOMParser();
  19032. const parsedSVG = parser.parseFromString(icons, 'image/svg+xml');
  19033. const errorNode = parsedSVG.querySelector('parsererror');
  19034. if (errorNode) {
  19035. log.warn('Failed to load SVG Icons. Falling back to Font Icons.');
  19036. this.options_.experimentalSvgIcons = null;
  19037. } else {
  19038. const sprite = parsedSVG.documentElement;
  19039. sprite.style.display = 'none';
  19040. this.el_.appendChild(sprite);
  19041. this.addClass('vjs-svg-icons-enabled');
  19042. }
  19043. }
  19044. this.initChildren();
  19045. // Set isAudio based on whether or not an audio tag was used
  19046. this.isAudio(tag.nodeName.toLowerCase() === 'audio');
  19047. // Update controls className. Can't do this when the controls are initially
  19048. // set because the element doesn't exist yet.
  19049. if (this.controls()) {
  19050. this.addClass('vjs-controls-enabled');
  19051. } else {
  19052. this.addClass('vjs-controls-disabled');
  19053. }
  19054. // Set ARIA label and region role depending on player type
  19055. this.el_.setAttribute('role', 'region');
  19056. if (this.isAudio()) {
  19057. this.el_.setAttribute('aria-label', this.localize('Audio Player'));
  19058. } else {
  19059. this.el_.setAttribute('aria-label', this.localize('Video Player'));
  19060. }
  19061. if (this.isAudio()) {
  19062. this.addClass('vjs-audio');
  19063. }
  19064. // TODO: Make this smarter. Toggle user state between touching/mousing
  19065. // using events, since devices can have both touch and mouse events.
  19066. // TODO: Make this check be performed again when the window switches between monitors
  19067. // (See https://github.com/videojs/video.js/issues/5683)
  19068. if (TOUCH_ENABLED) {
  19069. this.addClass('vjs-touch-enabled');
  19070. }
  19071. // iOS Safari has broken hover handling
  19072. if (!IS_IOS) {
  19073. this.addClass('vjs-workinghover');
  19074. }
  19075. // Make player easily findable by ID
  19076. Player.players[this.id_] = this;
  19077. // Add a major version class to aid css in plugins
  19078. const majorVersion = version.split('.')[0];
  19079. this.addClass(`vjs-v${majorVersion}`);
  19080. // When the player is first initialized, trigger activity so components
  19081. // like the control bar show themselves if needed
  19082. this.userActive(true);
  19083. this.reportUserActivity();
  19084. this.one('play', e => this.listenForUserActivity_(e));
  19085. this.on('keydown', e => this.handleKeyDown(e));
  19086. this.on('languagechange', e => this.handleLanguagechange(e));
  19087. this.breakpoints(this.options_.breakpoints);
  19088. this.responsive(this.options_.responsive);
  19089. // Calling both the audio mode methods after the player is fully
  19090. // setup to be able to listen to the events triggered by them
  19091. this.on('ready', () => {
  19092. // Calling the audioPosterMode method first so that
  19093. // the audioOnlyMode can take precedence when both options are set to true
  19094. this.audioPosterMode(this.options_.audioPosterMode);
  19095. this.audioOnlyMode(this.options_.audioOnlyMode);
  19096. });
  19097. }
  19098. /**
  19099. * Destroys the video player and does any necessary cleanup.
  19100. *
  19101. * This is especially helpful if you are dynamically adding and removing videos
  19102. * to/from the DOM.
  19103. *
  19104. * @fires Player#dispose
  19105. */
  19106. dispose() {
  19107. /**
  19108. * Called when the player is being disposed of.
  19109. *
  19110. * @event Player#dispose
  19111. * @type {Event}
  19112. */
  19113. this.trigger('dispose');
  19114. // prevent dispose from being called twice
  19115. this.off('dispose');
  19116. // Make sure all player-specific document listeners are unbound. This is
  19117. off(document__default["default"], this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
  19118. off(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
  19119. if (this.styleEl_ && this.styleEl_.parentNode) {
  19120. this.styleEl_.parentNode.removeChild(this.styleEl_);
  19121. this.styleEl_ = null;
  19122. }
  19123. // Kill reference to this player
  19124. Player.players[this.id_] = null;
  19125. if (this.tag && this.tag.player) {
  19126. this.tag.player = null;
  19127. }
  19128. if (this.el_ && this.el_.player) {
  19129. this.el_.player = null;
  19130. }
  19131. if (this.tech_) {
  19132. this.tech_.dispose();
  19133. this.isPosterFromTech_ = false;
  19134. this.poster_ = '';
  19135. }
  19136. if (this.playerElIngest_) {
  19137. this.playerElIngest_ = null;
  19138. }
  19139. if (this.tag) {
  19140. this.tag = null;
  19141. }
  19142. clearCacheForPlayer(this);
  19143. // remove all event handlers for track lists
  19144. // all tracks and track listeners are removed on
  19145. // tech dispose
  19146. ALL.names.forEach(name => {
  19147. const props = ALL[name];
  19148. const list = this[props.getterName]();
  19149. // if it is not a native list
  19150. // we have to manually remove event listeners
  19151. if (list && list.off) {
  19152. list.off();
  19153. }
  19154. });
  19155. // the actual .el_ is removed here, or replaced if
  19156. super.dispose({
  19157. restoreEl: this.options_.restoreEl
  19158. });
  19159. }
  19160. /**
  19161. * Create the `Player`'s DOM element.
  19162. *
  19163. * @return {Element}
  19164. * The DOM element that gets created.
  19165. */
  19166. createEl() {
  19167. let tag = this.tag;
  19168. let el;
  19169. let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
  19170. const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
  19171. if (playerElIngest) {
  19172. el = this.el_ = tag.parentNode;
  19173. } else if (!divEmbed) {
  19174. el = this.el_ = super.createEl('div');
  19175. }
  19176. // Copy over all the attributes from the tag, including ID and class
  19177. // ID will now reference player box, not the video tag
  19178. const attrs = getAttributes(tag);
  19179. if (divEmbed) {
  19180. el = this.el_ = tag;
  19181. tag = this.tag = document__default["default"].createElement('video');
  19182. while (el.children.length) {
  19183. tag.appendChild(el.firstChild);
  19184. }
  19185. if (!hasClass(el, 'video-js')) {
  19186. addClass(el, 'video-js');
  19187. }
  19188. el.appendChild(tag);
  19189. playerElIngest = this.playerElIngest_ = el;
  19190. // move properties over from our custom `video-js` element
  19191. // to our new `video` element. This will move things like
  19192. // `src` or `controls` that were set via js before the player
  19193. // was initialized.
  19194. Object.keys(el).forEach(k => {
  19195. try {
  19196. tag[k] = el[k];
  19197. } catch (e) {
  19198. // we got a a property like outerHTML which we can't actually copy, ignore it
  19199. }
  19200. });
  19201. }
  19202. // set tabindex to -1 to remove the video element from the focus order
  19203. tag.setAttribute('tabindex', '-1');
  19204. attrs.tabindex = '-1';
  19205. // Workaround for #4583 on Chrome (on Windows) with JAWS.
  19206. // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
  19207. // Note that we can't detect if JAWS is being used, but this ARIA attribute
  19208. // doesn't change behavior of Chrome if JAWS is not being used
  19209. if (IS_CHROME && IS_WINDOWS) {
  19210. tag.setAttribute('role', 'application');
  19211. attrs.role = 'application';
  19212. }
  19213. // Remove width/height attrs from tag so CSS can make it 100% width/height
  19214. tag.removeAttribute('width');
  19215. tag.removeAttribute('height');
  19216. if ('width' in attrs) {
  19217. delete attrs.width;
  19218. }
  19219. if ('height' in attrs) {
  19220. delete attrs.height;
  19221. }
  19222. Object.getOwnPropertyNames(attrs).forEach(function (attr) {
  19223. // don't copy over the class attribute to the player element when we're in a div embed
  19224. // the class is already set up properly in the divEmbed case
  19225. // and we want to make sure that the `video-js` class doesn't get lost
  19226. if (!(divEmbed && attr === 'class')) {
  19227. el.setAttribute(attr, attrs[attr]);
  19228. }
  19229. if (divEmbed) {
  19230. tag.setAttribute(attr, attrs[attr]);
  19231. }
  19232. });
  19233. // Update tag id/class for use as HTML5 playback tech
  19234. // Might think we should do this after embedding in container so .vjs-tech class
  19235. // doesn't flash 100% width/height, but class only applies with .video-js parent
  19236. tag.playerId = tag.id;
  19237. tag.id += '_html5_api';
  19238. tag.className = 'vjs-tech';
  19239. // Make player findable on elements
  19240. tag.player = el.player = this;
  19241. // Default state of video is paused
  19242. this.addClass('vjs-paused');
  19243. // Add a style element in the player that we'll use to set the width/height
  19244. // of the player in a way that's still overridable by CSS, just like the
  19245. // video element
  19246. if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE !== true) {
  19247. this.styleEl_ = createStyleElement('vjs-styles-dimensions');
  19248. const defaultsStyleEl = $('.vjs-styles-defaults');
  19249. const head = $('head');
  19250. head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
  19251. }
  19252. this.fill_ = false;
  19253. this.fluid_ = false;
  19254. // Pass in the width/height/aspectRatio options which will update the style el
  19255. this.width(this.options_.width);
  19256. this.height(this.options_.height);
  19257. this.fill(this.options_.fill);
  19258. this.fluid(this.options_.fluid);
  19259. this.aspectRatio(this.options_.aspectRatio);
  19260. // support both crossOrigin and crossorigin to reduce confusion and issues around the name
  19261. this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
  19262. // Hide any links within the video/audio tag,
  19263. // because IE doesn't hide them completely from screen readers.
  19264. const links = tag.getElementsByTagName('a');
  19265. for (let i = 0; i < links.length; i++) {
  19266. const linkEl = links.item(i);
  19267. addClass(linkEl, 'vjs-hidden');
  19268. linkEl.setAttribute('hidden', 'hidden');
  19269. }
  19270. // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
  19271. // keep track of the original for later so we can know if the source originally failed
  19272. tag.initNetworkState_ = tag.networkState;
  19273. // Wrap video tag in div (el/box) container
  19274. if (tag.parentNode && !playerElIngest) {
  19275. tag.parentNode.insertBefore(el, tag);
  19276. }
  19277. // insert the tag as the first child of the player element
  19278. // then manually add it to the children array so that this.addChild
  19279. // will work properly for other components
  19280. //
  19281. // Breaks iPhone, fixed in HTML5 setup.
  19282. prependTo(tag, el);
  19283. this.children_.unshift(tag);
  19284. // Set lang attr on player to ensure CSS :lang() in consistent with player
  19285. // if it's been set to something different to the doc
  19286. this.el_.setAttribute('lang', this.language_);
  19287. this.el_.setAttribute('translate', 'no');
  19288. this.el_ = el;
  19289. return el;
  19290. }
  19291. /**
  19292. * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
  19293. * sets the `crossOrigin` property on the `<video>` tag to control the CORS
  19294. * behavior.
  19295. *
  19296. * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
  19297. *
  19298. * @param {string|null} [value]
  19299. * The value to set the `Player`'s crossOrigin to. If an argument is
  19300. * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
  19301. *
  19302. * @return {string|null|undefined}
  19303. * - The current crossOrigin value of the `Player` when getting.
  19304. * - undefined when setting
  19305. */
  19306. crossOrigin(value) {
  19307. // `null` can be set to unset a value
  19308. if (typeof value === 'undefined') {
  19309. return this.techGet_('crossOrigin');
  19310. }
  19311. if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
  19312. log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
  19313. return;
  19314. }
  19315. this.techCall_('setCrossOrigin', value);
  19316. if (this.posterImage) {
  19317. this.posterImage.crossOrigin(value);
  19318. }
  19319. return;
  19320. }
  19321. /**
  19322. * A getter/setter for the `Player`'s width. Returns the player's configured value.
  19323. * To get the current width use `currentWidth()`.
  19324. *
  19325. * @param {number|string} [value]
  19326. * CSS value to set the `Player`'s width to.
  19327. *
  19328. * @return {number|undefined}
  19329. * - The current width of the `Player` when getting.
  19330. * - Nothing when setting
  19331. */
  19332. width(value) {
  19333. return this.dimension('width', value);
  19334. }
  19335. /**
  19336. * A getter/setter for the `Player`'s height. Returns the player's configured value.
  19337. * To get the current height use `currentheight()`.
  19338. *
  19339. * @param {number|string} [value]
  19340. * CSS value to set the `Player`'s height to.
  19341. *
  19342. * @return {number|undefined}
  19343. * - The current height of the `Player` when getting.
  19344. * - Nothing when setting
  19345. */
  19346. height(value) {
  19347. return this.dimension('height', value);
  19348. }
  19349. /**
  19350. * A getter/setter for the `Player`'s width & height.
  19351. *
  19352. * @param {string} dimension
  19353. * This string can be:
  19354. * - 'width'
  19355. * - 'height'
  19356. *
  19357. * @param {number|string} [value]
  19358. * Value for dimension specified in the first argument.
  19359. *
  19360. * @return {number}
  19361. * The dimension arguments value when getting (width/height).
  19362. */
  19363. dimension(dimension, value) {
  19364. const privDimension = dimension + '_';
  19365. if (value === undefined) {
  19366. return this[privDimension] || 0;
  19367. }
  19368. if (value === '' || value === 'auto') {
  19369. // If an empty string is given, reset the dimension to be automatic
  19370. this[privDimension] = undefined;
  19371. this.updateStyleEl_();
  19372. return;
  19373. }
  19374. const parsedVal = parseFloat(value);
  19375. if (isNaN(parsedVal)) {
  19376. log.error(`Improper value "${value}" supplied for for ${dimension}`);
  19377. return;
  19378. }
  19379. this[privDimension] = parsedVal;
  19380. this.updateStyleEl_();
  19381. }
  19382. /**
  19383. * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
  19384. *
  19385. * Turning this on will turn off fill mode.
  19386. *
  19387. * @param {boolean} [bool]
  19388. * - A value of true adds the class.
  19389. * - A value of false removes the class.
  19390. * - No value will be a getter.
  19391. *
  19392. * @return {boolean|undefined}
  19393. * - The value of fluid when getting.
  19394. * - `undefined` when setting.
  19395. */
  19396. fluid(bool) {
  19397. if (bool === undefined) {
  19398. return !!this.fluid_;
  19399. }
  19400. this.fluid_ = !!bool;
  19401. if (isEvented(this)) {
  19402. this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
  19403. }
  19404. if (bool) {
  19405. this.addClass('vjs-fluid');
  19406. this.fill(false);
  19407. addEventedCallback(this, () => {
  19408. this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
  19409. });
  19410. } else {
  19411. this.removeClass('vjs-fluid');
  19412. }
  19413. this.updateStyleEl_();
  19414. }
  19415. /**
  19416. * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
  19417. *
  19418. * Turning this on will turn off fluid mode.
  19419. *
  19420. * @param {boolean} [bool]
  19421. * - A value of true adds the class.
  19422. * - A value of false removes the class.
  19423. * - No value will be a getter.
  19424. *
  19425. * @return {boolean|undefined}
  19426. * - The value of fluid when getting.
  19427. * - `undefined` when setting.
  19428. */
  19429. fill(bool) {
  19430. if (bool === undefined) {
  19431. return !!this.fill_;
  19432. }
  19433. this.fill_ = !!bool;
  19434. if (bool) {
  19435. this.addClass('vjs-fill');
  19436. this.fluid(false);
  19437. } else {
  19438. this.removeClass('vjs-fill');
  19439. }
  19440. }
  19441. /**
  19442. * Get/Set the aspect ratio
  19443. *
  19444. * @param {string} [ratio]
  19445. * Aspect ratio for player
  19446. *
  19447. * @return {string|undefined}
  19448. * returns the current aspect ratio when getting
  19449. */
  19450. /**
  19451. * A getter/setter for the `Player`'s aspect ratio.
  19452. *
  19453. * @param {string} [ratio]
  19454. * The value to set the `Player`'s aspect ratio to.
  19455. *
  19456. * @return {string|undefined}
  19457. * - The current aspect ratio of the `Player` when getting.
  19458. * - undefined when setting
  19459. */
  19460. aspectRatio(ratio) {
  19461. if (ratio === undefined) {
  19462. return this.aspectRatio_;
  19463. }
  19464. // Check for width:height format
  19465. if (!/^\d+\:\d+$/.test(ratio)) {
  19466. throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
  19467. }
  19468. this.aspectRatio_ = ratio;
  19469. // We're assuming if you set an aspect ratio you want fluid mode,
  19470. // because in fixed mode you could calculate width and height yourself.
  19471. this.fluid(true);
  19472. this.updateStyleEl_();
  19473. }
  19474. /**
  19475. * Update styles of the `Player` element (height, width and aspect ratio).
  19476. *
  19477. * @private
  19478. * @listens Tech#loadedmetadata
  19479. */
  19480. updateStyleEl_() {
  19481. if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE === true) {
  19482. const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
  19483. const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
  19484. const techEl = this.tech_ && this.tech_.el();
  19485. if (techEl) {
  19486. if (width >= 0) {
  19487. techEl.width = width;
  19488. }
  19489. if (height >= 0) {
  19490. techEl.height = height;
  19491. }
  19492. }
  19493. return;
  19494. }
  19495. let width;
  19496. let height;
  19497. let aspectRatio;
  19498. let idClass;
  19499. // The aspect ratio is either used directly or to calculate width and height.
  19500. if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
  19501. // Use any aspectRatio that's been specifically set
  19502. aspectRatio = this.aspectRatio_;
  19503. } else if (this.videoWidth() > 0) {
  19504. // Otherwise try to get the aspect ratio from the video metadata
  19505. aspectRatio = this.videoWidth() + ':' + this.videoHeight();
  19506. } else {
  19507. // Or use a default. The video element's is 2:1, but 16:9 is more common.
  19508. aspectRatio = '16:9';
  19509. }
  19510. // Get the ratio as a decimal we can use to calculate dimensions
  19511. const ratioParts = aspectRatio.split(':');
  19512. const ratioMultiplier = ratioParts[1] / ratioParts[0];
  19513. if (this.width_ !== undefined) {
  19514. // Use any width that's been specifically set
  19515. width = this.width_;
  19516. } else if (this.height_ !== undefined) {
  19517. // Or calculate the width from the aspect ratio if a height has been set
  19518. width = this.height_ / ratioMultiplier;
  19519. } else {
  19520. // Or use the video's metadata, or use the video el's default of 300
  19521. width = this.videoWidth() || 300;
  19522. }
  19523. if (this.height_ !== undefined) {
  19524. // Use any height that's been specifically set
  19525. height = this.height_;
  19526. } else {
  19527. // Otherwise calculate the height from the ratio and the width
  19528. height = width * ratioMultiplier;
  19529. }
  19530. // Ensure the CSS class is valid by starting with an alpha character
  19531. if (/^[^a-zA-Z]/.test(this.id())) {
  19532. idClass = 'dimensions-' + this.id();
  19533. } else {
  19534. idClass = this.id() + '-dimensions';
  19535. }
  19536. // Ensure the right class is still on the player for the style element
  19537. this.addClass(idClass);
  19538. setTextContent(this.styleEl_, `
  19539. .${idClass} {
  19540. width: ${width}px;
  19541. height: ${height}px;
  19542. }
  19543. .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
  19544. padding-top: ${ratioMultiplier * 100}%;
  19545. }
  19546. `);
  19547. }
  19548. /**
  19549. * Load/Create an instance of playback {@link Tech} including element
  19550. * and API methods. Then append the `Tech` element in `Player` as a child.
  19551. *
  19552. * @param {string} techName
  19553. * name of the playback technology
  19554. *
  19555. * @param {string} source
  19556. * video source
  19557. *
  19558. * @private
  19559. */
  19560. loadTech_(techName, source) {
  19561. // Pause and remove current playback technology
  19562. if (this.tech_) {
  19563. this.unloadTech_();
  19564. }
  19565. const titleTechName = toTitleCase(techName);
  19566. const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
  19567. // get rid of the HTML5 video tag as soon as we are using another tech
  19568. if (titleTechName !== 'Html5' && this.tag) {
  19569. Tech.getTech('Html5').disposeMediaElement(this.tag);
  19570. this.tag.player = null;
  19571. this.tag = null;
  19572. }
  19573. this.techName_ = titleTechName;
  19574. // Turn off API access because we're loading a new tech that might load asynchronously
  19575. this.isReady_ = false;
  19576. let autoplay = this.autoplay();
  19577. // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
  19578. // because the player is going to handle autoplay on `loadstart`
  19579. if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
  19580. autoplay = false;
  19581. }
  19582. // Grab tech-specific options from player options and add source and parent element to use.
  19583. const techOptions = {
  19584. source,
  19585. autoplay,
  19586. 'nativeControlsForTouch': this.options_.nativeControlsForTouch,
  19587. 'playerId': this.id(),
  19588. 'techId': `${this.id()}_${camelTechName}_api`,
  19589. 'playsinline': this.options_.playsinline,
  19590. 'preload': this.options_.preload,
  19591. 'loop': this.options_.loop,
  19592. 'disablePictureInPicture': this.options_.disablePictureInPicture,
  19593. 'muted': this.options_.muted,
  19594. 'poster': this.poster(),
  19595. 'language': this.language(),
  19596. 'playerElIngest': this.playerElIngest_ || false,
  19597. 'vtt.js': this.options_['vtt.js'],
  19598. 'canOverridePoster': !!this.options_.techCanOverridePoster,
  19599. 'enableSourceset': this.options_.enableSourceset
  19600. };
  19601. ALL.names.forEach(name => {
  19602. const props = ALL[name];
  19603. techOptions[props.getterName] = this[props.privateName];
  19604. });
  19605. Object.assign(techOptions, this.options_[titleTechName]);
  19606. Object.assign(techOptions, this.options_[camelTechName]);
  19607. Object.assign(techOptions, this.options_[techName.toLowerCase()]);
  19608. if (this.tag) {
  19609. techOptions.tag = this.tag;
  19610. }
  19611. if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
  19612. techOptions.startTime = this.cache_.currentTime;
  19613. }
  19614. // Initialize tech instance
  19615. const TechClass = Tech.getTech(techName);
  19616. if (!TechClass) {
  19617. throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
  19618. }
  19619. this.tech_ = new TechClass(techOptions);
  19620. // player.triggerReady is always async, so don't need this to be async
  19621. this.tech_.ready(bind_(this, this.handleTechReady_), true);
  19622. textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
  19623. // Listen to all HTML5-defined events and trigger them on the player
  19624. TECH_EVENTS_RETRIGGER.forEach(event => {
  19625. this.on(this.tech_, event, e => this[`handleTech${toTitleCase(event)}_`](e));
  19626. });
  19627. Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
  19628. this.on(this.tech_, event, eventObj => {
  19629. if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
  19630. this.queuedCallbacks_.push({
  19631. callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
  19632. event: eventObj
  19633. });
  19634. return;
  19635. }
  19636. this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
  19637. });
  19638. });
  19639. this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
  19640. this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
  19641. this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
  19642. this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
  19643. this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
  19644. this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
  19645. this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
  19646. this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
  19647. this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
  19648. this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
  19649. this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
  19650. this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
  19651. this.on(this.tech_, 'error', e => this.handleTechError_(e));
  19652. this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
  19653. this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
  19654. this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
  19655. this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
  19656. this.usingNativeControls(this.techGet_('controls'));
  19657. if (this.controls() && !this.usingNativeControls()) {
  19658. this.addTechControlsListeners_();
  19659. }
  19660. // Add the tech element in the DOM if it was not already there
  19661. // Make sure to not insert the original video element if using Html5
  19662. if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
  19663. prependTo(this.tech_.el(), this.el());
  19664. }
  19665. // Get rid of the original video tag reference after the first tech is loaded
  19666. if (this.tag) {
  19667. this.tag.player = null;
  19668. this.tag = null;
  19669. }
  19670. }
  19671. /**
  19672. * Unload and dispose of the current playback {@link Tech}.
  19673. *
  19674. * @private
  19675. */
  19676. unloadTech_() {
  19677. // Save the current text tracks so that we can reuse the same text tracks with the next tech
  19678. ALL.names.forEach(name => {
  19679. const props = ALL[name];
  19680. this[props.privateName] = this[props.getterName]();
  19681. });
  19682. this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
  19683. this.isReady_ = false;
  19684. this.tech_.dispose();
  19685. this.tech_ = false;
  19686. if (this.isPosterFromTech_) {
  19687. this.poster_ = '';
  19688. this.trigger('posterchange');
  19689. }
  19690. this.isPosterFromTech_ = false;
  19691. }
  19692. /**
  19693. * Return a reference to the current {@link Tech}.
  19694. * It will print a warning by default about the danger of using the tech directly
  19695. * but any argument that is passed in will silence the warning.
  19696. *
  19697. * @param {*} [safety]
  19698. * Anything passed in to silence the warning
  19699. *
  19700. * @return {Tech}
  19701. * The Tech
  19702. */
  19703. tech(safety) {
  19704. if (safety === undefined) {
  19705. 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');
  19706. }
  19707. return this.tech_;
  19708. }
  19709. /**
  19710. * An object that contains Video.js version.
  19711. *
  19712. * @typedef {Object} PlayerVersion
  19713. *
  19714. * @property {string} 'video.js' - Video.js version
  19715. */
  19716. /**
  19717. * Returns an object with Video.js version.
  19718. *
  19719. * @return {PlayerVersion}
  19720. * An object with Video.js version.
  19721. */
  19722. version() {
  19723. return {
  19724. 'video.js': version
  19725. };
  19726. }
  19727. /**
  19728. * Set up click and touch listeners for the playback element
  19729. *
  19730. * - On desktops: a click on the video itself will toggle playback
  19731. * - On mobile devices: a click on the video toggles controls
  19732. * which is done by toggling the user state between active and
  19733. * inactive
  19734. * - A tap can signal that a user has become active or has become inactive
  19735. * e.g. a quick tap on an iPhone movie should reveal the controls. Another
  19736. * quick tap should hide them again (signaling the user is in an inactive
  19737. * viewing state)
  19738. * - In addition to this, we still want the user to be considered inactive after
  19739. * a few seconds of inactivity.
  19740. *
  19741. * > Note: the only part of iOS interaction we can't mimic with this setup
  19742. * is a touch and hold on the video element counting as activity in order to
  19743. * keep the controls showing, but that shouldn't be an issue. A touch and hold
  19744. * on any controls will still keep the user active
  19745. *
  19746. * @private
  19747. */
  19748. addTechControlsListeners_() {
  19749. // Make sure to remove all the previous listeners in case we are called multiple times.
  19750. this.removeTechControlsListeners_();
  19751. this.on(this.tech_, 'click', this.boundHandleTechClick_);
  19752. this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  19753. // If the controls were hidden we don't want that to change without a tap event
  19754. // so we'll check if the controls were already showing before reporting user
  19755. // activity
  19756. this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
  19757. this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
  19758. this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
  19759. // The tap listener needs to come after the touchend listener because the tap
  19760. // listener cancels out any reportedUserActivity when setting userActive(false)
  19761. this.on(this.tech_, 'tap', this.boundHandleTechTap_);
  19762. }
  19763. /**
  19764. * Remove the listeners used for click and tap controls. This is needed for
  19765. * toggling to controls disabled, where a tap/touch should do nothing.
  19766. *
  19767. * @private
  19768. */
  19769. removeTechControlsListeners_() {
  19770. // We don't want to just use `this.off()` because there might be other needed
  19771. // listeners added by techs that extend this.
  19772. this.off(this.tech_, 'tap', this.boundHandleTechTap_);
  19773. this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
  19774. this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
  19775. this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
  19776. this.off(this.tech_, 'click', this.boundHandleTechClick_);
  19777. this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  19778. }
  19779. /**
  19780. * Player waits for the tech to be ready
  19781. *
  19782. * @private
  19783. */
  19784. handleTechReady_() {
  19785. this.triggerReady();
  19786. // Keep the same volume as before
  19787. if (this.cache_.volume) {
  19788. this.techCall_('setVolume', this.cache_.volume);
  19789. }
  19790. // Look if the tech found a higher resolution poster while loading
  19791. this.handleTechPosterChange_();
  19792. // Update the duration if available
  19793. this.handleTechDurationChange_();
  19794. }
  19795. /**
  19796. * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
  19797. *
  19798. * @fires Player#loadstart
  19799. * @listens Tech#loadstart
  19800. * @private
  19801. */
  19802. handleTechLoadStart_() {
  19803. // TODO: Update to use `emptied` event instead. See #1277.
  19804. this.removeClass('vjs-ended', 'vjs-seeking');
  19805. // reset the error state
  19806. this.error(null);
  19807. // Update the duration
  19808. this.handleTechDurationChange_();
  19809. if (!this.paused()) {
  19810. /**
  19811. * Fired when the user agent begins looking for media data
  19812. *
  19813. * @event Player#loadstart
  19814. * @type {Event}
  19815. */
  19816. this.trigger('loadstart');
  19817. } else {
  19818. // reset the hasStarted state
  19819. this.hasStarted(false);
  19820. this.trigger('loadstart');
  19821. }
  19822. // autoplay happens after loadstart for the browser,
  19823. // so we mimic that behavior
  19824. this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
  19825. }
  19826. /**
  19827. * Handle autoplay string values, rather than the typical boolean
  19828. * values that should be handled by the tech. Note that this is not
  19829. * part of any specification. Valid values and what they do can be
  19830. * found on the autoplay getter at Player#autoplay()
  19831. */
  19832. manualAutoplay_(type) {
  19833. if (!this.tech_ || typeof type !== 'string') {
  19834. return;
  19835. }
  19836. // Save original muted() value, set muted to true, and attempt to play().
  19837. // On promise rejection, restore muted from saved value
  19838. const resolveMuted = () => {
  19839. const previouslyMuted = this.muted();
  19840. this.muted(true);
  19841. const restoreMuted = () => {
  19842. this.muted(previouslyMuted);
  19843. };
  19844. // restore muted on play terminatation
  19845. this.playTerminatedQueue_.push(restoreMuted);
  19846. const mutedPromise = this.play();
  19847. if (!isPromise(mutedPromise)) {
  19848. return;
  19849. }
  19850. return mutedPromise.catch(err => {
  19851. restoreMuted();
  19852. throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
  19853. });
  19854. };
  19855. let promise;
  19856. // if muted defaults to true
  19857. // the only thing we can do is call play
  19858. if (type === 'any' && !this.muted()) {
  19859. promise = this.play();
  19860. if (isPromise(promise)) {
  19861. promise = promise.catch(resolveMuted);
  19862. }
  19863. } else if (type === 'muted' && !this.muted()) {
  19864. promise = resolveMuted();
  19865. } else {
  19866. promise = this.play();
  19867. }
  19868. if (!isPromise(promise)) {
  19869. return;
  19870. }
  19871. return promise.then(() => {
  19872. this.trigger({
  19873. type: 'autoplay-success',
  19874. autoplay: type
  19875. });
  19876. }).catch(() => {
  19877. this.trigger({
  19878. type: 'autoplay-failure',
  19879. autoplay: type
  19880. });
  19881. });
  19882. }
  19883. /**
  19884. * Update the internal source caches so that we return the correct source from
  19885. * `src()`, `currentSource()`, and `currentSources()`.
  19886. *
  19887. * > Note: `currentSources` will not be updated if the source that is passed in exists
  19888. * in the current `currentSources` cache.
  19889. *
  19890. *
  19891. * @param {Tech~SourceObject} srcObj
  19892. * A string or object source to update our caches to.
  19893. */
  19894. updateSourceCaches_(srcObj = '') {
  19895. let src = srcObj;
  19896. let type = '';
  19897. if (typeof src !== 'string') {
  19898. src = srcObj.src;
  19899. type = srcObj.type;
  19900. }
  19901. // make sure all the caches are set to default values
  19902. // to prevent null checking
  19903. this.cache_.source = this.cache_.source || {};
  19904. this.cache_.sources = this.cache_.sources || [];
  19905. // try to get the type of the src that was passed in
  19906. if (src && !type) {
  19907. type = findMimetype(this, src);
  19908. }
  19909. // update `currentSource` cache always
  19910. this.cache_.source = merge({}, srcObj, {
  19911. src,
  19912. type
  19913. });
  19914. const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
  19915. const sourceElSources = [];
  19916. const sourceEls = this.$$('source');
  19917. const matchingSourceEls = [];
  19918. for (let i = 0; i < sourceEls.length; i++) {
  19919. const sourceObj = getAttributes(sourceEls[i]);
  19920. sourceElSources.push(sourceObj);
  19921. if (sourceObj.src && sourceObj.src === src) {
  19922. matchingSourceEls.push(sourceObj.src);
  19923. }
  19924. }
  19925. // if we have matching source els but not matching sources
  19926. // the current source cache is not up to date
  19927. if (matchingSourceEls.length && !matchingSources.length) {
  19928. this.cache_.sources = sourceElSources;
  19929. // if we don't have matching source or source els set the
  19930. // sources cache to the `currentSource` cache
  19931. } else if (!matchingSources.length) {
  19932. this.cache_.sources = [this.cache_.source];
  19933. }
  19934. // update the tech `src` cache
  19935. this.cache_.src = src;
  19936. }
  19937. /**
  19938. * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
  19939. * causing the media element to reload.
  19940. *
  19941. * It will fire for the initial source and each subsequent source.
  19942. * This event is a custom event from Video.js and is triggered by the {@link Tech}.
  19943. *
  19944. * The event object for this event contains a `src` property that will contain the source
  19945. * that was available when the event was triggered. This is generally only necessary if Video.js
  19946. * is switching techs while the source was being changed.
  19947. *
  19948. * It is also fired when `load` is called on the player (or media element)
  19949. * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
  19950. * says that the resource selection algorithm needs to be aborted and restarted.
  19951. * In this case, it is very likely that the `src` property will be set to the
  19952. * empty string `""` to indicate we do not know what the source will be but
  19953. * that it is changing.
  19954. *
  19955. * *This event is currently still experimental and may change in minor releases.*
  19956. * __To use this, pass `enableSourceset` option to the player.__
  19957. *
  19958. * @event Player#sourceset
  19959. * @type {Event}
  19960. * @prop {string} src
  19961. * The source url available when the `sourceset` was triggered.
  19962. * It will be an empty string if we cannot know what the source is
  19963. * but know that the source will change.
  19964. */
  19965. /**
  19966. * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
  19967. *
  19968. * @fires Player#sourceset
  19969. * @listens Tech#sourceset
  19970. * @private
  19971. */
  19972. handleTechSourceset_(event) {
  19973. // only update the source cache when the source
  19974. // was not updated using the player api
  19975. if (!this.changingSrc_) {
  19976. let updateSourceCaches = src => this.updateSourceCaches_(src);
  19977. const playerSrc = this.currentSource().src;
  19978. const eventSrc = event.src;
  19979. // if we have a playerSrc that is not a blob, and a tech src that is a blob
  19980. if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
  19981. // if both the tech source and the player source were updated we assume
  19982. // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
  19983. if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
  19984. updateSourceCaches = () => {};
  19985. }
  19986. }
  19987. // update the source to the initial source right away
  19988. // in some cases this will be empty string
  19989. updateSourceCaches(eventSrc);
  19990. // if the `sourceset` `src` was an empty string
  19991. // wait for a `loadstart` to update the cache to `currentSrc`.
  19992. // If a sourceset happens before a `loadstart`, we reset the state
  19993. if (!event.src) {
  19994. this.tech_.any(['sourceset', 'loadstart'], e => {
  19995. // if a sourceset happens before a `loadstart` there
  19996. // is nothing to do as this `handleTechSourceset_`
  19997. // will be called again and this will be handled there.
  19998. if (e.type === 'sourceset') {
  19999. return;
  20000. }
  20001. const techSrc = this.techGet_('currentSrc');
  20002. this.lastSource_.tech = techSrc;
  20003. this.updateSourceCaches_(techSrc);
  20004. });
  20005. }
  20006. }
  20007. this.lastSource_ = {
  20008. player: this.currentSource().src,
  20009. tech: event.src
  20010. };
  20011. this.trigger({
  20012. src: event.src,
  20013. type: 'sourceset'
  20014. });
  20015. }
  20016. /**
  20017. * Add/remove the vjs-has-started class
  20018. *
  20019. *
  20020. * @param {boolean} request
  20021. * - true: adds the class
  20022. * - false: remove the class
  20023. *
  20024. * @return {boolean}
  20025. * the boolean value of hasStarted_
  20026. */
  20027. hasStarted(request) {
  20028. if (request === undefined) {
  20029. // act as getter, if we have no request to change
  20030. return this.hasStarted_;
  20031. }
  20032. if (request === this.hasStarted_) {
  20033. return;
  20034. }
  20035. this.hasStarted_ = request;
  20036. if (this.hasStarted_) {
  20037. this.addClass('vjs-has-started');
  20038. } else {
  20039. this.removeClass('vjs-has-started');
  20040. }
  20041. }
  20042. /**
  20043. * Fired whenever the media begins or resumes playback
  20044. *
  20045. * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
  20046. * @fires Player#play
  20047. * @listens Tech#play
  20048. * @private
  20049. */
  20050. handleTechPlay_() {
  20051. this.removeClass('vjs-ended', 'vjs-paused');
  20052. this.addClass('vjs-playing');
  20053. // hide the poster when the user hits play
  20054. this.hasStarted(true);
  20055. /**
  20056. * Triggered whenever an {@link Tech#play} event happens. Indicates that
  20057. * playback has started or resumed.
  20058. *
  20059. * @event Player#play
  20060. * @type {Event}
  20061. */
  20062. this.trigger('play');
  20063. }
  20064. /**
  20065. * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
  20066. *
  20067. * If there were any events queued while the playback rate was zero, fire
  20068. * those events now.
  20069. *
  20070. * @private
  20071. * @method Player#handleTechRateChange_
  20072. * @fires Player#ratechange
  20073. * @listens Tech#ratechange
  20074. */
  20075. handleTechRateChange_() {
  20076. if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
  20077. this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
  20078. this.queuedCallbacks_ = [];
  20079. }
  20080. this.cache_.lastPlaybackRate = this.tech_.playbackRate();
  20081. /**
  20082. * Fires when the playing speed of the audio/video is changed
  20083. *
  20084. * @event Player#ratechange
  20085. * @type {event}
  20086. */
  20087. this.trigger('ratechange');
  20088. }
  20089. /**
  20090. * Retrigger the `waiting` event that was triggered by the {@link Tech}.
  20091. *
  20092. * @fires Player#waiting
  20093. * @listens Tech#waiting
  20094. * @private
  20095. */
  20096. handleTechWaiting_() {
  20097. this.addClass('vjs-waiting');
  20098. /**
  20099. * A readyState change on the DOM element has caused playback to stop.
  20100. *
  20101. * @event Player#waiting
  20102. * @type {Event}
  20103. */
  20104. this.trigger('waiting');
  20105. // Browsers may emit a timeupdate event after a waiting event. In order to prevent
  20106. // premature removal of the waiting class, wait for the time to change.
  20107. const timeWhenWaiting = this.currentTime();
  20108. const timeUpdateListener = () => {
  20109. if (timeWhenWaiting !== this.currentTime()) {
  20110. this.removeClass('vjs-waiting');
  20111. this.off('timeupdate', timeUpdateListener);
  20112. }
  20113. };
  20114. this.on('timeupdate', timeUpdateListener);
  20115. }
  20116. /**
  20117. * Retrigger the `canplay` event that was triggered by the {@link Tech}.
  20118. * > Note: This is not consistent between browsers. See #1351
  20119. *
  20120. * @fires Player#canplay
  20121. * @listens Tech#canplay
  20122. * @private
  20123. */
  20124. handleTechCanPlay_() {
  20125. this.removeClass('vjs-waiting');
  20126. /**
  20127. * The media has a readyState of HAVE_FUTURE_DATA or greater.
  20128. *
  20129. * @event Player#canplay
  20130. * @type {Event}
  20131. */
  20132. this.trigger('canplay');
  20133. }
  20134. /**
  20135. * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
  20136. *
  20137. * @fires Player#canplaythrough
  20138. * @listens Tech#canplaythrough
  20139. * @private
  20140. */
  20141. handleTechCanPlayThrough_() {
  20142. this.removeClass('vjs-waiting');
  20143. /**
  20144. * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
  20145. * entire media file can be played without buffering.
  20146. *
  20147. * @event Player#canplaythrough
  20148. * @type {Event}
  20149. */
  20150. this.trigger('canplaythrough');
  20151. }
  20152. /**
  20153. * Retrigger the `playing` event that was triggered by the {@link Tech}.
  20154. *
  20155. * @fires Player#playing
  20156. * @listens Tech#playing
  20157. * @private
  20158. */
  20159. handleTechPlaying_() {
  20160. this.removeClass('vjs-waiting');
  20161. /**
  20162. * The media is no longer blocked from playback, and has started playing.
  20163. *
  20164. * @event Player#playing
  20165. * @type {Event}
  20166. */
  20167. this.trigger('playing');
  20168. }
  20169. /**
  20170. * Retrigger the `seeking` event that was triggered by the {@link Tech}.
  20171. *
  20172. * @fires Player#seeking
  20173. * @listens Tech#seeking
  20174. * @private
  20175. */
  20176. handleTechSeeking_() {
  20177. this.addClass('vjs-seeking');
  20178. /**
  20179. * Fired whenever the player is jumping to a new time
  20180. *
  20181. * @event Player#seeking
  20182. * @type {Event}
  20183. */
  20184. this.trigger('seeking');
  20185. }
  20186. /**
  20187. * Retrigger the `seeked` event that was triggered by the {@link Tech}.
  20188. *
  20189. * @fires Player#seeked
  20190. * @listens Tech#seeked
  20191. * @private
  20192. */
  20193. handleTechSeeked_() {
  20194. this.removeClass('vjs-seeking', 'vjs-ended');
  20195. /**
  20196. * Fired when the player has finished jumping to a new time
  20197. *
  20198. * @event Player#seeked
  20199. * @type {Event}
  20200. */
  20201. this.trigger('seeked');
  20202. }
  20203. /**
  20204. * Retrigger the `pause` event that was triggered by the {@link Tech}.
  20205. *
  20206. * @fires Player#pause
  20207. * @listens Tech#pause
  20208. * @private
  20209. */
  20210. handleTechPause_() {
  20211. this.removeClass('vjs-playing');
  20212. this.addClass('vjs-paused');
  20213. /**
  20214. * Fired whenever the media has been paused
  20215. *
  20216. * @event Player#pause
  20217. * @type {Event}
  20218. */
  20219. this.trigger('pause');
  20220. }
  20221. /**
  20222. * Retrigger the `ended` event that was triggered by the {@link Tech}.
  20223. *
  20224. * @fires Player#ended
  20225. * @listens Tech#ended
  20226. * @private
  20227. */
  20228. handleTechEnded_() {
  20229. this.addClass('vjs-ended');
  20230. this.removeClass('vjs-waiting');
  20231. if (this.options_.loop) {
  20232. this.currentTime(0);
  20233. this.play();
  20234. } else if (!this.paused()) {
  20235. this.pause();
  20236. }
  20237. /**
  20238. * Fired when the end of the media resource is reached (currentTime == duration)
  20239. *
  20240. * @event Player#ended
  20241. * @type {Event}
  20242. */
  20243. this.trigger('ended');
  20244. }
  20245. /**
  20246. * Fired when the duration of the media resource is first known or changed
  20247. *
  20248. * @listens Tech#durationchange
  20249. * @private
  20250. */
  20251. handleTechDurationChange_() {
  20252. this.duration(this.techGet_('duration'));
  20253. }
  20254. /**
  20255. * Handle a click on the media element to play/pause
  20256. *
  20257. * @param {Event} event
  20258. * the event that caused this function to trigger
  20259. *
  20260. * @listens Tech#click
  20261. * @private
  20262. */
  20263. handleTechClick_(event) {
  20264. // When controls are disabled a click should not toggle playback because
  20265. // the click is considered a control
  20266. if (!this.controls_) {
  20267. return;
  20268. }
  20269. if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
  20270. if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
  20271. this.options_.userActions.click.call(this, event);
  20272. } else if (this.paused()) {
  20273. silencePromise(this.play());
  20274. } else {
  20275. this.pause();
  20276. }
  20277. }
  20278. }
  20279. /**
  20280. * Handle a double-click on the media element to enter/exit fullscreen
  20281. *
  20282. * @param {Event} event
  20283. * the event that caused this function to trigger
  20284. *
  20285. * @listens Tech#dblclick
  20286. * @private
  20287. */
  20288. handleTechDoubleClick_(event) {
  20289. if (!this.controls_) {
  20290. return;
  20291. }
  20292. // we do not want to toggle fullscreen state
  20293. // when double-clicking inside a control bar or a modal
  20294. const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
  20295. if (!inAllowedEls) {
  20296. /*
  20297. * options.userActions.doubleClick
  20298. *
  20299. * If `undefined` or `true`, double-click toggles fullscreen if controls are present
  20300. * Set to `false` to disable double-click handling
  20301. * Set to a function to substitute an external double-click handler
  20302. */
  20303. if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
  20304. if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
  20305. this.options_.userActions.doubleClick.call(this, event);
  20306. } else if (this.isFullscreen()) {
  20307. this.exitFullscreen();
  20308. } else {
  20309. this.requestFullscreen();
  20310. }
  20311. }
  20312. }
  20313. }
  20314. /**
  20315. * Handle a tap on the media element. It will toggle the user
  20316. * activity state, which hides and shows the controls.
  20317. *
  20318. * @listens Tech#tap
  20319. * @private
  20320. */
  20321. handleTechTap_() {
  20322. this.userActive(!this.userActive());
  20323. }
  20324. /**
  20325. * Handle touch to start
  20326. *
  20327. * @listens Tech#touchstart
  20328. * @private
  20329. */
  20330. handleTechTouchStart_() {
  20331. this.userWasActive = this.userActive();
  20332. }
  20333. /**
  20334. * Handle touch to move
  20335. *
  20336. * @listens Tech#touchmove
  20337. * @private
  20338. */
  20339. handleTechTouchMove_() {
  20340. if (this.userWasActive) {
  20341. this.reportUserActivity();
  20342. }
  20343. }
  20344. /**
  20345. * Handle touch to end
  20346. *
  20347. * @param {Event} event
  20348. * the touchend event that triggered
  20349. * this function
  20350. *
  20351. * @listens Tech#touchend
  20352. * @private
  20353. */
  20354. handleTechTouchEnd_(event) {
  20355. // Stop the mouse events from also happening
  20356. if (event.cancelable) {
  20357. event.preventDefault();
  20358. }
  20359. }
  20360. /**
  20361. * @private
  20362. */
  20363. toggleFullscreenClass_() {
  20364. if (this.isFullscreen()) {
  20365. this.addClass('vjs-fullscreen');
  20366. } else {
  20367. this.removeClass('vjs-fullscreen');
  20368. }
  20369. }
  20370. /**
  20371. * when the document fschange event triggers it calls this
  20372. */
  20373. documentFullscreenChange_(e) {
  20374. const targetPlayer = e.target.player;
  20375. // if another player was fullscreen
  20376. // do a null check for targetPlayer because older firefox's would put document as e.target
  20377. if (targetPlayer && targetPlayer !== this) {
  20378. return;
  20379. }
  20380. const el = this.el();
  20381. let isFs = document__default["default"][this.fsApi_.fullscreenElement] === el;
  20382. if (!isFs && el.matches) {
  20383. isFs = el.matches(':' + this.fsApi_.fullscreen);
  20384. }
  20385. this.isFullscreen(isFs);
  20386. }
  20387. /**
  20388. * Handle Tech Fullscreen Change
  20389. *
  20390. * @param {Event} event
  20391. * the fullscreenchange event that triggered this function
  20392. *
  20393. * @param {Object} data
  20394. * the data that was sent with the event
  20395. *
  20396. * @private
  20397. * @listens Tech#fullscreenchange
  20398. * @fires Player#fullscreenchange
  20399. */
  20400. handleTechFullscreenChange_(event, data) {
  20401. if (data) {
  20402. if (data.nativeIOSFullscreen) {
  20403. this.addClass('vjs-ios-native-fs');
  20404. this.tech_.one('webkitendfullscreen', () => {
  20405. this.removeClass('vjs-ios-native-fs');
  20406. });
  20407. }
  20408. this.isFullscreen(data.isFullscreen);
  20409. }
  20410. }
  20411. handleTechFullscreenError_(event, err) {
  20412. this.trigger('fullscreenerror', err);
  20413. }
  20414. /**
  20415. * @private
  20416. */
  20417. togglePictureInPictureClass_() {
  20418. if (this.isInPictureInPicture()) {
  20419. this.addClass('vjs-picture-in-picture');
  20420. } else {
  20421. this.removeClass('vjs-picture-in-picture');
  20422. }
  20423. }
  20424. /**
  20425. * Handle Tech Enter Picture-in-Picture.
  20426. *
  20427. * @param {Event} event
  20428. * the enterpictureinpicture event that triggered this function
  20429. *
  20430. * @private
  20431. * @listens Tech#enterpictureinpicture
  20432. */
  20433. handleTechEnterPictureInPicture_(event) {
  20434. this.isInPictureInPicture(true);
  20435. }
  20436. /**
  20437. * Handle Tech Leave Picture-in-Picture.
  20438. *
  20439. * @param {Event} event
  20440. * the leavepictureinpicture event that triggered this function
  20441. *
  20442. * @private
  20443. * @listens Tech#leavepictureinpicture
  20444. */
  20445. handleTechLeavePictureInPicture_(event) {
  20446. this.isInPictureInPicture(false);
  20447. }
  20448. /**
  20449. * Fires when an error occurred during the loading of an audio/video.
  20450. *
  20451. * @private
  20452. * @listens Tech#error
  20453. */
  20454. handleTechError_() {
  20455. const error = this.tech_.error();
  20456. if (error) {
  20457. this.error(error);
  20458. }
  20459. }
  20460. /**
  20461. * Retrigger the `textdata` event that was triggered by the {@link Tech}.
  20462. *
  20463. * @fires Player#textdata
  20464. * @listens Tech#textdata
  20465. * @private
  20466. */
  20467. handleTechTextData_() {
  20468. let data = null;
  20469. if (arguments.length > 1) {
  20470. data = arguments[1];
  20471. }
  20472. /**
  20473. * Fires when we get a textdata event from tech
  20474. *
  20475. * @event Player#textdata
  20476. * @type {Event}
  20477. */
  20478. this.trigger('textdata', data);
  20479. }
  20480. /**
  20481. * Get object for cached values.
  20482. *
  20483. * @return {Object}
  20484. * get the current object cache
  20485. */
  20486. getCache() {
  20487. return this.cache_;
  20488. }
  20489. /**
  20490. * Resets the internal cache object.
  20491. *
  20492. * Using this function outside the player constructor or reset method may
  20493. * have unintended side-effects.
  20494. *
  20495. * @private
  20496. */
  20497. resetCache_() {
  20498. this.cache_ = {
  20499. // Right now, the currentTime is not _really_ cached because it is always
  20500. // retrieved from the tech (see: currentTime). However, for completeness,
  20501. // we set it to zero here to ensure that if we do start actually caching
  20502. // it, we reset it along with everything else.
  20503. currentTime: 0,
  20504. initTime: 0,
  20505. inactivityTimeout: this.options_.inactivityTimeout,
  20506. duration: NaN,
  20507. lastVolume: 1,
  20508. lastPlaybackRate: this.defaultPlaybackRate(),
  20509. media: null,
  20510. src: '',
  20511. source: {},
  20512. sources: [],
  20513. playbackRates: [],
  20514. volume: 1
  20515. };
  20516. }
  20517. /**
  20518. * Pass values to the playback tech
  20519. *
  20520. * @param {string} [method]
  20521. * the method to call
  20522. *
  20523. * @param {Object} [arg]
  20524. * the argument to pass
  20525. *
  20526. * @private
  20527. */
  20528. techCall_(method, arg) {
  20529. // If it's not ready yet, call method when it is
  20530. this.ready(function () {
  20531. if (method in allowedSetters) {
  20532. return set(this.middleware_, this.tech_, method, arg);
  20533. } else if (method in allowedMediators) {
  20534. return mediate(this.middleware_, this.tech_, method, arg);
  20535. }
  20536. try {
  20537. if (this.tech_) {
  20538. this.tech_[method](arg);
  20539. }
  20540. } catch (e) {
  20541. log(e);
  20542. throw e;
  20543. }
  20544. }, true);
  20545. }
  20546. /**
  20547. * Mediate attempt to call playback tech method
  20548. * and return the value of the method called.
  20549. *
  20550. * @param {string} method
  20551. * Tech method
  20552. *
  20553. * @return {*}
  20554. * Value returned by the tech method called, undefined if tech
  20555. * is not ready or tech method is not present
  20556. *
  20557. * @private
  20558. */
  20559. techGet_(method) {
  20560. if (!this.tech_ || !this.tech_.isReady_) {
  20561. return;
  20562. }
  20563. if (method in allowedGetters) {
  20564. return get(this.middleware_, this.tech_, method);
  20565. } else if (method in allowedMediators) {
  20566. return mediate(this.middleware_, this.tech_, method);
  20567. }
  20568. // Log error when playback tech object is present but method
  20569. // is undefined or unavailable
  20570. try {
  20571. return this.tech_[method]();
  20572. } catch (e) {
  20573. // When building additional tech libs, an expected method may not be defined yet
  20574. if (this.tech_[method] === undefined) {
  20575. log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
  20576. throw e;
  20577. }
  20578. // When a method isn't available on the object it throws a TypeError
  20579. if (e.name === 'TypeError') {
  20580. log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
  20581. this.tech_.isReady_ = false;
  20582. throw e;
  20583. }
  20584. // If error unknown, just log and throw
  20585. log(e);
  20586. throw e;
  20587. }
  20588. }
  20589. /**
  20590. * Attempt to begin playback at the first opportunity.
  20591. *
  20592. * @return {Promise|undefined}
  20593. * Returns a promise if the browser supports Promises (or one
  20594. * was passed in as an option). This promise will be resolved on
  20595. * the return value of play. If this is undefined it will fulfill the
  20596. * promise chain otherwise the promise chain will be fulfilled when
  20597. * the promise from play is fulfilled.
  20598. */
  20599. play() {
  20600. return new Promise(resolve => {
  20601. this.play_(resolve);
  20602. });
  20603. }
  20604. /**
  20605. * The actual logic for play, takes a callback that will be resolved on the
  20606. * return value of play. This allows us to resolve to the play promise if there
  20607. * is one on modern browsers.
  20608. *
  20609. * @private
  20610. * @param {Function} [callback]
  20611. * The callback that should be called when the techs play is actually called
  20612. */
  20613. play_(callback = silencePromise) {
  20614. this.playCallbacks_.push(callback);
  20615. const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
  20616. const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
  20617. // treat calls to play_ somewhat like the `one` event function
  20618. if (this.waitToPlay_) {
  20619. this.off(['ready', 'loadstart'], this.waitToPlay_);
  20620. this.waitToPlay_ = null;
  20621. }
  20622. // if the player/tech is not ready or the src itself is not ready
  20623. // queue up a call to play on `ready` or `loadstart`
  20624. if (!this.isReady_ || !isSrcReady) {
  20625. this.waitToPlay_ = e => {
  20626. this.play_();
  20627. };
  20628. this.one(['ready', 'loadstart'], this.waitToPlay_);
  20629. // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
  20630. // in that case, we need to prime the video element by calling load so it'll be ready in time
  20631. if (!isSrcReady && isSafariOrIOS) {
  20632. this.load();
  20633. }
  20634. return;
  20635. }
  20636. // If the player/tech is ready and we have a source, we can attempt playback.
  20637. const val = this.techGet_('play');
  20638. // For native playback, reset the progress bar if we get a play call from a replay.
  20639. const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
  20640. if (isNativeReplay) {
  20641. this.resetProgressBar_();
  20642. }
  20643. // play was terminated if the returned value is null
  20644. if (val === null) {
  20645. this.runPlayTerminatedQueue_();
  20646. } else {
  20647. this.runPlayCallbacks_(val);
  20648. }
  20649. }
  20650. /**
  20651. * These functions will be run when if play is terminated. If play
  20652. * runPlayCallbacks_ is run these function will not be run. This allows us
  20653. * to differentiate between a terminated play and an actual call to play.
  20654. */
  20655. runPlayTerminatedQueue_() {
  20656. const queue = this.playTerminatedQueue_.slice(0);
  20657. this.playTerminatedQueue_ = [];
  20658. queue.forEach(function (q) {
  20659. q();
  20660. });
  20661. }
  20662. /**
  20663. * When a callback to play is delayed we have to run these
  20664. * callbacks when play is actually called on the tech. This function
  20665. * runs the callbacks that were delayed and accepts the return value
  20666. * from the tech.
  20667. *
  20668. * @param {undefined|Promise} val
  20669. * The return value from the tech.
  20670. */
  20671. runPlayCallbacks_(val) {
  20672. const callbacks = this.playCallbacks_.slice(0);
  20673. this.playCallbacks_ = [];
  20674. // clear play terminatedQueue since we finished a real play
  20675. this.playTerminatedQueue_ = [];
  20676. callbacks.forEach(function (cb) {
  20677. cb(val);
  20678. });
  20679. }
  20680. /**
  20681. * Pause the video playback
  20682. */
  20683. pause() {
  20684. this.techCall_('pause');
  20685. }
  20686. /**
  20687. * Check if the player is paused or has yet to play
  20688. *
  20689. * @return {boolean}
  20690. * - false: if the media is currently playing
  20691. * - true: if media is not currently playing
  20692. */
  20693. paused() {
  20694. // The initial state of paused should be true (in Safari it's actually false)
  20695. return this.techGet_('paused') === false ? false : true;
  20696. }
  20697. /**
  20698. * Get a TimeRange object representing the current ranges of time that the user
  20699. * has played.
  20700. *
  20701. * @return { import('./utils/time').TimeRange }
  20702. * A time range object that represents all the increments of time that have
  20703. * been played.
  20704. */
  20705. played() {
  20706. return this.techGet_('played') || createTimeRanges(0, 0);
  20707. }
  20708. /**
  20709. * Sets or returns whether or not the user is "scrubbing". Scrubbing is
  20710. * when the user has clicked the progress bar handle and is
  20711. * dragging it along the progress bar.
  20712. *
  20713. * @param {boolean} [isScrubbing]
  20714. * whether the user is or is not scrubbing
  20715. *
  20716. * @return {boolean|undefined}
  20717. * - The value of scrubbing when getting
  20718. * - Nothing when setting
  20719. */
  20720. scrubbing(isScrubbing) {
  20721. if (typeof isScrubbing === 'undefined') {
  20722. return this.scrubbing_;
  20723. }
  20724. this.scrubbing_ = !!isScrubbing;
  20725. this.techCall_('setScrubbing', this.scrubbing_);
  20726. if (isScrubbing) {
  20727. this.addClass('vjs-scrubbing');
  20728. } else {
  20729. this.removeClass('vjs-scrubbing');
  20730. }
  20731. }
  20732. /**
  20733. * Get or set the current time (in seconds)
  20734. *
  20735. * @param {number|string} [seconds]
  20736. * The time to seek to in seconds
  20737. *
  20738. * @return {number|undefined}
  20739. * - the current time in seconds when getting
  20740. * - Nothing when setting
  20741. */
  20742. currentTime(seconds) {
  20743. if (seconds === undefined) {
  20744. // cache last currentTime and return. default to 0 seconds
  20745. //
  20746. // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
  20747. // currentTime when scrubbing, but may not provide much performance benefit after all.
  20748. // Should be tested. Also something has to read the actual current time or the cache will
  20749. // never get updated.
  20750. this.cache_.currentTime = this.techGet_('currentTime') || 0;
  20751. return this.cache_.currentTime;
  20752. }
  20753. if (seconds < 0) {
  20754. seconds = 0;
  20755. }
  20756. if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
  20757. this.cache_.initTime = seconds;
  20758. this.off('canplay', this.boundApplyInitTime_);
  20759. this.one('canplay', this.boundApplyInitTime_);
  20760. return;
  20761. }
  20762. this.techCall_('setCurrentTime', seconds);
  20763. this.cache_.initTime = 0;
  20764. if (isFinite(seconds)) {
  20765. this.cache_.currentTime = Number(seconds);
  20766. }
  20767. }
  20768. /**
  20769. * Apply the value of initTime stored in cache as currentTime.
  20770. *
  20771. * @private
  20772. */
  20773. applyInitTime_() {
  20774. this.currentTime(this.cache_.initTime);
  20775. }
  20776. /**
  20777. * Normally gets the length in time of the video in seconds;
  20778. * in all but the rarest use cases an argument will NOT be passed to the method
  20779. *
  20780. * > **NOTE**: The video must have started loading before the duration can be
  20781. * known, and depending on preload behaviour may not be known until the video starts
  20782. * playing.
  20783. *
  20784. * @fires Player#durationchange
  20785. *
  20786. * @param {number} [seconds]
  20787. * The duration of the video to set in seconds
  20788. *
  20789. * @return {number|undefined}
  20790. * - The duration of the video in seconds when getting
  20791. * - Nothing when setting
  20792. */
  20793. duration(seconds) {
  20794. if (seconds === undefined) {
  20795. // return NaN if the duration is not known
  20796. return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
  20797. }
  20798. seconds = parseFloat(seconds);
  20799. // Standardize on Infinity for signaling video is live
  20800. if (seconds < 0) {
  20801. seconds = Infinity;
  20802. }
  20803. if (seconds !== this.cache_.duration) {
  20804. // Cache the last set value for optimized scrubbing
  20805. this.cache_.duration = seconds;
  20806. if (seconds === Infinity) {
  20807. this.addClass('vjs-live');
  20808. } else {
  20809. this.removeClass('vjs-live');
  20810. }
  20811. if (!isNaN(seconds)) {
  20812. // Do not fire durationchange unless the duration value is known.
  20813. // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
  20814. /**
  20815. * @event Player#durationchange
  20816. * @type {Event}
  20817. */
  20818. this.trigger('durationchange');
  20819. }
  20820. }
  20821. }
  20822. /**
  20823. * Calculates how much time is left in the video. Not part
  20824. * of the native video API.
  20825. *
  20826. * @return {number}
  20827. * The time remaining in seconds
  20828. */
  20829. remainingTime() {
  20830. return this.duration() - this.currentTime();
  20831. }
  20832. /**
  20833. * A remaining time function that is intended to be used when
  20834. * the time is to be displayed directly to the user.
  20835. *
  20836. * @return {number}
  20837. * The rounded time remaining in seconds
  20838. */
  20839. remainingTimeDisplay() {
  20840. return Math.floor(this.duration()) - Math.floor(this.currentTime());
  20841. }
  20842. //
  20843. // Kind of like an array of portions of the video that have been downloaded.
  20844. /**
  20845. * Get a TimeRange object with an array of the times of the video
  20846. * that have been downloaded. If you just want the percent of the
  20847. * video that's been downloaded, use bufferedPercent.
  20848. *
  20849. * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
  20850. *
  20851. * @return { import('./utils/time').TimeRange }
  20852. * A mock {@link TimeRanges} object (following HTML spec)
  20853. */
  20854. buffered() {
  20855. let buffered = this.techGet_('buffered');
  20856. if (!buffered || !buffered.length) {
  20857. buffered = createTimeRanges(0, 0);
  20858. }
  20859. return buffered;
  20860. }
  20861. /**
  20862. * Get the TimeRanges of the media that are currently available
  20863. * for seeking to.
  20864. *
  20865. * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable}
  20866. *
  20867. * @return { import('./utils/time').TimeRange }
  20868. * A mock {@link TimeRanges} object (following HTML spec)
  20869. */
  20870. seekable() {
  20871. let seekable = this.techGet_('seekable');
  20872. if (!seekable || !seekable.length) {
  20873. seekable = createTimeRanges(0, 0);
  20874. }
  20875. return seekable;
  20876. }
  20877. /**
  20878. * Returns whether the player is in the "seeking" state.
  20879. *
  20880. * @return {boolean} True if the player is in the seeking state, false if not.
  20881. */
  20882. seeking() {
  20883. return this.techGet_('seeking');
  20884. }
  20885. /**
  20886. * Returns whether the player is in the "ended" state.
  20887. *
  20888. * @return {boolean} True if the player is in the ended state, false if not.
  20889. */
  20890. ended() {
  20891. return this.techGet_('ended');
  20892. }
  20893. /**
  20894. * Returns the current state of network activity for the element, from
  20895. * the codes in the list below.
  20896. * - NETWORK_EMPTY (numeric value 0)
  20897. * The element has not yet been initialised. All attributes are in
  20898. * their initial states.
  20899. * - NETWORK_IDLE (numeric value 1)
  20900. * The element's resource selection algorithm is active and has
  20901. * selected a resource, but it is not actually using the network at
  20902. * this time.
  20903. * - NETWORK_LOADING (numeric value 2)
  20904. * The user agent is actively trying to download data.
  20905. * - NETWORK_NO_SOURCE (numeric value 3)
  20906. * The element's resource selection algorithm is active, but it has
  20907. * not yet found a resource to use.
  20908. *
  20909. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
  20910. * @return {number} the current network activity state
  20911. */
  20912. networkState() {
  20913. return this.techGet_('networkState');
  20914. }
  20915. /**
  20916. * Returns a value that expresses the current state of the element
  20917. * with respect to rendering the current playback position, from the
  20918. * codes in the list below.
  20919. * - HAVE_NOTHING (numeric value 0)
  20920. * No information regarding the media resource is available.
  20921. * - HAVE_METADATA (numeric value 1)
  20922. * Enough of the resource has been obtained that the duration of the
  20923. * resource is available.
  20924. * - HAVE_CURRENT_DATA (numeric value 2)
  20925. * Data for the immediate current playback position is available.
  20926. * - HAVE_FUTURE_DATA (numeric value 3)
  20927. * Data for the immediate current playback position is available, as
  20928. * well as enough data for the user agent to advance the current
  20929. * playback position in the direction of playback.
  20930. * - HAVE_ENOUGH_DATA (numeric value 4)
  20931. * The user agent estimates that enough data is available for
  20932. * playback to proceed uninterrupted.
  20933. *
  20934. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
  20935. * @return {number} the current playback rendering state
  20936. */
  20937. readyState() {
  20938. return this.techGet_('readyState');
  20939. }
  20940. /**
  20941. * Get the percent (as a decimal) of the video that's been downloaded.
  20942. * This method is not a part of the native HTML video API.
  20943. *
  20944. * @return {number}
  20945. * A decimal between 0 and 1 representing the percent
  20946. * that is buffered 0 being 0% and 1 being 100%
  20947. */
  20948. bufferedPercent() {
  20949. return bufferedPercent(this.buffered(), this.duration());
  20950. }
  20951. /**
  20952. * Get the ending time of the last buffered time range
  20953. * This is used in the progress bar to encapsulate all time ranges.
  20954. *
  20955. * @return {number}
  20956. * The end of the last buffered time range
  20957. */
  20958. bufferedEnd() {
  20959. const buffered = this.buffered();
  20960. const duration = this.duration();
  20961. let end = buffered.end(buffered.length - 1);
  20962. if (end > duration) {
  20963. end = duration;
  20964. }
  20965. return end;
  20966. }
  20967. /**
  20968. * Get or set the current volume of the media
  20969. *
  20970. * @param {number} [percentAsDecimal]
  20971. * The new volume as a decimal percent:
  20972. * - 0 is muted/0%/off
  20973. * - 1.0 is 100%/full
  20974. * - 0.5 is half volume or 50%
  20975. *
  20976. * @return {number|undefined}
  20977. * The current volume as a percent when getting
  20978. */
  20979. volume(percentAsDecimal) {
  20980. let vol;
  20981. if (percentAsDecimal !== undefined) {
  20982. // Force value to between 0 and 1
  20983. vol = Math.max(0, Math.min(1, percentAsDecimal));
  20984. this.cache_.volume = vol;
  20985. this.techCall_('setVolume', vol);
  20986. if (vol > 0) {
  20987. this.lastVolume_(vol);
  20988. }
  20989. return;
  20990. }
  20991. // Default to 1 when returning current volume.
  20992. vol = parseFloat(this.techGet_('volume'));
  20993. return isNaN(vol) ? 1 : vol;
  20994. }
  20995. /**
  20996. * Get the current muted state, or turn mute on or off
  20997. *
  20998. * @param {boolean} [muted]
  20999. * - true to mute
  21000. * - false to unmute
  21001. *
  21002. * @return {boolean|undefined}
  21003. * - true if mute is on and getting
  21004. * - false if mute is off and getting
  21005. * - nothing if setting
  21006. */
  21007. muted(muted) {
  21008. if (muted !== undefined) {
  21009. this.techCall_('setMuted', muted);
  21010. return;
  21011. }
  21012. return this.techGet_('muted') || false;
  21013. }
  21014. /**
  21015. * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
  21016. * indicates the state of muted on initial playback.
  21017. *
  21018. * ```js
  21019. * var myPlayer = videojs('some-player-id');
  21020. *
  21021. * myPlayer.src("http://www.example.com/path/to/video.mp4");
  21022. *
  21023. * // get, should be false
  21024. * console.log(myPlayer.defaultMuted());
  21025. * // set to true
  21026. * myPlayer.defaultMuted(true);
  21027. * // get should be true
  21028. * console.log(myPlayer.defaultMuted());
  21029. * ```
  21030. *
  21031. * @param {boolean} [defaultMuted]
  21032. * - true to mute
  21033. * - false to unmute
  21034. *
  21035. * @return {boolean|undefined}
  21036. * - true if defaultMuted is on and getting
  21037. * - false if defaultMuted is off and getting
  21038. * - Nothing when setting
  21039. */
  21040. defaultMuted(defaultMuted) {
  21041. if (defaultMuted !== undefined) {
  21042. this.techCall_('setDefaultMuted', defaultMuted);
  21043. }
  21044. return this.techGet_('defaultMuted') || false;
  21045. }
  21046. /**
  21047. * Get the last volume, or set it
  21048. *
  21049. * @param {number} [percentAsDecimal]
  21050. * The new last volume as a decimal percent:
  21051. * - 0 is muted/0%/off
  21052. * - 1.0 is 100%/full
  21053. * - 0.5 is half volume or 50%
  21054. *
  21055. * @return {number|undefined}
  21056. * - The current value of lastVolume as a percent when getting
  21057. * - Nothing when setting
  21058. *
  21059. * @private
  21060. */
  21061. lastVolume_(percentAsDecimal) {
  21062. if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
  21063. this.cache_.lastVolume = percentAsDecimal;
  21064. return;
  21065. }
  21066. return this.cache_.lastVolume;
  21067. }
  21068. /**
  21069. * Check if current tech can support native fullscreen
  21070. * (e.g. with built in controls like iOS)
  21071. *
  21072. * @return {boolean}
  21073. * if native fullscreen is supported
  21074. */
  21075. supportsFullScreen() {
  21076. return this.techGet_('supportsFullScreen') || false;
  21077. }
  21078. /**
  21079. * Check if the player is in fullscreen mode or tell the player that it
  21080. * is or is not in fullscreen mode.
  21081. *
  21082. * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
  21083. * property and instead document.fullscreenElement is used. But isFullscreen is
  21084. * still a valuable property for internal player workings.
  21085. *
  21086. * @param {boolean} [isFS]
  21087. * Set the players current fullscreen state
  21088. *
  21089. * @return {boolean|undefined}
  21090. * - true if fullscreen is on and getting
  21091. * - false if fullscreen is off and getting
  21092. * - Nothing when setting
  21093. */
  21094. isFullscreen(isFS) {
  21095. if (isFS !== undefined) {
  21096. const oldValue = this.isFullscreen_;
  21097. this.isFullscreen_ = Boolean(isFS);
  21098. // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
  21099. // this is the only place where we trigger fullscreenchange events for older browsers
  21100. // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
  21101. if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
  21102. /**
  21103. * @event Player#fullscreenchange
  21104. * @type {Event}
  21105. */
  21106. this.trigger('fullscreenchange');
  21107. }
  21108. this.toggleFullscreenClass_();
  21109. return;
  21110. }
  21111. return this.isFullscreen_;
  21112. }
  21113. /**
  21114. * Increase the size of the video to full screen
  21115. * In some browsers, full screen is not supported natively, so it enters
  21116. * "full window mode", where the video fills the browser window.
  21117. * In browsers and devices that support native full screen, sometimes the
  21118. * browser's default controls will be shown, and not the Video.js custom skin.
  21119. * This includes most mobile devices (iOS, Android) and older versions of
  21120. * Safari.
  21121. *
  21122. * @param {Object} [fullscreenOptions]
  21123. * Override the player fullscreen options
  21124. *
  21125. * @fires Player#fullscreenchange
  21126. */
  21127. requestFullscreen(fullscreenOptions) {
  21128. if (this.isInPictureInPicture()) {
  21129. this.exitPictureInPicture();
  21130. }
  21131. const self = this;
  21132. return new Promise((resolve, reject) => {
  21133. function offHandler() {
  21134. self.off('fullscreenerror', errorHandler);
  21135. self.off('fullscreenchange', changeHandler);
  21136. }
  21137. function changeHandler() {
  21138. offHandler();
  21139. resolve();
  21140. }
  21141. function errorHandler(e, err) {
  21142. offHandler();
  21143. reject(err);
  21144. }
  21145. self.one('fullscreenchange', changeHandler);
  21146. self.one('fullscreenerror', errorHandler);
  21147. const promise = self.requestFullscreenHelper_(fullscreenOptions);
  21148. if (promise) {
  21149. promise.then(offHandler, offHandler);
  21150. promise.then(resolve, reject);
  21151. }
  21152. });
  21153. }
  21154. requestFullscreenHelper_(fullscreenOptions) {
  21155. let fsOptions;
  21156. // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
  21157. // Use defaults or player configured option unless passed directly to this method.
  21158. if (!this.fsApi_.prefixed) {
  21159. fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
  21160. if (fullscreenOptions !== undefined) {
  21161. fsOptions = fullscreenOptions;
  21162. }
  21163. }
  21164. // This method works as follows:
  21165. // 1. if a fullscreen api is available, use it
  21166. // 1. call requestFullscreen with potential options
  21167. // 2. if we got a promise from above, use it to update isFullscreen()
  21168. // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
  21169. // This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
  21170. // 3. otherwise, use "fullWindow" mode
  21171. if (this.fsApi_.requestFullscreen) {
  21172. const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
  21173. // Even on browsers with promise support this may not return a promise
  21174. if (promise) {
  21175. promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
  21176. }
  21177. return promise;
  21178. } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
  21179. // we can't take the video.js controls fullscreen but we can go fullscreen
  21180. // with native controls
  21181. this.techCall_('enterFullScreen');
  21182. } else {
  21183. // fullscreen isn't supported so we'll just stretch the video element to
  21184. // fill the viewport
  21185. this.enterFullWindow();
  21186. }
  21187. }
  21188. /**
  21189. * Return the video to its normal size after having been in full screen mode
  21190. *
  21191. * @fires Player#fullscreenchange
  21192. */
  21193. exitFullscreen() {
  21194. const self = this;
  21195. return new Promise((resolve, reject) => {
  21196. function offHandler() {
  21197. self.off('fullscreenerror', errorHandler);
  21198. self.off('fullscreenchange', changeHandler);
  21199. }
  21200. function changeHandler() {
  21201. offHandler();
  21202. resolve();
  21203. }
  21204. function errorHandler(e, err) {
  21205. offHandler();
  21206. reject(err);
  21207. }
  21208. self.one('fullscreenchange', changeHandler);
  21209. self.one('fullscreenerror', errorHandler);
  21210. const promise = self.exitFullscreenHelper_();
  21211. if (promise) {
  21212. promise.then(offHandler, offHandler);
  21213. // map the promise to our resolve/reject methods
  21214. promise.then(resolve, reject);
  21215. }
  21216. });
  21217. }
  21218. exitFullscreenHelper_() {
  21219. if (this.fsApi_.requestFullscreen) {
  21220. const promise = document__default["default"][this.fsApi_.exitFullscreen]();
  21221. // Even on browsers with promise support this may not return a promise
  21222. if (promise) {
  21223. // we're splitting the promise here, so, we want to catch the
  21224. // potential error so that this chain doesn't have unhandled errors
  21225. silencePromise(promise.then(() => this.isFullscreen(false)));
  21226. }
  21227. return promise;
  21228. } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
  21229. this.techCall_('exitFullScreen');
  21230. } else {
  21231. this.exitFullWindow();
  21232. }
  21233. }
  21234. /**
  21235. * When fullscreen isn't supported we can stretch the
  21236. * video container to as wide as the browser will let us.
  21237. *
  21238. * @fires Player#enterFullWindow
  21239. */
  21240. enterFullWindow() {
  21241. this.isFullscreen(true);
  21242. this.isFullWindow = true;
  21243. // Storing original doc overflow value to return to when fullscreen is off
  21244. this.docOrigOverflow = document__default["default"].documentElement.style.overflow;
  21245. // Add listener for esc key to exit fullscreen
  21246. on(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
  21247. // Hide any scroll bars
  21248. document__default["default"].documentElement.style.overflow = 'hidden';
  21249. // Apply fullscreen styles
  21250. addClass(document__default["default"].body, 'vjs-full-window');
  21251. /**
  21252. * @event Player#enterFullWindow
  21253. * @type {Event}
  21254. */
  21255. this.trigger('enterFullWindow');
  21256. }
  21257. /**
  21258. * Check for call to either exit full window or
  21259. * full screen on ESC key
  21260. *
  21261. * @param {string} event
  21262. * Event to check for key press
  21263. */
  21264. fullWindowOnEscKey(event) {
  21265. if (keycode__default["default"].isEventKey(event, 'Esc')) {
  21266. if (this.isFullscreen() === true) {
  21267. if (!this.isFullWindow) {
  21268. this.exitFullscreen();
  21269. } else {
  21270. this.exitFullWindow();
  21271. }
  21272. }
  21273. }
  21274. }
  21275. /**
  21276. * Exit full window
  21277. *
  21278. * @fires Player#exitFullWindow
  21279. */
  21280. exitFullWindow() {
  21281. this.isFullscreen(false);
  21282. this.isFullWindow = false;
  21283. off(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
  21284. // Unhide scroll bars.
  21285. document__default["default"].documentElement.style.overflow = this.docOrigOverflow;
  21286. // Remove fullscreen styles
  21287. removeClass(document__default["default"].body, 'vjs-full-window');
  21288. // Resize the box, controller, and poster to original sizes
  21289. // this.positionAll();
  21290. /**
  21291. * @event Player#exitFullWindow
  21292. * @type {Event}
  21293. */
  21294. this.trigger('exitFullWindow');
  21295. }
  21296. /**
  21297. * Get or set disable Picture-in-Picture mode.
  21298. *
  21299. * @param {boolean} [value]
  21300. * - true will disable Picture-in-Picture mode
  21301. * - false will enable Picture-in-Picture mode
  21302. */
  21303. disablePictureInPicture(value) {
  21304. if (value === undefined) {
  21305. return this.techGet_('disablePictureInPicture');
  21306. }
  21307. this.techCall_('setDisablePictureInPicture', value);
  21308. this.options_.disablePictureInPicture = value;
  21309. this.trigger('disablepictureinpicturechanged');
  21310. }
  21311. /**
  21312. * Check if the player is in Picture-in-Picture mode or tell the player that it
  21313. * is or is not in Picture-in-Picture mode.
  21314. *
  21315. * @param {boolean} [isPiP]
  21316. * Set the players current Picture-in-Picture state
  21317. *
  21318. * @return {boolean|undefined}
  21319. * - true if Picture-in-Picture is on and getting
  21320. * - false if Picture-in-Picture is off and getting
  21321. * - nothing if setting
  21322. */
  21323. isInPictureInPicture(isPiP) {
  21324. if (isPiP !== undefined) {
  21325. this.isInPictureInPicture_ = !!isPiP;
  21326. this.togglePictureInPictureClass_();
  21327. return;
  21328. }
  21329. return !!this.isInPictureInPicture_;
  21330. }
  21331. /**
  21332. * Create a floating video window always on top of other windows so that users may
  21333. * continue consuming media while they interact with other content sites, or
  21334. * applications on their device.
  21335. *
  21336. * This can use document picture-in-picture or element picture in picture
  21337. *
  21338. * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
  21339. * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
  21340. *
  21341. *
  21342. * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
  21343. * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
  21344. *
  21345. * @fires Player#enterpictureinpicture
  21346. *
  21347. * @return {Promise}
  21348. * A promise with a Picture-in-Picture window.
  21349. */
  21350. requestPictureInPicture() {
  21351. if (this.options_.enableDocumentPictureInPicture && window__default["default"].documentPictureInPicture) {
  21352. const pipContainer = document__default["default"].createElement(this.el().tagName);
  21353. pipContainer.classList = this.el().classList;
  21354. pipContainer.classList.add('vjs-pip-container');
  21355. if (this.posterImage) {
  21356. pipContainer.appendChild(this.posterImage.el().cloneNode(true));
  21357. }
  21358. if (this.titleBar) {
  21359. pipContainer.appendChild(this.titleBar.el().cloneNode(true));
  21360. }
  21361. pipContainer.appendChild(createEl('p', {
  21362. className: 'vjs-pip-text'
  21363. }, {}, this.localize('Playing in picture-in-picture')));
  21364. return window__default["default"].documentPictureInPicture.requestWindow({
  21365. // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
  21366. width: this.videoWidth(),
  21367. height: this.videoHeight()
  21368. }).then(pipWindow => {
  21369. copyStyleSheetsToWindow(pipWindow);
  21370. this.el_.parentNode.insertBefore(pipContainer, this.el_);
  21371. pipWindow.document.body.appendChild(this.el_);
  21372. pipWindow.document.body.classList.add('vjs-pip-window');
  21373. this.player_.isInPictureInPicture(true);
  21374. this.player_.trigger('enterpictureinpicture');
  21375. // Listen for the PiP closing event to move the video back.
  21376. pipWindow.addEventListener('pagehide', event => {
  21377. const pipVideo = event.target.querySelector('.video-js');
  21378. pipContainer.parentNode.replaceChild(pipVideo, pipContainer);
  21379. this.player_.isInPictureInPicture(false);
  21380. this.player_.trigger('leavepictureinpicture');
  21381. });
  21382. return pipWindow;
  21383. });
  21384. }
  21385. if ('pictureInPictureEnabled' in document__default["default"] && this.disablePictureInPicture() === false) {
  21386. /**
  21387. * This event fires when the player enters picture in picture mode
  21388. *
  21389. * @event Player#enterpictureinpicture
  21390. * @type {Event}
  21391. */
  21392. return this.techGet_('requestPictureInPicture');
  21393. }
  21394. return Promise.reject('No PiP mode is available');
  21395. }
  21396. /**
  21397. * Exit Picture-in-Picture mode.
  21398. *
  21399. * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
  21400. *
  21401. * @fires Player#leavepictureinpicture
  21402. *
  21403. * @return {Promise}
  21404. * A promise.
  21405. */
  21406. exitPictureInPicture() {
  21407. if (window__default["default"].documentPictureInPicture && window__default["default"].documentPictureInPicture.window) {
  21408. // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler
  21409. window__default["default"].documentPictureInPicture.window.close();
  21410. return Promise.resolve();
  21411. }
  21412. if ('pictureInPictureEnabled' in document__default["default"]) {
  21413. /**
  21414. * This event fires when the player leaves picture in picture mode
  21415. *
  21416. * @event Player#leavepictureinpicture
  21417. * @type {Event}
  21418. */
  21419. return document__default["default"].exitPictureInPicture();
  21420. }
  21421. }
  21422. /**
  21423. * Called when this Player has focus and a key gets pressed down, or when
  21424. * any Component of this player receives a key press that it doesn't handle.
  21425. * This allows player-wide hotkeys (either as defined below, or optionally
  21426. * by an external function).
  21427. *
  21428. * @param {KeyboardEvent} event
  21429. * The `keydown` event that caused this function to be called.
  21430. *
  21431. * @listens keydown
  21432. */
  21433. handleKeyDown(event) {
  21434. const {
  21435. userActions
  21436. } = this.options_;
  21437. // Bail out if hotkeys are not configured.
  21438. if (!userActions || !userActions.hotkeys) {
  21439. return;
  21440. }
  21441. // Function that determines whether or not to exclude an element from
  21442. // hotkeys handling.
  21443. const excludeElement = el => {
  21444. const tagName = el.tagName.toLowerCase();
  21445. // The first and easiest test is for `contenteditable` elements.
  21446. if (el.isContentEditable) {
  21447. return true;
  21448. }
  21449. // Inputs matching these types will still trigger hotkey handling as
  21450. // they are not text inputs.
  21451. const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
  21452. if (tagName === 'input') {
  21453. return allowedInputTypes.indexOf(el.type) === -1;
  21454. }
  21455. // The final test is by tag name. These tags will be excluded entirely.
  21456. const excludedTags = ['textarea'];
  21457. return excludedTags.indexOf(tagName) !== -1;
  21458. };
  21459. // Bail out if the user is focused on an interactive form element.
  21460. if (excludeElement(this.el_.ownerDocument.activeElement)) {
  21461. return;
  21462. }
  21463. if (typeof userActions.hotkeys === 'function') {
  21464. userActions.hotkeys.call(this, event);
  21465. } else {
  21466. this.handleHotkeys(event);
  21467. }
  21468. }
  21469. /**
  21470. * Called when this Player receives a hotkey keydown event.
  21471. * Supported player-wide hotkeys are:
  21472. *
  21473. * f - toggle fullscreen
  21474. * m - toggle mute
  21475. * k or Space - toggle play/pause
  21476. *
  21477. * @param {Event} event
  21478. * The `keydown` event that caused this function to be called.
  21479. */
  21480. handleHotkeys(event) {
  21481. const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
  21482. // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
  21483. const {
  21484. fullscreenKey = keydownEvent => keycode__default["default"].isEventKey(keydownEvent, 'f'),
  21485. muteKey = keydownEvent => keycode__default["default"].isEventKey(keydownEvent, 'm'),
  21486. playPauseKey = keydownEvent => keycode__default["default"].isEventKey(keydownEvent, 'k') || keycode__default["default"].isEventKey(keydownEvent, 'Space')
  21487. } = hotkeys;
  21488. if (fullscreenKey.call(this, event)) {
  21489. event.preventDefault();
  21490. event.stopPropagation();
  21491. const FSToggle = Component.getComponent('FullscreenToggle');
  21492. if (document__default["default"][this.fsApi_.fullscreenEnabled] !== false) {
  21493. FSToggle.prototype.handleClick.call(this, event);
  21494. }
  21495. } else if (muteKey.call(this, event)) {
  21496. event.preventDefault();
  21497. event.stopPropagation();
  21498. const MuteToggle = Component.getComponent('MuteToggle');
  21499. MuteToggle.prototype.handleClick.call(this, event);
  21500. } else if (playPauseKey.call(this, event)) {
  21501. event.preventDefault();
  21502. event.stopPropagation();
  21503. const PlayToggle = Component.getComponent('PlayToggle');
  21504. PlayToggle.prototype.handleClick.call(this, event);
  21505. }
  21506. }
  21507. /**
  21508. * Check whether the player can play a given mimetype
  21509. *
  21510. * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
  21511. *
  21512. * @param {string} type
  21513. * The mimetype to check
  21514. *
  21515. * @return {string}
  21516. * 'probably', 'maybe', or '' (empty string)
  21517. */
  21518. canPlayType(type) {
  21519. let can;
  21520. // Loop through each playback technology in the options order
  21521. for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
  21522. const techName = j[i];
  21523. let tech = Tech.getTech(techName);
  21524. // Support old behavior of techs being registered as components.
  21525. // Remove once that deprecated behavior is removed.
  21526. if (!tech) {
  21527. tech = Component.getComponent(techName);
  21528. }
  21529. // Check if the current tech is defined before continuing
  21530. if (!tech) {
  21531. log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
  21532. continue;
  21533. }
  21534. // Check if the browser supports this technology
  21535. if (tech.isSupported()) {
  21536. can = tech.canPlayType(type);
  21537. if (can) {
  21538. return can;
  21539. }
  21540. }
  21541. }
  21542. return '';
  21543. }
  21544. /**
  21545. * Select source based on tech-order or source-order
  21546. * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
  21547. * defaults to tech-order selection
  21548. *
  21549. * @param {Array} sources
  21550. * The sources for a media asset
  21551. *
  21552. * @return {Object|boolean}
  21553. * Object of source and tech order or false
  21554. */
  21555. selectSource(sources) {
  21556. // Get only the techs specified in `techOrder` that exist and are supported by the
  21557. // current platform
  21558. const techs = this.options_.techOrder.map(techName => {
  21559. return [techName, Tech.getTech(techName)];
  21560. }).filter(([techName, tech]) => {
  21561. // Check if the current tech is defined before continuing
  21562. if (tech) {
  21563. // Check if the browser supports this technology
  21564. return tech.isSupported();
  21565. }
  21566. log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
  21567. return false;
  21568. });
  21569. // Iterate over each `innerArray` element once per `outerArray` element and execute
  21570. // `tester` with both. If `tester` returns a non-falsy value, exit early and return
  21571. // that value.
  21572. const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
  21573. let found;
  21574. outerArray.some(outerChoice => {
  21575. return innerArray.some(innerChoice => {
  21576. found = tester(outerChoice, innerChoice);
  21577. if (found) {
  21578. return true;
  21579. }
  21580. });
  21581. });
  21582. return found;
  21583. };
  21584. let foundSourceAndTech;
  21585. const flip = fn => (a, b) => fn(b, a);
  21586. const finder = ([techName, tech], source) => {
  21587. if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
  21588. return {
  21589. source,
  21590. tech: techName
  21591. };
  21592. }
  21593. };
  21594. // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
  21595. // to select from them based on their priority.
  21596. if (this.options_.sourceOrder) {
  21597. // Source-first ordering
  21598. foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
  21599. } else {
  21600. // Tech-first ordering
  21601. foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
  21602. }
  21603. return foundSourceAndTech || false;
  21604. }
  21605. /**
  21606. * Executes source setting and getting logic
  21607. *
  21608. * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
  21609. * A SourceObject, an array of SourceObjects, or a string referencing
  21610. * a URL to a media source. It is _highly recommended_ that an object
  21611. * or array of objects is used here, so that source selection
  21612. * algorithms can take the `type` into account.
  21613. *
  21614. * If not provided, this method acts as a getter.
  21615. * @param {boolean} [isRetry]
  21616. * Indicates whether this is being called internally as a result of a retry
  21617. *
  21618. * @return {string|undefined}
  21619. * If the `source` argument is missing, returns the current source
  21620. * URL. Otherwise, returns nothing/undefined.
  21621. */
  21622. handleSrc_(source, isRetry) {
  21623. // getter usage
  21624. if (typeof source === 'undefined') {
  21625. return this.cache_.src || '';
  21626. }
  21627. // Reset retry behavior for new source
  21628. if (this.resetRetryOnError_) {
  21629. this.resetRetryOnError_();
  21630. }
  21631. // filter out invalid sources and turn our source into
  21632. // an array of source objects
  21633. const sources = filterSource(source);
  21634. // if a source was passed in then it is invalid because
  21635. // it was filtered to a zero length Array. So we have to
  21636. // show an error
  21637. if (!sources.length) {
  21638. this.setTimeout(function () {
  21639. this.error({
  21640. code: 4,
  21641. message: this.options_.notSupportedMessage
  21642. });
  21643. }, 0);
  21644. return;
  21645. }
  21646. // initial sources
  21647. this.changingSrc_ = true;
  21648. // Only update the cached source list if we are not retrying a new source after error,
  21649. // since in that case we want to include the failed source(s) in the cache
  21650. if (!isRetry) {
  21651. this.cache_.sources = sources;
  21652. }
  21653. this.updateSourceCaches_(sources[0]);
  21654. // middlewareSource is the source after it has been changed by middleware
  21655. setSource(this, sources[0], (middlewareSource, mws) => {
  21656. this.middleware_ = mws;
  21657. // since sourceSet is async we have to update the cache again after we select a source since
  21658. // the source that is selected could be out of order from the cache update above this callback.
  21659. if (!isRetry) {
  21660. this.cache_.sources = sources;
  21661. }
  21662. this.updateSourceCaches_(middlewareSource);
  21663. const err = this.src_(middlewareSource);
  21664. if (err) {
  21665. if (sources.length > 1) {
  21666. return this.handleSrc_(sources.slice(1));
  21667. }
  21668. this.changingSrc_ = false;
  21669. // We need to wrap this in a timeout to give folks a chance to add error event handlers
  21670. this.setTimeout(function () {
  21671. this.error({
  21672. code: 4,
  21673. message: this.options_.notSupportedMessage
  21674. });
  21675. }, 0);
  21676. // we could not find an appropriate tech, but let's still notify the delegate that this is it
  21677. // this needs a better comment about why this is needed
  21678. this.triggerReady();
  21679. return;
  21680. }
  21681. setTech(mws, this.tech_);
  21682. });
  21683. // Try another available source if this one fails before playback.
  21684. if (sources.length > 1) {
  21685. const retry = () => {
  21686. // Remove the error modal
  21687. this.error(null);
  21688. this.handleSrc_(sources.slice(1), true);
  21689. };
  21690. const stopListeningForErrors = () => {
  21691. this.off('error', retry);
  21692. };
  21693. this.one('error', retry);
  21694. this.one('playing', stopListeningForErrors);
  21695. this.resetRetryOnError_ = () => {
  21696. this.off('error', retry);
  21697. this.off('playing', stopListeningForErrors);
  21698. };
  21699. }
  21700. }
  21701. /**
  21702. * Get or set the video source.
  21703. *
  21704. * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
  21705. * A SourceObject, an array of SourceObjects, or a string referencing
  21706. * a URL to a media source. It is _highly recommended_ that an object
  21707. * or array of objects is used here, so that source selection
  21708. * algorithms can take the `type` into account.
  21709. *
  21710. * If not provided, this method acts as a getter.
  21711. *
  21712. * @return {string|undefined}
  21713. * If the `source` argument is missing, returns the current source
  21714. * URL. Otherwise, returns nothing/undefined.
  21715. */
  21716. src(source) {
  21717. return this.handleSrc_(source, false);
  21718. }
  21719. /**
  21720. * Set the source object on the tech, returns a boolean that indicates whether
  21721. * there is a tech that can play the source or not
  21722. *
  21723. * @param {Tech~SourceObject} source
  21724. * The source object to set on the Tech
  21725. *
  21726. * @return {boolean}
  21727. * - True if there is no Tech to playback this source
  21728. * - False otherwise
  21729. *
  21730. * @private
  21731. */
  21732. src_(source) {
  21733. const sourceTech = this.selectSource([source]);
  21734. if (!sourceTech) {
  21735. return true;
  21736. }
  21737. if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
  21738. this.changingSrc_ = true;
  21739. // load this technology with the chosen source
  21740. this.loadTech_(sourceTech.tech, sourceTech.source);
  21741. this.tech_.ready(() => {
  21742. this.changingSrc_ = false;
  21743. });
  21744. return false;
  21745. }
  21746. // wait until the tech is ready to set the source
  21747. // and set it synchronously if possible (#2326)
  21748. this.ready(function () {
  21749. // The setSource tech method was added with source handlers
  21750. // so older techs won't support it
  21751. // We need to check the direct prototype for the case where subclasses
  21752. // of the tech do not support source handlers
  21753. if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
  21754. this.techCall_('setSource', source);
  21755. } else {
  21756. this.techCall_('src', source.src);
  21757. }
  21758. this.changingSrc_ = false;
  21759. }, true);
  21760. return false;
  21761. }
  21762. /**
  21763. * Begin loading the src data.
  21764. */
  21765. load() {
  21766. // Workaround to use the load method with the VHS.
  21767. // Does not cover the case when the load method is called directly from the mediaElement.
  21768. if (this.tech_ && this.tech_.vhs) {
  21769. this.src(this.currentSource());
  21770. return;
  21771. }
  21772. this.techCall_('load');
  21773. }
  21774. /**
  21775. * Reset the player. Loads the first tech in the techOrder,
  21776. * removes all the text tracks in the existing `tech`,
  21777. * and calls `reset` on the `tech`.
  21778. */
  21779. reset() {
  21780. if (this.paused()) {
  21781. this.doReset_();
  21782. } else {
  21783. const playPromise = this.play();
  21784. silencePromise(playPromise.then(() => this.doReset_()));
  21785. }
  21786. }
  21787. doReset_() {
  21788. if (this.tech_) {
  21789. this.tech_.clearTracks('text');
  21790. }
  21791. this.removeClass('vjs-playing');
  21792. this.addClass('vjs-paused');
  21793. this.resetCache_();
  21794. this.poster('');
  21795. this.loadTech_(this.options_.techOrder[0], null);
  21796. this.techCall_('reset');
  21797. this.resetControlBarUI_();
  21798. this.error(null);
  21799. if (this.titleBar) {
  21800. this.titleBar.update({
  21801. title: undefined,
  21802. description: undefined
  21803. });
  21804. }
  21805. if (isEvented(this)) {
  21806. this.trigger('playerreset');
  21807. }
  21808. }
  21809. /**
  21810. * Reset Control Bar's UI by calling sub-methods that reset
  21811. * all of Control Bar's components
  21812. */
  21813. resetControlBarUI_() {
  21814. this.resetProgressBar_();
  21815. this.resetPlaybackRate_();
  21816. this.resetVolumeBar_();
  21817. }
  21818. /**
  21819. * Reset tech's progress so progress bar is reset in the UI
  21820. */
  21821. resetProgressBar_() {
  21822. this.currentTime(0);
  21823. const {
  21824. currentTimeDisplay,
  21825. durationDisplay,
  21826. progressControl,
  21827. remainingTimeDisplay
  21828. } = this.controlBar || {};
  21829. const {
  21830. seekBar
  21831. } = progressControl || {};
  21832. if (currentTimeDisplay) {
  21833. currentTimeDisplay.updateContent();
  21834. }
  21835. if (durationDisplay) {
  21836. durationDisplay.updateContent();
  21837. }
  21838. if (remainingTimeDisplay) {
  21839. remainingTimeDisplay.updateContent();
  21840. }
  21841. if (seekBar) {
  21842. seekBar.update();
  21843. if (seekBar.loadProgressBar) {
  21844. seekBar.loadProgressBar.update();
  21845. }
  21846. }
  21847. }
  21848. /**
  21849. * Reset Playback ratio
  21850. */
  21851. resetPlaybackRate_() {
  21852. this.playbackRate(this.defaultPlaybackRate());
  21853. this.handleTechRateChange_();
  21854. }
  21855. /**
  21856. * Reset Volume bar
  21857. */
  21858. resetVolumeBar_() {
  21859. this.volume(1.0);
  21860. this.trigger('volumechange');
  21861. }
  21862. /**
  21863. * Returns all of the current source objects.
  21864. *
  21865. * @return {Tech~SourceObject[]}
  21866. * The current source objects
  21867. */
  21868. currentSources() {
  21869. const source = this.currentSource();
  21870. const sources = [];
  21871. // assume `{}` or `{ src }`
  21872. if (Object.keys(source).length !== 0) {
  21873. sources.push(source);
  21874. }
  21875. return this.cache_.sources || sources;
  21876. }
  21877. /**
  21878. * Returns the current source object.
  21879. *
  21880. * @return {Tech~SourceObject}
  21881. * The current source object
  21882. */
  21883. currentSource() {
  21884. return this.cache_.source || {};
  21885. }
  21886. /**
  21887. * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
  21888. * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
  21889. *
  21890. * @return {string}
  21891. * The current source
  21892. */
  21893. currentSrc() {
  21894. return this.currentSource() && this.currentSource().src || '';
  21895. }
  21896. /**
  21897. * Get the current source type e.g. video/mp4
  21898. * This can allow you rebuild the current source object so that you could load the same
  21899. * source and tech later
  21900. *
  21901. * @return {string}
  21902. * The source MIME type
  21903. */
  21904. currentType() {
  21905. return this.currentSource() && this.currentSource().type || '';
  21906. }
  21907. /**
  21908. * Get or set the preload attribute
  21909. *
  21910. * @param {'none'|'auto'|'metadata'} [value]
  21911. * Preload mode to pass to tech
  21912. *
  21913. * @return {string|undefined}
  21914. * - The preload attribute value when getting
  21915. * - Nothing when setting
  21916. */
  21917. preload(value) {
  21918. if (value !== undefined) {
  21919. this.techCall_('setPreload', value);
  21920. this.options_.preload = value;
  21921. return;
  21922. }
  21923. return this.techGet_('preload');
  21924. }
  21925. /**
  21926. * Get or set the autoplay option. When this is a boolean it will
  21927. * modify the attribute on the tech. When this is a string the attribute on
  21928. * the tech will be removed and `Player` will handle autoplay on loadstarts.
  21929. *
  21930. * @param {boolean|'play'|'muted'|'any'} [value]
  21931. * - true: autoplay using the browser behavior
  21932. * - false: do not autoplay
  21933. * - 'play': call play() on every loadstart
  21934. * - 'muted': call muted() then play() on every loadstart
  21935. * - 'any': call play() on every loadstart. if that fails call muted() then play().
  21936. * - *: values other than those listed here will be set `autoplay` to true
  21937. *
  21938. * @return {boolean|string|undefined}
  21939. * - The current value of autoplay when getting
  21940. * - Nothing when setting
  21941. */
  21942. autoplay(value) {
  21943. // getter usage
  21944. if (value === undefined) {
  21945. return this.options_.autoplay || false;
  21946. }
  21947. let techAutoplay;
  21948. // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
  21949. if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
  21950. this.options_.autoplay = value;
  21951. this.manualAutoplay_(typeof value === 'string' ? value : 'play');
  21952. techAutoplay = false;
  21953. // any falsy value sets autoplay to false in the browser,
  21954. // lets do the same
  21955. } else if (!value) {
  21956. this.options_.autoplay = false;
  21957. // any other value (ie truthy) sets autoplay to true
  21958. } else {
  21959. this.options_.autoplay = true;
  21960. }
  21961. techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
  21962. // if we don't have a tech then we do not queue up
  21963. // a setAutoplay call on tech ready. We do this because the
  21964. // autoplay option will be passed in the constructor and we
  21965. // do not need to set it twice
  21966. if (this.tech_) {
  21967. this.techCall_('setAutoplay', techAutoplay);
  21968. }
  21969. }
  21970. /**
  21971. * Set or unset the playsinline attribute.
  21972. * Playsinline tells the browser that non-fullscreen playback is preferred.
  21973. *
  21974. * @param {boolean} [value]
  21975. * - true means that we should try to play inline by default
  21976. * - false means that we should use the browser's default playback mode,
  21977. * which in most cases is inline. iOS Safari is a notable exception
  21978. * and plays fullscreen by default.
  21979. *
  21980. * @return {string|undefined}
  21981. * - the current value of playsinline
  21982. * - Nothing when setting
  21983. *
  21984. * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
  21985. */
  21986. playsinline(value) {
  21987. if (value !== undefined) {
  21988. this.techCall_('setPlaysinline', value);
  21989. this.options_.playsinline = value;
  21990. }
  21991. return this.techGet_('playsinline');
  21992. }
  21993. /**
  21994. * Get or set the loop attribute on the video element.
  21995. *
  21996. * @param {boolean} [value]
  21997. * - true means that we should loop the video
  21998. * - false means that we should not loop the video
  21999. *
  22000. * @return {boolean|undefined}
  22001. * - The current value of loop when getting
  22002. * - Nothing when setting
  22003. */
  22004. loop(value) {
  22005. if (value !== undefined) {
  22006. this.techCall_('setLoop', value);
  22007. this.options_.loop = value;
  22008. return;
  22009. }
  22010. return this.techGet_('loop');
  22011. }
  22012. /**
  22013. * Get or set the poster image source url
  22014. *
  22015. * @fires Player#posterchange
  22016. *
  22017. * @param {string} [src]
  22018. * Poster image source URL
  22019. *
  22020. * @return {string|undefined}
  22021. * - The current value of poster when getting
  22022. * - Nothing when setting
  22023. */
  22024. poster(src) {
  22025. if (src === undefined) {
  22026. return this.poster_;
  22027. }
  22028. // The correct way to remove a poster is to set as an empty string
  22029. // other falsey values will throw errors
  22030. if (!src) {
  22031. src = '';
  22032. }
  22033. if (src === this.poster_) {
  22034. return;
  22035. }
  22036. // update the internal poster variable
  22037. this.poster_ = src;
  22038. // update the tech's poster
  22039. this.techCall_('setPoster', src);
  22040. this.isPosterFromTech_ = false;
  22041. // alert components that the poster has been set
  22042. /**
  22043. * This event fires when the poster image is changed on the player.
  22044. *
  22045. * @event Player#posterchange
  22046. * @type {Event}
  22047. */
  22048. this.trigger('posterchange');
  22049. }
  22050. /**
  22051. * Some techs (e.g. YouTube) can provide a poster source in an
  22052. * asynchronous way. We want the poster component to use this
  22053. * poster source so that it covers up the tech's controls.
  22054. * (YouTube's play button). However we only want to use this
  22055. * source if the player user hasn't set a poster through
  22056. * the normal APIs.
  22057. *
  22058. * @fires Player#posterchange
  22059. * @listens Tech#posterchange
  22060. * @private
  22061. */
  22062. handleTechPosterChange_() {
  22063. if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
  22064. const newPoster = this.tech_.poster() || '';
  22065. if (newPoster !== this.poster_) {
  22066. this.poster_ = newPoster;
  22067. this.isPosterFromTech_ = true;
  22068. // Let components know the poster has changed
  22069. this.trigger('posterchange');
  22070. }
  22071. }
  22072. }
  22073. /**
  22074. * Get or set whether or not the controls are showing.
  22075. *
  22076. * @fires Player#controlsenabled
  22077. *
  22078. * @param {boolean} [bool]
  22079. * - true to turn controls on
  22080. * - false to turn controls off
  22081. *
  22082. * @return {boolean|undefined}
  22083. * - The current value of controls when getting
  22084. * - Nothing when setting
  22085. */
  22086. controls(bool) {
  22087. if (bool === undefined) {
  22088. return !!this.controls_;
  22089. }
  22090. bool = !!bool;
  22091. // Don't trigger a change event unless it actually changed
  22092. if (this.controls_ === bool) {
  22093. return;
  22094. }
  22095. this.controls_ = bool;
  22096. if (this.usingNativeControls()) {
  22097. this.techCall_('setControls', bool);
  22098. }
  22099. if (this.controls_) {
  22100. this.removeClass('vjs-controls-disabled');
  22101. this.addClass('vjs-controls-enabled');
  22102. /**
  22103. * @event Player#controlsenabled
  22104. * @type {Event}
  22105. */
  22106. this.trigger('controlsenabled');
  22107. if (!this.usingNativeControls()) {
  22108. this.addTechControlsListeners_();
  22109. }
  22110. } else {
  22111. this.removeClass('vjs-controls-enabled');
  22112. this.addClass('vjs-controls-disabled');
  22113. /**
  22114. * @event Player#controlsdisabled
  22115. * @type {Event}
  22116. */
  22117. this.trigger('controlsdisabled');
  22118. if (!this.usingNativeControls()) {
  22119. this.removeTechControlsListeners_();
  22120. }
  22121. }
  22122. }
  22123. /**
  22124. * Toggle native controls on/off. Native controls are the controls built into
  22125. * devices (e.g. default iPhone controls) or other techs
  22126. * (e.g. Vimeo Controls)
  22127. * **This should only be set by the current tech, because only the tech knows
  22128. * if it can support native controls**
  22129. *
  22130. * @fires Player#usingnativecontrols
  22131. * @fires Player#usingcustomcontrols
  22132. *
  22133. * @param {boolean} [bool]
  22134. * - true to turn native controls on
  22135. * - false to turn native controls off
  22136. *
  22137. * @return {boolean|undefined}
  22138. * - The current value of native controls when getting
  22139. * - Nothing when setting
  22140. */
  22141. usingNativeControls(bool) {
  22142. if (bool === undefined) {
  22143. return !!this.usingNativeControls_;
  22144. }
  22145. bool = !!bool;
  22146. // Don't trigger a change event unless it actually changed
  22147. if (this.usingNativeControls_ === bool) {
  22148. return;
  22149. }
  22150. this.usingNativeControls_ = bool;
  22151. if (this.usingNativeControls_) {
  22152. this.addClass('vjs-using-native-controls');
  22153. /**
  22154. * player is using the native device controls
  22155. *
  22156. * @event Player#usingnativecontrols
  22157. * @type {Event}
  22158. */
  22159. this.trigger('usingnativecontrols');
  22160. } else {
  22161. this.removeClass('vjs-using-native-controls');
  22162. /**
  22163. * player is using the custom HTML controls
  22164. *
  22165. * @event Player#usingcustomcontrols
  22166. * @type {Event}
  22167. */
  22168. this.trigger('usingcustomcontrols');
  22169. }
  22170. }
  22171. /**
  22172. * Set or get the current MediaError
  22173. *
  22174. * @fires Player#error
  22175. *
  22176. * @param {MediaError|string|number} [err]
  22177. * A MediaError or a string/number to be turned
  22178. * into a MediaError
  22179. *
  22180. * @return {MediaError|null|undefined}
  22181. * - The current MediaError when getting (or null)
  22182. * - Nothing when setting
  22183. */
  22184. error(err) {
  22185. if (err === undefined) {
  22186. return this.error_ || null;
  22187. }
  22188. // allow hooks to modify error object
  22189. hooks('beforeerror').forEach(hookFunction => {
  22190. const newErr = hookFunction(this, err);
  22191. if (!(isObject(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
  22192. this.log.error('please return a value that MediaError expects in beforeerror hooks');
  22193. return;
  22194. }
  22195. err = newErr;
  22196. });
  22197. // Suppress the first error message for no compatible source until
  22198. // user interaction
  22199. if (this.options_.suppressNotSupportedError && err && err.code === 4) {
  22200. const triggerSuppressedError = function () {
  22201. this.error(err);
  22202. };
  22203. this.options_.suppressNotSupportedError = false;
  22204. this.any(['click', 'touchstart'], triggerSuppressedError);
  22205. this.one('loadstart', function () {
  22206. this.off(['click', 'touchstart'], triggerSuppressedError);
  22207. });
  22208. return;
  22209. }
  22210. // restoring to default
  22211. if (err === null) {
  22212. this.error_ = null;
  22213. this.removeClass('vjs-error');
  22214. if (this.errorDisplay) {
  22215. this.errorDisplay.close();
  22216. }
  22217. return;
  22218. }
  22219. this.error_ = new MediaError(err);
  22220. // add the vjs-error classname to the player
  22221. this.addClass('vjs-error');
  22222. // log the name of the error type and any message
  22223. // IE11 logs "[object object]" and required you to expand message to see error object
  22224. log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
  22225. /**
  22226. * @event Player#error
  22227. * @type {Event}
  22228. */
  22229. this.trigger('error');
  22230. // notify hooks of the per player error
  22231. hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
  22232. return;
  22233. }
  22234. /**
  22235. * Report user activity
  22236. *
  22237. * @param {Object} event
  22238. * Event object
  22239. */
  22240. reportUserActivity(event) {
  22241. this.userActivity_ = true;
  22242. }
  22243. /**
  22244. * Get/set if user is active
  22245. *
  22246. * @fires Player#useractive
  22247. * @fires Player#userinactive
  22248. *
  22249. * @param {boolean} [bool]
  22250. * - true if the user is active
  22251. * - false if the user is inactive
  22252. *
  22253. * @return {boolean|undefined}
  22254. * - The current value of userActive when getting
  22255. * - Nothing when setting
  22256. */
  22257. userActive(bool) {
  22258. if (bool === undefined) {
  22259. return this.userActive_;
  22260. }
  22261. bool = !!bool;
  22262. if (bool === this.userActive_) {
  22263. return;
  22264. }
  22265. this.userActive_ = bool;
  22266. if (this.userActive_) {
  22267. this.userActivity_ = true;
  22268. this.removeClass('vjs-user-inactive');
  22269. this.addClass('vjs-user-active');
  22270. /**
  22271. * @event Player#useractive
  22272. * @type {Event}
  22273. */
  22274. this.trigger('useractive');
  22275. return;
  22276. }
  22277. // Chrome/Safari/IE have bugs where when you change the cursor it can
  22278. // trigger a mousemove event. This causes an issue when you're hiding
  22279. // the cursor when the user is inactive, and a mousemove signals user
  22280. // activity. Making it impossible to go into inactive mode. Specifically
  22281. // this happens in fullscreen when we really need to hide the cursor.
  22282. //
  22283. // When this gets resolved in ALL browsers it can be removed
  22284. // https://code.google.com/p/chromium/issues/detail?id=103041
  22285. if (this.tech_) {
  22286. this.tech_.one('mousemove', function (e) {
  22287. e.stopPropagation();
  22288. e.preventDefault();
  22289. });
  22290. }
  22291. this.userActivity_ = false;
  22292. this.removeClass('vjs-user-active');
  22293. this.addClass('vjs-user-inactive');
  22294. /**
  22295. * @event Player#userinactive
  22296. * @type {Event}
  22297. */
  22298. this.trigger('userinactive');
  22299. }
  22300. /**
  22301. * Listen for user activity based on timeout value
  22302. *
  22303. * @private
  22304. */
  22305. listenForUserActivity_() {
  22306. let mouseInProgress;
  22307. let lastMoveX;
  22308. let lastMoveY;
  22309. const handleActivity = bind_(this, this.reportUserActivity);
  22310. const handleMouseMove = function (e) {
  22311. // #1068 - Prevent mousemove spamming
  22312. // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
  22313. if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
  22314. lastMoveX = e.screenX;
  22315. lastMoveY = e.screenY;
  22316. handleActivity();
  22317. }
  22318. };
  22319. const handleMouseDown = function () {
  22320. handleActivity();
  22321. // For as long as the they are touching the device or have their mouse down,
  22322. // we consider them active even if they're not moving their finger or mouse.
  22323. // So we want to continue to update that they are active
  22324. this.clearInterval(mouseInProgress);
  22325. // Setting userActivity=true now and setting the interval to the same time
  22326. // as the activityCheck interval (250) should ensure we never miss the
  22327. // next activityCheck
  22328. mouseInProgress = this.setInterval(handleActivity, 250);
  22329. };
  22330. const handleMouseUpAndMouseLeave = function (event) {
  22331. handleActivity();
  22332. // Stop the interval that maintains activity if the mouse/touch is down
  22333. this.clearInterval(mouseInProgress);
  22334. };
  22335. // Any mouse movement will be considered user activity
  22336. this.on('mousedown', handleMouseDown);
  22337. this.on('mousemove', handleMouseMove);
  22338. this.on('mouseup', handleMouseUpAndMouseLeave);
  22339. this.on('mouseleave', handleMouseUpAndMouseLeave);
  22340. const controlBar = this.getChild('controlBar');
  22341. // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
  22342. // controlBar would no longer be hidden by default timeout.
  22343. if (controlBar && !IS_IOS && !IS_ANDROID) {
  22344. controlBar.on('mouseenter', function (event) {
  22345. if (this.player().options_.inactivityTimeout !== 0) {
  22346. this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
  22347. }
  22348. this.player().options_.inactivityTimeout = 0;
  22349. });
  22350. controlBar.on('mouseleave', function (event) {
  22351. this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
  22352. });
  22353. }
  22354. // Listen for keyboard navigation
  22355. // Shouldn't need to use inProgress interval because of key repeat
  22356. this.on('keydown', handleActivity);
  22357. this.on('keyup', handleActivity);
  22358. // Run an interval every 250 milliseconds instead of stuffing everything into
  22359. // the mousemove/touchmove function itself, to prevent performance degradation.
  22360. // `this.reportUserActivity` simply sets this.userActivity_ to true, which
  22361. // then gets picked up by this loop
  22362. // http://ejohn.org/blog/learning-from-twitter/
  22363. let inactivityTimeout;
  22364. /** @this Player */
  22365. const activityCheck = function () {
  22366. // Check to see if mouse/touch activity has happened
  22367. if (!this.userActivity_) {
  22368. return;
  22369. }
  22370. // Reset the activity tracker
  22371. this.userActivity_ = false;
  22372. // If the user state was inactive, set the state to active
  22373. this.userActive(true);
  22374. // Clear any existing inactivity timeout to start the timer over
  22375. this.clearTimeout(inactivityTimeout);
  22376. const timeout = this.options_.inactivityTimeout;
  22377. if (timeout <= 0) {
  22378. return;
  22379. }
  22380. // In <timeout> milliseconds, if no more activity has occurred the
  22381. // user will be considered inactive
  22382. inactivityTimeout = this.setTimeout(function () {
  22383. // Protect against the case where the inactivityTimeout can trigger just
  22384. // before the next user activity is picked up by the activity check loop
  22385. // causing a flicker
  22386. if (!this.userActivity_) {
  22387. this.userActive(false);
  22388. }
  22389. }, timeout);
  22390. };
  22391. this.setInterval(activityCheck, 250);
  22392. }
  22393. /**
  22394. * Gets or sets the current playback rate. A playback rate of
  22395. * 1.0 represents normal speed and 0.5 would indicate half-speed
  22396. * playback, for instance.
  22397. *
  22398. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
  22399. *
  22400. * @param {number} [rate]
  22401. * New playback rate to set.
  22402. *
  22403. * @return {number|undefined}
  22404. * - The current playback rate when getting or 1.0
  22405. * - Nothing when setting
  22406. */
  22407. playbackRate(rate) {
  22408. if (rate !== undefined) {
  22409. // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
  22410. // that is registered above
  22411. this.techCall_('setPlaybackRate', rate);
  22412. return;
  22413. }
  22414. if (this.tech_ && this.tech_.featuresPlaybackRate) {
  22415. return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
  22416. }
  22417. return 1.0;
  22418. }
  22419. /**
  22420. * Gets or sets the current default playback rate. A default playback rate of
  22421. * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
  22422. * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
  22423. * not the current playbackRate.
  22424. *
  22425. * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
  22426. *
  22427. * @param {number} [rate]
  22428. * New default playback rate to set.
  22429. *
  22430. * @return {number|undefined}
  22431. * - The default playback rate when getting or 1.0
  22432. * - Nothing when setting
  22433. */
  22434. defaultPlaybackRate(rate) {
  22435. if (rate !== undefined) {
  22436. return this.techCall_('setDefaultPlaybackRate', rate);
  22437. }
  22438. if (this.tech_ && this.tech_.featuresPlaybackRate) {
  22439. return this.techGet_('defaultPlaybackRate');
  22440. }
  22441. return 1.0;
  22442. }
  22443. /**
  22444. * Gets or sets the audio flag
  22445. *
  22446. * @param {boolean} [bool]
  22447. * - true signals that this is an audio player
  22448. * - false signals that this is not an audio player
  22449. *
  22450. * @return {boolean|undefined}
  22451. * - The current value of isAudio when getting
  22452. * - Nothing when setting
  22453. */
  22454. isAudio(bool) {
  22455. if (bool !== undefined) {
  22456. this.isAudio_ = !!bool;
  22457. return;
  22458. }
  22459. return !!this.isAudio_;
  22460. }
  22461. enableAudioOnlyUI_() {
  22462. // Update styling immediately to show the control bar so we can get its height
  22463. this.addClass('vjs-audio-only-mode');
  22464. const playerChildren = this.children();
  22465. const controlBar = this.getChild('ControlBar');
  22466. const controlBarHeight = controlBar && controlBar.currentHeight();
  22467. // Hide all player components except the control bar. Control bar components
  22468. // needed only for video are hidden with CSS
  22469. playerChildren.forEach(child => {
  22470. if (child === controlBar) {
  22471. return;
  22472. }
  22473. if (child.el_ && !child.hasClass('vjs-hidden')) {
  22474. child.hide();
  22475. this.audioOnlyCache_.hiddenChildren.push(child);
  22476. }
  22477. });
  22478. this.audioOnlyCache_.playerHeight = this.currentHeight();
  22479. // Set the player height the same as the control bar
  22480. this.height(controlBarHeight);
  22481. this.trigger('audioonlymodechange');
  22482. }
  22483. disableAudioOnlyUI_() {
  22484. this.removeClass('vjs-audio-only-mode');
  22485. // Show player components that were previously hidden
  22486. this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
  22487. // Reset player height
  22488. this.height(this.audioOnlyCache_.playerHeight);
  22489. this.trigger('audioonlymodechange');
  22490. }
  22491. /**
  22492. * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
  22493. *
  22494. * Setting this to `true` will hide all player components except the control bar,
  22495. * as well as control bar components needed only for video.
  22496. *
  22497. * @param {boolean} [value]
  22498. * The value to set audioOnlyMode to.
  22499. *
  22500. * @return {Promise|boolean}
  22501. * A Promise is returned when setting the state, and a boolean when getting
  22502. * the present state
  22503. */
  22504. audioOnlyMode(value) {
  22505. if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
  22506. return this.audioOnlyMode_;
  22507. }
  22508. this.audioOnlyMode_ = value;
  22509. // Enable Audio Only Mode
  22510. if (value) {
  22511. const exitPromises = [];
  22512. // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
  22513. if (this.isInPictureInPicture()) {
  22514. exitPromises.push(this.exitPictureInPicture());
  22515. }
  22516. if (this.isFullscreen()) {
  22517. exitPromises.push(this.exitFullscreen());
  22518. }
  22519. if (this.audioPosterMode()) {
  22520. exitPromises.push(this.audioPosterMode(false));
  22521. }
  22522. return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
  22523. }
  22524. // Disable Audio Only Mode
  22525. return Promise.resolve().then(() => this.disableAudioOnlyUI_());
  22526. }
  22527. enablePosterModeUI_() {
  22528. // Hide the video element and show the poster image to enable posterModeUI
  22529. const tech = this.tech_ && this.tech_;
  22530. tech.hide();
  22531. this.addClass('vjs-audio-poster-mode');
  22532. this.trigger('audiopostermodechange');
  22533. }
  22534. disablePosterModeUI_() {
  22535. // Show the video element and hide the poster image to disable posterModeUI
  22536. const tech = this.tech_ && this.tech_;
  22537. tech.show();
  22538. this.removeClass('vjs-audio-poster-mode');
  22539. this.trigger('audiopostermodechange');
  22540. }
  22541. /**
  22542. * Get the current audioPosterMode state or set audioPosterMode to true or false
  22543. *
  22544. * @param {boolean} [value]
  22545. * The value to set audioPosterMode to.
  22546. *
  22547. * @return {Promise|boolean}
  22548. * A Promise is returned when setting the state, and a boolean when getting
  22549. * the present state
  22550. */
  22551. audioPosterMode(value) {
  22552. if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
  22553. return this.audioPosterMode_;
  22554. }
  22555. this.audioPosterMode_ = value;
  22556. if (value) {
  22557. if (this.audioOnlyMode()) {
  22558. const audioOnlyModePromise = this.audioOnlyMode(false);
  22559. return audioOnlyModePromise.then(() => {
  22560. // enable audio poster mode after audio only mode is disabled
  22561. this.enablePosterModeUI_();
  22562. });
  22563. }
  22564. return Promise.resolve().then(() => {
  22565. // enable audio poster mode
  22566. this.enablePosterModeUI_();
  22567. });
  22568. }
  22569. return Promise.resolve().then(() => {
  22570. // disable audio poster mode
  22571. this.disablePosterModeUI_();
  22572. });
  22573. }
  22574. /**
  22575. * A helper method for adding a {@link TextTrack} to our
  22576. * {@link TextTrackList}.
  22577. *
  22578. * In addition to the W3C settings we allow adding additional info through options.
  22579. *
  22580. * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
  22581. *
  22582. * @param {string} [kind]
  22583. * the kind of TextTrack you are adding
  22584. *
  22585. * @param {string} [label]
  22586. * the label to give the TextTrack label
  22587. *
  22588. * @param {string} [language]
  22589. * the language to set on the TextTrack
  22590. *
  22591. * @return {TextTrack|undefined}
  22592. * the TextTrack that was added or undefined
  22593. * if there is no tech
  22594. */
  22595. addTextTrack(kind, label, language) {
  22596. if (this.tech_) {
  22597. return this.tech_.addTextTrack(kind, label, language);
  22598. }
  22599. }
  22600. /**
  22601. * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
  22602. *
  22603. * @param {Object} options
  22604. * Options to pass to {@link HTMLTrackElement} during creation. See
  22605. * {@link HTMLTrackElement} for object properties that you should use.
  22606. *
  22607. * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
  22608. * from the TextTrackList and HtmlTrackElementList
  22609. * after a source change
  22610. *
  22611. * @return { import('./tracks/html-track-element').default }
  22612. * the HTMLTrackElement that was created and added
  22613. * to the HtmlTrackElementList and the remote
  22614. * TextTrackList
  22615. *
  22616. */
  22617. addRemoteTextTrack(options, manualCleanup) {
  22618. if (this.tech_) {
  22619. return this.tech_.addRemoteTextTrack(options, manualCleanup);
  22620. }
  22621. }
  22622. /**
  22623. * Remove a remote {@link TextTrack} from the respective
  22624. * {@link TextTrackList} and {@link HtmlTrackElementList}.
  22625. *
  22626. * @param {Object} track
  22627. * Remote {@link TextTrack} to remove
  22628. *
  22629. * @return {undefined}
  22630. * does not return anything
  22631. */
  22632. removeRemoteTextTrack(obj = {}) {
  22633. let {
  22634. track
  22635. } = obj;
  22636. if (!track) {
  22637. track = obj;
  22638. }
  22639. // destructure the input into an object with a track argument, defaulting to arguments[0]
  22640. // default the whole argument to an empty object if nothing was passed in
  22641. if (this.tech_) {
  22642. return this.tech_.removeRemoteTextTrack(track);
  22643. }
  22644. }
  22645. /**
  22646. * Gets available media playback quality metrics as specified by the W3C's Media
  22647. * Playback Quality API.
  22648. *
  22649. * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
  22650. *
  22651. * @return {Object|undefined}
  22652. * An object with supported media playback quality metrics or undefined if there
  22653. * is no tech or the tech does not support it.
  22654. */
  22655. getVideoPlaybackQuality() {
  22656. return this.techGet_('getVideoPlaybackQuality');
  22657. }
  22658. /**
  22659. * Get video width
  22660. *
  22661. * @return {number}
  22662. * current video width
  22663. */
  22664. videoWidth() {
  22665. return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
  22666. }
  22667. /**
  22668. * Get video height
  22669. *
  22670. * @return {number}
  22671. * current video height
  22672. */
  22673. videoHeight() {
  22674. return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
  22675. }
  22676. /**
  22677. * Set or get the player's language code.
  22678. *
  22679. * Changing the language will trigger
  22680. * [languagechange]{@link Player#event:languagechange}
  22681. * which Components can use to update control text.
  22682. * ClickableComponent will update its control text by default on
  22683. * [languagechange]{@link Player#event:languagechange}.
  22684. *
  22685. * @fires Player#languagechange
  22686. *
  22687. * @param {string} [code]
  22688. * the language code to set the player to
  22689. *
  22690. * @return {string|undefined}
  22691. * - The current language code when getting
  22692. * - Nothing when setting
  22693. */
  22694. language(code) {
  22695. if (code === undefined) {
  22696. return this.language_;
  22697. }
  22698. if (this.language_ !== String(code).toLowerCase()) {
  22699. this.language_ = String(code).toLowerCase();
  22700. // during first init, it's possible some things won't be evented
  22701. if (isEvented(this)) {
  22702. /**
  22703. * fires when the player language change
  22704. *
  22705. * @event Player#languagechange
  22706. * @type {Event}
  22707. */
  22708. this.trigger('languagechange');
  22709. }
  22710. }
  22711. }
  22712. /**
  22713. * Get the player's language dictionary
  22714. * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
  22715. * Languages specified directly in the player options have precedence
  22716. *
  22717. * @return {Array}
  22718. * An array of of supported languages
  22719. */
  22720. languages() {
  22721. return merge(Player.prototype.options_.languages, this.languages_);
  22722. }
  22723. /**
  22724. * returns a JavaScript object representing the current track
  22725. * information. **DOES not return it as JSON**
  22726. *
  22727. * @return {Object}
  22728. * Object representing the current of track info
  22729. */
  22730. toJSON() {
  22731. const options = merge(this.options_);
  22732. const tracks = options.tracks;
  22733. options.tracks = [];
  22734. for (let i = 0; i < tracks.length; i++) {
  22735. let track = tracks[i];
  22736. // deep merge tracks and null out player so no circular references
  22737. track = merge(track);
  22738. track.player = undefined;
  22739. options.tracks[i] = track;
  22740. }
  22741. return options;
  22742. }
  22743. /**
  22744. * Creates a simple modal dialog (an instance of the {@link ModalDialog}
  22745. * component) that immediately overlays the player with arbitrary
  22746. * content and removes itself when closed.
  22747. *
  22748. * @param {string|Function|Element|Array|null} content
  22749. * Same as {@link ModalDialog#content}'s param of the same name.
  22750. * The most straight-forward usage is to provide a string or DOM
  22751. * element.
  22752. *
  22753. * @param {Object} [options]
  22754. * Extra options which will be passed on to the {@link ModalDialog}.
  22755. *
  22756. * @return {ModalDialog}
  22757. * the {@link ModalDialog} that was created
  22758. */
  22759. createModal(content, options) {
  22760. options = options || {};
  22761. options.content = content || '';
  22762. const modal = new ModalDialog(this, options);
  22763. this.addChild(modal);
  22764. modal.on('dispose', () => {
  22765. this.removeChild(modal);
  22766. });
  22767. modal.open();
  22768. return modal;
  22769. }
  22770. /**
  22771. * Change breakpoint classes when the player resizes.
  22772. *
  22773. * @private
  22774. */
  22775. updateCurrentBreakpoint_() {
  22776. if (!this.responsive()) {
  22777. return;
  22778. }
  22779. const currentBreakpoint = this.currentBreakpoint();
  22780. const currentWidth = this.currentWidth();
  22781. for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
  22782. const candidateBreakpoint = BREAKPOINT_ORDER[i];
  22783. const maxWidth = this.breakpoints_[candidateBreakpoint];
  22784. if (currentWidth <= maxWidth) {
  22785. // The current breakpoint did not change, nothing to do.
  22786. if (currentBreakpoint === candidateBreakpoint) {
  22787. return;
  22788. }
  22789. // Only remove a class if there is a current breakpoint.
  22790. if (currentBreakpoint) {
  22791. this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
  22792. }
  22793. this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
  22794. this.breakpoint_ = candidateBreakpoint;
  22795. break;
  22796. }
  22797. }
  22798. }
  22799. /**
  22800. * Removes the current breakpoint.
  22801. *
  22802. * @private
  22803. */
  22804. removeCurrentBreakpoint_() {
  22805. const className = this.currentBreakpointClass();
  22806. this.breakpoint_ = '';
  22807. if (className) {
  22808. this.removeClass(className);
  22809. }
  22810. }
  22811. /**
  22812. * Get or set breakpoints on the player.
  22813. *
  22814. * Calling this method with an object or `true` will remove any previous
  22815. * custom breakpoints and start from the defaults again.
  22816. *
  22817. * @param {Object|boolean} [breakpoints]
  22818. * If an object is given, it can be used to provide custom
  22819. * breakpoints. If `true` is given, will set default breakpoints.
  22820. * If this argument is not given, will simply return the current
  22821. * breakpoints.
  22822. *
  22823. * @param {number} [breakpoints.tiny]
  22824. * The maximum width for the "vjs-layout-tiny" class.
  22825. *
  22826. * @param {number} [breakpoints.xsmall]
  22827. * The maximum width for the "vjs-layout-x-small" class.
  22828. *
  22829. * @param {number} [breakpoints.small]
  22830. * The maximum width for the "vjs-layout-small" class.
  22831. *
  22832. * @param {number} [breakpoints.medium]
  22833. * The maximum width for the "vjs-layout-medium" class.
  22834. *
  22835. * @param {number} [breakpoints.large]
  22836. * The maximum width for the "vjs-layout-large" class.
  22837. *
  22838. * @param {number} [breakpoints.xlarge]
  22839. * The maximum width for the "vjs-layout-x-large" class.
  22840. *
  22841. * @param {number} [breakpoints.huge]
  22842. * The maximum width for the "vjs-layout-huge" class.
  22843. *
  22844. * @return {Object}
  22845. * An object mapping breakpoint names to maximum width values.
  22846. */
  22847. breakpoints(breakpoints) {
  22848. // Used as a getter.
  22849. if (breakpoints === undefined) {
  22850. return Object.assign(this.breakpoints_);
  22851. }
  22852. this.breakpoint_ = '';
  22853. this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
  22854. // When breakpoint definitions change, we need to update the currently
  22855. // selected breakpoint.
  22856. this.updateCurrentBreakpoint_();
  22857. // Clone the breakpoints before returning.
  22858. return Object.assign(this.breakpoints_);
  22859. }
  22860. /**
  22861. * Get or set a flag indicating whether or not this player should adjust
  22862. * its UI based on its dimensions.
  22863. *
  22864. * @param {boolean} [value]
  22865. * Should be `true` if the player should adjust its UI based on its
  22866. * dimensions; otherwise, should be `false`.
  22867. *
  22868. * @return {boolean|undefined}
  22869. * Will be `true` if this player should adjust its UI based on its
  22870. * dimensions; otherwise, will be `false`.
  22871. * Nothing if setting
  22872. */
  22873. responsive(value) {
  22874. // Used as a getter.
  22875. if (value === undefined) {
  22876. return this.responsive_;
  22877. }
  22878. value = Boolean(value);
  22879. const current = this.responsive_;
  22880. // Nothing changed.
  22881. if (value === current) {
  22882. return;
  22883. }
  22884. // The value actually changed, set it.
  22885. this.responsive_ = value;
  22886. // Start listening for breakpoints and set the initial breakpoint if the
  22887. // player is now responsive.
  22888. if (value) {
  22889. this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
  22890. this.updateCurrentBreakpoint_();
  22891. // Stop listening for breakpoints if the player is no longer responsive.
  22892. } else {
  22893. this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
  22894. this.removeCurrentBreakpoint_();
  22895. }
  22896. return value;
  22897. }
  22898. /**
  22899. * Get current breakpoint name, if any.
  22900. *
  22901. * @return {string}
  22902. * If there is currently a breakpoint set, returns a the key from the
  22903. * breakpoints object matching it. Otherwise, returns an empty string.
  22904. */
  22905. currentBreakpoint() {
  22906. return this.breakpoint_;
  22907. }
  22908. /**
  22909. * Get the current breakpoint class name.
  22910. *
  22911. * @return {string}
  22912. * The matching class name (e.g. `"vjs-layout-tiny"` or
  22913. * `"vjs-layout-large"`) for the current breakpoint. Empty string if
  22914. * there is no current breakpoint.
  22915. */
  22916. currentBreakpointClass() {
  22917. return BREAKPOINT_CLASSES[this.breakpoint_] || '';
  22918. }
  22919. /**
  22920. * An object that describes a single piece of media.
  22921. *
  22922. * Properties that are not part of this type description will be retained; so,
  22923. * this can be viewed as a generic metadata storage mechanism as well.
  22924. *
  22925. * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
  22926. * @typedef {Object} Player~MediaObject
  22927. *
  22928. * @property {string} [album]
  22929. * Unused, except if this object is passed to the `MediaSession`
  22930. * API.
  22931. *
  22932. * @property {string} [artist]
  22933. * Unused, except if this object is passed to the `MediaSession`
  22934. * API.
  22935. *
  22936. * @property {Object[]} [artwork]
  22937. * Unused, except if this object is passed to the `MediaSession`
  22938. * API. If not specified, will be populated via the `poster`, if
  22939. * available.
  22940. *
  22941. * @property {string} [poster]
  22942. * URL to an image that will display before playback.
  22943. *
  22944. * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
  22945. * A single source object, an array of source objects, or a string
  22946. * referencing a URL to a media source. It is _highly recommended_
  22947. * that an object or array of objects is used here, so that source
  22948. * selection algorithms can take the `type` into account.
  22949. *
  22950. * @property {string} [title]
  22951. * Unused, except if this object is passed to the `MediaSession`
  22952. * API.
  22953. *
  22954. * @property {Object[]} [textTracks]
  22955. * An array of objects to be used to create text tracks, following
  22956. * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
  22957. * For ease of removal, these will be created as "remote" text
  22958. * tracks and set to automatically clean up on source changes.
  22959. *
  22960. * These objects may have properties like `src`, `kind`, `label`,
  22961. * and `language`, see {@link Tech#createRemoteTextTrack}.
  22962. */
  22963. /**
  22964. * Populate the player using a {@link Player~MediaObject|MediaObject}.
  22965. *
  22966. * @param {Player~MediaObject} media
  22967. * A media object.
  22968. *
  22969. * @param {Function} ready
  22970. * A callback to be called when the player is ready.
  22971. */
  22972. loadMedia(media, ready) {
  22973. if (!media || typeof media !== 'object') {
  22974. return;
  22975. }
  22976. const crossOrigin = this.crossOrigin();
  22977. this.reset();
  22978. // Clone the media object so it cannot be mutated from outside.
  22979. this.cache_.media = merge(media);
  22980. const {
  22981. artist,
  22982. artwork,
  22983. description,
  22984. poster,
  22985. src,
  22986. textTracks,
  22987. title
  22988. } = this.cache_.media;
  22989. // If `artwork` is not given, create it using `poster`.
  22990. if (!artwork && poster) {
  22991. this.cache_.media.artwork = [{
  22992. src: poster,
  22993. type: getMimetype(poster)
  22994. }];
  22995. }
  22996. if (crossOrigin) {
  22997. this.crossOrigin(crossOrigin);
  22998. }
  22999. if (src) {
  23000. this.src(src);
  23001. }
  23002. if (poster) {
  23003. this.poster(poster);
  23004. }
  23005. if (Array.isArray(textTracks)) {
  23006. textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
  23007. }
  23008. if (this.titleBar) {
  23009. this.titleBar.update({
  23010. title,
  23011. description: description || artist || ''
  23012. });
  23013. }
  23014. this.ready(ready);
  23015. }
  23016. /**
  23017. * Get a clone of the current {@link Player~MediaObject} for this player.
  23018. *
  23019. * If the `loadMedia` method has not been used, will attempt to return a
  23020. * {@link Player~MediaObject} based on the current state of the player.
  23021. *
  23022. * @return {Player~MediaObject}
  23023. */
  23024. getMedia() {
  23025. if (!this.cache_.media) {
  23026. const poster = this.poster();
  23027. const src = this.currentSources();
  23028. const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
  23029. kind: tt.kind,
  23030. label: tt.label,
  23031. language: tt.language,
  23032. src: tt.src
  23033. }));
  23034. const media = {
  23035. src,
  23036. textTracks
  23037. };
  23038. if (poster) {
  23039. media.poster = poster;
  23040. media.artwork = [{
  23041. src: media.poster,
  23042. type: getMimetype(media.poster)
  23043. }];
  23044. }
  23045. return media;
  23046. }
  23047. return merge(this.cache_.media);
  23048. }
  23049. /**
  23050. * Gets tag settings
  23051. *
  23052. * @param {Element} tag
  23053. * The player tag
  23054. *
  23055. * @return {Object}
  23056. * An object containing all of the settings
  23057. * for a player tag
  23058. */
  23059. static getTagSettings(tag) {
  23060. const baseOptions = {
  23061. sources: [],
  23062. tracks: []
  23063. };
  23064. const tagOptions = getAttributes(tag);
  23065. const dataSetup = tagOptions['data-setup'];
  23066. if (hasClass(tag, 'vjs-fill')) {
  23067. tagOptions.fill = true;
  23068. }
  23069. if (hasClass(tag, 'vjs-fluid')) {
  23070. tagOptions.fluid = true;
  23071. }
  23072. // Check if data-setup attr exists.
  23073. if (dataSetup !== null) {
  23074. // Parse options JSON
  23075. // If empty string, make it a parsable json object.
  23076. const [err, data] = safeParseTuple__default["default"](dataSetup || '{}');
  23077. if (err) {
  23078. log.error(err);
  23079. }
  23080. Object.assign(tagOptions, data);
  23081. }
  23082. Object.assign(baseOptions, tagOptions);
  23083. // Get tag children settings
  23084. if (tag.hasChildNodes()) {
  23085. const children = tag.childNodes;
  23086. for (let i = 0, j = children.length; i < j; i++) {
  23087. const child = children[i];
  23088. // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
  23089. const childName = child.nodeName.toLowerCase();
  23090. if (childName === 'source') {
  23091. baseOptions.sources.push(getAttributes(child));
  23092. } else if (childName === 'track') {
  23093. baseOptions.tracks.push(getAttributes(child));
  23094. }
  23095. }
  23096. }
  23097. return baseOptions;
  23098. }
  23099. /**
  23100. * Set debug mode to enable/disable logs at info level.
  23101. *
  23102. * @param {boolean} enabled
  23103. * @fires Player#debugon
  23104. * @fires Player#debugoff
  23105. * @return {boolean|undefined}
  23106. */
  23107. debug(enabled) {
  23108. if (enabled === undefined) {
  23109. return this.debugEnabled_;
  23110. }
  23111. if (enabled) {
  23112. this.trigger('debugon');
  23113. this.previousLogLevel_ = this.log.level;
  23114. this.log.level('debug');
  23115. this.debugEnabled_ = true;
  23116. } else {
  23117. this.trigger('debugoff');
  23118. this.log.level(this.previousLogLevel_);
  23119. this.previousLogLevel_ = undefined;
  23120. this.debugEnabled_ = false;
  23121. }
  23122. }
  23123. /**
  23124. * Set or get current playback rates.
  23125. * Takes an array and updates the playback rates menu with the new items.
  23126. * Pass in an empty array to hide the menu.
  23127. * Values other than arrays are ignored.
  23128. *
  23129. * @fires Player#playbackrateschange
  23130. * @param {number[]} newRates
  23131. * The new rates that the playback rates menu should update to.
  23132. * An empty array will hide the menu
  23133. * @return {number[]} When used as a getter will return the current playback rates
  23134. */
  23135. playbackRates(newRates) {
  23136. if (newRates === undefined) {
  23137. return this.cache_.playbackRates;
  23138. }
  23139. // ignore any value that isn't an array
  23140. if (!Array.isArray(newRates)) {
  23141. return;
  23142. }
  23143. // ignore any arrays that don't only contain numbers
  23144. if (!newRates.every(rate => typeof rate === 'number')) {
  23145. return;
  23146. }
  23147. this.cache_.playbackRates = newRates;
  23148. /**
  23149. * fires when the playback rates in a player are changed
  23150. *
  23151. * @event Player#playbackrateschange
  23152. * @type {Event}
  23153. */
  23154. this.trigger('playbackrateschange');
  23155. }
  23156. }
  23157. /**
  23158. * Get the {@link VideoTrackList}
  23159. *
  23160. * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
  23161. *
  23162. * @return {VideoTrackList}
  23163. * the current video track list
  23164. *
  23165. * @method Player.prototype.videoTracks
  23166. */
  23167. /**
  23168. * Get the {@link AudioTrackList}
  23169. *
  23170. * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
  23171. *
  23172. * @return {AudioTrackList}
  23173. * the current audio track list
  23174. *
  23175. * @method Player.prototype.audioTracks
  23176. */
  23177. /**
  23178. * Get the {@link TextTrackList}
  23179. *
  23180. * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
  23181. *
  23182. * @return {TextTrackList}
  23183. * the current text track list
  23184. *
  23185. * @method Player.prototype.textTracks
  23186. */
  23187. /**
  23188. * Get the remote {@link TextTrackList}
  23189. *
  23190. * @return {TextTrackList}
  23191. * The current remote text track list
  23192. *
  23193. * @method Player.prototype.remoteTextTracks
  23194. */
  23195. /**
  23196. * Get the remote {@link HtmlTrackElementList} tracks.
  23197. *
  23198. * @return {HtmlTrackElementList}
  23199. * The current remote text track element list
  23200. *
  23201. * @method Player.prototype.remoteTextTrackEls
  23202. */
  23203. ALL.names.forEach(function (name) {
  23204. const props = ALL[name];
  23205. Player.prototype[props.getterName] = function () {
  23206. if (this.tech_) {
  23207. return this.tech_[props.getterName]();
  23208. }
  23209. // if we have not yet loadTech_, we create {video,audio,text}Tracks_
  23210. // these will be passed to the tech during loading
  23211. this[props.privateName] = this[props.privateName] || new props.ListClass();
  23212. return this[props.privateName];
  23213. };
  23214. });
  23215. /**
  23216. * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
  23217. * sets the `crossOrigin` property on the `<video>` tag to control the CORS
  23218. * behavior.
  23219. *
  23220. * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
  23221. *
  23222. * @param {string} [value]
  23223. * The value to set the `Player`'s crossorigin to. If an argument is
  23224. * given, must be one of `anonymous` or `use-credentials`.
  23225. *
  23226. * @return {string|undefined}
  23227. * - The current crossorigin value of the `Player` when getting.
  23228. * - undefined when setting
  23229. */
  23230. Player.prototype.crossorigin = Player.prototype.crossOrigin;
  23231. /**
  23232. * Global enumeration of players.
  23233. *
  23234. * The keys are the player IDs and the values are either the {@link Player}
  23235. * instance or `null` for disposed players.
  23236. *
  23237. * @type {Object}
  23238. */
  23239. Player.players = {};
  23240. const navigator = window__default["default"].navigator;
  23241. /*
  23242. * Player instance options, surfaced using options
  23243. * options = Player.prototype.options_
  23244. * Make changes in options, not here.
  23245. *
  23246. * @type {Object}
  23247. * @private
  23248. */
  23249. Player.prototype.options_ = {
  23250. // Default order of fallback technology
  23251. techOrder: Tech.defaultTechOrder_,
  23252. html5: {},
  23253. // enable sourceset by default
  23254. enableSourceset: true,
  23255. // default inactivity timeout
  23256. inactivityTimeout: 2000,
  23257. // default playback rates
  23258. playbackRates: [],
  23259. // Add playback rate selection by adding rates
  23260. // 'playbackRates': [0.5, 1, 1.5, 2],
  23261. liveui: false,
  23262. // Included control sets
  23263. children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
  23264. language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
  23265. // locales and their language translations
  23266. languages: {},
  23267. // Default message to show when a video cannot be played.
  23268. notSupportedMessage: 'No compatible source was found for this media.',
  23269. normalizeAutoplay: false,
  23270. fullscreen: {
  23271. options: {
  23272. navigationUI: 'hide'
  23273. }
  23274. },
  23275. breakpoints: {},
  23276. responsive: false,
  23277. audioOnlyMode: false,
  23278. audioPosterMode: false,
  23279. // Default smooth seeking to false
  23280. enableSmoothSeeking: false
  23281. };
  23282. TECH_EVENTS_RETRIGGER.forEach(function (event) {
  23283. Player.prototype[`handleTech${toTitleCase(event)}_`] = function () {
  23284. return this.trigger(event);
  23285. };
  23286. });
  23287. /**
  23288. * Fired when the player has initial duration and dimension information
  23289. *
  23290. * @event Player#loadedmetadata
  23291. * @type {Event}
  23292. */
  23293. /**
  23294. * Fired when the player has downloaded data at the current playback position
  23295. *
  23296. * @event Player#loadeddata
  23297. * @type {Event}
  23298. */
  23299. /**
  23300. * Fired when the current playback position has changed *
  23301. * During playback this is fired every 15-250 milliseconds, depending on the
  23302. * playback technology in use.
  23303. *
  23304. * @event Player#timeupdate
  23305. * @type {Event}
  23306. */
  23307. /**
  23308. * Fired when the volume changes
  23309. *
  23310. * @event Player#volumechange
  23311. * @type {Event}
  23312. */
  23313. /**
  23314. * Reports whether or not a player has a plugin available.
  23315. *
  23316. * This does not report whether or not the plugin has ever been initialized
  23317. * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
  23318. *
  23319. * @method Player#hasPlugin
  23320. * @param {string} name
  23321. * The name of a plugin.
  23322. *
  23323. * @return {boolean}
  23324. * Whether or not this player has the requested plugin available.
  23325. */
  23326. /**
  23327. * Reports whether or not a player is using a plugin by name.
  23328. *
  23329. * For basic plugins, this only reports whether the plugin has _ever_ been
  23330. * initialized on this player.
  23331. *
  23332. * @method Player#usingPlugin
  23333. * @param {string} name
  23334. * The name of a plugin.
  23335. *
  23336. * @return {boolean}
  23337. * Whether or not this player is using the requested plugin.
  23338. */
  23339. Component.registerComponent('Player', Player);
  23340. /**
  23341. * @file plugin.js
  23342. */
  23343. /**
  23344. * The base plugin name.
  23345. *
  23346. * @private
  23347. * @constant
  23348. * @type {string}
  23349. */
  23350. const BASE_PLUGIN_NAME = 'plugin';
  23351. /**
  23352. * The key on which a player's active plugins cache is stored.
  23353. *
  23354. * @private
  23355. * @constant
  23356. * @type {string}
  23357. */
  23358. const PLUGIN_CACHE_KEY = 'activePlugins_';
  23359. /**
  23360. * Stores registered plugins in a private space.
  23361. *
  23362. * @private
  23363. * @type {Object}
  23364. */
  23365. const pluginStorage = {};
  23366. /**
  23367. * Reports whether or not a plugin has been registered.
  23368. *
  23369. * @private
  23370. * @param {string} name
  23371. * The name of a plugin.
  23372. *
  23373. * @return {boolean}
  23374. * Whether or not the plugin has been registered.
  23375. */
  23376. const pluginExists = name => pluginStorage.hasOwnProperty(name);
  23377. /**
  23378. * Get a single registered plugin by name.
  23379. *
  23380. * @private
  23381. * @param {string} name
  23382. * The name of a plugin.
  23383. *
  23384. * @return {typeof Plugin|Function|undefined}
  23385. * The plugin (or undefined).
  23386. */
  23387. const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
  23388. /**
  23389. * Marks a plugin as "active" on a player.
  23390. *
  23391. * Also, ensures that the player has an object for tracking active plugins.
  23392. *
  23393. * @private
  23394. * @param {Player} player
  23395. * A Video.js player instance.
  23396. *
  23397. * @param {string} name
  23398. * The name of a plugin.
  23399. */
  23400. const markPluginAsActive = (player, name) => {
  23401. player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
  23402. player[PLUGIN_CACHE_KEY][name] = true;
  23403. };
  23404. /**
  23405. * Triggers a pair of plugin setup events.
  23406. *
  23407. * @private
  23408. * @param {Player} player
  23409. * A Video.js player instance.
  23410. *
  23411. * @param {PluginEventHash} hash
  23412. * A plugin event hash.
  23413. *
  23414. * @param {boolean} [before]
  23415. * If true, prefixes the event name with "before". In other words,
  23416. * use this to trigger "beforepluginsetup" instead of "pluginsetup".
  23417. */
  23418. const triggerSetupEvent = (player, hash, before) => {
  23419. const eventName = (before ? 'before' : '') + 'pluginsetup';
  23420. player.trigger(eventName, hash);
  23421. player.trigger(eventName + ':' + hash.name, hash);
  23422. };
  23423. /**
  23424. * Takes a basic plugin function and returns a wrapper function which marks
  23425. * on the player that the plugin has been activated.
  23426. *
  23427. * @private
  23428. * @param {string} name
  23429. * The name of the plugin.
  23430. *
  23431. * @param {Function} plugin
  23432. * The basic plugin.
  23433. *
  23434. * @return {Function}
  23435. * A wrapper function for the given plugin.
  23436. */
  23437. const createBasicPlugin = function (name, plugin) {
  23438. const basicPluginWrapper = function () {
  23439. // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
  23440. // regardless, but we want the hash to be consistent with the hash provided
  23441. // for advanced plugins.
  23442. //
  23443. // The only potentially counter-intuitive thing here is the `instance` in
  23444. // the "pluginsetup" event is the value returned by the `plugin` function.
  23445. triggerSetupEvent(this, {
  23446. name,
  23447. plugin,
  23448. instance: null
  23449. }, true);
  23450. const instance = plugin.apply(this, arguments);
  23451. markPluginAsActive(this, name);
  23452. triggerSetupEvent(this, {
  23453. name,
  23454. plugin,
  23455. instance
  23456. });
  23457. return instance;
  23458. };
  23459. Object.keys(plugin).forEach(function (prop) {
  23460. basicPluginWrapper[prop] = plugin[prop];
  23461. });
  23462. return basicPluginWrapper;
  23463. };
  23464. /**
  23465. * Takes a plugin sub-class and returns a factory function for generating
  23466. * instances of it.
  23467. *
  23468. * This factory function will replace itself with an instance of the requested
  23469. * sub-class of Plugin.
  23470. *
  23471. * @private
  23472. * @param {string} name
  23473. * The name of the plugin.
  23474. *
  23475. * @param {Plugin} PluginSubClass
  23476. * The advanced plugin.
  23477. *
  23478. * @return {Function}
  23479. */
  23480. const createPluginFactory = (name, PluginSubClass) => {
  23481. // Add a `name` property to the plugin prototype so that each plugin can
  23482. // refer to itself by name.
  23483. PluginSubClass.prototype.name = name;
  23484. return function (...args) {
  23485. triggerSetupEvent(this, {
  23486. name,
  23487. plugin: PluginSubClass,
  23488. instance: null
  23489. }, true);
  23490. const instance = new PluginSubClass(...[this, ...args]);
  23491. // The plugin is replaced by a function that returns the current instance.
  23492. this[name] = () => instance;
  23493. triggerSetupEvent(this, instance.getEventHash());
  23494. return instance;
  23495. };
  23496. };
  23497. /**
  23498. * Parent class for all advanced plugins.
  23499. *
  23500. * @mixes module:evented~EventedMixin
  23501. * @mixes module:stateful~StatefulMixin
  23502. * @fires Player#beforepluginsetup
  23503. * @fires Player#beforepluginsetup:$name
  23504. * @fires Player#pluginsetup
  23505. * @fires Player#pluginsetup:$name
  23506. * @listens Player#dispose
  23507. * @throws {Error}
  23508. * If attempting to instantiate the base {@link Plugin} class
  23509. * directly instead of via a sub-class.
  23510. */
  23511. class Plugin {
  23512. /**
  23513. * Creates an instance of this class.
  23514. *
  23515. * Sub-classes should call `super` to ensure plugins are properly initialized.
  23516. *
  23517. * @param {Player} player
  23518. * A Video.js player instance.
  23519. */
  23520. constructor(player) {
  23521. if (this.constructor === Plugin) {
  23522. throw new Error('Plugin must be sub-classed; not directly instantiated.');
  23523. }
  23524. this.player = player;
  23525. if (!this.log) {
  23526. this.log = this.player.log.createLogger(this.name);
  23527. }
  23528. // Make this object evented, but remove the added `trigger` method so we
  23529. // use the prototype version instead.
  23530. evented(this);
  23531. delete this.trigger;
  23532. stateful(this, this.constructor.defaultState);
  23533. markPluginAsActive(player, this.name);
  23534. // Auto-bind the dispose method so we can use it as a listener and unbind
  23535. // it later easily.
  23536. this.dispose = this.dispose.bind(this);
  23537. // If the player is disposed, dispose the plugin.
  23538. player.on('dispose', this.dispose);
  23539. }
  23540. /**
  23541. * Get the version of the plugin that was set on <pluginName>.VERSION
  23542. */
  23543. version() {
  23544. return this.constructor.VERSION;
  23545. }
  23546. /**
  23547. * Each event triggered by plugins includes a hash of additional data with
  23548. * conventional properties.
  23549. *
  23550. * This returns that object or mutates an existing hash.
  23551. *
  23552. * @param {Object} [hash={}]
  23553. * An object to be used as event an event hash.
  23554. *
  23555. * @return {PluginEventHash}
  23556. * An event hash object with provided properties mixed-in.
  23557. */
  23558. getEventHash(hash = {}) {
  23559. hash.name = this.name;
  23560. hash.plugin = this.constructor;
  23561. hash.instance = this;
  23562. return hash;
  23563. }
  23564. /**
  23565. * Triggers an event on the plugin object and overrides
  23566. * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
  23567. *
  23568. * @param {string|Object} event
  23569. * An event type or an object with a type property.
  23570. *
  23571. * @param {Object} [hash={}]
  23572. * Additional data hash to merge with a
  23573. * {@link PluginEventHash|PluginEventHash}.
  23574. *
  23575. * @return {boolean}
  23576. * Whether or not default was prevented.
  23577. */
  23578. trigger(event, hash = {}) {
  23579. return trigger(this.eventBusEl_, event, this.getEventHash(hash));
  23580. }
  23581. /**
  23582. * Handles "statechanged" events on the plugin. No-op by default, override by
  23583. * subclassing.
  23584. *
  23585. * @abstract
  23586. * @param {Event} e
  23587. * An event object provided by a "statechanged" event.
  23588. *
  23589. * @param {Object} e.changes
  23590. * An object describing changes that occurred with the "statechanged"
  23591. * event.
  23592. */
  23593. handleStateChanged(e) {}
  23594. /**
  23595. * Disposes a plugin.
  23596. *
  23597. * Subclasses can override this if they want, but for the sake of safety,
  23598. * it's probably best to subscribe the "dispose" event.
  23599. *
  23600. * @fires Plugin#dispose
  23601. */
  23602. dispose() {
  23603. const {
  23604. name,
  23605. player
  23606. } = this;
  23607. /**
  23608. * Signals that a advanced plugin is about to be disposed.
  23609. *
  23610. * @event Plugin#dispose
  23611. * @type {Event}
  23612. */
  23613. this.trigger('dispose');
  23614. this.off();
  23615. player.off('dispose', this.dispose);
  23616. // Eliminate any possible sources of leaking memory by clearing up
  23617. // references between the player and the plugin instance and nulling out
  23618. // the plugin's state and replacing methods with a function that throws.
  23619. player[PLUGIN_CACHE_KEY][name] = false;
  23620. this.player = this.state = null;
  23621. // Finally, replace the plugin name on the player with a new factory
  23622. // function, so that the plugin is ready to be set up again.
  23623. player[name] = createPluginFactory(name, pluginStorage[name]);
  23624. }
  23625. /**
  23626. * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
  23627. *
  23628. * @param {string|Function} plugin
  23629. * If a string, matches the name of a plugin. If a function, will be
  23630. * tested directly.
  23631. *
  23632. * @return {boolean}
  23633. * Whether or not a plugin is a basic plugin.
  23634. */
  23635. static isBasic(plugin) {
  23636. const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
  23637. return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
  23638. }
  23639. /**
  23640. * Register a Video.js plugin.
  23641. *
  23642. * @param {string} name
  23643. * The name of the plugin to be registered. Must be a string and
  23644. * must not match an existing plugin or a method on the `Player`
  23645. * prototype.
  23646. *
  23647. * @param {typeof Plugin|Function} plugin
  23648. * A sub-class of `Plugin` or a function for basic plugins.
  23649. *
  23650. * @return {typeof Plugin|Function}
  23651. * For advanced plugins, a factory function for that plugin. For
  23652. * basic plugins, a wrapper function that initializes the plugin.
  23653. */
  23654. static registerPlugin(name, plugin) {
  23655. if (typeof name !== 'string') {
  23656. throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
  23657. }
  23658. if (pluginExists(name)) {
  23659. log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
  23660. } else if (Player.prototype.hasOwnProperty(name)) {
  23661. throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
  23662. }
  23663. if (typeof plugin !== 'function') {
  23664. throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
  23665. }
  23666. pluginStorage[name] = plugin;
  23667. // Add a player prototype method for all sub-classed plugins (but not for
  23668. // the base Plugin class).
  23669. if (name !== BASE_PLUGIN_NAME) {
  23670. if (Plugin.isBasic(plugin)) {
  23671. Player.prototype[name] = createBasicPlugin(name, plugin);
  23672. } else {
  23673. Player.prototype[name] = createPluginFactory(name, plugin);
  23674. }
  23675. }
  23676. return plugin;
  23677. }
  23678. /**
  23679. * De-register a Video.js plugin.
  23680. *
  23681. * @param {string} name
  23682. * The name of the plugin to be de-registered. Must be a string that
  23683. * matches an existing plugin.
  23684. *
  23685. * @throws {Error}
  23686. * If an attempt is made to de-register the base plugin.
  23687. */
  23688. static deregisterPlugin(name) {
  23689. if (name === BASE_PLUGIN_NAME) {
  23690. throw new Error('Cannot de-register base plugin.');
  23691. }
  23692. if (pluginExists(name)) {
  23693. delete pluginStorage[name];
  23694. delete Player.prototype[name];
  23695. }
  23696. }
  23697. /**
  23698. * Gets an object containing multiple Video.js plugins.
  23699. *
  23700. * @param {Array} [names]
  23701. * If provided, should be an array of plugin names. Defaults to _all_
  23702. * plugin names.
  23703. *
  23704. * @return {Object|undefined}
  23705. * An object containing plugin(s) associated with their name(s) or
  23706. * `undefined` if no matching plugins exist).
  23707. */
  23708. static getPlugins(names = Object.keys(pluginStorage)) {
  23709. let result;
  23710. names.forEach(name => {
  23711. const plugin = getPlugin(name);
  23712. if (plugin) {
  23713. result = result || {};
  23714. result[name] = plugin;
  23715. }
  23716. });
  23717. return result;
  23718. }
  23719. /**
  23720. * Gets a plugin's version, if available
  23721. *
  23722. * @param {string} name
  23723. * The name of a plugin.
  23724. *
  23725. * @return {string}
  23726. * The plugin's version or an empty string.
  23727. */
  23728. static getPluginVersion(name) {
  23729. const plugin = getPlugin(name);
  23730. return plugin && plugin.VERSION || '';
  23731. }
  23732. }
  23733. /**
  23734. * Gets a plugin by name if it exists.
  23735. *
  23736. * @static
  23737. * @method getPlugin
  23738. * @memberOf Plugin
  23739. * @param {string} name
  23740. * The name of a plugin.
  23741. *
  23742. * @returns {typeof Plugin|Function|undefined}
  23743. * The plugin (or `undefined`).
  23744. */
  23745. Plugin.getPlugin = getPlugin;
  23746. /**
  23747. * The name of the base plugin class as it is registered.
  23748. *
  23749. * @type {string}
  23750. */
  23751. Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
  23752. Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
  23753. /**
  23754. * Documented in player.js
  23755. *
  23756. * @ignore
  23757. */
  23758. Player.prototype.usingPlugin = function (name) {
  23759. return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
  23760. };
  23761. /**
  23762. * Documented in player.js
  23763. *
  23764. * @ignore
  23765. */
  23766. Player.prototype.hasPlugin = function (name) {
  23767. return !!pluginExists(name);
  23768. };
  23769. /**
  23770. * Signals that a plugin is about to be set up on a player.
  23771. *
  23772. * @event Player#beforepluginsetup
  23773. * @type {PluginEventHash}
  23774. */
  23775. /**
  23776. * Signals that a plugin is about to be set up on a player - by name. The name
  23777. * is the name of the plugin.
  23778. *
  23779. * @event Player#beforepluginsetup:$name
  23780. * @type {PluginEventHash}
  23781. */
  23782. /**
  23783. * Signals that a plugin has just been set up on a player.
  23784. *
  23785. * @event Player#pluginsetup
  23786. * @type {PluginEventHash}
  23787. */
  23788. /**
  23789. * Signals that a plugin has just been set up on a player - by name. The name
  23790. * is the name of the plugin.
  23791. *
  23792. * @event Player#pluginsetup:$name
  23793. * @type {PluginEventHash}
  23794. */
  23795. /**
  23796. * @typedef {Object} PluginEventHash
  23797. *
  23798. * @property {string} instance
  23799. * For basic plugins, the return value of the plugin function. For
  23800. * advanced plugins, the plugin instance on which the event is fired.
  23801. *
  23802. * @property {string} name
  23803. * The name of the plugin.
  23804. *
  23805. * @property {string} plugin
  23806. * For basic plugins, the plugin function. For advanced plugins, the
  23807. * plugin class/constructor.
  23808. */
  23809. /**
  23810. * @file deprecate.js
  23811. * @module deprecate
  23812. */
  23813. /**
  23814. * Decorate a function with a deprecation message the first time it is called.
  23815. *
  23816. * @param {string} message
  23817. * A deprecation message to log the first time the returned function
  23818. * is called.
  23819. *
  23820. * @param {Function} fn
  23821. * The function to be deprecated.
  23822. *
  23823. * @return {Function}
  23824. * A wrapper function that will log a deprecation warning the first
  23825. * time it is called. The return value will be the return value of
  23826. * the wrapped function.
  23827. */
  23828. function deprecate(message, fn) {
  23829. let warned = false;
  23830. return function (...args) {
  23831. if (!warned) {
  23832. log.warn(message);
  23833. }
  23834. warned = true;
  23835. return fn.apply(this, args);
  23836. };
  23837. }
  23838. /**
  23839. * Internal function used to mark a function as deprecated in the next major
  23840. * version with consistent messaging.
  23841. *
  23842. * @param {number} major The major version where it will be removed
  23843. * @param {string} oldName The old function name
  23844. * @param {string} newName The new function name
  23845. * @param {Function} fn The function to deprecate
  23846. * @return {Function} The decorated function
  23847. */
  23848. function deprecateForMajor(major, oldName, newName, fn) {
  23849. return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
  23850. }
  23851. /**
  23852. * @file video.js
  23853. * @module videojs
  23854. */
  23855. /**
  23856. * Normalize an `id` value by trimming off a leading `#`
  23857. *
  23858. * @private
  23859. * @param {string} id
  23860. * A string, maybe with a leading `#`.
  23861. *
  23862. * @return {string}
  23863. * The string, without any leading `#`.
  23864. */
  23865. const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
  23866. /**
  23867. * A callback that is called when a component is ready. Does not have any
  23868. * parameters and any callback value will be ignored. See: {@link Component~ReadyCallback}
  23869. *
  23870. * @callback ReadyCallback
  23871. */
  23872. /**
  23873. * The `videojs()` function doubles as the main function for users to create a
  23874. * {@link Player} instance as well as the main library namespace.
  23875. *
  23876. * It can also be used as a getter for a pre-existing {@link Player} instance.
  23877. * However, we _strongly_ recommend using `videojs.getPlayer()` for this
  23878. * purpose because it avoids any potential for unintended initialization.
  23879. *
  23880. * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
  23881. * of our JSDoc template, we cannot properly document this as both a function
  23882. * and a namespace, so its function signature is documented here.
  23883. *
  23884. * #### Arguments
  23885. * ##### id
  23886. * string|Element, **required**
  23887. *
  23888. * Video element or video element ID.
  23889. *
  23890. * ##### options
  23891. * Object, optional
  23892. *
  23893. * Options object for providing settings.
  23894. * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
  23895. *
  23896. * ##### ready
  23897. * {@link Component~ReadyCallback}, optional
  23898. *
  23899. * A function to be called when the {@link Player} and {@link Tech} are ready.
  23900. *
  23901. * #### Return Value
  23902. *
  23903. * The `videojs()` function returns a {@link Player} instance.
  23904. *
  23905. * @namespace
  23906. *
  23907. * @borrows AudioTrack as AudioTrack
  23908. * @borrows Component.getComponent as getComponent
  23909. * @borrows module:events.on as on
  23910. * @borrows module:events.one as one
  23911. * @borrows module:events.off as off
  23912. * @borrows module:events.trigger as trigger
  23913. * @borrows EventTarget as EventTarget
  23914. * @borrows module:middleware.use as use
  23915. * @borrows Player.players as players
  23916. * @borrows Plugin.registerPlugin as registerPlugin
  23917. * @borrows Plugin.deregisterPlugin as deregisterPlugin
  23918. * @borrows Plugin.getPlugins as getPlugins
  23919. * @borrows Plugin.getPlugin as getPlugin
  23920. * @borrows Plugin.getPluginVersion as getPluginVersion
  23921. * @borrows Tech.getTech as getTech
  23922. * @borrows Tech.registerTech as registerTech
  23923. * @borrows TextTrack as TextTrack
  23924. * @borrows VideoTrack as VideoTrack
  23925. *
  23926. * @param {string|Element} id
  23927. * Video element or video element ID.
  23928. *
  23929. * @param {Object} [options]
  23930. * Options object for providing settings.
  23931. * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
  23932. *
  23933. * @param {ReadyCallback} [ready]
  23934. * A function to be called when the {@link Player} and {@link Tech} are
  23935. * ready.
  23936. *
  23937. * @return {Player}
  23938. * The `videojs()` function returns a {@link Player|Player} instance.
  23939. */
  23940. function videojs(id, options, ready) {
  23941. let player = videojs.getPlayer(id);
  23942. if (player) {
  23943. if (options) {
  23944. log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
  23945. }
  23946. if (ready) {
  23947. player.ready(ready);
  23948. }
  23949. return player;
  23950. }
  23951. const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
  23952. if (!isEl(el)) {
  23953. throw new TypeError('The element or ID supplied is not valid. (videojs)');
  23954. }
  23955. // document.body.contains(el) will only check if el is contained within that one document.
  23956. // This causes problems for elements in iframes.
  23957. // Instead, use the element's ownerDocument instead of the global document.
  23958. // This will make sure that the element is indeed in the dom of that document.
  23959. // Additionally, check that the document in question has a default view.
  23960. // If the document is no longer attached to the dom, the defaultView of the document will be null.
  23961. // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body
  23962. // always returns false. Instead, use the Shadow DOM root.
  23963. const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window__default["default"].ShadowRoot : false;
  23964. const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body;
  23965. if (!el.ownerDocument.defaultView || !rootNode.contains(el)) {
  23966. log.warn('The element supplied is not included in the DOM');
  23967. }
  23968. options = options || {};
  23969. // Store a copy of the el before modification, if it is to be restored in destroy()
  23970. // If div ingest, store the parent div
  23971. if (options.restoreEl === true) {
  23972. options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
  23973. }
  23974. hooks('beforesetup').forEach(hookFunction => {
  23975. const opts = hookFunction(el, merge(options));
  23976. if (!isObject(opts) || Array.isArray(opts)) {
  23977. log.error('please return an object in beforesetup hooks');
  23978. return;
  23979. }
  23980. options = merge(options, opts);
  23981. });
  23982. // We get the current "Player" component here in case an integration has
  23983. // replaced it with a custom player.
  23984. const PlayerComponent = Component.getComponent('Player');
  23985. player = new PlayerComponent(el, options, ready);
  23986. hooks('setup').forEach(hookFunction => hookFunction(player));
  23987. return player;
  23988. }
  23989. videojs.hooks_ = hooks_;
  23990. videojs.hooks = hooks;
  23991. videojs.hook = hook;
  23992. videojs.hookOnce = hookOnce;
  23993. videojs.removeHook = removeHook;
  23994. // Add default styles
  23995. if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
  23996. let style = $('.vjs-styles-defaults');
  23997. if (!style) {
  23998. style = createStyleElement('vjs-styles-defaults');
  23999. const head = $('head');
  24000. if (head) {
  24001. head.insertBefore(style, head.firstChild);
  24002. }
  24003. setTextContent(style, `
  24004. .video-js {
  24005. width: 300px;
  24006. height: 150px;
  24007. }
  24008. .vjs-fluid:not(.vjs-audio-only-mode) {
  24009. padding-top: 56.25%
  24010. }
  24011. `);
  24012. }
  24013. }
  24014. // Run Auto-load players
  24015. // You have to wait at least once in case this script is loaded after your
  24016. // video in the DOM (weird behavior only with minified version)
  24017. autoSetupTimeout(1, videojs);
  24018. /**
  24019. * Current Video.js version. Follows [semantic versioning](https://semver.org/).
  24020. *
  24021. * @type {string}
  24022. */
  24023. videojs.VERSION = version;
  24024. /**
  24025. * The global options object. These are the settings that take effect
  24026. * if no overrides are specified when the player is created.
  24027. *
  24028. * @type {Object}
  24029. */
  24030. videojs.options = Player.prototype.options_;
  24031. /**
  24032. * Get an object with the currently created players, keyed by player ID
  24033. *
  24034. * @return {Object}
  24035. * The created players
  24036. */
  24037. videojs.getPlayers = () => Player.players;
  24038. /**
  24039. * Get a single player based on an ID or DOM element.
  24040. *
  24041. * This is useful if you want to check if an element or ID has an associated
  24042. * Video.js player, but not create one if it doesn't.
  24043. *
  24044. * @param {string|Element} id
  24045. * An HTML element - `<video>`, `<audio>`, or `<video-js>` -
  24046. * or a string matching the `id` of such an element.
  24047. *
  24048. * @return {Player|undefined}
  24049. * A player instance or `undefined` if there is no player instance
  24050. * matching the argument.
  24051. */
  24052. videojs.getPlayer = id => {
  24053. const players = Player.players;
  24054. let tag;
  24055. if (typeof id === 'string') {
  24056. const nId = normalizeId(id);
  24057. const player = players[nId];
  24058. if (player) {
  24059. return player;
  24060. }
  24061. tag = $('#' + nId);
  24062. } else {
  24063. tag = id;
  24064. }
  24065. if (isEl(tag)) {
  24066. const {
  24067. player,
  24068. playerId
  24069. } = tag;
  24070. // Element may have a `player` property referring to an already created
  24071. // player instance. If so, return that.
  24072. if (player || players[playerId]) {
  24073. return player || players[playerId];
  24074. }
  24075. }
  24076. };
  24077. /**
  24078. * Returns an array of all current players.
  24079. *
  24080. * @return {Array}
  24081. * An array of all players. The array will be in the order that
  24082. * `Object.keys` provides, which could potentially vary between
  24083. * JavaScript engines.
  24084. *
  24085. */
  24086. videojs.getAllPlayers = () =>
  24087. // Disposed players leave a key with a `null` value, so we need to make sure
  24088. // we filter those out.
  24089. Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
  24090. videojs.players = Player.players;
  24091. videojs.getComponent = Component.getComponent;
  24092. /**
  24093. * Register a component so it can referred to by name. Used when adding to other
  24094. * components, either through addChild `component.addChild('myComponent')` or through
  24095. * default children options `{ children: ['myComponent'] }`.
  24096. *
  24097. * > NOTE: You could also just initialize the component before adding.
  24098. * `component.addChild(new MyComponent());`
  24099. *
  24100. * @param {string} name
  24101. * The class name of the component
  24102. *
  24103. * @param {typeof Component} comp
  24104. * The component class
  24105. *
  24106. * @return {typeof Component}
  24107. * The newly registered component
  24108. */
  24109. videojs.registerComponent = (name, comp) => {
  24110. if (Tech.isTech(comp)) {
  24111. log.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
  24112. }
  24113. return Component.registerComponent.call(Component, name, comp);
  24114. };
  24115. videojs.getTech = Tech.getTech;
  24116. videojs.registerTech = Tech.registerTech;
  24117. videojs.use = use;
  24118. /**
  24119. * An object that can be returned by a middleware to signify
  24120. * that the middleware is being terminated.
  24121. *
  24122. * @type {object}
  24123. * @property {object} middleware.TERMINATOR
  24124. */
  24125. Object.defineProperty(videojs, 'middleware', {
  24126. value: {},
  24127. writeable: false,
  24128. enumerable: true
  24129. });
  24130. Object.defineProperty(videojs.middleware, 'TERMINATOR', {
  24131. value: TERMINATOR,
  24132. writeable: false,
  24133. enumerable: true
  24134. });
  24135. /**
  24136. * A reference to the {@link module:browser|browser utility module} as an object.
  24137. *
  24138. * @type {Object}
  24139. * @see {@link module:browser|browser}
  24140. */
  24141. videojs.browser = browser;
  24142. /**
  24143. * A reference to the {@link module:obj|obj utility module} as an object.
  24144. *
  24145. * @type {Object}
  24146. * @see {@link module:obj|obj}
  24147. */
  24148. videojs.obj = Obj;
  24149. /**
  24150. * Deprecated reference to the {@link module:obj.merge|merge function}
  24151. *
  24152. * @type {Function}
  24153. * @see {@link module:obj.merge|merge}
  24154. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
  24155. */
  24156. videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge);
  24157. /**
  24158. * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
  24159. *
  24160. * @type {Function}
  24161. * @see {@link module:obj.defineLazyProperty|defineLazyProperty}
  24162. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
  24163. */
  24164. videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
  24165. /**
  24166. * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
  24167. *
  24168. * @type {Function}
  24169. * @see {@link module:fn.bind_|fn.bind_}
  24170. * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
  24171. */
  24172. videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
  24173. videojs.registerPlugin = Plugin.registerPlugin;
  24174. videojs.deregisterPlugin = Plugin.deregisterPlugin;
  24175. /**
  24176. * Deprecated method to register a plugin with Video.js
  24177. *
  24178. * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
  24179. *
  24180. * @param {string} name
  24181. * The plugin name
  24182. *
  24183. * @param {typeof Plugin|Function} plugin
  24184. * The plugin sub-class or function
  24185. *
  24186. * @return {typeof Plugin|Function}
  24187. */
  24188. videojs.plugin = (name, plugin) => {
  24189. log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
  24190. return Plugin.registerPlugin(name, plugin);
  24191. };
  24192. videojs.getPlugins = Plugin.getPlugins;
  24193. videojs.getPlugin = Plugin.getPlugin;
  24194. videojs.getPluginVersion = Plugin.getPluginVersion;
  24195. /**
  24196. * Adding languages so that they're available to all players.
  24197. * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
  24198. *
  24199. * @param {string} code
  24200. * The language code or dictionary property
  24201. *
  24202. * @param {Object} data
  24203. * The data values to be translated
  24204. *
  24205. * @return {Object}
  24206. * The resulting language dictionary object
  24207. */
  24208. videojs.addLanguage = function (code, data) {
  24209. code = ('' + code).toLowerCase();
  24210. videojs.options.languages = merge(videojs.options.languages, {
  24211. [code]: data
  24212. });
  24213. return videojs.options.languages[code];
  24214. };
  24215. /**
  24216. * A reference to the {@link module:log|log utility module} as an object.
  24217. *
  24218. * @type {Function}
  24219. * @see {@link module:log|log}
  24220. */
  24221. videojs.log = log;
  24222. videojs.createLogger = createLogger;
  24223. /**
  24224. * A reference to the {@link module:time|time utility module} as an object.
  24225. *
  24226. * @type {Object}
  24227. * @see {@link module:time|time}
  24228. */
  24229. videojs.time = Time;
  24230. /**
  24231. * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
  24232. *
  24233. * @type {Function}
  24234. * @see {@link module:time.createTimeRanges|createTimeRanges}
  24235. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
  24236. */
  24237. videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges);
  24238. /**
  24239. * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
  24240. *
  24241. * @type {Function}
  24242. * @see {@link module:time.createTimeRanges|createTimeRanges}
  24243. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
  24244. */
  24245. videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges);
  24246. /**
  24247. * Deprecated reference to the {@link module:time.formatTime|formatTime function}
  24248. *
  24249. * @type {Function}
  24250. * @see {@link module:time.formatTime|formatTime}
  24251. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
  24252. */
  24253. videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
  24254. /**
  24255. * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
  24256. *
  24257. * @type {Function}
  24258. * @see {@link module:time.setFormatTime|setFormatTime}
  24259. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
  24260. */
  24261. videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
  24262. /**
  24263. * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
  24264. *
  24265. * @type {Function}
  24266. * @see {@link module:time.resetFormatTime|resetFormatTime}
  24267. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
  24268. */
  24269. videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
  24270. /**
  24271. * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
  24272. *
  24273. * @type {Function}
  24274. * @see {@link module:url.parseUrl|parseUrl}
  24275. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
  24276. */
  24277. videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
  24278. /**
  24279. * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
  24280. *
  24281. * @type {Function}
  24282. * @see {@link module:url.isCrossOrigin|isCrossOrigin}
  24283. * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
  24284. */
  24285. videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
  24286. videojs.EventTarget = EventTarget;
  24287. videojs.any = any;
  24288. videojs.on = on;
  24289. videojs.one = one;
  24290. videojs.off = off;
  24291. videojs.trigger = trigger;
  24292. /**
  24293. * A cross-browser XMLHttpRequest wrapper.
  24294. *
  24295. * @function
  24296. * @param {Object} options
  24297. * Settings for the request.
  24298. *
  24299. * @return {XMLHttpRequest|XDomainRequest}
  24300. * The request object.
  24301. *
  24302. * @see https://github.com/Raynos/xhr
  24303. */
  24304. videojs.xhr = XHR__default["default"];
  24305. videojs.TextTrack = TextTrack;
  24306. videojs.AudioTrack = AudioTrack;
  24307. videojs.VideoTrack = VideoTrack;
  24308. ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
  24309. videojs[k] = function () {
  24310. log.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
  24311. return Dom[k].apply(null, arguments);
  24312. };
  24313. });
  24314. videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
  24315. /**
  24316. * A reference to the {@link module:dom|DOM utility module} as an object.
  24317. *
  24318. * @type {Object}
  24319. * @see {@link module:dom|dom}
  24320. */
  24321. videojs.dom = Dom;
  24322. /**
  24323. * A reference to the {@link module:fn|fn utility module} as an object.
  24324. *
  24325. * @type {Object}
  24326. * @see {@link module:fn|fn}
  24327. */
  24328. videojs.fn = Fn;
  24329. /**
  24330. * A reference to the {@link module:num|num utility module} as an object.
  24331. *
  24332. * @type {Object}
  24333. * @see {@link module:num|num}
  24334. */
  24335. videojs.num = Num;
  24336. /**
  24337. * A reference to the {@link module:str|str utility module} as an object.
  24338. *
  24339. * @type {Object}
  24340. * @see {@link module:str|str}
  24341. */
  24342. videojs.str = Str;
  24343. /**
  24344. * A reference to the {@link module:url|URL utility module} as an object.
  24345. *
  24346. * @type {Object}
  24347. * @see {@link module:url|url}
  24348. */
  24349. videojs.url = Url;
  24350. module.exports = videojs;