plyr.js 305 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137
  1. typeof navigator === "object" && (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define('Plyr', factory) :
  4. (global = global || self, global.Plyr = factory());
  5. }(this, function () { 'use strict';
  6. function _classCallCheck(instance, Constructor) {
  7. if (!(instance instanceof Constructor)) {
  8. throw new TypeError("Cannot call a class as a function");
  9. }
  10. }
  11. function _defineProperties(target, props) {
  12. for (var i = 0; i < props.length; i++) {
  13. var descriptor = props[i];
  14. descriptor.enumerable = descriptor.enumerable || false;
  15. descriptor.configurable = true;
  16. if ("value" in descriptor) descriptor.writable = true;
  17. Object.defineProperty(target, descriptor.key, descriptor);
  18. }
  19. }
  20. function _createClass(Constructor, protoProps, staticProps) {
  21. if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  22. if (staticProps) _defineProperties(Constructor, staticProps);
  23. return Constructor;
  24. }
  25. function _defineProperty(obj, key, value) {
  26. if (key in obj) {
  27. Object.defineProperty(obj, key, {
  28. value: value,
  29. enumerable: true,
  30. configurable: true,
  31. writable: true
  32. });
  33. } else {
  34. obj[key] = value;
  35. }
  36. return obj;
  37. }
  38. function _slicedToArray(arr, i) {
  39. return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
  40. }
  41. function _toConsumableArray(arr) {
  42. return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
  43. }
  44. function _arrayWithoutHoles(arr) {
  45. if (Array.isArray(arr)) {
  46. for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
  47. return arr2;
  48. }
  49. }
  50. function _arrayWithHoles(arr) {
  51. if (Array.isArray(arr)) return arr;
  52. }
  53. function _iterableToArray(iter) {
  54. if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
  55. }
  56. function _iterableToArrayLimit(arr, i) {
  57. var _arr = [];
  58. var _n = true;
  59. var _d = false;
  60. var _e = undefined;
  61. try {
  62. for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
  63. _arr.push(_s.value);
  64. if (i && _arr.length === i) break;
  65. }
  66. } catch (err) {
  67. _d = true;
  68. _e = err;
  69. } finally {
  70. try {
  71. if (!_n && _i["return"] != null) _i["return"]();
  72. } finally {
  73. if (_d) throw _e;
  74. }
  75. }
  76. return _arr;
  77. }
  78. function _nonIterableSpread() {
  79. throw new TypeError("Invalid attempt to spread non-iterable instance");
  80. }
  81. function _nonIterableRest() {
  82. throw new TypeError("Invalid attempt to destructure non-iterable instance");
  83. }
  84. var defaults = {
  85. addCSS: true,
  86. // Add CSS to the element to improve usability (required here or in your CSS!)
  87. thumbWidth: 15,
  88. // The width of the thumb handle
  89. watch: true // Watch for new elements that match a string target
  90. };
  91. // Element matches a selector
  92. function matches(element, selector) {
  93. function match() {
  94. return Array.from(document.querySelectorAll(selector)).includes(this);
  95. }
  96. var matches = match;
  97. return matches.call(element, selector);
  98. }
  99. // Trigger event
  100. function trigger(element, type) {
  101. if (!element || !type) {
  102. return;
  103. } // Create and dispatch the event
  104. var event = new Event(type); // Dispatch the event
  105. element.dispatchEvent(event);
  106. }
  107. // ==========================================================================
  108. // Type checking utils
  109. // ==========================================================================
  110. var getConstructor = function getConstructor(input) {
  111. return input !== null && typeof input !== 'undefined' ? input.constructor : null;
  112. };
  113. var instanceOf = function instanceOf(input, constructor) {
  114. return Boolean(input && constructor && input instanceof constructor);
  115. };
  116. var isNullOrUndefined = function isNullOrUndefined(input) {
  117. return input === null || typeof input === 'undefined';
  118. };
  119. var isObject = function isObject(input) {
  120. return getConstructor(input) === Object;
  121. };
  122. var isNumber = function isNumber(input) {
  123. return getConstructor(input) === Number && !Number.isNaN(input);
  124. };
  125. var isString = function isString(input) {
  126. return getConstructor(input) === String;
  127. };
  128. var isBoolean = function isBoolean(input) {
  129. return getConstructor(input) === Boolean;
  130. };
  131. var isFunction = function isFunction(input) {
  132. return getConstructor(input) === Function;
  133. };
  134. var isArray = function isArray(input) {
  135. return Array.isArray(input);
  136. };
  137. var isNodeList = function isNodeList(input) {
  138. return instanceOf(input, NodeList);
  139. };
  140. var isElement = function isElement(input) {
  141. return instanceOf(input, Element);
  142. };
  143. var isEvent = function isEvent(input) {
  144. return instanceOf(input, Event);
  145. };
  146. var isEmpty = function isEmpty(input) {
  147. return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;
  148. };
  149. var is = {
  150. nullOrUndefined: isNullOrUndefined,
  151. object: isObject,
  152. number: isNumber,
  153. string: isString,
  154. boolean: isBoolean,
  155. function: isFunction,
  156. array: isArray,
  157. nodeList: isNodeList,
  158. element: isElement,
  159. event: isEvent,
  160. empty: isEmpty
  161. };
  162. // Get the number of decimal places
  163. function getDecimalPlaces(value) {
  164. var match = "".concat(value).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
  165. if (!match) {
  166. return 0;
  167. }
  168. return Math.max(0, // Number of digits right of decimal point.
  169. (match[1] ? match[1].length : 0) - ( // Adjust for scientific notation.
  170. match[2] ? +match[2] : 0));
  171. } // Round to the nearest step
  172. function round(number, step) {
  173. if (step < 1) {
  174. var places = getDecimalPlaces(step);
  175. return parseFloat(number.toFixed(places));
  176. }
  177. return Math.round(number / step) * step;
  178. }
  179. var RangeTouch =
  180. /*#__PURE__*/
  181. function () {
  182. /**
  183. * Setup a new instance
  184. * @param {String|Element} target
  185. * @param {Object} options
  186. */
  187. function RangeTouch(target, options) {
  188. _classCallCheck(this, RangeTouch);
  189. if (is.element(target)) {
  190. // An Element is passed, use it directly
  191. this.element = target;
  192. } else if (is.string(target)) {
  193. // A CSS Selector is passed, fetch it from the DOM
  194. this.element = document.querySelector(target);
  195. }
  196. if (!is.element(this.element) || !is.empty(this.element.rangeTouch)) {
  197. return;
  198. }
  199. this.config = Object.assign({}, defaults, options);
  200. this.init();
  201. }
  202. _createClass(RangeTouch, [{
  203. key: "init",
  204. value: function init() {
  205. // Bail if not a touch enabled device
  206. if (!RangeTouch.enabled) {
  207. return;
  208. } // Add useful CSS
  209. if (this.config.addCSS) {
  210. // TODO: Restore original values on destroy
  211. this.element.style.userSelect = 'none';
  212. this.element.style.webKitUserSelect = 'none';
  213. this.element.style.touchAction = 'manipulation';
  214. }
  215. this.listeners(true);
  216. this.element.rangeTouch = this;
  217. }
  218. }, {
  219. key: "destroy",
  220. value: function destroy() {
  221. // Bail if not a touch enabled device
  222. if (!RangeTouch.enabled) {
  223. return;
  224. }
  225. this.listeners(false);
  226. this.element.rangeTouch = null;
  227. }
  228. }, {
  229. key: "listeners",
  230. value: function listeners(toggle) {
  231. var _this = this;
  232. var method = toggle ? 'addEventListener' : 'removeEventListener'; // Listen for events
  233. ['touchstart', 'touchmove', 'touchend'].forEach(function (type) {
  234. _this.element[method](type, function (event) {
  235. return _this.set(event);
  236. }, false);
  237. });
  238. }
  239. /**
  240. * Get the value based on touch position
  241. * @param {Event} event
  242. */
  243. }, {
  244. key: "get",
  245. value: function get(event) {
  246. if (!RangeTouch.enabled || !is.event(event)) {
  247. return null;
  248. }
  249. var input = event.target;
  250. var touch = event.changedTouches[0];
  251. var min = parseFloat(input.getAttribute('min')) || 0;
  252. var max = parseFloat(input.getAttribute('max')) || 100;
  253. var step = parseFloat(input.getAttribute('step')) || 1;
  254. var delta = max - min; // Calculate percentage
  255. var percent;
  256. var clientRect = input.getBoundingClientRect();
  257. var thumbWidth = 100 / clientRect.width * (this.config.thumbWidth / 2) / 100; // Determine left percentage
  258. percent = 100 / clientRect.width * (touch.clientX - clientRect.left); // Don't allow outside bounds
  259. if (percent < 0) {
  260. percent = 0;
  261. } else if (percent > 100) {
  262. percent = 100;
  263. } // Factor in the thumb offset
  264. if (percent < 50) {
  265. percent -= (100 - percent * 2) * thumbWidth;
  266. } else if (percent > 50) {
  267. percent += (percent - 50) * 2 * thumbWidth;
  268. } // Find the closest step to the mouse position
  269. return min + round(delta * (percent / 100), step);
  270. }
  271. /**
  272. * Update range value based on position
  273. * @param {Event} event
  274. */
  275. }, {
  276. key: "set",
  277. value: function set(event) {
  278. if (!RangeTouch.enabled || !is.event(event) || event.target.disabled) {
  279. return;
  280. } // Prevent text highlight on iOS
  281. event.preventDefault(); // Set value
  282. event.target.value = this.get(event); // Trigger event
  283. trigger(event.target, event.type === 'touchend' ? 'change' : 'input');
  284. }
  285. }], [{
  286. key: "setup",
  287. /**
  288. * Setup multiple instances
  289. * @param {String|Element|NodeList|Array} target
  290. * @param {Object} options
  291. */
  292. value: function setup(target) {
  293. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  294. var targets = null;
  295. if (is.empty(target) || is.string(target)) {
  296. targets = Array.from(document.querySelectorAll(is.string(target) ? target : 'input[type="range"]'));
  297. } else if (is.element(target)) {
  298. targets = [target];
  299. } else if (is.nodeList(target)) {
  300. targets = Array.from(target);
  301. } else if (is.array(target)) {
  302. targets = target.filter(is.element);
  303. }
  304. if (is.empty(targets)) {
  305. return null;
  306. }
  307. var config = Object.assign({}, defaults, options);
  308. if (is.string(target) && config.watch) {
  309. // Create an observer instance
  310. var observer = new MutationObserver(function (mutations) {
  311. Array.from(mutations).forEach(function (mutation) {
  312. Array.from(mutation.addedNodes).forEach(function (node) {
  313. if (!is.element(node) || !matches(node, target)) {
  314. return;
  315. } // eslint-disable-next-line no-unused-vars
  316. var range = new RangeTouch(node, config);
  317. });
  318. });
  319. }); // Pass in the target node, as well as the observer options
  320. observer.observe(document.body, {
  321. childList: true,
  322. subtree: true
  323. });
  324. }
  325. return targets.map(function (t) {
  326. return new RangeTouch(t, options);
  327. });
  328. }
  329. }, {
  330. key: "enabled",
  331. get: function get() {
  332. return 'ontouchstart' in document.documentElement;
  333. }
  334. }]);
  335. return RangeTouch;
  336. }();
  337. // ==========================================================================
  338. // Type checking utils
  339. // ==========================================================================
  340. var getConstructor$1 = function getConstructor(input) {
  341. return input !== null && typeof input !== 'undefined' ? input.constructor : null;
  342. };
  343. var instanceOf$1 = function instanceOf(input, constructor) {
  344. return Boolean(input && constructor && input instanceof constructor);
  345. };
  346. var isNullOrUndefined$1 = function isNullOrUndefined(input) {
  347. return input === null || typeof input === 'undefined';
  348. };
  349. var isObject$1 = function isObject(input) {
  350. return getConstructor$1(input) === Object;
  351. };
  352. var isNumber$1 = function isNumber(input) {
  353. return getConstructor$1(input) === Number && !Number.isNaN(input);
  354. };
  355. var isString$1 = function isString(input) {
  356. return getConstructor$1(input) === String;
  357. };
  358. var isBoolean$1 = function isBoolean(input) {
  359. return getConstructor$1(input) === Boolean;
  360. };
  361. var isFunction$1 = function isFunction(input) {
  362. return getConstructor$1(input) === Function;
  363. };
  364. var isArray$1 = function isArray(input) {
  365. return Array.isArray(input);
  366. };
  367. var isWeakMap = function isWeakMap(input) {
  368. return instanceOf$1(input, WeakMap);
  369. };
  370. var isNodeList$1 = function isNodeList(input) {
  371. return instanceOf$1(input, NodeList);
  372. };
  373. var isElement$1 = function isElement(input) {
  374. return instanceOf$1(input, Element);
  375. };
  376. var isTextNode = function isTextNode(input) {
  377. return getConstructor$1(input) === Text;
  378. };
  379. var isEvent$1 = function isEvent(input) {
  380. return instanceOf$1(input, Event);
  381. };
  382. var isKeyboardEvent = function isKeyboardEvent(input) {
  383. return instanceOf$1(input, KeyboardEvent);
  384. };
  385. var isCue = function isCue(input) {
  386. return instanceOf$1(input, window.TextTrackCue) || instanceOf$1(input, window.VTTCue);
  387. };
  388. var isTrack = function isTrack(input) {
  389. return instanceOf$1(input, TextTrack) || !isNullOrUndefined$1(input) && isString$1(input.kind);
  390. };
  391. var isPromise = function isPromise(input) {
  392. return instanceOf$1(input, Promise);
  393. };
  394. var isEmpty$1 = function isEmpty(input) {
  395. return isNullOrUndefined$1(input) || (isString$1(input) || isArray$1(input) || isNodeList$1(input)) && !input.length || isObject$1(input) && !Object.keys(input).length;
  396. };
  397. var isUrl = function isUrl(input) {
  398. // Accept a URL object
  399. if (instanceOf$1(input, window.URL)) {
  400. return true;
  401. } // Must be string from here
  402. if (!isString$1(input)) {
  403. return false;
  404. } // Add the protocol if required
  405. var string = input;
  406. if (!input.startsWith('http://') || !input.startsWith('https://')) {
  407. string = "http://".concat(input);
  408. }
  409. try {
  410. return !isEmpty$1(new URL(string).hostname);
  411. } catch (e) {
  412. return false;
  413. }
  414. };
  415. var is$1 = {
  416. nullOrUndefined: isNullOrUndefined$1,
  417. object: isObject$1,
  418. number: isNumber$1,
  419. string: isString$1,
  420. boolean: isBoolean$1,
  421. function: isFunction$1,
  422. array: isArray$1,
  423. weakMap: isWeakMap,
  424. nodeList: isNodeList$1,
  425. element: isElement$1,
  426. textNode: isTextNode,
  427. event: isEvent$1,
  428. keyboardEvent: isKeyboardEvent,
  429. cue: isCue,
  430. track: isTrack,
  431. promise: isPromise,
  432. url: isUrl,
  433. empty: isEmpty$1
  434. };
  435. // ==========================================================================
  436. // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
  437. // https://www.youtube.com/watch?v=NPM6172J22g
  438. var supportsPassiveListeners = function () {
  439. // Test via a getter in the options object to see if the passive property is accessed
  440. var supported = false;
  441. try {
  442. var options = Object.defineProperty({}, 'passive', {
  443. get: function get() {
  444. supported = true;
  445. return null;
  446. }
  447. });
  448. window.addEventListener('test', null, options);
  449. window.removeEventListener('test', null, options);
  450. } catch (e) {// Do nothing
  451. }
  452. return supported;
  453. }(); // Toggle event listener
  454. function toggleListener(element, event, callback) {
  455. var _this = this;
  456. var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
  457. var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
  458. var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
  459. // Bail if no element, event, or callback
  460. if (!element || !('addEventListener' in element) || is$1.empty(event) || !is$1.function(callback)) {
  461. return;
  462. } // Allow multiple events
  463. var events = event.split(' '); // Build options
  464. // Default to just the capture boolean for browsers with no passive listener support
  465. var options = capture; // If passive events listeners are supported
  466. if (supportsPassiveListeners) {
  467. options = {
  468. // Whether the listener can be passive (i.e. default never prevented)
  469. passive: passive,
  470. // Whether the listener is a capturing listener or not
  471. capture: capture
  472. };
  473. } // If a single node is passed, bind the event listener
  474. events.forEach(function (type) {
  475. if (_this && _this.eventListeners && toggle) {
  476. // Cache event listener
  477. _this.eventListeners.push({
  478. element: element,
  479. type: type,
  480. callback: callback,
  481. options: options
  482. });
  483. }
  484. element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
  485. });
  486. } // Bind event handler
  487. function on(element) {
  488. var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  489. var callback = arguments.length > 2 ? arguments[2] : undefined;
  490. var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
  491. var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
  492. toggleListener.call(this, element, events, callback, true, passive, capture);
  493. } // Unbind event handler
  494. function off(element) {
  495. var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  496. var callback = arguments.length > 2 ? arguments[2] : undefined;
  497. var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
  498. var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
  499. toggleListener.call(this, element, events, callback, false, passive, capture);
  500. } // Bind once-only event handler
  501. function once(element) {
  502. var _this2 = this;
  503. var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  504. var callback = arguments.length > 2 ? arguments[2] : undefined;
  505. var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
  506. var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
  507. var onceCallback = function onceCallback() {
  508. off(element, events, onceCallback, passive, capture);
  509. for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
  510. args[_key] = arguments[_key];
  511. }
  512. callback.apply(_this2, args);
  513. };
  514. toggleListener.call(this, element, events, onceCallback, true, passive, capture);
  515. } // Trigger event
  516. function triggerEvent(element) {
  517. var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  518. var bubbles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
  519. var detail = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
  520. // Bail if no element
  521. if (!is$1.element(element) || is$1.empty(type)) {
  522. return;
  523. } // Create and dispatch the event
  524. var event = new CustomEvent(type, {
  525. bubbles: bubbles,
  526. detail: Object.assign({}, detail, {
  527. plyr: this
  528. })
  529. }); // Dispatch the event
  530. element.dispatchEvent(event);
  531. } // Unbind all cached event listeners
  532. function unbindListeners() {
  533. if (this && this.eventListeners) {
  534. this.eventListeners.forEach(function (item) {
  535. var element = item.element,
  536. type = item.type,
  537. callback = item.callback,
  538. options = item.options;
  539. element.removeEventListener(type, callback, options);
  540. });
  541. this.eventListeners = [];
  542. }
  543. } // Run method when / if player is ready
  544. function ready() {
  545. var _this3 = this;
  546. return new Promise(function (resolve) {
  547. return _this3.ready ? setTimeout(resolve, 0) : on.call(_this3, _this3.elements.container, 'ready', resolve);
  548. }).then(function () {});
  549. }
  550. function cloneDeep(object) {
  551. return JSON.parse(JSON.stringify(object));
  552. } // Get a nested value in an object
  553. function getDeep(object, path) {
  554. return path.split('.').reduce(function (obj, key) {
  555. return obj && obj[key];
  556. }, object);
  557. } // Deep extend destination object with N more objects
  558. function extend() {
  559. var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  560. for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  561. sources[_key - 1] = arguments[_key];
  562. }
  563. if (!sources.length) {
  564. return target;
  565. }
  566. var source = sources.shift();
  567. if (!is$1.object(source)) {
  568. return target;
  569. }
  570. Object.keys(source).forEach(function (key) {
  571. if (is$1.object(source[key])) {
  572. if (!Object.keys(target).includes(key)) {
  573. Object.assign(target, _defineProperty({}, key, {}));
  574. }
  575. extend(target[key], source[key]);
  576. } else {
  577. Object.assign(target, _defineProperty({}, key, source[key]));
  578. }
  579. });
  580. return extend.apply(void 0, [target].concat(sources));
  581. }
  582. function wrap(elements, wrapper) {
  583. // Convert `elements` to an array, if necessary.
  584. var targets = elements.length ? elements : [elements]; // Loops backwards to prevent having to clone the wrapper on the
  585. // first element (see `child` below).
  586. Array.from(targets).reverse().forEach(function (element, index) {
  587. var child = index > 0 ? wrapper.cloneNode(true) : wrapper; // Cache the current parent and sibling.
  588. var parent = element.parentNode;
  589. var sibling = element.nextSibling; // Wrap the element (is automatically removed from its current
  590. // parent).
  591. child.appendChild(element); // If the element had a sibling, insert the wrapper before
  592. // the sibling to maintain the HTML structure; otherwise, just
  593. // append it to the parent.
  594. if (sibling) {
  595. parent.insertBefore(child, sibling);
  596. } else {
  597. parent.appendChild(child);
  598. }
  599. });
  600. } // Set attributes
  601. function setAttributes(element, attributes) {
  602. if (!is$1.element(element) || is$1.empty(attributes)) {
  603. return;
  604. } // Assume null and undefined attributes should be left out,
  605. // Setting them would otherwise convert them to "null" and "undefined"
  606. Object.entries(attributes).filter(function (_ref) {
  607. var _ref2 = _slicedToArray(_ref, 2),
  608. value = _ref2[1];
  609. return !is$1.nullOrUndefined(value);
  610. }).forEach(function (_ref3) {
  611. var _ref4 = _slicedToArray(_ref3, 2),
  612. key = _ref4[0],
  613. value = _ref4[1];
  614. return element.setAttribute(key, value);
  615. });
  616. } // Create a DocumentFragment
  617. function createElement(type, attributes, text) {
  618. // Create a new <element>
  619. var element = document.createElement(type); // Set all passed attributes
  620. if (is$1.object(attributes)) {
  621. setAttributes(element, attributes);
  622. } // Add text node
  623. if (is$1.string(text)) {
  624. element.innerText = text;
  625. } // Return built element
  626. return element;
  627. } // Inaert an element after another
  628. function insertAfter(element, target) {
  629. if (!is$1.element(element) || !is$1.element(target)) {
  630. return;
  631. }
  632. target.parentNode.insertBefore(element, target.nextSibling);
  633. } // Insert a DocumentFragment
  634. function insertElement(type, parent, attributes, text) {
  635. if (!is$1.element(parent)) {
  636. return;
  637. }
  638. parent.appendChild(createElement(type, attributes, text));
  639. } // Remove element(s)
  640. function removeElement(element) {
  641. if (is$1.nodeList(element) || is$1.array(element)) {
  642. Array.from(element).forEach(removeElement);
  643. return;
  644. }
  645. if (!is$1.element(element) || !is$1.element(element.parentNode)) {
  646. return;
  647. }
  648. element.parentNode.removeChild(element);
  649. } // Remove all child elements
  650. function emptyElement(element) {
  651. if (!is$1.element(element)) {
  652. return;
  653. }
  654. var length = element.childNodes.length;
  655. while (length > 0) {
  656. element.removeChild(element.lastChild);
  657. length -= 1;
  658. }
  659. } // Replace element
  660. function replaceElement(newChild, oldChild) {
  661. if (!is$1.element(oldChild) || !is$1.element(oldChild.parentNode) || !is$1.element(newChild)) {
  662. return null;
  663. }
  664. oldChild.parentNode.replaceChild(newChild, oldChild);
  665. return newChild;
  666. } // Get an attribute object from a string selector
  667. function getAttributesFromSelector(sel, existingAttributes) {
  668. // For example:
  669. // '.test' to { class: 'test' }
  670. // '#test' to { id: 'test' }
  671. // '[data-test="test"]' to { 'data-test': 'test' }
  672. if (!is$1.string(sel) || is$1.empty(sel)) {
  673. return {};
  674. }
  675. var attributes = {};
  676. var existing = extend({}, existingAttributes);
  677. sel.split(',').forEach(function (s) {
  678. // Remove whitespace
  679. var selector = s.trim();
  680. var className = selector.replace('.', '');
  681. var stripped = selector.replace(/[[\]]/g, ''); // Get the parts and value
  682. var parts = stripped.split('=');
  683. var _parts = _slicedToArray(parts, 1),
  684. key = _parts[0];
  685. var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; // Get the first character
  686. var start = selector.charAt(0);
  687. switch (start) {
  688. case '.':
  689. // Add to existing classname
  690. if (is$1.string(existing.class)) {
  691. attributes.class = "".concat(existing.class, " ").concat(className);
  692. } else {
  693. attributes.class = className;
  694. }
  695. break;
  696. case '#':
  697. // ID selector
  698. attributes.id = selector.replace('#', '');
  699. break;
  700. case '[':
  701. // Attribute selector
  702. attributes[key] = value;
  703. break;
  704. default:
  705. break;
  706. }
  707. });
  708. return extend(existing, attributes);
  709. } // Toggle hidden
  710. function toggleHidden(element, hidden) {
  711. if (!is$1.element(element)) {
  712. return;
  713. }
  714. var hide = hidden;
  715. if (!is$1.boolean(hide)) {
  716. hide = !element.hidden;
  717. }
  718. if (hide) {
  719. element.setAttribute('hidden', '');
  720. } else {
  721. element.removeAttribute('hidden');
  722. }
  723. } // Mirror Element.classList.toggle, with IE compatibility for "force" argument
  724. function toggleClass(element, className, force) {
  725. if (is$1.nodeList(element)) {
  726. return Array.from(element).map(function (e) {
  727. return toggleClass(e, className, force);
  728. });
  729. }
  730. if (is$1.element(element)) {
  731. var method = 'toggle';
  732. if (typeof force !== 'undefined') {
  733. method = force ? 'add' : 'remove';
  734. }
  735. element.classList[method](className);
  736. return element.classList.contains(className);
  737. }
  738. return false;
  739. } // Has class name
  740. function hasClass(element, className) {
  741. return is$1.element(element) && element.classList.contains(className);
  742. } // Element matches selector
  743. function matches$1(element, selector) {
  744. function match() {
  745. return Array.from(document.querySelectorAll(selector)).includes(this);
  746. }
  747. var matches = match;
  748. return matches.call(element, selector);
  749. } // Find all elements
  750. function getElements(selector) {
  751. return this.elements.container.querySelectorAll(selector);
  752. } // Find a single element
  753. function getElement(selector) {
  754. return this.elements.container.querySelector(selector);
  755. } // Trap focus inside container
  756. function trapFocus() {
  757. var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
  758. var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  759. if (!is$1.element(element)) {
  760. return;
  761. }
  762. var focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
  763. var first = focusable[0];
  764. var last = focusable[focusable.length - 1];
  765. var trap = function trap(event) {
  766. // Bail if not tab key or not fullscreen
  767. if (event.key !== 'Tab' || event.keyCode !== 9) {
  768. return;
  769. } // Get the current focused element
  770. var focused = document.activeElement;
  771. if (focused === last && !event.shiftKey) {
  772. // Move focus to first element that can be tabbed if Shift isn't used
  773. first.focus();
  774. event.preventDefault();
  775. } else if (focused === first && event.shiftKey) {
  776. // Move focus to last element that can be tabbed if Shift is used
  777. last.focus();
  778. event.preventDefault();
  779. }
  780. };
  781. toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
  782. } // Set focus and tab focus class
  783. function setFocus() {
  784. var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
  785. var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  786. if (!is$1.element(element)) {
  787. return;
  788. } // Set regular focus
  789. element.focus({
  790. preventScroll: true
  791. }); // If we want to mimic keyboard focus via tab
  792. if (tabFocus) {
  793. toggleClass(element, this.config.classNames.tabFocus);
  794. }
  795. }
  796. // ==========================================================================
  797. var transitionEndEvent = function () {
  798. var element = document.createElement('span');
  799. var events = {
  800. WebkitTransition: 'webkitTransitionEnd',
  801. MozTransition: 'transitionend',
  802. OTransition: 'oTransitionEnd otransitionend',
  803. transition: 'transitionend'
  804. };
  805. var type = Object.keys(events).find(function (event) {
  806. return element.style[event] !== undefined;
  807. });
  808. return is$1.string(type) ? events[type] : false;
  809. }(); // Force repaint of element
  810. function repaint(element) {
  811. setTimeout(function () {
  812. try {
  813. toggleHidden(element, true);
  814. element.offsetHeight; // eslint-disable-line
  815. toggleHidden(element, false);
  816. } catch (e) {// Do nothing
  817. }
  818. }, 0);
  819. }
  820. // ==========================================================================
  821. // Browser sniffing
  822. // Unfortunately, due to mixed support, UA sniffing is required
  823. // ==========================================================================
  824. var browser = {
  825. isIE:
  826. /* @cc_on!@ */
  827. !!document.documentMode,
  828. isEdge: window.navigator.userAgent.includes('Edge'),
  829. isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
  830. isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
  831. isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform)
  832. };
  833. var defaultCodecs = {
  834. 'audio/ogg': 'vorbis',
  835. 'audio/wav': '1',
  836. 'video/webm': 'vp8, vorbis',
  837. 'video/mp4': 'avc1.42E01E, mp4a.40.2',
  838. 'video/ogg': 'theora'
  839. }; // Check for feature support
  840. var support = {
  841. // Basic support
  842. audio: 'canPlayType' in document.createElement('audio'),
  843. video: 'canPlayType' in document.createElement('video'),
  844. // Check for support
  845. // Basic functionality vs full UI
  846. check: function check(type, provider, playsinline) {
  847. var canPlayInline = browser.isIPhone && playsinline && support.playsinline;
  848. var api = support[type] || provider !== 'html5';
  849. var ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
  850. return {
  851. api: api,
  852. ui: ui
  853. };
  854. },
  855. // Picture-in-picture support
  856. // Safari & Chrome only currently
  857. pip: function () {
  858. if (browser.isIPhone) {
  859. return false;
  860. } // Safari
  861. // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
  862. if (is$1.function(createElement('video').webkitSetPresentationMode)) {
  863. return true;
  864. } // Chrome
  865. // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
  866. if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
  867. return true;
  868. }
  869. return false;
  870. }(),
  871. // Airplay support
  872. // Safari only currently
  873. airplay: is$1.function(window.WebKitPlaybackTargetAvailabilityEvent),
  874. // Inline playback support
  875. // https://webkit.org/blog/6784/new-video-policies-for-ios/
  876. playsinline: 'playsInline' in document.createElement('video'),
  877. // Check for mime type support against a player instance
  878. // Credits: http://diveintohtml5.info/everything.html
  879. // Related: http://www.leanbackplayer.com/test/h5mt.html
  880. mime: function mime(input) {
  881. if (is$1.empty(input)) {
  882. return false;
  883. }
  884. var _input$split = input.split('/'),
  885. _input$split2 = _slicedToArray(_input$split, 1),
  886. mediaType = _input$split2[0];
  887. var type = input; // Verify we're using HTML5 and there's no media type mismatch
  888. if (!this.isHTML5 || mediaType !== this.type) {
  889. return false;
  890. } // Add codec if required
  891. if (Object.keys(defaultCodecs).includes(type)) {
  892. type += "; codecs=\"".concat(defaultCodecs[input], "\"");
  893. }
  894. try {
  895. return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
  896. } catch (e) {
  897. return false;
  898. }
  899. },
  900. // Check for textTracks support
  901. textTracks: 'textTracks' in document.createElement('video'),
  902. // <input type="range"> Sliders
  903. rangeInput: function () {
  904. var range = document.createElement('input');
  905. range.type = 'range';
  906. return range.type === 'range';
  907. }(),
  908. // Touch
  909. // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
  910. touch: 'ontouchstart' in document.documentElement,
  911. // Detect transitions support
  912. transitions: transitionEndEvent !== false,
  913. // Reduced motion iOS & MacOS setting
  914. // https://webkit.org/blog/7551/responsive-design-for-motion/
  915. reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
  916. };
  917. function validateRatio(input) {
  918. if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) {
  919. return false;
  920. }
  921. var ratio = is$1.array(input) ? input : input.split(':');
  922. return ratio.map(Number).every(is$1.number);
  923. }
  924. function reduceAspectRatio(ratio) {
  925. if (!is$1.array(ratio) || !ratio.every(is$1.number)) {
  926. return null;
  927. }
  928. var _ratio = _slicedToArray(ratio, 2),
  929. width = _ratio[0],
  930. height = _ratio[1];
  931. var getDivider = function getDivider(w, h) {
  932. return h === 0 ? w : getDivider(h, w % h);
  933. };
  934. var divider = getDivider(width, height);
  935. return [width / divider, height / divider];
  936. }
  937. function getAspectRatio(input) {
  938. var parse = function parse(ratio) {
  939. if (!validateRatio(ratio)) {
  940. return null;
  941. }
  942. return ratio.split(':').map(Number);
  943. }; // Provided ratio
  944. var ratio = parse(input); // Get from config
  945. if (ratio === null) {
  946. ratio = parse(this.config.ratio);
  947. } // Get from embed
  948. if (ratio === null && !is$1.empty(this.embed) && is$1.array(this.embed.ratio)) {
  949. ratio = this.embed.ratio;
  950. } // Get from HTML5 video
  951. if (ratio === null && this.isHTML5) {
  952. var _this$media = this.media,
  953. videoWidth = _this$media.videoWidth,
  954. videoHeight = _this$media.videoHeight;
  955. ratio = reduceAspectRatio([videoWidth, videoHeight]);
  956. }
  957. return ratio;
  958. } // Set aspect ratio for responsive container
  959. function setAspectRatio(input) {
  960. if (!this.isVideo) {
  961. return {};
  962. }
  963. var ratio = getAspectRatio.call(this, input);
  964. var _ref = is$1.array(ratio) ? ratio : [0, 0],
  965. _ref2 = _slicedToArray(_ref, 2),
  966. w = _ref2[0],
  967. h = _ref2[1];
  968. var padding = 100 / w * h;
  969. this.elements.wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI
  970. if (this.isVimeo && this.supported.ui) {
  971. var height = 240;
  972. var offset = (height - padding) / (height / 50);
  973. this.media.style.transform = "translateY(-".concat(offset, "%)");
  974. } else if (this.isHTML5) {
  975. this.elements.wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
  976. }
  977. return {
  978. padding: padding,
  979. ratio: ratio
  980. };
  981. }
  982. // ==========================================================================
  983. var html5 = {
  984. getSources: function getSources() {
  985. var _this = this;
  986. if (!this.isHTML5) {
  987. return [];
  988. }
  989. var sources = Array.from(this.media.querySelectorAll('source')); // Filter out unsupported sources (if type is specified)
  990. return sources.filter(function (source) {
  991. var type = source.getAttribute('type');
  992. if (is$1.empty(type)) {
  993. return true;
  994. }
  995. return support.mime.call(_this, type);
  996. });
  997. },
  998. // Get quality levels
  999. getQualityOptions: function getQualityOptions() {
  1000. // Get sizes from <source> elements
  1001. return html5.getSources.call(this).map(function (source) {
  1002. return Number(source.getAttribute('size'));
  1003. }).filter(Boolean);
  1004. },
  1005. extend: function extend() {
  1006. if (!this.isHTML5) {
  1007. return;
  1008. }
  1009. var player = this; // Set aspect ratio if set
  1010. setAspectRatio.call(player); // Quality
  1011. Object.defineProperty(player.media, 'quality', {
  1012. get: function get() {
  1013. // Get sources
  1014. var sources = html5.getSources.call(player);
  1015. var source = sources.find(function (source) {
  1016. return source.getAttribute('src') === player.source;
  1017. }); // Return size, if match is found
  1018. return source && Number(source.getAttribute('size'));
  1019. },
  1020. set: function set(input) {
  1021. // Get sources
  1022. var sources = html5.getSources.call(player); // Get first match for requested size
  1023. var source = sources.find(function (source) {
  1024. return Number(source.getAttribute('size')) === input;
  1025. }); // No matching source found
  1026. if (!source) {
  1027. return;
  1028. } // Get current state
  1029. var _player$media = player.media,
  1030. currentTime = _player$media.currentTime,
  1031. paused = _player$media.paused,
  1032. preload = _player$media.preload,
  1033. readyState = _player$media.readyState; // Set new source
  1034. player.media.src = source.getAttribute('src'); // Prevent loading if preload="none" and the current source isn't loaded (#1044)
  1035. if (preload !== 'none' || readyState) {
  1036. // Restore time
  1037. player.once('loadedmetadata', function () {
  1038. player.currentTime = currentTime; // Resume playing
  1039. if (!paused) {
  1040. player.play();
  1041. }
  1042. }); // Load new source
  1043. player.media.load();
  1044. } // Trigger change event
  1045. triggerEvent.call(player, player.media, 'qualitychange', false, {
  1046. quality: input
  1047. });
  1048. }
  1049. });
  1050. },
  1051. // Cancel current network requests
  1052. // See https://github.com/sampotts/plyr/issues/174
  1053. cancelRequests: function cancelRequests() {
  1054. if (!this.isHTML5) {
  1055. return;
  1056. } // Remove child sources
  1057. removeElement(html5.getSources.call(this)); // Set blank video src attribute
  1058. // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
  1059. // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
  1060. this.media.setAttribute('src', this.config.blankVideo); // Load the new empty source
  1061. // This will cancel existing requests
  1062. // See https://github.com/sampotts/plyr/issues/174
  1063. this.media.load(); // Debugging
  1064. this.debug.log('Cancelled network requests');
  1065. }
  1066. };
  1067. // ==========================================================================
  1068. function dedupe(array) {
  1069. if (!is$1.array(array)) {
  1070. return array;
  1071. }
  1072. return array.filter(function (item, index) {
  1073. return array.indexOf(item) === index;
  1074. });
  1075. } // Get the closest value in an array
  1076. function closest(array, value) {
  1077. if (!is$1.array(array) || !array.length) {
  1078. return null;
  1079. }
  1080. return array.reduce(function (prev, curr) {
  1081. return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
  1082. });
  1083. }
  1084. // ==========================================================================
  1085. function generateId(prefix) {
  1086. return "".concat(prefix, "-").concat(Math.floor(Math.random() * 10000));
  1087. } // Format string
  1088. function format(input) {
  1089. for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  1090. args[_key - 1] = arguments[_key];
  1091. }
  1092. if (is$1.empty(input)) {
  1093. return input;
  1094. }
  1095. return input.toString().replace(/{(\d+)}/g, function (match, i) {
  1096. return args[i].toString();
  1097. });
  1098. } // Get percentage
  1099. function getPercentage(current, max) {
  1100. if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
  1101. return 0;
  1102. }
  1103. return (current / max * 100).toFixed(2);
  1104. } // Replace all occurances of a string in a string
  1105. function replaceAll() {
  1106. var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  1107. var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  1108. var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
  1109. return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
  1110. } // Convert to title case
  1111. function toTitleCase() {
  1112. var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  1113. return input.toString().replace(/\w\S*/g, function (text) {
  1114. return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
  1115. });
  1116. } // Convert string to pascalCase
  1117. function toPascalCase() {
  1118. var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  1119. var string = input.toString(); // Convert kebab case
  1120. string = replaceAll(string, '-', ' '); // Convert snake case
  1121. string = replaceAll(string, '_', ' '); // Convert to title case
  1122. string = toTitleCase(string); // Convert to pascal case
  1123. return replaceAll(string, ' ', '');
  1124. } // Convert string to pascalCase
  1125. function toCamelCase() {
  1126. var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  1127. var string = input.toString(); // Convert to pascal case
  1128. string = toPascalCase(string); // Convert first character to lowercase
  1129. return string.charAt(0).toLowerCase() + string.slice(1);
  1130. } // Remove HTML from a string
  1131. function stripHTML(source) {
  1132. var fragment = document.createDocumentFragment();
  1133. var element = document.createElement('div');
  1134. fragment.appendChild(element);
  1135. element.innerHTML = source;
  1136. return fragment.firstChild.innerText;
  1137. } // Like outerHTML, but also works for DocumentFragment
  1138. function getHTML(element) {
  1139. var wrapper = document.createElement('div');
  1140. wrapper.appendChild(element);
  1141. return wrapper.innerHTML;
  1142. }
  1143. var resources = {
  1144. pip: 'PIP',
  1145. airplay: 'AirPlay',
  1146. html5: 'HTML5',
  1147. vimeo: 'Vimeo',
  1148. youtube: 'YouTube'
  1149. };
  1150. var i18n = {
  1151. get: function get() {
  1152. var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  1153. var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  1154. if (is$1.empty(key) || is$1.empty(config)) {
  1155. return '';
  1156. }
  1157. var string = getDeep(config.i18n, key);
  1158. if (is$1.empty(string)) {
  1159. if (Object.keys(resources).includes(key)) {
  1160. return resources[key];
  1161. }
  1162. return '';
  1163. }
  1164. var replace = {
  1165. '{seektime}': config.seekTime,
  1166. '{title}': config.title
  1167. };
  1168. Object.entries(replace).forEach(function (_ref) {
  1169. var _ref2 = _slicedToArray(_ref, 2),
  1170. key = _ref2[0],
  1171. value = _ref2[1];
  1172. string = replaceAll(string, key, value);
  1173. });
  1174. return string;
  1175. }
  1176. };
  1177. var Storage =
  1178. /*#__PURE__*/
  1179. function () {
  1180. function Storage(player) {
  1181. _classCallCheck(this, Storage);
  1182. this.enabled = player.config.storage.enabled;
  1183. this.key = player.config.storage.key;
  1184. } // Check for actual support (see if we can use it)
  1185. _createClass(Storage, [{
  1186. key: "get",
  1187. value: function get(key) {
  1188. if (!Storage.supported || !this.enabled) {
  1189. return null;
  1190. }
  1191. var store = window.localStorage.getItem(this.key);
  1192. if (is$1.empty(store)) {
  1193. return null;
  1194. }
  1195. var json = JSON.parse(store);
  1196. return is$1.string(key) && key.length ? json[key] : json;
  1197. }
  1198. }, {
  1199. key: "set",
  1200. value: function set(object) {
  1201. // Bail if we don't have localStorage support or it's disabled
  1202. if (!Storage.supported || !this.enabled) {
  1203. return;
  1204. } // Can only store objectst
  1205. if (!is$1.object(object)) {
  1206. return;
  1207. } // Get current storage
  1208. var storage = this.get(); // Default to empty object
  1209. if (is$1.empty(storage)) {
  1210. storage = {};
  1211. } // Update the working copy of the values
  1212. extend(storage, object); // Update storage
  1213. window.localStorage.setItem(this.key, JSON.stringify(storage));
  1214. }
  1215. }], [{
  1216. key: "supported",
  1217. get: function get() {
  1218. try {
  1219. if (!('localStorage' in window)) {
  1220. return false;
  1221. }
  1222. var test = '___test'; // Try to use it (it might be disabled, e.g. user is in private mode)
  1223. // see: https://github.com/sampotts/plyr/issues/131
  1224. window.localStorage.setItem(test, test);
  1225. window.localStorage.removeItem(test);
  1226. return true;
  1227. } catch (e) {
  1228. return false;
  1229. }
  1230. }
  1231. }]);
  1232. return Storage;
  1233. }();
  1234. // ==========================================================================
  1235. // Fetch wrapper
  1236. // Using XHR to avoid issues with older browsers
  1237. // ==========================================================================
  1238. function fetch(url) {
  1239. var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text';
  1240. return new Promise(function (resolve, reject) {
  1241. try {
  1242. var request = new XMLHttpRequest(); // Check for CORS support
  1243. if (!('withCredentials' in request)) {
  1244. return;
  1245. }
  1246. request.addEventListener('load', function () {
  1247. if (responseType === 'text') {
  1248. try {
  1249. resolve(JSON.parse(request.responseText));
  1250. } catch (e) {
  1251. resolve(request.responseText);
  1252. }
  1253. } else {
  1254. resolve(request.response);
  1255. }
  1256. });
  1257. request.addEventListener('error', function () {
  1258. throw new Error(request.status);
  1259. });
  1260. request.open('GET', url, true); // Set the required response type
  1261. request.responseType = responseType;
  1262. request.send();
  1263. } catch (e) {
  1264. reject(e);
  1265. }
  1266. });
  1267. }
  1268. // ==========================================================================
  1269. function loadSprite(url, id) {
  1270. if (!is$1.string(url)) {
  1271. return;
  1272. }
  1273. var prefix = 'cache';
  1274. var hasId = is$1.string(id);
  1275. var isCached = false;
  1276. var exists = function exists() {
  1277. return document.getElementById(id) !== null;
  1278. };
  1279. var update = function update(container, data) {
  1280. container.innerHTML = data; // Check again incase of race condition
  1281. if (hasId && exists()) {
  1282. return;
  1283. } // Inject the SVG to the body
  1284. document.body.insertAdjacentElement('afterbegin', container);
  1285. }; // Only load once if ID set
  1286. if (!hasId || !exists()) {
  1287. var useStorage = Storage.supported; // Create container
  1288. var container = document.createElement('div');
  1289. container.setAttribute('hidden', '');
  1290. if (hasId) {
  1291. container.setAttribute('id', id);
  1292. } // Check in cache
  1293. if (useStorage) {
  1294. var cached = window.localStorage.getItem("".concat(prefix, "-").concat(id));
  1295. isCached = cached !== null;
  1296. if (isCached) {
  1297. var data = JSON.parse(cached);
  1298. update(container, data.content);
  1299. }
  1300. } // Get the sprite
  1301. fetch(url).then(function (result) {
  1302. if (is$1.empty(result)) {
  1303. return;
  1304. }
  1305. if (useStorage) {
  1306. window.localStorage.setItem("".concat(prefix, "-").concat(id), JSON.stringify({
  1307. content: result
  1308. }));
  1309. }
  1310. update(container, result);
  1311. }).catch(function () {});
  1312. }
  1313. }
  1314. // ==========================================================================
  1315. var getHours = function getHours(value) {
  1316. return Math.trunc(value / 60 / 60 % 60, 10);
  1317. };
  1318. var getMinutes = function getMinutes(value) {
  1319. return Math.trunc(value / 60 % 60, 10);
  1320. };
  1321. var getSeconds = function getSeconds(value) {
  1322. return Math.trunc(value % 60, 10);
  1323. }; // Format time to UI friendly string
  1324. function formatTime() {
  1325. var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
  1326. var displayHours = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  1327. var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
  1328. // Bail if the value isn't a number
  1329. if (!is$1.number(time)) {
  1330. return formatTime(null, displayHours, inverted);
  1331. } // Format time component to add leading zero
  1332. var format = function format(value) {
  1333. return "0".concat(value).slice(-2);
  1334. }; // Breakdown to hours, mins, secs
  1335. var hours = getHours(time);
  1336. var mins = getMinutes(time);
  1337. var secs = getSeconds(time); // Do we need to display hours?
  1338. if (displayHours || hours > 0) {
  1339. hours = "".concat(hours, ":");
  1340. } else {
  1341. hours = '';
  1342. } // Render
  1343. return "".concat(inverted && time > 0 ? '-' : '').concat(hours).concat(format(mins), ":").concat(format(secs));
  1344. }
  1345. var controls = {
  1346. // Get icon URL
  1347. getIconUrl: function getIconUrl() {
  1348. var url = new URL(this.config.iconUrl, window.location);
  1349. var cors = url.host !== window.location.host || browser.isIE && !window.svg4everybody;
  1350. return {
  1351. url: this.config.iconUrl,
  1352. cors: cors
  1353. };
  1354. },
  1355. // Find the UI controls
  1356. findElements: function findElements() {
  1357. try {
  1358. this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); // Buttons
  1359. this.elements.buttons = {
  1360. play: getElements.call(this, this.config.selectors.buttons.play),
  1361. pause: getElement.call(this, this.config.selectors.buttons.pause),
  1362. restart: getElement.call(this, this.config.selectors.buttons.restart),
  1363. rewind: getElement.call(this, this.config.selectors.buttons.rewind),
  1364. fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),
  1365. mute: getElement.call(this, this.config.selectors.buttons.mute),
  1366. pip: getElement.call(this, this.config.selectors.buttons.pip),
  1367. airplay: getElement.call(this, this.config.selectors.buttons.airplay),
  1368. settings: getElement.call(this, this.config.selectors.buttons.settings),
  1369. captions: getElement.call(this, this.config.selectors.buttons.captions),
  1370. fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)
  1371. }; // Progress
  1372. this.elements.progress = getElement.call(this, this.config.selectors.progress); // Inputs
  1373. this.elements.inputs = {
  1374. seek: getElement.call(this, this.config.selectors.inputs.seek),
  1375. volume: getElement.call(this, this.config.selectors.inputs.volume)
  1376. }; // Display
  1377. this.elements.display = {
  1378. buffer: getElement.call(this, this.config.selectors.display.buffer),
  1379. currentTime: getElement.call(this, this.config.selectors.display.currentTime),
  1380. duration: getElement.call(this, this.config.selectors.display.duration)
  1381. }; // Seek tooltip
  1382. if (is$1.element(this.elements.progress)) {
  1383. this.elements.display.seekTooltip = this.elements.progress.querySelector(".".concat(this.config.classNames.tooltip));
  1384. }
  1385. return true;
  1386. } catch (error) {
  1387. // Log it
  1388. this.debug.warn('It looks like there is a problem with your custom controls HTML', error); // Restore native video controls
  1389. this.toggleNativeControls(true);
  1390. return false;
  1391. }
  1392. },
  1393. // Create <svg> icon
  1394. createIcon: function createIcon(type, attributes) {
  1395. var namespace = 'http://www.w3.org/2000/svg';
  1396. var iconUrl = controls.getIconUrl.call(this);
  1397. var iconPath = "".concat(!iconUrl.cors ? iconUrl.url : '', "#").concat(this.config.iconPrefix); // Create <svg>
  1398. var icon = document.createElementNS(namespace, 'svg');
  1399. setAttributes(icon, extend(attributes, {
  1400. role: 'presentation',
  1401. focusable: 'false'
  1402. })); // Create the <use> to reference sprite
  1403. var use = document.createElementNS(namespace, 'use');
  1404. var path = "".concat(iconPath, "-").concat(type); // Set `href` attributes
  1405. // https://github.com/sampotts/plyr/issues/460
  1406. // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
  1407. if ('href' in use) {
  1408. use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
  1409. } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
  1410. use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>
  1411. icon.appendChild(use);
  1412. return icon;
  1413. },
  1414. // Create hidden text label
  1415. createLabel: function createLabel(key) {
  1416. var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  1417. var text = i18n.get(key, this.config);
  1418. var attributes = Object.assign({}, attr, {
  1419. class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
  1420. });
  1421. return createElement('span', attributes, text);
  1422. },
  1423. // Create a badge
  1424. createBadge: function createBadge(text) {
  1425. if (is$1.empty(text)) {
  1426. return null;
  1427. }
  1428. var badge = createElement('span', {
  1429. class: this.config.classNames.menu.value
  1430. });
  1431. badge.appendChild(createElement('span', {
  1432. class: this.config.classNames.menu.badge
  1433. }, text));
  1434. return badge;
  1435. },
  1436. // Create a <button>
  1437. createButton: function createButton(buttonType, attr) {
  1438. var _this = this;
  1439. var attributes = extend({}, attr);
  1440. var type = toCamelCase(buttonType);
  1441. var props = {
  1442. element: 'button',
  1443. toggle: false,
  1444. label: null,
  1445. icon: null,
  1446. labelPressed: null,
  1447. iconPressed: null
  1448. };
  1449. ['element', 'icon', 'label'].forEach(function (key) {
  1450. if (Object.keys(attributes).includes(key)) {
  1451. props[key] = attributes[key];
  1452. delete attributes[key];
  1453. }
  1454. }); // Default to 'button' type to prevent form submission
  1455. if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
  1456. attributes.type = 'button';
  1457. } // Set class name
  1458. if (Object.keys(attributes).includes('class')) {
  1459. if (!attributes.class.split(' ').some(function (c) {
  1460. return c === _this.config.classNames.control;
  1461. })) {
  1462. extend(attributes, {
  1463. class: "".concat(attributes.class, " ").concat(this.config.classNames.control)
  1464. });
  1465. }
  1466. } else {
  1467. attributes.class = this.config.classNames.control;
  1468. } // Large play button
  1469. switch (buttonType) {
  1470. case 'play':
  1471. props.toggle = true;
  1472. props.label = 'play';
  1473. props.labelPressed = 'pause';
  1474. props.icon = 'play';
  1475. props.iconPressed = 'pause';
  1476. break;
  1477. case 'mute':
  1478. props.toggle = true;
  1479. props.label = 'mute';
  1480. props.labelPressed = 'unmute';
  1481. props.icon = 'volume';
  1482. props.iconPressed = 'muted';
  1483. break;
  1484. case 'captions':
  1485. props.toggle = true;
  1486. props.label = 'enableCaptions';
  1487. props.labelPressed = 'disableCaptions';
  1488. props.icon = 'captions-off';
  1489. props.iconPressed = 'captions-on';
  1490. break;
  1491. case 'fullscreen':
  1492. props.toggle = true;
  1493. props.label = 'enterFullscreen';
  1494. props.labelPressed = 'exitFullscreen';
  1495. props.icon = 'enter-fullscreen';
  1496. props.iconPressed = 'exit-fullscreen';
  1497. break;
  1498. case 'play-large':
  1499. attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
  1500. type = 'play';
  1501. props.label = 'play';
  1502. props.icon = 'play';
  1503. break;
  1504. default:
  1505. if (is$1.empty(props.label)) {
  1506. props.label = type;
  1507. }
  1508. if (is$1.empty(props.icon)) {
  1509. props.icon = buttonType;
  1510. }
  1511. }
  1512. var button = createElement(props.element); // Setup toggle icon and labels
  1513. if (props.toggle) {
  1514. // Icon
  1515. button.appendChild(controls.createIcon.call(this, props.iconPressed, {
  1516. class: 'icon--pressed'
  1517. }));
  1518. button.appendChild(controls.createIcon.call(this, props.icon, {
  1519. class: 'icon--not-pressed'
  1520. })); // Label/Tooltip
  1521. button.appendChild(controls.createLabel.call(this, props.labelPressed, {
  1522. class: 'label--pressed'
  1523. }));
  1524. button.appendChild(controls.createLabel.call(this, props.label, {
  1525. class: 'label--not-pressed'
  1526. }));
  1527. } else {
  1528. button.appendChild(controls.createIcon.call(this, props.icon));
  1529. button.appendChild(controls.createLabel.call(this, props.label));
  1530. } // Merge and set attributes
  1531. extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
  1532. setAttributes(button, attributes); // We have multiple play buttons
  1533. if (type === 'play') {
  1534. if (!is$1.array(this.elements.buttons[type])) {
  1535. this.elements.buttons[type] = [];
  1536. }
  1537. this.elements.buttons[type].push(button);
  1538. } else {
  1539. this.elements.buttons[type] = button;
  1540. }
  1541. return button;
  1542. },
  1543. // Create an <input type='range'>
  1544. createRange: function createRange(type, attributes) {
  1545. // Seek input
  1546. var input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
  1547. type: 'range',
  1548. min: 0,
  1549. max: 100,
  1550. step: 0.01,
  1551. value: 0,
  1552. autocomplete: 'off',
  1553. // A11y fixes for https://github.com/sampotts/plyr/issues/905
  1554. role: 'slider',
  1555. 'aria-label': i18n.get(type, this.config),
  1556. 'aria-valuemin': 0,
  1557. 'aria-valuemax': 100,
  1558. 'aria-valuenow': 0
  1559. }, attributes));
  1560. this.elements.inputs[type] = input; // Set the fill for webkit now
  1561. controls.updateRangeFill.call(this, input); // Improve support on touch devices
  1562. RangeTouch.setup(input);
  1563. return input;
  1564. },
  1565. // Create a <progress>
  1566. createProgress: function createProgress(type, attributes) {
  1567. var progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {
  1568. min: 0,
  1569. max: 100,
  1570. value: 0,
  1571. role: 'progressbar',
  1572. 'aria-hidden': true
  1573. }, attributes)); // Create the label inside
  1574. if (type !== 'volume') {
  1575. progress.appendChild(createElement('span', null, '0'));
  1576. var suffixKey = {
  1577. played: 'played',
  1578. buffer: 'buffered'
  1579. }[type];
  1580. var suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
  1581. progress.innerText = "% ".concat(suffix.toLowerCase());
  1582. }
  1583. this.elements.display[type] = progress;
  1584. return progress;
  1585. },
  1586. // Create time display
  1587. createTime: function createTime(type, attrs) {
  1588. var attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
  1589. var container = createElement('div', extend(attributes, {
  1590. class: "".concat(attributes.class ? attributes.class : '', " ").concat(this.config.classNames.display.time, " ").trim(),
  1591. 'aria-label': i18n.get(type, this.config)
  1592. }), '00:00'); // Reference for updates
  1593. this.elements.display[type] = container;
  1594. return container;
  1595. },
  1596. // Bind keyboard shortcuts for a menu item
  1597. // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
  1598. // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
  1599. bindMenuItemShortcuts: function bindMenuItemShortcuts(menuItem, type) {
  1600. var _this2 = this;
  1601. // Navigate through menus via arrow keys and space
  1602. on(menuItem, 'keydown keyup', function (event) {
  1603. // We only care about space and ⬆️ ⬇️️ ➡️
  1604. if (![32, 38, 39, 40].includes(event.which)) {
  1605. return;
  1606. } // Prevent play / seek
  1607. event.preventDefault();
  1608. event.stopPropagation(); // We're just here to prevent the keydown bubbling
  1609. if (event.type === 'keydown') {
  1610. return;
  1611. }
  1612. var isRadioButton = matches$1(menuItem, '[role="menuitemradio"]'); // Show the respective menu
  1613. if (!isRadioButton && [32, 39].includes(event.which)) {
  1614. controls.showMenuPanel.call(_this2, type, true);
  1615. } else {
  1616. var target;
  1617. if (event.which !== 32) {
  1618. if (event.which === 40 || isRadioButton && event.which === 39) {
  1619. target = menuItem.nextElementSibling;
  1620. if (!is$1.element(target)) {
  1621. target = menuItem.parentNode.firstElementChild;
  1622. }
  1623. } else {
  1624. target = menuItem.previousElementSibling;
  1625. if (!is$1.element(target)) {
  1626. target = menuItem.parentNode.lastElementChild;
  1627. }
  1628. }
  1629. setFocus.call(_this2, target, true);
  1630. }
  1631. }
  1632. }, false); // Enter will fire a `click` event but we still need to manage focus
  1633. // So we bind to keyup which fires after and set focus here
  1634. on(menuItem, 'keyup', function (event) {
  1635. if (event.which !== 13) {
  1636. return;
  1637. }
  1638. controls.focusFirstMenuItem.call(_this2, null, true);
  1639. });
  1640. },
  1641. // Create a settings menu item
  1642. createMenuItem: function createMenuItem(_ref) {
  1643. var _this3 = this;
  1644. var value = _ref.value,
  1645. list = _ref.list,
  1646. type = _ref.type,
  1647. title = _ref.title,
  1648. _ref$badge = _ref.badge,
  1649. badge = _ref$badge === void 0 ? null : _ref$badge,
  1650. _ref$checked = _ref.checked,
  1651. checked = _ref$checked === void 0 ? false : _ref$checked;
  1652. var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
  1653. var menuItem = createElement('button', extend(attributes, {
  1654. type: 'button',
  1655. role: 'menuitemradio',
  1656. class: "".concat(this.config.classNames.control, " ").concat(attributes.class ? attributes.class : '').trim(),
  1657. 'aria-checked': checked,
  1658. value: value
  1659. }));
  1660. var flex = createElement('span'); // We have to set as HTML incase of special characters
  1661. flex.innerHTML = title;
  1662. if (is$1.element(badge)) {
  1663. flex.appendChild(badge);
  1664. }
  1665. menuItem.appendChild(flex); // Replicate radio button behaviour
  1666. Object.defineProperty(menuItem, 'checked', {
  1667. enumerable: true,
  1668. get: function get() {
  1669. return menuItem.getAttribute('aria-checked') === 'true';
  1670. },
  1671. set: function set(checked) {
  1672. // Ensure exclusivity
  1673. if (checked) {
  1674. Array.from(menuItem.parentNode.children).filter(function (node) {
  1675. return matches$1(node, '[role="menuitemradio"]');
  1676. }).forEach(function (node) {
  1677. return node.setAttribute('aria-checked', 'false');
  1678. });
  1679. }
  1680. menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
  1681. }
  1682. });
  1683. this.listeners.bind(menuItem, 'click keyup', function (event) {
  1684. if (is$1.keyboardEvent(event) && event.which !== 32) {
  1685. return;
  1686. }
  1687. event.preventDefault();
  1688. event.stopPropagation();
  1689. menuItem.checked = true;
  1690. switch (type) {
  1691. case 'language':
  1692. _this3.currentTrack = Number(value);
  1693. break;
  1694. case 'quality':
  1695. _this3.quality = value;
  1696. break;
  1697. case 'speed':
  1698. _this3.speed = parseFloat(value);
  1699. break;
  1700. default:
  1701. break;
  1702. }
  1703. controls.showMenuPanel.call(_this3, 'home', is$1.keyboardEvent(event));
  1704. }, type, false);
  1705. controls.bindMenuItemShortcuts.call(this, menuItem, type);
  1706. list.appendChild(menuItem);
  1707. },
  1708. // Format a time for display
  1709. formatTime: function formatTime$1() {
  1710. var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
  1711. var inverted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  1712. // Bail if the value isn't a number
  1713. if (!is$1.number(time)) {
  1714. return time;
  1715. } // Always display hours if duration is over an hour
  1716. var forceHours = getHours(this.duration) > 0;
  1717. return formatTime(time, forceHours, inverted);
  1718. },
  1719. // Update the displayed time
  1720. updateTimeDisplay: function updateTimeDisplay() {
  1721. var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
  1722. var time = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  1723. var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
  1724. // Bail if there's no element to display or the value isn't a number
  1725. if (!is$1.element(target) || !is$1.number(time)) {
  1726. return;
  1727. } // eslint-disable-next-line no-param-reassign
  1728. target.innerText = controls.formatTime(time, inverted);
  1729. },
  1730. // Update volume UI and storage
  1731. updateVolume: function updateVolume() {
  1732. if (!this.supported.ui) {
  1733. return;
  1734. } // Update range
  1735. if (is$1.element(this.elements.inputs.volume)) {
  1736. controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
  1737. } // Update mute state
  1738. if (is$1.element(this.elements.buttons.mute)) {
  1739. this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
  1740. }
  1741. },
  1742. // Update seek value and lower fill
  1743. setRange: function setRange(target) {
  1744. var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  1745. if (!is$1.element(target)) {
  1746. return;
  1747. } // eslint-disable-next-line
  1748. target.value = value; // Webkit range fill
  1749. controls.updateRangeFill.call(this, target);
  1750. },
  1751. // Update <progress> elements
  1752. updateProgress: function updateProgress(event) {
  1753. var _this4 = this;
  1754. if (!this.supported.ui || !is$1.event(event)) {
  1755. return;
  1756. }
  1757. var value = 0;
  1758. var setProgress = function setProgress(target, input) {
  1759. var value = is$1.number(input) ? input : 0;
  1760. var progress = is$1.element(target) ? target : _this4.elements.display.buffer; // Update value and label
  1761. if (is$1.element(progress)) {
  1762. progress.value = value; // Update text label inside
  1763. var label = progress.getElementsByTagName('span')[0];
  1764. if (is$1.element(label)) {
  1765. label.childNodes[0].nodeValue = value;
  1766. }
  1767. }
  1768. };
  1769. if (event) {
  1770. switch (event.type) {
  1771. // Video playing
  1772. case 'timeupdate':
  1773. case 'seeking':
  1774. case 'seeked':
  1775. value = getPercentage(this.currentTime, this.duration); // Set seek range value only if it's a 'natural' time event
  1776. if (event.type === 'timeupdate') {
  1777. controls.setRange.call(this, this.elements.inputs.seek, value);
  1778. }
  1779. break;
  1780. // Check buffer status
  1781. case 'playing':
  1782. case 'progress':
  1783. setProgress(this.elements.display.buffer, this.buffered * 100);
  1784. break;
  1785. default:
  1786. break;
  1787. }
  1788. }
  1789. },
  1790. // Webkit polyfill for lower fill range
  1791. updateRangeFill: function updateRangeFill(target) {
  1792. // Get range from event if event passed
  1793. var range = is$1.event(target) ? target.target : target; // Needs to be a valid <input type='range'>
  1794. if (!is$1.element(range) || range.getAttribute('type') !== 'range') {
  1795. return;
  1796. } // Set aria values for https://github.com/sampotts/plyr/issues/905
  1797. if (matches$1(range, this.config.selectors.inputs.seek)) {
  1798. range.setAttribute('aria-valuenow', this.currentTime);
  1799. var currentTime = controls.formatTime(this.currentTime);
  1800. var duration = controls.formatTime(this.duration);
  1801. var format = i18n.get('seekLabel', this.config);
  1802. range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));
  1803. } else if (matches$1(range, this.config.selectors.inputs.volume)) {
  1804. var percent = range.value * 100;
  1805. range.setAttribute('aria-valuenow', percent);
  1806. range.setAttribute('aria-valuetext', "".concat(percent.toFixed(1), "%"));
  1807. } else {
  1808. range.setAttribute('aria-valuenow', range.value);
  1809. } // WebKit only
  1810. if (!browser.isWebkit) {
  1811. return;
  1812. } // Set CSS custom property
  1813. range.style.setProperty('--value', "".concat(range.value / range.max * 100, "%"));
  1814. },
  1815. // Update hover tooltip for seeking
  1816. updateSeekTooltip: function updateSeekTooltip(event) {
  1817. var _this5 = this;
  1818. // Bail if setting not true
  1819. if (!this.config.tooltips.seek || !is$1.element(this.elements.inputs.seek) || !is$1.element(this.elements.display.seekTooltip) || this.duration === 0) {
  1820. return;
  1821. } // Calculate percentage
  1822. var percent = 0;
  1823. var clientRect = this.elements.progress.getBoundingClientRect();
  1824. var visible = "".concat(this.config.classNames.tooltip, "--visible");
  1825. var toggle = function toggle(_toggle) {
  1826. toggleClass(_this5.elements.display.seekTooltip, visible, _toggle);
  1827. }; // Hide on touch
  1828. if (this.touch) {
  1829. toggle(false);
  1830. return;
  1831. } // Determine percentage, if already visible
  1832. if (is$1.event(event)) {
  1833. percent = 100 / clientRect.width * (event.pageX - clientRect.left);
  1834. } else if (hasClass(this.elements.display.seekTooltip, visible)) {
  1835. percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
  1836. } else {
  1837. return;
  1838. } // Set bounds
  1839. if (percent < 0) {
  1840. percent = 0;
  1841. } else if (percent > 100) {
  1842. percent = 100;
  1843. } // Display the time a click would seek to
  1844. controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position
  1845. this.elements.display.seekTooltip.style.left = "".concat(percent, "%"); // Show/hide the tooltip
  1846. // If the event is a moues in/out and percentage is inside bounds
  1847. if (is$1.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
  1848. toggle(event.type === 'mouseenter');
  1849. }
  1850. },
  1851. // Handle time change event
  1852. timeUpdate: function timeUpdate(event) {
  1853. // Only invert if only one time element is displayed and used for both duration and currentTime
  1854. var invert = !is$1.element(this.elements.display.duration) && this.config.invertTime; // Duration
  1855. controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); // Ignore updates while seeking
  1856. if (event && event.type === 'timeupdate' && this.media.seeking) {
  1857. return;
  1858. } // Playing progress
  1859. controls.updateProgress.call(this, event);
  1860. },
  1861. // Show the duration on metadataloaded or durationchange events
  1862. durationUpdate: function durationUpdate() {
  1863. // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
  1864. if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
  1865. return;
  1866. } // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.
  1867. // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
  1868. // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
  1869. // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338
  1870. if (this.duration >= Math.pow(2, 32)) {
  1871. toggleHidden(this.elements.display.currentTime, true);
  1872. toggleHidden(this.elements.progress, true);
  1873. return;
  1874. } // Update ARIA values
  1875. if (is$1.element(this.elements.inputs.seek)) {
  1876. this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
  1877. } // If there's a spot to display duration
  1878. var hasDuration = is$1.element(this.elements.display.duration); // If there's only one time display, display duration there
  1879. if (!hasDuration && this.config.displayDuration && this.paused) {
  1880. controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
  1881. } // If there's a duration element, update content
  1882. if (hasDuration) {
  1883. controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
  1884. } // Update the tooltip (if visible)
  1885. controls.updateSeekTooltip.call(this);
  1886. },
  1887. // Hide/show a tab
  1888. toggleMenuButton: function toggleMenuButton(setting, toggle) {
  1889. toggleHidden(this.elements.settings.buttons[setting], !toggle);
  1890. },
  1891. // Update the selected setting
  1892. updateSetting: function updateSetting(setting, container, input) {
  1893. var pane = this.elements.settings.panels[setting];
  1894. var value = null;
  1895. var list = container;
  1896. if (setting === 'captions') {
  1897. value = this.currentTrack;
  1898. } else {
  1899. value = !is$1.empty(input) ? input : this[setting]; // Get default
  1900. if (is$1.empty(value)) {
  1901. value = this.config[setting].default;
  1902. } // Unsupported value
  1903. if (!is$1.empty(this.options[setting]) && !this.options[setting].includes(value)) {
  1904. this.debug.warn("Unsupported value of '".concat(value, "' for ").concat(setting));
  1905. return;
  1906. } // Disabled value
  1907. if (!this.config[setting].options.includes(value)) {
  1908. this.debug.warn("Disabled value of '".concat(value, "' for ").concat(setting));
  1909. return;
  1910. }
  1911. } // Get the list if we need to
  1912. if (!is$1.element(list)) {
  1913. list = pane && pane.querySelector('[role="menu"]');
  1914. } // If there's no list it means it's not been rendered...
  1915. if (!is$1.element(list)) {
  1916. return;
  1917. } // Update the label
  1918. var label = this.elements.settings.buttons[setting].querySelector(".".concat(this.config.classNames.menu.value));
  1919. label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it
  1920. var target = list && list.querySelector("[value=\"".concat(value, "\"]"));
  1921. if (is$1.element(target)) {
  1922. target.checked = true;
  1923. }
  1924. },
  1925. // Translate a value into a nice label
  1926. getLabel: function getLabel(setting, value) {
  1927. switch (setting) {
  1928. case 'speed':
  1929. return value === 1 ? i18n.get('normal', this.config) : "".concat(value, "&times;");
  1930. case 'quality':
  1931. if (is$1.number(value)) {
  1932. var label = i18n.get("qualityLabel.".concat(value), this.config);
  1933. if (!label.length) {
  1934. return "".concat(value, "p");
  1935. }
  1936. return label;
  1937. }
  1938. return toTitleCase(value);
  1939. case 'captions':
  1940. return captions.getLabel.call(this);
  1941. default:
  1942. return null;
  1943. }
  1944. },
  1945. // Set the quality menu
  1946. setQualityMenu: function setQualityMenu(options) {
  1947. var _this6 = this;
  1948. // Menu required
  1949. if (!is$1.element(this.elements.settings.panels.quality)) {
  1950. return;
  1951. }
  1952. var type = 'quality';
  1953. var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config
  1954. if (is$1.array(options)) {
  1955. this.options.quality = dedupe(options).filter(function (quality) {
  1956. return _this6.config.quality.options.includes(quality);
  1957. });
  1958. } // Toggle the pane and tab
  1959. var toggle = !is$1.empty(this.options.quality) && this.options.quality.length > 1;
  1960. controls.toggleMenuButton.call(this, type, toggle); // Empty the menu
  1961. emptyElement(list); // Check if we need to toggle the parent
  1962. controls.checkMenu.call(this); // If we're hiding, nothing more to do
  1963. if (!toggle) {
  1964. return;
  1965. } // Get the badge HTML for HD, 4K etc
  1966. var getBadge = function getBadge(quality) {
  1967. var label = i18n.get("qualityBadge.".concat(quality), _this6.config);
  1968. if (!label.length) {
  1969. return null;
  1970. }
  1971. return controls.createBadge.call(_this6, label);
  1972. }; // Sort options by the config and then render options
  1973. this.options.quality.sort(function (a, b) {
  1974. var sorting = _this6.config.quality.options;
  1975. return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
  1976. }).forEach(function (quality) {
  1977. controls.createMenuItem.call(_this6, {
  1978. value: quality,
  1979. list: list,
  1980. type: type,
  1981. title: controls.getLabel.call(_this6, 'quality', quality),
  1982. badge: getBadge(quality)
  1983. });
  1984. });
  1985. controls.updateSetting.call(this, type, list);
  1986. },
  1987. // Set the looping options
  1988. /* setLoopMenu() {
  1989. // Menu required
  1990. if (!is.element(this.elements.settings.panels.loop)) {
  1991. return;
  1992. }
  1993. const options = ['start', 'end', 'all', 'reset'];
  1994. const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
  1995. // Show the pane and tab
  1996. toggleHidden(this.elements.settings.buttons.loop, false);
  1997. toggleHidden(this.elements.settings.panels.loop, false);
  1998. // Toggle the pane and tab
  1999. const toggle = !is.empty(this.loop.options);
  2000. controls.toggleMenuButton.call(this, 'loop', toggle);
  2001. // Empty the menu
  2002. emptyElement(list);
  2003. options.forEach(option => {
  2004. const item = createElement('li');
  2005. const button = createElement(
  2006. 'button',
  2007. extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
  2008. type: 'button',
  2009. class: this.config.classNames.control,
  2010. 'data-plyr-loop-action': option,
  2011. }),
  2012. i18n.get(option, this.config)
  2013. );
  2014. if (['start', 'end'].includes(option)) {
  2015. const badge = controls.createBadge.call(this, '00:00');
  2016. button.appendChild(badge);
  2017. }
  2018. item.appendChild(button);
  2019. list.appendChild(item);
  2020. });
  2021. }, */
  2022. // Get current selected caption language
  2023. // TODO: rework this to user the getter in the API?
  2024. // Set a list of available captions languages
  2025. setCaptionsMenu: function setCaptionsMenu() {
  2026. var _this7 = this;
  2027. // Menu required
  2028. if (!is$1.element(this.elements.settings.panels.captions)) {
  2029. return;
  2030. } // TODO: Captions or language? Currently it's mixed
  2031. var type = 'captions';
  2032. var list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
  2033. var tracks = captions.getTracks.call(this);
  2034. var toggle = Boolean(tracks.length); // Toggle the pane and tab
  2035. controls.toggleMenuButton.call(this, type, toggle); // Empty the menu
  2036. emptyElement(list); // Check if we need to toggle the parent
  2037. controls.checkMenu.call(this); // If there's no captions, bail
  2038. if (!toggle) {
  2039. return;
  2040. } // Generate options data
  2041. var options = tracks.map(function (track, value) {
  2042. return {
  2043. value: value,
  2044. checked: _this7.captions.toggled && _this7.currentTrack === value,
  2045. title: captions.getLabel.call(_this7, track),
  2046. badge: track.language && controls.createBadge.call(_this7, track.language.toUpperCase()),
  2047. list: list,
  2048. type: 'language'
  2049. };
  2050. }); // Add the "Disabled" option to turn off captions
  2051. options.unshift({
  2052. value: -1,
  2053. checked: !this.captions.toggled,
  2054. title: i18n.get('disabled', this.config),
  2055. list: list,
  2056. type: 'language'
  2057. }); // Generate options
  2058. options.forEach(controls.createMenuItem.bind(this));
  2059. controls.updateSetting.call(this, type, list);
  2060. },
  2061. // Set a list of available captions languages
  2062. setSpeedMenu: function setSpeedMenu(options) {
  2063. var _this8 = this;
  2064. // Menu required
  2065. if (!is$1.element(this.elements.settings.panels.speed)) {
  2066. return;
  2067. }
  2068. var type = 'speed';
  2069. var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options
  2070. if (is$1.array(options)) {
  2071. this.options.speed = options;
  2072. } else if (this.isHTML5 || this.isVimeo) {
  2073. this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
  2074. } // Set options if passed and filter based on config
  2075. this.options.speed = this.options.speed.filter(function (speed) {
  2076. return _this8.config.speed.options.includes(speed);
  2077. }); // Toggle the pane and tab
  2078. var toggle = !is$1.empty(this.options.speed) && this.options.speed.length > 1;
  2079. controls.toggleMenuButton.call(this, type, toggle); // Empty the menu
  2080. emptyElement(list); // Check if we need to toggle the parent
  2081. controls.checkMenu.call(this); // If we're hiding, nothing more to do
  2082. if (!toggle) {
  2083. return;
  2084. } // Create items
  2085. this.options.speed.forEach(function (speed) {
  2086. controls.createMenuItem.call(_this8, {
  2087. value: speed,
  2088. list: list,
  2089. type: type,
  2090. title: controls.getLabel.call(_this8, 'speed', speed)
  2091. });
  2092. });
  2093. controls.updateSetting.call(this, type, list);
  2094. },
  2095. // Check if we need to hide/show the settings menu
  2096. checkMenu: function checkMenu() {
  2097. var buttons = this.elements.settings.buttons;
  2098. var visible = !is$1.empty(buttons) && Object.values(buttons).some(function (button) {
  2099. return !button.hidden;
  2100. });
  2101. toggleHidden(this.elements.settings.menu, !visible);
  2102. },
  2103. // Focus the first menu item in a given (or visible) menu
  2104. focusFirstMenuItem: function focusFirstMenuItem(pane) {
  2105. var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  2106. if (this.elements.settings.popup.hidden) {
  2107. return;
  2108. }
  2109. var target = pane;
  2110. if (!is$1.element(target)) {
  2111. target = Object.values(this.elements.settings.panels).find(function (pane) {
  2112. return !pane.hidden;
  2113. });
  2114. }
  2115. var firstItem = target.querySelector('[role^="menuitem"]');
  2116. setFocus.call(this, firstItem, tabFocus);
  2117. },
  2118. // Show/hide menu
  2119. toggleMenu: function toggleMenu(input) {
  2120. var popup = this.elements.settings.popup;
  2121. var button = this.elements.buttons.settings; // Menu and button are required
  2122. if (!is$1.element(popup) || !is$1.element(button)) {
  2123. return;
  2124. } // True toggle by default
  2125. var hidden = popup.hidden;
  2126. var show = hidden;
  2127. if (is$1.boolean(input)) {
  2128. show = input;
  2129. } else if (is$1.keyboardEvent(input) && input.which === 27) {
  2130. show = false;
  2131. } else if (is$1.event(input)) {
  2132. // If Plyr is in a shadowDOM, the event target is set to the component, instead of the
  2133. // Element in the shadowDOM. The path, if available, is complete.
  2134. var target = is$1.function(input.composedPath) ? input.composedPath()[0] : input.target;
  2135. var isMenuItem = popup.contains(target); // If the click was inside the menu or if the click
  2136. // wasn't the button or menu item and we're trying to
  2137. // show the menu (a doc click shouldn't show the menu)
  2138. if (isMenuItem || !isMenuItem && input.target !== button && show) {
  2139. return;
  2140. }
  2141. } // Set button attributes
  2142. button.setAttribute('aria-expanded', show); // Show the actual popup
  2143. toggleHidden(popup, !show); // Add class hook
  2144. toggleClass(this.elements.container, this.config.classNames.menu.open, show); // Focus the first item if key interaction
  2145. if (show && is$1.keyboardEvent(input)) {
  2146. controls.focusFirstMenuItem.call(this, null, true);
  2147. } else if (!show && !hidden) {
  2148. // If closing, re-focus the button
  2149. setFocus.call(this, button, is$1.keyboardEvent(input));
  2150. }
  2151. },
  2152. // Get the natural size of a menu panel
  2153. getMenuSize: function getMenuSize(tab) {
  2154. var clone = tab.cloneNode(true);
  2155. clone.style.position = 'absolute';
  2156. clone.style.opacity = 0;
  2157. clone.removeAttribute('hidden'); // Append to parent so we get the "real" size
  2158. tab.parentNode.appendChild(clone); // Get the sizes before we remove
  2159. var width = clone.scrollWidth;
  2160. var height = clone.scrollHeight; // Remove from the DOM
  2161. removeElement(clone);
  2162. return {
  2163. width: width,
  2164. height: height
  2165. };
  2166. },
  2167. // Show a panel in the menu
  2168. showMenuPanel: function showMenuPanel() {
  2169. var _this9 = this;
  2170. var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  2171. var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  2172. var target = this.elements.container.querySelector("#plyr-settings-".concat(this.id, "-").concat(type)); // Nothing to show, bail
  2173. if (!is$1.element(target)) {
  2174. return;
  2175. } // Hide all other panels
  2176. var container = target.parentNode;
  2177. var current = Array.from(container.children).find(function (node) {
  2178. return !node.hidden;
  2179. }); // If we can do fancy animations, we'll animate the height/width
  2180. if (support.transitions && !support.reducedMotion) {
  2181. // Set the current width as a base
  2182. container.style.width = "".concat(current.scrollWidth, "px");
  2183. container.style.height = "".concat(current.scrollHeight, "px"); // Get potential sizes
  2184. var size = controls.getMenuSize.call(this, target); // Restore auto height/width
  2185. var restore = function restore(event) {
  2186. // We're only bothered about height and width on the container
  2187. if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
  2188. return;
  2189. } // Revert back to auto
  2190. container.style.width = '';
  2191. container.style.height = ''; // Only listen once
  2192. off.call(_this9, container, transitionEndEvent, restore);
  2193. }; // Listen for the transition finishing and restore auto height/width
  2194. on.call(this, container, transitionEndEvent, restore); // Set dimensions to target
  2195. container.style.width = "".concat(size.width, "px");
  2196. container.style.height = "".concat(size.height, "px");
  2197. } // Set attributes on current tab
  2198. toggleHidden(current, true); // Set attributes on target
  2199. toggleHidden(target, false); // Focus the first item
  2200. controls.focusFirstMenuItem.call(this, target, tabFocus);
  2201. },
  2202. // Set the download URL
  2203. setDownloadUrl: function setDownloadUrl() {
  2204. var button = this.elements.buttons.download; // Bail if no button
  2205. if (!is$1.element(button)) {
  2206. return;
  2207. } // Set attribute
  2208. button.setAttribute('href', this.download);
  2209. },
  2210. // Build the default HTML
  2211. create: function create(data) {
  2212. var _this10 = this;
  2213. var bindMenuItemShortcuts = controls.bindMenuItemShortcuts,
  2214. createButton = controls.createButton,
  2215. createProgress = controls.createProgress,
  2216. createRange = controls.createRange,
  2217. createTime = controls.createTime,
  2218. setQualityMenu = controls.setQualityMenu,
  2219. setSpeedMenu = controls.setSpeedMenu,
  2220. showMenuPanel = controls.showMenuPanel;
  2221. this.elements.controls = null; // Larger overlaid play button
  2222. if (this.config.controls.includes('play-large')) {
  2223. this.elements.container.appendChild(createButton.call(this, 'play-large'));
  2224. } // Create the container
  2225. var container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
  2226. this.elements.controls = container; // Default item attributes
  2227. var defaultAttributes = {
  2228. class: 'plyr__controls__item'
  2229. }; // Loop through controls in order
  2230. dedupe(this.config.controls).forEach(function (control) {
  2231. // Restart button
  2232. if (control === 'restart') {
  2233. container.appendChild(createButton.call(_this10, 'restart', defaultAttributes));
  2234. } // Rewind button
  2235. if (control === 'rewind') {
  2236. container.appendChild(createButton.call(_this10, 'rewind', defaultAttributes));
  2237. } // Play/Pause button
  2238. if (control === 'play') {
  2239. container.appendChild(createButton.call(_this10, 'play', defaultAttributes));
  2240. } // Fast forward button
  2241. if (control === 'fast-forward') {
  2242. container.appendChild(createButton.call(_this10, 'fast-forward', defaultAttributes));
  2243. } // Progress
  2244. if (control === 'progress') {
  2245. var progressContainer = createElement('div', {
  2246. class: "".concat(defaultAttributes.class, " plyr__progress__container")
  2247. });
  2248. var progress = createElement('div', getAttributesFromSelector(_this10.config.selectors.progress)); // Seek range slider
  2249. progress.appendChild(createRange.call(_this10, 'seek', {
  2250. id: "plyr-seek-".concat(data.id)
  2251. })); // Buffer progress
  2252. progress.appendChild(createProgress.call(_this10, 'buffer')); // TODO: Add loop display indicator
  2253. // Seek tooltip
  2254. if (_this10.config.tooltips.seek) {
  2255. var tooltip = createElement('span', {
  2256. class: _this10.config.classNames.tooltip
  2257. }, '00:00');
  2258. progress.appendChild(tooltip);
  2259. _this10.elements.display.seekTooltip = tooltip;
  2260. }
  2261. _this10.elements.progress = progress;
  2262. progressContainer.appendChild(_this10.elements.progress);
  2263. container.appendChild(progressContainer);
  2264. } // Media current time display
  2265. if (control === 'current-time') {
  2266. container.appendChild(createTime.call(_this10, 'currentTime', defaultAttributes));
  2267. } // Media duration display
  2268. if (control === 'duration') {
  2269. container.appendChild(createTime.call(_this10, 'duration', defaultAttributes));
  2270. } // Volume controls
  2271. if (control === 'mute' || control === 'volume') {
  2272. var volume = _this10.elements.volume; // Create the volume container if needed
  2273. if (!is$1.element(volume) || !container.contains(volume)) {
  2274. volume = createElement('div', extend({}, defaultAttributes, {
  2275. class: "".concat(defaultAttributes.class, " plyr__volume").trim()
  2276. }));
  2277. _this10.elements.volume = volume;
  2278. container.appendChild(volume);
  2279. } // Toggle mute button
  2280. if (control === 'mute') {
  2281. volume.appendChild(createButton.call(_this10, 'mute'));
  2282. } // Volume range control
  2283. if (control === 'volume') {
  2284. // Set the attributes
  2285. var attributes = {
  2286. max: 1,
  2287. step: 0.05,
  2288. value: _this10.config.volume
  2289. }; // Create the volume range slider
  2290. volume.appendChild(createRange.call(_this10, 'volume', extend(attributes, {
  2291. id: "plyr-volume-".concat(data.id)
  2292. })));
  2293. }
  2294. } // Toggle captions button
  2295. if (control === 'captions') {
  2296. container.appendChild(createButton.call(_this10, 'captions', defaultAttributes));
  2297. } // Settings button / menu
  2298. if (control === 'settings' && !is$1.empty(_this10.config.settings)) {
  2299. var _control = createElement('div', extend({}, defaultAttributes, {
  2300. class: "".concat(defaultAttributes.class, " plyr__menu").trim(),
  2301. hidden: ''
  2302. }));
  2303. _control.appendChild(createButton.call(_this10, 'settings', {
  2304. 'aria-haspopup': true,
  2305. 'aria-controls': "plyr-settings-".concat(data.id),
  2306. 'aria-expanded': false
  2307. }));
  2308. var popup = createElement('div', {
  2309. class: 'plyr__menu__container',
  2310. id: "plyr-settings-".concat(data.id),
  2311. hidden: ''
  2312. });
  2313. var inner = createElement('div');
  2314. var home = createElement('div', {
  2315. id: "plyr-settings-".concat(data.id, "-home")
  2316. }); // Create the menu
  2317. var menu = createElement('div', {
  2318. role: 'menu'
  2319. });
  2320. home.appendChild(menu);
  2321. inner.appendChild(home);
  2322. _this10.elements.settings.panels.home = home; // Build the menu items
  2323. _this10.config.settings.forEach(function (type) {
  2324. // TODO: bundle this with the createMenuItem helper and bindings
  2325. var menuItem = createElement('button', extend(getAttributesFromSelector(_this10.config.selectors.buttons.settings), {
  2326. type: 'button',
  2327. class: "".concat(_this10.config.classNames.control, " ").concat(_this10.config.classNames.control, "--forward"),
  2328. role: 'menuitem',
  2329. 'aria-haspopup': true,
  2330. hidden: ''
  2331. })); // Bind menu shortcuts for keyboard users
  2332. bindMenuItemShortcuts.call(_this10, menuItem, type); // Show menu on click
  2333. on(menuItem, 'click', function () {
  2334. showMenuPanel.call(_this10, type, false);
  2335. });
  2336. var flex = createElement('span', null, i18n.get(type, _this10.config));
  2337. var value = createElement('span', {
  2338. class: _this10.config.classNames.menu.value
  2339. }); // Speed contains HTML entities
  2340. value.innerHTML = data[type];
  2341. flex.appendChild(value);
  2342. menuItem.appendChild(flex);
  2343. menu.appendChild(menuItem); // Build the panes
  2344. var pane = createElement('div', {
  2345. id: "plyr-settings-".concat(data.id, "-").concat(type),
  2346. hidden: ''
  2347. }); // Back button
  2348. var backButton = createElement('button', {
  2349. type: 'button',
  2350. class: "".concat(_this10.config.classNames.control, " ").concat(_this10.config.classNames.control, "--back")
  2351. }); // Visible label
  2352. backButton.appendChild(createElement('span', {
  2353. 'aria-hidden': true
  2354. }, i18n.get(type, _this10.config))); // Screen reader label
  2355. backButton.appendChild(createElement('span', {
  2356. class: _this10.config.classNames.hidden
  2357. }, i18n.get('menuBack', _this10.config))); // Go back via keyboard
  2358. on(pane, 'keydown', function (event) {
  2359. // We only care about <-
  2360. if (event.which !== 37) {
  2361. return;
  2362. } // Prevent seek
  2363. event.preventDefault();
  2364. event.stopPropagation(); // Show the respective menu
  2365. showMenuPanel.call(_this10, 'home', true);
  2366. }, false); // Go back via button click
  2367. on(backButton, 'click', function () {
  2368. showMenuPanel.call(_this10, 'home', false);
  2369. }); // Add to pane
  2370. pane.appendChild(backButton); // Menu
  2371. pane.appendChild(createElement('div', {
  2372. role: 'menu'
  2373. }));
  2374. inner.appendChild(pane);
  2375. _this10.elements.settings.buttons[type] = menuItem;
  2376. _this10.elements.settings.panels[type] = pane;
  2377. });
  2378. popup.appendChild(inner);
  2379. _control.appendChild(popup);
  2380. container.appendChild(_control);
  2381. _this10.elements.settings.popup = popup;
  2382. _this10.elements.settings.menu = _control;
  2383. } // Picture in picture button
  2384. if (control === 'pip' && support.pip) {
  2385. container.appendChild(createButton.call(_this10, 'pip', defaultAttributes));
  2386. } // Airplay button
  2387. if (control === 'airplay' && support.airplay) {
  2388. container.appendChild(createButton.call(_this10, 'airplay', defaultAttributes));
  2389. } // Download button
  2390. if (control === 'download') {
  2391. var _attributes = extend({}, defaultAttributes, {
  2392. element: 'a',
  2393. href: _this10.download,
  2394. target: '_blank'
  2395. });
  2396. var download = _this10.config.urls.download;
  2397. if (!is$1.url(download) && _this10.isEmbed) {
  2398. extend(_attributes, {
  2399. icon: "logo-".concat(_this10.provider),
  2400. label: _this10.provider
  2401. });
  2402. }
  2403. container.appendChild(createButton.call(_this10, 'download', _attributes));
  2404. } // Toggle fullscreen button
  2405. if (control === 'fullscreen') {
  2406. container.appendChild(createButton.call(_this10, 'fullscreen', defaultAttributes));
  2407. }
  2408. }); // Set available quality levels
  2409. if (this.isHTML5) {
  2410. setQualityMenu.call(this, html5.getQualityOptions.call(this));
  2411. }
  2412. setSpeedMenu.call(this);
  2413. return container;
  2414. },
  2415. // Insert controls
  2416. inject: function inject() {
  2417. var _this11 = this;
  2418. // Sprite
  2419. if (this.config.loadSprite) {
  2420. var icon = controls.getIconUrl.call(this); // Only load external sprite using AJAX
  2421. if (icon.cors) {
  2422. loadSprite(icon.url, 'sprite-plyr');
  2423. }
  2424. } // Create a unique ID
  2425. this.id = Math.floor(Math.random() * 10000); // Null by default
  2426. var container = null;
  2427. this.elements.controls = null; // Set template properties
  2428. var props = {
  2429. id: this.id,
  2430. seektime: this.config.seekTime,
  2431. title: this.config.title
  2432. };
  2433. var update = true; // If function, run it and use output
  2434. if (is$1.function(this.config.controls)) {
  2435. this.config.controls = this.config.controls.call(this, props);
  2436. } // Convert falsy controls to empty array (primarily for empty strings)
  2437. if (!this.config.controls) {
  2438. this.config.controls = [];
  2439. }
  2440. if (is$1.element(this.config.controls) || is$1.string(this.config.controls)) {
  2441. // HTMLElement or Non-empty string passed as the option
  2442. container = this.config.controls;
  2443. } else {
  2444. // Create controls
  2445. container = controls.create.call(this, {
  2446. id: this.id,
  2447. seektime: this.config.seekTime,
  2448. speed: this.speed,
  2449. quality: this.quality,
  2450. captions: captions.getLabel.call(this) // TODO: Looping
  2451. // loop: 'None',
  2452. });
  2453. update = false;
  2454. } // Replace props with their value
  2455. var replace = function replace(input) {
  2456. var result = input;
  2457. Object.entries(props).forEach(function (_ref2) {
  2458. var _ref3 = _slicedToArray(_ref2, 2),
  2459. key = _ref3[0],
  2460. value = _ref3[1];
  2461. result = replaceAll(result, "{".concat(key, "}"), value);
  2462. });
  2463. return result;
  2464. }; // Update markup
  2465. if (update) {
  2466. if (is$1.string(this.config.controls)) {
  2467. container = replace(container);
  2468. } else if (is$1.element(container)) {
  2469. container.innerHTML = replace(container.innerHTML);
  2470. }
  2471. } // Controls container
  2472. var target; // Inject to custom location
  2473. if (is$1.string(this.config.selectors.controls.container)) {
  2474. target = document.querySelector(this.config.selectors.controls.container);
  2475. } // Inject into the container by default
  2476. if (!is$1.element(target)) {
  2477. target = this.elements.container;
  2478. } // Inject controls HTML (needs to be before captions, hence "afterbegin")
  2479. var insertMethod = is$1.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';
  2480. target[insertMethod]('afterbegin', container); // Find the elements if need be
  2481. if (!is$1.element(this.elements.controls)) {
  2482. controls.findElements.call(this);
  2483. } // Add pressed property to buttons
  2484. if (!is$1.empty(this.elements.buttons)) {
  2485. var addProperty = function addProperty(button) {
  2486. var className = _this11.config.classNames.controlPressed;
  2487. Object.defineProperty(button, 'pressed', {
  2488. enumerable: true,
  2489. get: function get() {
  2490. return hasClass(button, className);
  2491. },
  2492. set: function set() {
  2493. var pressed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  2494. toggleClass(button, className, pressed);
  2495. }
  2496. });
  2497. }; // Toggle classname when pressed property is set
  2498. Object.values(this.elements.buttons).filter(Boolean).forEach(function (button) {
  2499. if (is$1.array(button) || is$1.nodeList(button)) {
  2500. Array.from(button).filter(Boolean).forEach(addProperty);
  2501. } else {
  2502. addProperty(button);
  2503. }
  2504. });
  2505. } // Edge sometimes doesn't finish the paint so force a repaint
  2506. if (browser.isEdge) {
  2507. repaint(target);
  2508. } // Setup tooltips
  2509. if (this.config.tooltips.controls) {
  2510. var _this$config = this.config,
  2511. classNames = _this$config.classNames,
  2512. selectors = _this$config.selectors;
  2513. var selector = "".concat(selectors.controls.wrapper, " ").concat(selectors.labels, " .").concat(classNames.hidden);
  2514. var labels = getElements.call(this, selector);
  2515. Array.from(labels).forEach(function (label) {
  2516. toggleClass(label, _this11.config.classNames.hidden, false);
  2517. toggleClass(label, _this11.config.classNames.tooltip, true);
  2518. });
  2519. }
  2520. }
  2521. };
  2522. /**
  2523. * Parse a string to a URL object
  2524. * @param {String} input - the URL to be parsed
  2525. * @param {Boolean} safe - failsafe parsing
  2526. */
  2527. function parseUrl(input) {
  2528. var safe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  2529. var url = input;
  2530. if (safe) {
  2531. var parser = document.createElement('a');
  2532. parser.href = url;
  2533. url = parser.href;
  2534. }
  2535. try {
  2536. return new URL(url);
  2537. } catch (e) {
  2538. return null;
  2539. }
  2540. } // Convert object to URLSearchParams
  2541. function buildUrlParams(input) {
  2542. var params = new URLSearchParams();
  2543. if (is$1.object(input)) {
  2544. Object.entries(input).forEach(function (_ref) {
  2545. var _ref2 = _slicedToArray(_ref, 2),
  2546. key = _ref2[0],
  2547. value = _ref2[1];
  2548. params.set(key, value);
  2549. });
  2550. }
  2551. return params;
  2552. }
  2553. var captions = {
  2554. // Setup captions
  2555. setup: function setup() {
  2556. // Requires UI support
  2557. if (!this.supported.ui) {
  2558. return;
  2559. } // Only Vimeo and HTML5 video supported at this point
  2560. if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {
  2561. // Clear menu and hide
  2562. if (is$1.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
  2563. controls.setCaptionsMenu.call(this);
  2564. }
  2565. return;
  2566. } // Inject the container
  2567. if (!is$1.element(this.elements.captions)) {
  2568. this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
  2569. insertAfter(this.elements.captions, this.elements.wrapper);
  2570. } // Fix IE captions if CORS is used
  2571. // Fetch captions and inject as blobs instead (data URIs not supported!)
  2572. if (browser.isIE && window.URL) {
  2573. var elements = this.media.querySelectorAll('track');
  2574. Array.from(elements).forEach(function (track) {
  2575. var src = track.getAttribute('src');
  2576. var url = parseUrl(src);
  2577. if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {
  2578. fetch(src, 'blob').then(function (blob) {
  2579. track.setAttribute('src', window.URL.createObjectURL(blob));
  2580. }).catch(function () {
  2581. removeElement(track);
  2582. });
  2583. }
  2584. });
  2585. } // Get and set initial data
  2586. // The "preferred" options are not realized unless / until the wanted language has a match
  2587. // * languages: Array of user's browser languages.
  2588. // * language: The language preferred by user settings or config
  2589. // * active: The state preferred by user settings or config
  2590. // * toggled: The real captions state
  2591. var browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
  2592. var languages = dedupe(browserLanguages.map(function (language) {
  2593. return language.split('-')[0];
  2594. }));
  2595. var language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); // Use first browser language when language is 'auto'
  2596. if (language === 'auto') {
  2597. var _languages = _slicedToArray(languages, 1);
  2598. language = _languages[0];
  2599. }
  2600. var active = this.storage.get('captions');
  2601. if (!is$1.boolean(active)) {
  2602. active = this.config.captions.active;
  2603. }
  2604. Object.assign(this.captions, {
  2605. toggled: false,
  2606. active: active,
  2607. language: language,
  2608. languages: languages
  2609. }); // Watch changes to textTracks and update captions menu
  2610. if (this.isHTML5) {
  2611. var trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
  2612. on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
  2613. } // Update available languages in list next tick (the event must not be triggered before the listeners)
  2614. setTimeout(captions.update.bind(this), 0);
  2615. },
  2616. // Update available language options in settings based on tracks
  2617. update: function update() {
  2618. var _this = this;
  2619. var tracks = captions.getTracks.call(this, true); // Get the wanted language
  2620. var _this$captions = this.captions,
  2621. active = _this$captions.active,
  2622. language = _this$captions.language,
  2623. meta = _this$captions.meta,
  2624. currentTrackNode = _this$captions.currentTrackNode;
  2625. var languageExists = Boolean(tracks.find(function (track) {
  2626. return track.language === language;
  2627. })); // Handle tracks (add event listener and "pseudo"-default)
  2628. if (this.isHTML5 && this.isVideo) {
  2629. tracks.filter(function (track) {
  2630. return !meta.get(track);
  2631. }).forEach(function (track) {
  2632. _this.debug.log('Track added', track); // Attempt to store if the original dom element was "default"
  2633. meta.set(track, {
  2634. default: track.mode === 'showing'
  2635. }); // Turn off native caption rendering to avoid double captions
  2636. track.mode = 'hidden'; // Add event listener for cue changes
  2637. on.call(_this, track, 'cuechange', function () {
  2638. return captions.updateCues.call(_this);
  2639. });
  2640. });
  2641. } // Update language first time it matches, or if the previous matching track was removed
  2642. if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {
  2643. captions.setLanguage.call(this, language);
  2644. captions.toggle.call(this, active && languageExists);
  2645. } // Enable or disable captions based on track length
  2646. toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is$1.empty(tracks)); // Update available languages in list
  2647. if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
  2648. controls.setCaptionsMenu.call(this);
  2649. }
  2650. },
  2651. // Toggle captions display
  2652. // Used internally for the toggleCaptions method, with the passive option forced to false
  2653. toggle: function toggle(input) {
  2654. var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  2655. // If there's no full support
  2656. if (!this.supported.ui) {
  2657. return;
  2658. }
  2659. var toggled = this.captions.toggled; // Current state
  2660. var activeClass = this.config.classNames.captions.active; // Get the next state
  2661. // If the method is called without parameter, toggle based on current value
  2662. var active = is$1.nullOrUndefined(input) ? !toggled : input; // Update state and trigger event
  2663. if (active !== toggled) {
  2664. // When passive, don't override user preferences
  2665. if (!passive) {
  2666. this.captions.active = active;
  2667. this.storage.set({
  2668. captions: active
  2669. });
  2670. } // Force language if the call isn't passive and there is no matching language to toggle to
  2671. if (!this.language && active && !passive) {
  2672. var tracks = captions.getTracks.call(this);
  2673. var track = captions.findTrack.call(this, [this.captions.language].concat(_toConsumableArray(this.captions.languages)), true); // Override user preferences to avoid switching languages if a matching track is added
  2674. this.captions.language = track.language; // Set caption, but don't store in localStorage as user preference
  2675. captions.set.call(this, tracks.indexOf(track));
  2676. return;
  2677. } // Toggle button if it's enabled
  2678. if (this.elements.buttons.captions) {
  2679. this.elements.buttons.captions.pressed = active;
  2680. } // Add class hook
  2681. toggleClass(this.elements.container, activeClass, active);
  2682. this.captions.toggled = active; // Update settings menu
  2683. controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)
  2684. triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
  2685. }
  2686. },
  2687. // Set captions by track index
  2688. // Used internally for the currentTrack setter with the passive option forced to false
  2689. set: function set(index) {
  2690. var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  2691. var tracks = captions.getTracks.call(this); // Disable captions if setting to -1
  2692. if (index === -1) {
  2693. captions.toggle.call(this, false, passive);
  2694. return;
  2695. }
  2696. if (!is$1.number(index)) {
  2697. this.debug.warn('Invalid caption argument', index);
  2698. return;
  2699. }
  2700. if (!(index in tracks)) {
  2701. this.debug.warn('Track not found', index);
  2702. return;
  2703. }
  2704. if (this.captions.currentTrack !== index) {
  2705. this.captions.currentTrack = index;
  2706. var track = tracks[index];
  2707. var _ref = track || {},
  2708. language = _ref.language; // Store reference to node for invalidation on remove
  2709. this.captions.currentTrackNode = track; // Update settings menu
  2710. controls.updateSetting.call(this, 'captions'); // When passive, don't override user preferences
  2711. if (!passive) {
  2712. this.captions.language = language;
  2713. this.storage.set({
  2714. language: language
  2715. });
  2716. } // Handle Vimeo captions
  2717. if (this.isVimeo) {
  2718. this.embed.enableTextTrack(language);
  2719. } // Trigger event
  2720. triggerEvent.call(this, this.media, 'languagechange');
  2721. } // Show captions
  2722. captions.toggle.call(this, true, passive);
  2723. if (this.isHTML5 && this.isVideo) {
  2724. // If we change the active track while a cue is already displayed we need to update it
  2725. captions.updateCues.call(this);
  2726. }
  2727. },
  2728. // Set captions by language
  2729. // Used internally for the language setter with the passive option forced to false
  2730. setLanguage: function setLanguage(input) {
  2731. var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  2732. if (!is$1.string(input)) {
  2733. this.debug.warn('Invalid language argument', input);
  2734. return;
  2735. } // Normalize
  2736. var language = input.toLowerCase();
  2737. this.captions.language = language; // Set currentTrack
  2738. var tracks = captions.getTracks.call(this);
  2739. var track = captions.findTrack.call(this, [language]);
  2740. captions.set.call(this, tracks.indexOf(track), passive);
  2741. },
  2742. // Get current valid caption tracks
  2743. // If update is false it will also ignore tracks without metadata
  2744. // This is used to "freeze" the language options when captions.update is false
  2745. getTracks: function getTracks() {
  2746. var _this2 = this;
  2747. var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  2748. // Handle media or textTracks missing or null
  2749. var tracks = Array.from((this.media || {}).textTracks || []); // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
  2750. // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
  2751. return tracks.filter(function (track) {
  2752. return !_this2.isHTML5 || update || _this2.captions.meta.has(track);
  2753. }).filter(function (track) {
  2754. return ['captions', 'subtitles'].includes(track.kind);
  2755. });
  2756. },
  2757. // Match tracks based on languages and get the first
  2758. findTrack: function findTrack(languages) {
  2759. var _this3 = this;
  2760. var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  2761. var tracks = captions.getTracks.call(this);
  2762. var sortIsDefault = function sortIsDefault(track) {
  2763. return Number((_this3.captions.meta.get(track) || {}).default);
  2764. };
  2765. var sorted = Array.from(tracks).sort(function (a, b) {
  2766. return sortIsDefault(b) - sortIsDefault(a);
  2767. });
  2768. var track;
  2769. languages.every(function (language) {
  2770. track = sorted.find(function (track) {
  2771. return track.language === language;
  2772. });
  2773. return !track; // Break iteration if there is a match
  2774. }); // If no match is found but is required, get first
  2775. return track || (force ? sorted[0] : undefined);
  2776. },
  2777. // Get the current track
  2778. getCurrentTrack: function getCurrentTrack() {
  2779. return captions.getTracks.call(this)[this.currentTrack];
  2780. },
  2781. // Get UI label for track
  2782. getLabel: function getLabel(track) {
  2783. var currentTrack = track;
  2784. if (!is$1.track(currentTrack) && support.textTracks && this.captions.toggled) {
  2785. currentTrack = captions.getCurrentTrack.call(this);
  2786. }
  2787. if (is$1.track(currentTrack)) {
  2788. if (!is$1.empty(currentTrack.label)) {
  2789. return currentTrack.label;
  2790. }
  2791. if (!is$1.empty(currentTrack.language)) {
  2792. return track.language.toUpperCase();
  2793. }
  2794. return i18n.get('enabled', this.config);
  2795. }
  2796. return i18n.get('disabled', this.config);
  2797. },
  2798. // Update captions using current track's active cues
  2799. // Also optional array argument in case there isn't any track (ex: vimeo)
  2800. updateCues: function updateCues(input) {
  2801. // Requires UI
  2802. if (!this.supported.ui) {
  2803. return;
  2804. }
  2805. if (!is$1.element(this.elements.captions)) {
  2806. this.debug.warn('No captions element to render to');
  2807. return;
  2808. } // Only accept array or empty input
  2809. if (!is$1.nullOrUndefined(input) && !Array.isArray(input)) {
  2810. this.debug.warn('updateCues: Invalid input', input);
  2811. return;
  2812. }
  2813. var cues = input; // Get cues from track
  2814. if (!cues) {
  2815. var track = captions.getCurrentTrack.call(this);
  2816. cues = Array.from((track || {}).activeCues || []).map(function (cue) {
  2817. return cue.getCueAsHTML();
  2818. }).map(getHTML);
  2819. } // Set new caption text
  2820. var content = cues.map(function (cueText) {
  2821. return cueText.trim();
  2822. }).join('\n');
  2823. var changed = content !== this.elements.captions.innerHTML;
  2824. if (changed) {
  2825. // Empty the container and create a new child element
  2826. emptyElement(this.elements.captions);
  2827. var caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
  2828. caption.innerHTML = content;
  2829. this.elements.captions.appendChild(caption); // Trigger event
  2830. triggerEvent.call(this, this.media, 'cuechange');
  2831. }
  2832. }
  2833. };
  2834. // ==========================================================================
  2835. // Plyr default config
  2836. // ==========================================================================
  2837. var defaults$1 = {
  2838. // Disable
  2839. enabled: true,
  2840. // Custom media title
  2841. title: '',
  2842. // Logging to console
  2843. debug: false,
  2844. // Auto play (if supported)
  2845. autoplay: false,
  2846. // Only allow one media playing at once (vimeo only)
  2847. autopause: true,
  2848. // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
  2849. // TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
  2850. playsinline: true,
  2851. // Default time to skip when rewind/fast forward
  2852. seekTime: 10,
  2853. // Default volume
  2854. volume: 1,
  2855. muted: false,
  2856. // Pass a custom duration
  2857. duration: null,
  2858. // Display the media duration on load in the current time position
  2859. // If you have opted to display both duration and currentTime, this is ignored
  2860. displayDuration: true,
  2861. // Invert the current time to be a countdown
  2862. invertTime: true,
  2863. // Clicking the currentTime inverts it's value to show time left rather than elapsed
  2864. toggleInvert: true,
  2865. // Force an aspect ratio
  2866. // The format must be `'w:h'` (e.g. `'16:9'`)
  2867. ratio: null,
  2868. // Click video container to play/pause
  2869. clickToPlay: true,
  2870. // Auto hide the controls
  2871. hideControls: true,
  2872. // Reset to start when playback ended
  2873. resetOnEnd: false,
  2874. // Disable the standard context menu
  2875. disableContextMenu: true,
  2876. // Sprite (for icons)
  2877. loadSprite: true,
  2878. iconPrefix: 'plyr',
  2879. iconUrl: 'https://cdn.plyr.io/3.5.4/plyr.svg',
  2880. // Blank video (used to prevent errors on source change)
  2881. blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
  2882. // Quality default
  2883. quality: {
  2884. default: 576,
  2885. options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240]
  2886. },
  2887. // Set loops
  2888. loop: {
  2889. active: false // start: null,
  2890. // end: null,
  2891. },
  2892. // Speed default and options to display
  2893. speed: {
  2894. selected: 1,
  2895. options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
  2896. },
  2897. // Keyboard shortcut settings
  2898. keyboard: {
  2899. focused: true,
  2900. global: false
  2901. },
  2902. // Display tooltips
  2903. tooltips: {
  2904. controls: false,
  2905. seek: true
  2906. },
  2907. // Captions settings
  2908. captions: {
  2909. active: false,
  2910. language: 'auto',
  2911. // Listen to new tracks added after Plyr is initialized.
  2912. // This is needed for streaming captions, but may result in unselectable options
  2913. update: false
  2914. },
  2915. // Fullscreen settings
  2916. fullscreen: {
  2917. enabled: true,
  2918. // Allow fullscreen?
  2919. fallback: true,
  2920. // Fallback using full viewport/window
  2921. iosNative: false // Use the native fullscreen in iOS (disables custom controls)
  2922. },
  2923. // Local storage
  2924. storage: {
  2925. enabled: true,
  2926. key: 'plyr'
  2927. },
  2928. // Default controls
  2929. controls: ['play-large', // 'restart',
  2930. // 'rewind',
  2931. 'play', // 'fast-forward',
  2932. 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
  2933. 'fullscreen'],
  2934. settings: ['captions', 'quality', 'speed'],
  2935. // Localisation
  2936. i18n: {
  2937. restart: 'Restart',
  2938. rewind: 'Rewind {seektime}s',
  2939. play: 'Play',
  2940. pause: 'Pause',
  2941. fastForward: 'Forward {seektime}s',
  2942. seek: 'Seek',
  2943. seekLabel: '{currentTime} of {duration}',
  2944. played: 'Played',
  2945. buffered: 'Buffered',
  2946. currentTime: 'Current time',
  2947. duration: 'Duration',
  2948. volume: 'Volume',
  2949. mute: 'Mute',
  2950. unmute: 'Unmute',
  2951. enableCaptions: 'Enable captions',
  2952. disableCaptions: 'Disable captions',
  2953. download: 'Download',
  2954. enterFullscreen: 'Enter fullscreen',
  2955. exitFullscreen: 'Exit fullscreen',
  2956. frameTitle: 'Player for {title}',
  2957. captions: 'Captions',
  2958. settings: 'Settings',
  2959. menuBack: 'Go back to previous menu',
  2960. speed: 'Speed',
  2961. normal: 'Normal',
  2962. quality: 'Quality',
  2963. loop: 'Loop',
  2964. start: 'Start',
  2965. end: 'End',
  2966. all: 'All',
  2967. reset: 'Reset',
  2968. disabled: 'Disabled',
  2969. enabled: 'Enabled',
  2970. advertisement: 'Ad',
  2971. qualityBadge: {
  2972. 2160: '4K',
  2973. 1440: 'HD',
  2974. 1080: 'HD',
  2975. 720: 'HD',
  2976. 576: 'SD',
  2977. 480: 'SD'
  2978. }
  2979. },
  2980. // URLs
  2981. urls: {
  2982. download: null,
  2983. vimeo: {
  2984. sdk: 'https://player.vimeo.com/api/player.js',
  2985. iframe: 'https://player.vimeo.com/video/{0}?{1}',
  2986. api: 'https://vimeo.com/api/v2/video/{0}.json'
  2987. },
  2988. youtube: {
  2989. sdk: 'https://www.youtube.com/iframe_api',
  2990. api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}' // 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title),fileDetails)&part=snippet',
  2991. },
  2992. googleIMA: {
  2993. sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'
  2994. }
  2995. },
  2996. // Custom control listeners
  2997. listeners: {
  2998. seek: null,
  2999. play: null,
  3000. pause: null,
  3001. restart: null,
  3002. rewind: null,
  3003. fastForward: null,
  3004. mute: null,
  3005. volume: null,
  3006. captions: null,
  3007. download: null,
  3008. fullscreen: null,
  3009. pip: null,
  3010. airplay: null,
  3011. speed: null,
  3012. quality: null,
  3013. loop: null,
  3014. language: null
  3015. },
  3016. // Events to watch and bubble
  3017. events: [// Events to watch on HTML5 media elements and bubble
  3018. // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
  3019. 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
  3020. 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
  3021. 'statechange', // Quality
  3022. 'qualitychange', // Ads
  3023. 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
  3024. // Selectors
  3025. // Change these to match your template if using custom HTML
  3026. selectors: {
  3027. editable: 'input, textarea, select, [contenteditable]',
  3028. container: '.plyr',
  3029. controls: {
  3030. container: null,
  3031. wrapper: '.plyr__controls'
  3032. },
  3033. labels: '[data-plyr]',
  3034. buttons: {
  3035. play: '[data-plyr="play"]',
  3036. pause: '[data-plyr="pause"]',
  3037. restart: '[data-plyr="restart"]',
  3038. rewind: '[data-plyr="rewind"]',
  3039. fastForward: '[data-plyr="fast-forward"]',
  3040. mute: '[data-plyr="mute"]',
  3041. captions: '[data-plyr="captions"]',
  3042. download: '[data-plyr="download"]',
  3043. fullscreen: '[data-plyr="fullscreen"]',
  3044. pip: '[data-plyr="pip"]',
  3045. airplay: '[data-plyr="airplay"]',
  3046. settings: '[data-plyr="settings"]',
  3047. loop: '[data-plyr="loop"]'
  3048. },
  3049. inputs: {
  3050. seek: '[data-plyr="seek"]',
  3051. volume: '[data-plyr="volume"]',
  3052. speed: '[data-plyr="speed"]',
  3053. language: '[data-plyr="language"]',
  3054. quality: '[data-plyr="quality"]'
  3055. },
  3056. display: {
  3057. currentTime: '.plyr__time--current',
  3058. duration: '.plyr__time--duration',
  3059. buffer: '.plyr__progress__buffer',
  3060. loop: '.plyr__progress__loop',
  3061. // Used later
  3062. volume: '.plyr__volume--display'
  3063. },
  3064. progress: '.plyr__progress',
  3065. captions: '.plyr__captions',
  3066. caption: '.plyr__caption'
  3067. },
  3068. // Class hooks added to the player in different states
  3069. classNames: {
  3070. type: 'plyr--{0}',
  3071. provider: 'plyr--{0}',
  3072. video: 'plyr__video-wrapper',
  3073. embed: 'plyr__video-embed',
  3074. videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
  3075. embedContainer: 'plyr__video-embed__container',
  3076. poster: 'plyr__poster',
  3077. posterEnabled: 'plyr__poster-enabled',
  3078. ads: 'plyr__ads',
  3079. control: 'plyr__control',
  3080. controlPressed: 'plyr__control--pressed',
  3081. playing: 'plyr--playing',
  3082. paused: 'plyr--paused',
  3083. stopped: 'plyr--stopped',
  3084. loading: 'plyr--loading',
  3085. hover: 'plyr--hover',
  3086. tooltip: 'plyr__tooltip',
  3087. cues: 'plyr__cues',
  3088. hidden: 'plyr__sr-only',
  3089. hideControls: 'plyr--hide-controls',
  3090. isIos: 'plyr--is-ios',
  3091. isTouch: 'plyr--is-touch',
  3092. uiSupported: 'plyr--full-ui',
  3093. noTransition: 'plyr--no-transition',
  3094. display: {
  3095. time: 'plyr__time'
  3096. },
  3097. menu: {
  3098. value: 'plyr__menu__value',
  3099. badge: 'plyr__badge',
  3100. open: 'plyr--menu-open'
  3101. },
  3102. captions: {
  3103. enabled: 'plyr--captions-enabled',
  3104. active: 'plyr--captions-active'
  3105. },
  3106. fullscreen: {
  3107. enabled: 'plyr--fullscreen-enabled',
  3108. fallback: 'plyr--fullscreen-fallback'
  3109. },
  3110. pip: {
  3111. supported: 'plyr--pip-supported',
  3112. active: 'plyr--pip-active'
  3113. },
  3114. airplay: {
  3115. supported: 'plyr--airplay-supported',
  3116. active: 'plyr--airplay-active'
  3117. },
  3118. tabFocus: 'plyr__tab-focus',
  3119. previewThumbnails: {
  3120. // Tooltip thumbs
  3121. thumbContainer: 'plyr__preview-thumb',
  3122. thumbContainerShown: 'plyr__preview-thumb--is-shown',
  3123. imageContainer: 'plyr__preview-thumb__image-container',
  3124. timeContainer: 'plyr__preview-thumb__time-container',
  3125. // Scrubbing
  3126. scrubbingContainer: 'plyr__preview-scrubbing',
  3127. scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'
  3128. }
  3129. },
  3130. // Embed attributes
  3131. attributes: {
  3132. embed: {
  3133. provider: 'data-plyr-provider',
  3134. id: 'data-plyr-embed-id'
  3135. }
  3136. },
  3137. // Advertisements plugin
  3138. // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
  3139. ads: {
  3140. enabled: false,
  3141. publisherId: '',
  3142. tagUrl: ''
  3143. },
  3144. // Preview Thumbnails plugin
  3145. previewThumbnails: {
  3146. enabled: false,
  3147. src: ''
  3148. },
  3149. // Vimeo plugin
  3150. vimeo: {
  3151. byline: false,
  3152. portrait: false,
  3153. title: false,
  3154. speed: true,
  3155. transparent: false
  3156. },
  3157. // YouTube plugin
  3158. youtube: {
  3159. noCookie: false,
  3160. // Whether to use an alternative version of YouTube without cookies
  3161. rel: 0,
  3162. // No related vids
  3163. showinfo: 0,
  3164. // Hide info
  3165. iv_load_policy: 3,
  3166. // Hide annotations
  3167. modestbranding: 1 // Hide logos as much as possible (they still show one in the corner when paused)
  3168. }
  3169. };
  3170. // ==========================================================================
  3171. // Plyr states
  3172. // ==========================================================================
  3173. var pip = {
  3174. active: 'picture-in-picture',
  3175. inactive: 'inline'
  3176. };
  3177. // ==========================================================================
  3178. // Plyr supported types and providers
  3179. // ==========================================================================
  3180. var providers = {
  3181. html5: 'html5',
  3182. youtube: 'youtube',
  3183. vimeo: 'vimeo'
  3184. };
  3185. var types = {
  3186. audio: 'audio',
  3187. video: 'video'
  3188. };
  3189. /**
  3190. * Get provider by URL
  3191. * @param {String} url
  3192. */
  3193. function getProviderByUrl(url) {
  3194. // YouTube
  3195. if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
  3196. return providers.youtube;
  3197. } // Vimeo
  3198. if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
  3199. return providers.vimeo;
  3200. }
  3201. return null;
  3202. }
  3203. // ==========================================================================
  3204. // Console wrapper
  3205. // ==========================================================================
  3206. var noop = function noop() {};
  3207. var Console =
  3208. /*#__PURE__*/
  3209. function () {
  3210. function Console() {
  3211. var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  3212. _classCallCheck(this, Console);
  3213. this.enabled = window.console && enabled;
  3214. if (this.enabled) {
  3215. this.log('Debugging enabled');
  3216. }
  3217. }
  3218. _createClass(Console, [{
  3219. key: "log",
  3220. get: function get() {
  3221. // eslint-disable-next-line no-console
  3222. return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
  3223. }
  3224. }, {
  3225. key: "warn",
  3226. get: function get() {
  3227. // eslint-disable-next-line no-console
  3228. return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
  3229. }
  3230. }, {
  3231. key: "error",
  3232. get: function get() {
  3233. // eslint-disable-next-line no-console
  3234. return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
  3235. }
  3236. }]);
  3237. return Console;
  3238. }();
  3239. function onChange() {
  3240. if (!this.enabled) {
  3241. return;
  3242. } // Update toggle button
  3243. var button = this.player.elements.buttons.fullscreen;
  3244. if (is$1.element(button)) {
  3245. button.pressed = this.active;
  3246. } // Trigger an event
  3247. triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); // Trap focus in container
  3248. if (!browser.isIos) {
  3249. trapFocus.call(this.player, this.target, this.active);
  3250. }
  3251. }
  3252. function toggleFallback() {
  3253. var _this = this;
  3254. var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  3255. // Store or restore scroll position
  3256. if (toggle) {
  3257. this.scrollPosition = {
  3258. x: window.scrollX || 0,
  3259. y: window.scrollY || 0
  3260. };
  3261. } else {
  3262. window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
  3263. } // Toggle scroll
  3264. document.body.style.overflow = toggle ? 'hidden' : ''; // Toggle class hook
  3265. toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); // Force full viewport on iPhone X+
  3266. if (browser.isIos) {
  3267. var viewport = document.head.querySelector('meta[name="viewport"]');
  3268. var property = 'viewport-fit=cover'; // Inject the viewport meta if required
  3269. if (!viewport) {
  3270. viewport = document.createElement('meta');
  3271. viewport.setAttribute('name', 'viewport');
  3272. } // Check if the property already exists
  3273. var hasProperty = is$1.string(viewport.content) && viewport.content.includes(property);
  3274. if (toggle) {
  3275. this.cleanupViewport = !hasProperty;
  3276. if (!hasProperty) {
  3277. viewport.content += ",".concat(property);
  3278. }
  3279. } else if (this.cleanupViewport) {
  3280. viewport.content = viewport.content.split(',').filter(function (part) {
  3281. return part.trim() !== property;
  3282. }).join(',');
  3283. } // Force a repaint as sometimes Safari doesn't want to fill the screen
  3284. setTimeout(function () {
  3285. return repaint(_this.target);
  3286. }, 100);
  3287. } // Toggle button and fire events
  3288. onChange.call(this);
  3289. }
  3290. var Fullscreen =
  3291. /*#__PURE__*/
  3292. function () {
  3293. function Fullscreen(player) {
  3294. var _this2 = this;
  3295. _classCallCheck(this, Fullscreen);
  3296. // Keep reference to parent
  3297. this.player = player; // Get prefix
  3298. this.prefix = Fullscreen.prefix;
  3299. this.property = Fullscreen.property; // Scroll position
  3300. this.scrollPosition = {
  3301. x: 0,
  3302. y: 0
  3303. }; // Force the use of 'full window/browser' rather than fullscreen
  3304. this.forceFallback = player.config.fullscreen.fallback === 'force'; // Register event listeners
  3305. // Handle event (incase user presses escape etc)
  3306. on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
  3307. // TODO: Filter for target??
  3308. onChange.call(_this2);
  3309. }); // Fullscreen toggle on double click
  3310. on.call(this.player, this.player.elements.container, 'dblclick', function (event) {
  3311. // Ignore double click in controls
  3312. if (is$1.element(_this2.player.elements.controls) && _this2.player.elements.controls.contains(event.target)) {
  3313. return;
  3314. }
  3315. _this2.toggle();
  3316. }); // Update the UI
  3317. this.update();
  3318. } // Determine if native supported
  3319. _createClass(Fullscreen, [{
  3320. key: "update",
  3321. // Update UI
  3322. value: function update() {
  3323. if (this.enabled) {
  3324. var mode;
  3325. if (this.forceFallback) {
  3326. mode = 'Fallback (forced)';
  3327. } else if (Fullscreen.native) {
  3328. mode = 'Native';
  3329. } else {
  3330. mode = 'Fallback';
  3331. }
  3332. this.player.debug.log("".concat(mode, " fullscreen enabled"));
  3333. } else {
  3334. this.player.debug.log('Fullscreen not supported and fallback disabled');
  3335. } // Add styling hook to show button
  3336. toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
  3337. } // Make an element fullscreen
  3338. }, {
  3339. key: "enter",
  3340. value: function enter() {
  3341. if (!this.enabled) {
  3342. return;
  3343. } // iOS native fullscreen doesn't need the request step
  3344. if (browser.isIos && this.player.config.fullscreen.iosNative) {
  3345. this.target.webkitEnterFullscreen();
  3346. } else if (!Fullscreen.native || this.forceFallback) {
  3347. toggleFallback.call(this, true);
  3348. } else if (!this.prefix) {
  3349. this.target.requestFullscreen();
  3350. } else if (!is$1.empty(this.prefix)) {
  3351. this.target["".concat(this.prefix, "Request").concat(this.property)]();
  3352. }
  3353. } // Bail from fullscreen
  3354. }, {
  3355. key: "exit",
  3356. value: function exit() {
  3357. if (!this.enabled) {
  3358. return;
  3359. } // iOS native fullscreen
  3360. if (browser.isIos && this.player.config.fullscreen.iosNative) {
  3361. this.target.webkitExitFullscreen();
  3362. this.player.play();
  3363. } else if (!Fullscreen.native || this.forceFallback) {
  3364. toggleFallback.call(this, false);
  3365. } else if (!this.prefix) {
  3366. (document.cancelFullScreen || document.exitFullscreen).call(document);
  3367. } else if (!is$1.empty(this.prefix)) {
  3368. var action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
  3369. document["".concat(this.prefix).concat(action).concat(this.property)]();
  3370. }
  3371. } // Toggle state
  3372. }, {
  3373. key: "toggle",
  3374. value: function toggle() {
  3375. if (!this.active) {
  3376. this.enter();
  3377. } else {
  3378. this.exit();
  3379. }
  3380. }
  3381. }, {
  3382. key: "usingNative",
  3383. // If we're actually using native
  3384. get: function get() {
  3385. return Fullscreen.native && !this.forceFallback;
  3386. } // Get the prefix for handlers
  3387. }, {
  3388. key: "enabled",
  3389. // Determine if fullscreen is enabled
  3390. get: function get() {
  3391. return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
  3392. } // Get active state
  3393. }, {
  3394. key: "active",
  3395. get: function get() {
  3396. if (!this.enabled) {
  3397. return false;
  3398. } // Fallback using classname
  3399. if (!Fullscreen.native || this.forceFallback) {
  3400. return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
  3401. }
  3402. var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
  3403. return element === this.target;
  3404. } // Get target element
  3405. }, {
  3406. key: "target",
  3407. get: function get() {
  3408. return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
  3409. }
  3410. }], [{
  3411. key: "native",
  3412. get: function get() {
  3413. return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
  3414. }
  3415. }, {
  3416. key: "prefix",
  3417. get: function get() {
  3418. // No prefix
  3419. if (is$1.function(document.exitFullscreen)) {
  3420. return '';
  3421. } // Check for fullscreen support by vendor prefix
  3422. var value = '';
  3423. var prefixes = ['webkit', 'moz', 'ms'];
  3424. prefixes.some(function (pre) {
  3425. if (is$1.function(document["".concat(pre, "ExitFullscreen")]) || is$1.function(document["".concat(pre, "CancelFullScreen")])) {
  3426. value = pre;
  3427. return true;
  3428. }
  3429. return false;
  3430. });
  3431. return value;
  3432. }
  3433. }, {
  3434. key: "property",
  3435. get: function get() {
  3436. return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
  3437. }
  3438. }]);
  3439. return Fullscreen;
  3440. }();
  3441. // ==========================================================================
  3442. // Load image avoiding xhr/fetch CORS issues
  3443. // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
  3444. // By default it checks if it is at least 1px, but you can add a second argument to change this
  3445. // ==========================================================================
  3446. function loadImage(src) {
  3447. var minWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
  3448. return new Promise(function (resolve, reject) {
  3449. var image = new Image();
  3450. var handler = function handler() {
  3451. delete image.onload;
  3452. delete image.onerror;
  3453. (image.naturalWidth >= minWidth ? resolve : reject)(image);
  3454. };
  3455. Object.assign(image, {
  3456. onload: handler,
  3457. onerror: handler,
  3458. src: src
  3459. });
  3460. });
  3461. }
  3462. // ==========================================================================
  3463. var ui = {
  3464. addStyleHook: function addStyleHook() {
  3465. toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
  3466. toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
  3467. },
  3468. // Toggle native HTML5 media controls
  3469. toggleNativeControls: function toggleNativeControls() {
  3470. var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  3471. if (toggle && this.isHTML5) {
  3472. this.media.setAttribute('controls', '');
  3473. } else {
  3474. this.media.removeAttribute('controls');
  3475. }
  3476. },
  3477. // Setup the UI
  3478. build: function build() {
  3479. var _this = this;
  3480. // Re-attach media element listeners
  3481. // TODO: Use event bubbling?
  3482. this.listeners.media(); // Don't setup interface if no support
  3483. if (!this.supported.ui) {
  3484. this.debug.warn("Basic support only for ".concat(this.provider, " ").concat(this.type)); // Restore native controls
  3485. ui.toggleNativeControls.call(this, true); // Bail
  3486. return;
  3487. } // Inject custom controls if not present
  3488. if (!is$1.element(this.elements.controls)) {
  3489. // Inject custom controls
  3490. controls.inject.call(this); // Re-attach control listeners
  3491. this.listeners.controls();
  3492. } // Remove native controls
  3493. ui.toggleNativeControls.call(this); // Setup captions for HTML5
  3494. if (this.isHTML5) {
  3495. captions.setup.call(this);
  3496. } // Reset volume
  3497. this.volume = null; // Reset mute state
  3498. this.muted = null; // Reset loop state
  3499. this.loop = null; // Reset quality setting
  3500. this.quality = null; // Reset speed
  3501. this.speed = null; // Reset volume display
  3502. controls.updateVolume.call(this); // Reset time display
  3503. controls.timeUpdate.call(this); // Update the UI
  3504. ui.checkPlaying.call(this); // Check for picture-in-picture support
  3505. toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support
  3506. toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class
  3507. toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class
  3508. toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls
  3509. this.ready = true; // Ready event at end of execution stack
  3510. setTimeout(function () {
  3511. triggerEvent.call(_this, _this.media, 'ready');
  3512. }, 0); // Set the title
  3513. ui.setTitle.call(this); // Assure the poster image is set, if the property was added before the element was created
  3514. if (this.poster) {
  3515. ui.setPoster.call(this, this.poster, false).catch(function () {});
  3516. } // Manually set the duration if user has overridden it.
  3517. // The event listeners for it doesn't get called if preload is disabled (#701)
  3518. if (this.config.duration) {
  3519. controls.durationUpdate.call(this);
  3520. }
  3521. },
  3522. // Setup aria attribute for play and iframe title
  3523. setTitle: function setTitle() {
  3524. // Find the current text
  3525. var label = i18n.get('play', this.config); // If there's a media title set, use that for the label
  3526. if (is$1.string(this.config.title) && !is$1.empty(this.config.title)) {
  3527. label += ", ".concat(this.config.title);
  3528. } // If there's a play button, set label
  3529. Array.from(this.elements.buttons.play || []).forEach(function (button) {
  3530. button.setAttribute('aria-label', label);
  3531. }); // Set iframe title
  3532. // https://github.com/sampotts/plyr/issues/124
  3533. if (this.isEmbed) {
  3534. var iframe = getElement.call(this, 'iframe');
  3535. if (!is$1.element(iframe)) {
  3536. return;
  3537. } // Default to media type
  3538. var title = !is$1.empty(this.config.title) ? this.config.title : 'video';
  3539. var format = i18n.get('frameTitle', this.config);
  3540. iframe.setAttribute('title', format.replace('{title}', title));
  3541. }
  3542. },
  3543. // Toggle poster
  3544. togglePoster: function togglePoster(enable) {
  3545. toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
  3546. },
  3547. // Set the poster image (async)
  3548. // Used internally for the poster setter, with the passive option forced to false
  3549. setPoster: function setPoster(poster) {
  3550. var _this2 = this;
  3551. var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  3552. // Don't override if call is passive
  3553. if (passive && this.poster) {
  3554. return Promise.reject(new Error('Poster already set'));
  3555. } // Set property synchronously to respect the call order
  3556. this.media.setAttribute('poster', poster); // Wait until ui is ready
  3557. return ready.call(this) // Load image
  3558. .then(function () {
  3559. return loadImage(poster);
  3560. }).catch(function (err) {
  3561. // Hide poster on error unless it's been set by another call
  3562. if (poster === _this2.poster) {
  3563. ui.togglePoster.call(_this2, false);
  3564. } // Rethrow
  3565. throw err;
  3566. }).then(function () {
  3567. // Prevent race conditions
  3568. if (poster !== _this2.poster) {
  3569. throw new Error('setPoster cancelled by later call to setPoster');
  3570. }
  3571. }).then(function () {
  3572. Object.assign(_this2.elements.poster.style, {
  3573. backgroundImage: "url('".concat(poster, "')"),
  3574. // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
  3575. backgroundSize: ''
  3576. });
  3577. ui.togglePoster.call(_this2, true);
  3578. return poster;
  3579. });
  3580. },
  3581. // Check playing state
  3582. checkPlaying: function checkPlaying(event) {
  3583. var _this3 = this;
  3584. // Class hooks
  3585. toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
  3586. toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
  3587. toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set state
  3588. Array.from(this.elements.buttons.play || []).forEach(function (target) {
  3589. target.pressed = _this3.playing;
  3590. }); // Only update controls on non timeupdate events
  3591. if (is$1.event(event) && event.type === 'timeupdate') {
  3592. return;
  3593. } // Toggle controls
  3594. ui.toggleControls.call(this);
  3595. },
  3596. // Check if media is loading
  3597. checkLoading: function checkLoading(event) {
  3598. var _this4 = this;
  3599. this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer
  3600. clearTimeout(this.timers.loading); // Timer to prevent flicker when seeking
  3601. this.timers.loading = setTimeout(function () {
  3602. // Update progress bar loading class state
  3603. toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading); // Update controls visibility
  3604. ui.toggleControls.call(_this4);
  3605. }, this.loading ? 250 : 0);
  3606. },
  3607. // Toggle controls based on state and `force` argument
  3608. toggleControls: function toggleControls(force) {
  3609. var controls = this.elements.controls;
  3610. if (controls && this.config.hideControls) {
  3611. // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
  3612. var recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
  3613. this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
  3614. }
  3615. }
  3616. };
  3617. var Listeners =
  3618. /*#__PURE__*/
  3619. function () {
  3620. function Listeners(player) {
  3621. _classCallCheck(this, Listeners);
  3622. this.player = player;
  3623. this.lastKey = null;
  3624. this.focusTimer = null;
  3625. this.lastKeyDown = null;
  3626. this.handleKey = this.handleKey.bind(this);
  3627. this.toggleMenu = this.toggleMenu.bind(this);
  3628. this.setTabFocus = this.setTabFocus.bind(this);
  3629. this.firstTouch = this.firstTouch.bind(this);
  3630. } // Handle key presses
  3631. _createClass(Listeners, [{
  3632. key: "handleKey",
  3633. value: function handleKey(event) {
  3634. var player = this.player;
  3635. var elements = player.elements;
  3636. var code = event.keyCode ? event.keyCode : event.which;
  3637. var pressed = event.type === 'keydown';
  3638. var repeat = pressed && code === this.lastKey; // Bail if a modifier key is set
  3639. if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
  3640. return;
  3641. } // If the event is bubbled from the media element
  3642. // Firefox doesn't get the keycode for whatever reason
  3643. if (!is$1.number(code)) {
  3644. return;
  3645. } // Seek by the number keys
  3646. var seekByKey = function seekByKey() {
  3647. // Divide the max duration into 10th's and times by the number value
  3648. player.currentTime = player.duration / 10 * (code - 48);
  3649. }; // Handle the key on keydown
  3650. // Reset on keyup
  3651. if (pressed) {
  3652. // Check focused element
  3653. // and if the focused element is not editable (e.g. text input)
  3654. // and any that accept key input http://webaim.org/techniques/keyboard/
  3655. var focused = document.activeElement;
  3656. if (is$1.element(focused)) {
  3657. var editable = player.config.selectors.editable;
  3658. var seek = elements.inputs.seek;
  3659. if (focused !== seek && matches$1(focused, editable)) {
  3660. return;
  3661. }
  3662. if (event.which === 32 && matches$1(focused, 'button, [role^="menuitem"]')) {
  3663. return;
  3664. }
  3665. } // Which keycodes should we prevent default
  3666. var preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79]; // If the code is found prevent default (e.g. prevent scrolling for arrows)
  3667. if (preventDefault.includes(code)) {
  3668. event.preventDefault();
  3669. event.stopPropagation();
  3670. }
  3671. switch (code) {
  3672. case 48:
  3673. case 49:
  3674. case 50:
  3675. case 51:
  3676. case 52:
  3677. case 53:
  3678. case 54:
  3679. case 55:
  3680. case 56:
  3681. case 57:
  3682. // 0-9
  3683. if (!repeat) {
  3684. seekByKey();
  3685. }
  3686. break;
  3687. case 32:
  3688. case 75:
  3689. // Space and K key
  3690. if (!repeat) {
  3691. player.togglePlay();
  3692. }
  3693. break;
  3694. case 38:
  3695. // Arrow up
  3696. player.increaseVolume(0.1);
  3697. break;
  3698. case 40:
  3699. // Arrow down
  3700. player.decreaseVolume(0.1);
  3701. break;
  3702. case 77:
  3703. // M key
  3704. if (!repeat) {
  3705. player.muted = !player.muted;
  3706. }
  3707. break;
  3708. case 39:
  3709. // Arrow forward
  3710. player.forward();
  3711. break;
  3712. case 37:
  3713. // Arrow back
  3714. player.rewind();
  3715. break;
  3716. case 70:
  3717. // F key
  3718. player.fullscreen.toggle();
  3719. break;
  3720. case 67:
  3721. // C key
  3722. if (!repeat) {
  3723. player.toggleCaptions();
  3724. }
  3725. break;
  3726. case 76:
  3727. // L key
  3728. player.loop = !player.loop;
  3729. break;
  3730. /* case 73:
  3731. this.setLoop('start');
  3732. break;
  3733. case 76:
  3734. this.setLoop();
  3735. break;
  3736. case 79:
  3737. this.setLoop('end');
  3738. break; */
  3739. default:
  3740. break;
  3741. } // Escape is handle natively when in full screen
  3742. // So we only need to worry about non native
  3743. if (code === 27 && !player.fullscreen.usingNative && player.fullscreen.active) {
  3744. player.fullscreen.toggle();
  3745. } // Store last code for next cycle
  3746. this.lastKey = code;
  3747. } else {
  3748. this.lastKey = null;
  3749. }
  3750. } // Toggle menu
  3751. }, {
  3752. key: "toggleMenu",
  3753. value: function toggleMenu(event) {
  3754. controls.toggleMenu.call(this.player, event);
  3755. } // Device is touch enabled
  3756. }, {
  3757. key: "firstTouch",
  3758. value: function firstTouch() {
  3759. var player = this.player;
  3760. var elements = player.elements;
  3761. player.touch = true; // Add touch class
  3762. toggleClass(elements.container, player.config.classNames.isTouch, true);
  3763. }
  3764. }, {
  3765. key: "setTabFocus",
  3766. value: function setTabFocus(event) {
  3767. var player = this.player;
  3768. var elements = player.elements;
  3769. clearTimeout(this.focusTimer); // Ignore any key other than tab
  3770. if (event.type === 'keydown' && event.which !== 9) {
  3771. return;
  3772. } // Store reference to event timeStamp
  3773. if (event.type === 'keydown') {
  3774. this.lastKeyDown = event.timeStamp;
  3775. } // Remove current classes
  3776. var removeCurrent = function removeCurrent() {
  3777. var className = player.config.classNames.tabFocus;
  3778. var current = getElements.call(player, ".".concat(className));
  3779. toggleClass(current, className, false);
  3780. }; // Determine if a key was pressed to trigger this event
  3781. var wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; // Ignore focus events if a key was pressed prior
  3782. if (event.type === 'focus' && !wasKeyDown) {
  3783. return;
  3784. } // Remove all current
  3785. removeCurrent(); // Delay the adding of classname until the focus has changed
  3786. // This event fires before the focusin event
  3787. this.focusTimer = setTimeout(function () {
  3788. var focused = document.activeElement; // Ignore if current focus element isn't inside the player
  3789. if (!elements.container.contains(focused)) {
  3790. return;
  3791. }
  3792. toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
  3793. }, 10);
  3794. } // Global window & document listeners
  3795. }, {
  3796. key: "global",
  3797. value: function global() {
  3798. var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
  3799. var player = this.player; // Keyboard shortcuts
  3800. if (player.config.keyboard.global) {
  3801. toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);
  3802. } // Click anywhere closes menu
  3803. toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events
  3804. once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
  3805. toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true);
  3806. } // Container listeners
  3807. }, {
  3808. key: "container",
  3809. value: function container() {
  3810. var player = this.player;
  3811. var config = player.config,
  3812. elements = player.elements,
  3813. timers = player.timers; // Keyboard shortcuts
  3814. if (!config.keyboard.global && config.keyboard.focused) {
  3815. on.call(player, elements.container, 'keydown keyup', this.handleKey, false);
  3816. } // Toggle controls on mouse events and entering fullscreen
  3817. on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) {
  3818. var controls = elements.controls; // Remove button states for fullscreen
  3819. if (controls && event.type === 'enterfullscreen') {
  3820. controls.pressed = false;
  3821. controls.hover = false;
  3822. } // Show, then hide after a timeout unless another control event occurs
  3823. var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
  3824. var delay = 0;
  3825. if (show) {
  3826. ui.toggleControls.call(player, true); // Use longer timeout for touch devices
  3827. delay = player.touch ? 3000 : 2000;
  3828. } // Clear timer
  3829. clearTimeout(timers.controls); // Set new timer to prevent flicker when seeking
  3830. timers.controls = setTimeout(function () {
  3831. return ui.toggleControls.call(player, false);
  3832. }, delay);
  3833. }); // Force edge to repaint on exit fullscreen
  3834. // TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
  3835. /* if (browser.isEdge) {
  3836. on.call(player, elements.container, 'exitfullscreen', () => {
  3837. setTimeout(() => repaint(elements.container), 100);
  3838. });
  3839. } */
  3840. // Set a gutter for Vimeo
  3841. var setGutter = function setGutter(ratio, padding, toggle) {
  3842. if (!player.isVimeo) {
  3843. return;
  3844. }
  3845. var target = player.elements.wrapper.firstChild;
  3846. var _ratio = _slicedToArray(ratio, 2),
  3847. y = _ratio[1];
  3848. var _getAspectRatio$call = getAspectRatio.call(player),
  3849. _getAspectRatio$call2 = _slicedToArray(_getAspectRatio$call, 2),
  3850. videoX = _getAspectRatio$call2[0],
  3851. videoY = _getAspectRatio$call2[1];
  3852. target.style.maxWidth = toggle ? "".concat(y / videoY * videoX, "px") : null;
  3853. target.style.margin = toggle ? '0 auto' : null;
  3854. }; // Resize on fullscreen change
  3855. var setPlayerSize = function setPlayerSize(measure) {
  3856. // If we don't need to measure the viewport
  3857. if (!measure) {
  3858. return setAspectRatio.call(player);
  3859. }
  3860. var rect = elements.container.getBoundingClientRect();
  3861. var width = rect.width,
  3862. height = rect.height;
  3863. return setAspectRatio.call(player, "".concat(width, ":").concat(height));
  3864. };
  3865. var resized = function resized() {
  3866. window.clearTimeout(timers.resized);
  3867. timers.resized = window.setTimeout(setPlayerSize, 50);
  3868. };
  3869. on.call(player, elements.container, 'enterfullscreen exitfullscreen', function (event) {
  3870. var _player$fullscreen = player.fullscreen,
  3871. target = _player$fullscreen.target,
  3872. usingNative = _player$fullscreen.usingNative; // Ignore for iOS native
  3873. if (!player.isEmbed || target !== elements.container) {
  3874. return;
  3875. }
  3876. var isEnter = event.type === 'enterfullscreen'; // Set the player size when entering fullscreen to viewport size
  3877. var _setPlayerSize = setPlayerSize(isEnter),
  3878. padding = _setPlayerSize.padding,
  3879. ratio = _setPlayerSize.ratio; // Set Vimeo gutter
  3880. setGutter(ratio, padding, isEnter); // If not using native fullscreen, we need to check for resizes of viewport
  3881. if (!usingNative) {
  3882. if (isEnter) {
  3883. on.call(player, window, 'resize', resized);
  3884. } else {
  3885. off.call(player, window, 'resize', resized);
  3886. }
  3887. }
  3888. });
  3889. } // Listen for media events
  3890. }, {
  3891. key: "media",
  3892. value: function media() {
  3893. var _this = this;
  3894. var player = this.player;
  3895. var elements = player.elements; // Time change on media
  3896. on.call(player, player.media, 'timeupdate seeking seeked', function (event) {
  3897. return controls.timeUpdate.call(player, event);
  3898. }); // Display duration
  3899. on.call(player, player.media, 'durationchange loadeddata loadedmetadata', function (event) {
  3900. return controls.durationUpdate.call(player, event);
  3901. }); // Check for audio tracks on load
  3902. // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
  3903. on.call(player, player.media, 'canplay loadeddata', function () {
  3904. toggleHidden(elements.volume, !player.hasAudio);
  3905. toggleHidden(elements.buttons.mute, !player.hasAudio);
  3906. }); // Handle the media finishing
  3907. on.call(player, player.media, 'ended', function () {
  3908. // Show poster on end
  3909. if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
  3910. // Restart
  3911. player.restart();
  3912. }
  3913. }); // Check for buffer progress
  3914. on.call(player, player.media, 'progress playing seeking seeked', function (event) {
  3915. return controls.updateProgress.call(player, event);
  3916. }); // Handle volume changes
  3917. on.call(player, player.media, 'volumechange', function (event) {
  3918. return controls.updateVolume.call(player, event);
  3919. }); // Handle play/pause
  3920. on.call(player, player.media, 'playing play pause ended emptied timeupdate', function (event) {
  3921. return ui.checkPlaying.call(player, event);
  3922. }); // Loading state
  3923. on.call(player, player.media, 'waiting canplay seeked playing', function (event) {
  3924. return ui.checkLoading.call(player, event);
  3925. }); // Click video
  3926. if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {
  3927. // Re-fetch the wrapper
  3928. var wrapper = getElement.call(player, ".".concat(player.config.classNames.video)); // Bail if there's no wrapper (this should never happen)
  3929. if (!is$1.element(wrapper)) {
  3930. return;
  3931. } // On click play, pause or restart
  3932. on.call(player, elements.container, 'click', function (event) {
  3933. var targets = [elements.container, wrapper]; // Ignore if click if not container or in video wrapper
  3934. if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
  3935. return;
  3936. } // Touch devices will just show controls (if hidden)
  3937. if (player.touch && player.config.hideControls) {
  3938. return;
  3939. }
  3940. if (player.ended) {
  3941. _this.proxy(event, player.restart, 'restart');
  3942. _this.proxy(event, player.play, 'play');
  3943. } else {
  3944. _this.proxy(event, player.togglePlay, 'play');
  3945. }
  3946. });
  3947. } // Disable right click
  3948. if (player.supported.ui && player.config.disableContextMenu) {
  3949. on.call(player, elements.wrapper, 'contextmenu', function (event) {
  3950. event.preventDefault();
  3951. }, false);
  3952. } // Volume change
  3953. on.call(player, player.media, 'volumechange', function () {
  3954. // Save to storage
  3955. player.storage.set({
  3956. volume: player.volume,
  3957. muted: player.muted
  3958. });
  3959. }); // Speed change
  3960. on.call(player, player.media, 'ratechange', function () {
  3961. // Update UI
  3962. controls.updateSetting.call(player, 'speed'); // Save to storage
  3963. player.storage.set({
  3964. speed: player.speed
  3965. });
  3966. }); // Quality change
  3967. on.call(player, player.media, 'qualitychange', function (event) {
  3968. // Update UI
  3969. controls.updateSetting.call(player, 'quality', null, event.detail.quality);
  3970. }); // Update download link when ready and if quality changes
  3971. on.call(player, player.media, 'ready qualitychange', function () {
  3972. controls.setDownloadUrl.call(player);
  3973. }); // Proxy events to container
  3974. // Bubble up key events for Edge
  3975. var proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
  3976. on.call(player, player.media, proxyEvents, function (event) {
  3977. var _event$detail = event.detail,
  3978. detail = _event$detail === void 0 ? {} : _event$detail; // Get error details from media
  3979. if (event.type === 'error') {
  3980. detail = player.media.error;
  3981. }
  3982. triggerEvent.call(player, elements.container, event.type, true, detail);
  3983. });
  3984. } // Run default and custom handlers
  3985. }, {
  3986. key: "proxy",
  3987. value: function proxy(event, defaultHandler, customHandlerKey) {
  3988. var player = this.player;
  3989. var customHandler = player.config.listeners[customHandlerKey];
  3990. var hasCustomHandler = is$1.function(customHandler);
  3991. var returned = true; // Execute custom handler
  3992. if (hasCustomHandler) {
  3993. returned = customHandler.call(player, event);
  3994. } // Only call default handler if not prevented in custom handler
  3995. if (returned && is$1.function(defaultHandler)) {
  3996. defaultHandler.call(player, event);
  3997. }
  3998. } // Trigger custom and default handlers
  3999. }, {
  4000. key: "bind",
  4001. value: function bind(element, type, defaultHandler, customHandlerKey) {
  4002. var _this2 = this;
  4003. var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
  4004. var player = this.player;
  4005. var customHandler = player.config.listeners[customHandlerKey];
  4006. var hasCustomHandler = is$1.function(customHandler);
  4007. on.call(player, element, type, function (event) {
  4008. return _this2.proxy(event, defaultHandler, customHandlerKey);
  4009. }, passive && !hasCustomHandler);
  4010. } // Listen for control events
  4011. }, {
  4012. key: "controls",
  4013. value: function controls$1() {
  4014. var _this3 = this;
  4015. var player = this.player;
  4016. var elements = player.elements; // IE doesn't support input event, so we fallback to change
  4017. var inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle
  4018. if (elements.buttons.play) {
  4019. Array.from(elements.buttons.play).forEach(function (button) {
  4020. _this3.bind(button, 'click', player.togglePlay, 'play');
  4021. });
  4022. } // Pause
  4023. this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind
  4024. this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind
  4025. this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle
  4026. this.bind(elements.buttons.mute, 'click', function () {
  4027. player.muted = !player.muted;
  4028. }, 'mute'); // Captions toggle
  4029. this.bind(elements.buttons.captions, 'click', function () {
  4030. return player.toggleCaptions();
  4031. }); // Download
  4032. this.bind(elements.buttons.download, 'click', function () {
  4033. triggerEvent.call(player, player.media, 'download');
  4034. }, 'download'); // Fullscreen toggle
  4035. this.bind(elements.buttons.fullscreen, 'click', function () {
  4036. player.fullscreen.toggle();
  4037. }, 'fullscreen'); // Picture-in-Picture
  4038. this.bind(elements.buttons.pip, 'click', function () {
  4039. player.pip = 'toggle';
  4040. }, 'pip'); // Airplay
  4041. this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle
  4042. this.bind(elements.buttons.settings, 'click', function (event) {
  4043. // Prevent the document click listener closing the menu
  4044. event.stopPropagation();
  4045. controls.toggleMenu.call(player, event);
  4046. }); // Settings menu - keyboard toggle
  4047. // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
  4048. // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
  4049. this.bind(elements.buttons.settings, 'keyup', function (event) {
  4050. var code = event.which; // We only care about space and return
  4051. if (![13, 32].includes(code)) {
  4052. return;
  4053. } // Because return triggers a click anyway, all we need to do is set focus
  4054. if (code === 13) {
  4055. controls.focusFirstMenuItem.call(player, null, true);
  4056. return;
  4057. } // Prevent scroll
  4058. event.preventDefault(); // Prevent playing video (Firefox)
  4059. event.stopPropagation(); // Toggle menu
  4060. controls.toggleMenu.call(player, event);
  4061. }, null, false // Can't be passive as we're preventing default
  4062. ); // Escape closes menu
  4063. this.bind(elements.settings.menu, 'keydown', function (event) {
  4064. if (event.which === 27) {
  4065. controls.toggleMenu.call(player, event);
  4066. }
  4067. }); // Set range input alternative "value", which matches the tooltip time (#954)
  4068. this.bind(elements.inputs.seek, 'mousedown mousemove', function (event) {
  4069. var rect = elements.progress.getBoundingClientRect();
  4070. var percent = 100 / rect.width * (event.pageX - rect.left);
  4071. event.currentTarget.setAttribute('seek-value', percent);
  4072. }); // Pause while seeking
  4073. this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) {
  4074. var seek = event.currentTarget;
  4075. var code = event.keyCode ? event.keyCode : event.which;
  4076. var attribute = 'play-on-seeked';
  4077. if (is$1.keyboardEvent(event) && code !== 39 && code !== 37) {
  4078. return;
  4079. } // Record seek time so we can prevent hiding controls for a few seconds after seek
  4080. player.lastSeekTime = Date.now(); // Was playing before?
  4081. var play = seek.hasAttribute(attribute); // Done seeking
  4082. var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
  4083. if (play && done) {
  4084. seek.removeAttribute(attribute);
  4085. player.play();
  4086. } else if (!done && player.playing) {
  4087. seek.setAttribute(attribute, '');
  4088. player.pause();
  4089. }
  4090. }); // Fix range inputs on iOS
  4091. // Super weird iOS bug where after you interact with an <input type="range">,
  4092. // it takes over further interactions on the page. This is a hack
  4093. if (browser.isIos) {
  4094. var inputs = getElements.call(player, 'input[type="range"]');
  4095. Array.from(inputs).forEach(function (input) {
  4096. return _this3.bind(input, inputEvent, function (event) {
  4097. return repaint(event.target);
  4098. });
  4099. });
  4100. } // Seek
  4101. this.bind(elements.inputs.seek, inputEvent, function (event) {
  4102. var seek = event.currentTarget; // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
  4103. var seekTo = seek.getAttribute('seek-value');
  4104. if (is$1.empty(seekTo)) {
  4105. seekTo = seek.value;
  4106. }
  4107. seek.removeAttribute('seek-value');
  4108. player.currentTime = seekTo / seek.max * player.duration;
  4109. }, 'seek'); // Seek tooltip
  4110. this.bind(elements.progress, 'mouseenter mouseleave mousemove', function (event) {
  4111. return controls.updateSeekTooltip.call(player, event);
  4112. }); // Preview thumbnails plugin
  4113. // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this
  4114. this.bind(elements.progress, 'mousemove touchmove', function (event) {
  4115. var previewThumbnails = player.previewThumbnails;
  4116. if (previewThumbnails && previewThumbnails.loaded) {
  4117. previewThumbnails.startMove(event);
  4118. }
  4119. }); // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
  4120. this.bind(elements.progress, 'mouseleave click', function () {
  4121. var previewThumbnails = player.previewThumbnails;
  4122. if (previewThumbnails && previewThumbnails.loaded) {
  4123. previewThumbnails.endMove(false, true);
  4124. }
  4125. }); // Show scrubbing preview
  4126. this.bind(elements.progress, 'mousedown touchstart', function (event) {
  4127. var previewThumbnails = player.previewThumbnails;
  4128. if (previewThumbnails && previewThumbnails.loaded) {
  4129. previewThumbnails.startScrubbing(event);
  4130. }
  4131. });
  4132. this.bind(elements.progress, 'mouseup touchend', function (event) {
  4133. var previewThumbnails = player.previewThumbnails;
  4134. if (previewThumbnails && previewThumbnails.loaded) {
  4135. previewThumbnails.endScrubbing(event);
  4136. }
  4137. }); // Polyfill for lower fill in <input type="range"> for webkit
  4138. if (browser.isWebkit) {
  4139. Array.from(getElements.call(player, 'input[type="range"]')).forEach(function (element) {
  4140. _this3.bind(element, 'input', function (event) {
  4141. return controls.updateRangeFill.call(player, event.target);
  4142. });
  4143. });
  4144. } // Current time invert
  4145. // Only if one time element is used for both currentTime and duration
  4146. if (player.config.toggleInvert && !is$1.element(elements.display.duration)) {
  4147. this.bind(elements.display.currentTime, 'click', function () {
  4148. // Do nothing if we're at the start
  4149. if (player.currentTime === 0) {
  4150. return;
  4151. }
  4152. player.config.invertTime = !player.config.invertTime;
  4153. controls.timeUpdate.call(player);
  4154. });
  4155. } // Volume
  4156. this.bind(elements.inputs.volume, inputEvent, function (event) {
  4157. player.volume = event.target.value;
  4158. }, 'volume'); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
  4159. this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
  4160. elements.controls.hover = !player.touch && event.type === 'mouseenter';
  4161. }); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
  4162. this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
  4163. elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
  4164. }); // Show controls when they receive focus (e.g., when using keyboard tab key)
  4165. this.bind(elements.controls, 'focusin', function () {
  4166. var config = player.config,
  4167. elements = player.elements,
  4168. timers = player.timers; // Skip transition to prevent focus from scrolling the parent element
  4169. toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
  4170. ui.toggleControls.call(player, true); // Restore transition
  4171. setTimeout(function () {
  4172. toggleClass(elements.controls, config.classNames.noTransition, false);
  4173. }, 0); // Delay a little more for mouse users
  4174. var delay = _this3.touch ? 3000 : 4000; // Clear timer
  4175. clearTimeout(timers.controls); // Hide again after delay
  4176. timers.controls = setTimeout(function () {
  4177. return ui.toggleControls.call(player, false);
  4178. }, delay);
  4179. }); // Mouse wheel for volume
  4180. this.bind(elements.inputs.volume, 'wheel', function (event) {
  4181. // Detect "natural" scroll - suppored on OS X Safari only
  4182. // Other browsers on OS X will be inverted until support improves
  4183. var inverted = event.webkitDirectionInvertedFromDevice; // Get delta from event. Invert if `inverted` is true
  4184. var _map = [event.deltaX, -event.deltaY].map(function (value) {
  4185. return inverted ? -value : value;
  4186. }),
  4187. _map2 = _slicedToArray(_map, 2),
  4188. x = _map2[0],
  4189. y = _map2[1]; // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
  4190. var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2%
  4191. player.increaseVolume(direction / 50); // Don't break page scrolling at max and min
  4192. var volume = player.media.volume;
  4193. if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {
  4194. event.preventDefault();
  4195. }
  4196. }, 'volume', false);
  4197. }
  4198. }]);
  4199. return Listeners;
  4200. }();
  4201. var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
  4202. function createCommonjsModule(fn, module) {
  4203. return module = { exports: {} }, fn(module, module.exports), module.exports;
  4204. }
  4205. var loadjs_umd = createCommonjsModule(function (module, exports) {
  4206. (function (root, factory) {
  4207. {
  4208. module.exports = factory();
  4209. }
  4210. })(commonjsGlobal, function () {
  4211. /**
  4212. * Global dependencies.
  4213. * @global {Object} document - DOM
  4214. */
  4215. var devnull = function devnull() {},
  4216. bundleIdCache = {},
  4217. bundleResultCache = {},
  4218. bundleCallbackQueue = {};
  4219. /**
  4220. * Subscribe to bundle load event.
  4221. * @param {string[]} bundleIds - Bundle ids
  4222. * @param {Function} callbackFn - The callback function
  4223. */
  4224. function subscribe(bundleIds, callbackFn) {
  4225. // listify
  4226. bundleIds = bundleIds.push ? bundleIds : [bundleIds];
  4227. var depsNotFound = [],
  4228. i = bundleIds.length,
  4229. numWaiting = i,
  4230. fn,
  4231. bundleId,
  4232. r,
  4233. q; // define callback function
  4234. fn = function fn(bundleId, pathsNotFound) {
  4235. if (pathsNotFound.length) depsNotFound.push(bundleId);
  4236. numWaiting--;
  4237. if (!numWaiting) callbackFn(depsNotFound);
  4238. }; // register callback
  4239. while (i--) {
  4240. bundleId = bundleIds[i]; // execute callback if in result cache
  4241. r = bundleResultCache[bundleId];
  4242. if (r) {
  4243. fn(bundleId, r);
  4244. continue;
  4245. } // add to callback queue
  4246. q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
  4247. q.push(fn);
  4248. }
  4249. }
  4250. /**
  4251. * Publish bundle load event.
  4252. * @param {string} bundleId - Bundle id
  4253. * @param {string[]} pathsNotFound - List of files not found
  4254. */
  4255. function publish(bundleId, pathsNotFound) {
  4256. // exit if id isn't defined
  4257. if (!bundleId) return;
  4258. var q = bundleCallbackQueue[bundleId]; // cache result
  4259. bundleResultCache[bundleId] = pathsNotFound; // exit if queue is empty
  4260. if (!q) return; // empty callback queue
  4261. while (q.length) {
  4262. q[0](bundleId, pathsNotFound);
  4263. q.splice(0, 1);
  4264. }
  4265. }
  4266. /**
  4267. * Execute callbacks.
  4268. * @param {Object or Function} args - The callback args
  4269. * @param {string[]} depsNotFound - List of dependencies not found
  4270. */
  4271. function executeCallbacks(args, depsNotFound) {
  4272. // accept function as argument
  4273. if (args.call) args = {
  4274. success: args
  4275. }; // success and error callbacks
  4276. if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);
  4277. }
  4278. /**
  4279. * Load individual file.
  4280. * @param {string} path - The file path
  4281. * @param {Function} callbackFn - The callback function
  4282. */
  4283. function loadFile(path, callbackFn, args, numTries) {
  4284. var doc = document,
  4285. async = args.async,
  4286. maxTries = (args.numRetries || 0) + 1,
  4287. beforeCallbackFn = args.before || devnull,
  4288. pathStripped = path.replace(/^(css|img)!/, ''),
  4289. isLegacyIECss,
  4290. e;
  4291. numTries = numTries || 0;
  4292. if (/(^css!|\.css$)/.test(path)) {
  4293. // css
  4294. e = doc.createElement('link');
  4295. e.rel = 'stylesheet';
  4296. e.href = pathStripped; // tag IE9+
  4297. isLegacyIECss = 'hideFocus' in e; // use preload in IE Edge (to detect load errors)
  4298. if (isLegacyIECss && e.relList) {
  4299. isLegacyIECss = 0;
  4300. e.rel = 'preload';
  4301. e.as = 'style';
  4302. }
  4303. } else if (/(^img!|\.(png|gif|jpg|svg)$)/.test(path)) {
  4304. // image
  4305. e = doc.createElement('img');
  4306. e.src = pathStripped;
  4307. } else {
  4308. // javascript
  4309. e = doc.createElement('script');
  4310. e.src = path;
  4311. e.async = async === undefined ? true : async;
  4312. }
  4313. e.onload = e.onerror = e.onbeforeload = function (ev) {
  4314. var result = ev.type[0]; // treat empty stylesheets as failures to get around lack of onerror
  4315. // support in IE9-11
  4316. if (isLegacyIECss) {
  4317. try {
  4318. if (!e.sheet.cssText.length) result = 'e';
  4319. } catch (x) {
  4320. // sheets objects created from load errors don't allow access to
  4321. // `cssText` (unless error is Code:18 SecurityError)
  4322. if (x.code != 18) result = 'e';
  4323. }
  4324. } // handle retries in case of load failure
  4325. if (result == 'e') {
  4326. // increment counter
  4327. numTries += 1; // exit function and try again
  4328. if (numTries < maxTries) {
  4329. return loadFile(path, callbackFn, args, numTries);
  4330. }
  4331. } else if (e.rel == 'preload' && e.as == 'style') {
  4332. // activate preloaded stylesheets
  4333. return e.rel = 'stylesheet'; // jshint ignore:line
  4334. } // execute callback
  4335. callbackFn(path, result, ev.defaultPrevented);
  4336. }; // add to document (unless callback returns `false`)
  4337. if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
  4338. }
  4339. /**
  4340. * Load multiple files.
  4341. * @param {string[]} paths - The file paths
  4342. * @param {Function} callbackFn - The callback function
  4343. */
  4344. function loadFiles(paths, callbackFn, args) {
  4345. // listify paths
  4346. paths = paths.push ? paths : [paths];
  4347. var numWaiting = paths.length,
  4348. x = numWaiting,
  4349. pathsNotFound = [],
  4350. fn,
  4351. i; // define callback function
  4352. fn = function fn(path, result, defaultPrevented) {
  4353. // handle error
  4354. if (result == 'e') pathsNotFound.push(path); // handle beforeload event. If defaultPrevented then that means the load
  4355. // will be blocked (ex. Ghostery/ABP on Safari)
  4356. if (result == 'b') {
  4357. if (defaultPrevented) pathsNotFound.push(path);else return;
  4358. }
  4359. numWaiting--;
  4360. if (!numWaiting) callbackFn(pathsNotFound);
  4361. }; // load scripts
  4362. for (i = 0; i < x; i++) {
  4363. loadFile(paths[i], fn, args);
  4364. }
  4365. }
  4366. /**
  4367. * Initiate script load and register bundle.
  4368. * @param {(string|string[])} paths - The file paths
  4369. * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success
  4370. * callback or (3) object literal with success/error arguments, numRetries,
  4371. * etc.
  4372. * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object
  4373. * literal with success/error arguments, numRetries, etc.
  4374. */
  4375. function loadjs(paths, arg1, arg2) {
  4376. var bundleId, args; // bundleId (if string)
  4377. if (arg1 && arg1.trim) bundleId = arg1; // args (default is {})
  4378. args = (bundleId ? arg2 : arg1) || {}; // throw error if bundle is already defined
  4379. if (bundleId) {
  4380. if (bundleId in bundleIdCache) {
  4381. throw "LoadJS";
  4382. } else {
  4383. bundleIdCache[bundleId] = true;
  4384. }
  4385. }
  4386. function loadFn(resolve, reject) {
  4387. loadFiles(paths, function (pathsNotFound) {
  4388. // execute callbacks
  4389. executeCallbacks(args, pathsNotFound); // resolve Promise
  4390. if (resolve) {
  4391. executeCallbacks({
  4392. success: resolve,
  4393. error: reject
  4394. }, pathsNotFound);
  4395. } // publish bundle load event
  4396. publish(bundleId, pathsNotFound);
  4397. }, args);
  4398. }
  4399. if (args.returnPromise) return new Promise(loadFn);else loadFn();
  4400. }
  4401. /**
  4402. * Execute callbacks when dependencies have been satisfied.
  4403. * @param {(string|string[])} deps - List of bundle ids
  4404. * @param {Object} args - success/error arguments
  4405. */
  4406. loadjs.ready = function ready(deps, args) {
  4407. // subscribe to bundle load event
  4408. subscribe(deps, function (depsNotFound) {
  4409. // execute callbacks
  4410. executeCallbacks(args, depsNotFound);
  4411. });
  4412. return loadjs;
  4413. };
  4414. /**
  4415. * Manually satisfy bundle dependencies.
  4416. * @param {string} bundleId - The bundle id
  4417. */
  4418. loadjs.done = function done(bundleId) {
  4419. publish(bundleId, []);
  4420. };
  4421. /**
  4422. * Reset loadjs dependencies statuses
  4423. */
  4424. loadjs.reset = function reset() {
  4425. bundleIdCache = {};
  4426. bundleResultCache = {};
  4427. bundleCallbackQueue = {};
  4428. };
  4429. /**
  4430. * Determine if bundle has already been defined
  4431. * @param String} bundleId - The bundle id
  4432. */
  4433. loadjs.isDefined = function isDefined(bundleId) {
  4434. return bundleId in bundleIdCache;
  4435. }; // export
  4436. return loadjs;
  4437. });
  4438. });
  4439. // ==========================================================================
  4440. function loadScript(url) {
  4441. return new Promise(function (resolve, reject) {
  4442. loadjs_umd(url, {
  4443. success: resolve,
  4444. error: reject
  4445. });
  4446. });
  4447. }
  4448. function parseId(url) {
  4449. if (is$1.empty(url)) {
  4450. return null;
  4451. }
  4452. if (is$1.number(Number(url))) {
  4453. return url;
  4454. }
  4455. var regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
  4456. return url.match(regex) ? RegExp.$2 : url;
  4457. } // Set playback state and trigger change (only on actual change)
  4458. function assurePlaybackState(play) {
  4459. if (play && !this.embed.hasPlayed) {
  4460. this.embed.hasPlayed = true;
  4461. }
  4462. if (this.media.paused === play) {
  4463. this.media.paused = !play;
  4464. triggerEvent.call(this, this.media, play ? 'play' : 'pause');
  4465. }
  4466. }
  4467. var vimeo = {
  4468. setup: function setup() {
  4469. var _this = this;
  4470. // Add embed class for responsive
  4471. toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set intial ratio
  4472. setAspectRatio.call(this); // Load the SDK if not already
  4473. if (!is$1.object(window.Vimeo)) {
  4474. loadScript(this.config.urls.vimeo.sdk).then(function () {
  4475. vimeo.ready.call(_this);
  4476. }).catch(function (error) {
  4477. _this.debug.warn('Vimeo SDK (player.js) failed to load', error);
  4478. });
  4479. } else {
  4480. vimeo.ready.call(this);
  4481. }
  4482. },
  4483. // API Ready
  4484. ready: function ready() {
  4485. var _this2 = this;
  4486. var player = this;
  4487. var config = player.config.vimeo; // Get Vimeo params for the iframe
  4488. var params = buildUrlParams(extend({}, {
  4489. loop: player.config.loop.active,
  4490. autoplay: player.autoplay,
  4491. muted: player.muted,
  4492. gesture: 'media',
  4493. playsinline: !this.config.fullscreen.iosNative
  4494. }, config)); // Get the source URL or ID
  4495. var source = player.media.getAttribute('src'); // Get from <div> if needed
  4496. if (is$1.empty(source)) {
  4497. source = player.media.getAttribute(player.config.attributes.embed.id);
  4498. }
  4499. var id = parseId(source); // Build an iframe
  4500. var iframe = createElement('iframe');
  4501. var src = format(player.config.urls.vimeo.iframe, id, params);
  4502. iframe.setAttribute('src', src);
  4503. iframe.setAttribute('allowfullscreen', '');
  4504. iframe.setAttribute('allowtransparency', '');
  4505. iframe.setAttribute('allow', 'autoplay'); // Get poster, if already set
  4506. var poster = player.poster; // Inject the package
  4507. var wrapper = createElement('div', {
  4508. poster: poster,
  4509. class: player.config.classNames.embedContainer
  4510. });
  4511. wrapper.appendChild(iframe);
  4512. player.media = replaceElement(wrapper, player.media); // Get poster image
  4513. fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
  4514. if (is$1.empty(response)) {
  4515. return;
  4516. } // Get the URL for thumbnail
  4517. var url = new URL(response[0].thumbnail_large); // Get original image
  4518. url.pathname = "".concat(url.pathname.split('_')[0], ".jpg"); // Set and show poster
  4519. ui.setPoster.call(player, url.href).catch(function () {});
  4520. }); // Setup instance
  4521. // https://github.com/vimeo/player.js
  4522. player.embed = new window.Vimeo.Player(iframe, {
  4523. autopause: player.config.autopause,
  4524. muted: player.muted
  4525. });
  4526. player.media.paused = true;
  4527. player.media.currentTime = 0; // Disable native text track rendering
  4528. if (player.supported.ui) {
  4529. player.embed.disableTextTrack();
  4530. } // Create a faux HTML5 API using the Vimeo API
  4531. player.media.play = function () {
  4532. assurePlaybackState.call(player, true);
  4533. return player.embed.play();
  4534. };
  4535. player.media.pause = function () {
  4536. assurePlaybackState.call(player, false);
  4537. return player.embed.pause();
  4538. };
  4539. player.media.stop = function () {
  4540. player.pause();
  4541. player.currentTime = 0;
  4542. }; // Seeking
  4543. var currentTime = player.media.currentTime;
  4544. Object.defineProperty(player.media, 'currentTime', {
  4545. get: function get() {
  4546. return currentTime;
  4547. },
  4548. set: function set(time) {
  4549. // Vimeo will automatically play on seek if the video hasn't been played before
  4550. // Get current paused state and volume etc
  4551. var embed = player.embed,
  4552. media = player.media,
  4553. paused = player.paused,
  4554. volume = player.volume;
  4555. var restorePause = paused && !embed.hasPlayed; // Set seeking state and trigger event
  4556. media.seeking = true;
  4557. triggerEvent.call(player, media, 'seeking'); // If paused, mute until seek is complete
  4558. Promise.resolve(restorePause && embed.setVolume(0)) // Seek
  4559. .then(function () {
  4560. return embed.setCurrentTime(time);
  4561. }) // Restore paused
  4562. .then(function () {
  4563. return restorePause && embed.pause();
  4564. }) // Restore volume
  4565. .then(function () {
  4566. return restorePause && embed.setVolume(volume);
  4567. }).catch(function () {// Do nothing
  4568. });
  4569. }
  4570. }); // Playback speed
  4571. var speed = player.config.speed.selected;
  4572. Object.defineProperty(player.media, 'playbackRate', {
  4573. get: function get() {
  4574. return speed;
  4575. },
  4576. set: function set(input) {
  4577. player.embed.setPlaybackRate(input).then(function () {
  4578. speed = input;
  4579. triggerEvent.call(player, player.media, 'ratechange');
  4580. }).catch(function (error) {
  4581. // Hide menu item (and menu if empty)
  4582. if (error.name === 'Error') {
  4583. controls.setSpeedMenu.call(player, []);
  4584. }
  4585. });
  4586. }
  4587. }); // Volume
  4588. var volume = player.config.volume;
  4589. Object.defineProperty(player.media, 'volume', {
  4590. get: function get() {
  4591. return volume;
  4592. },
  4593. set: function set(input) {
  4594. player.embed.setVolume(input).then(function () {
  4595. volume = input;
  4596. triggerEvent.call(player, player.media, 'volumechange');
  4597. });
  4598. }
  4599. }); // Muted
  4600. var muted = player.config.muted;
  4601. Object.defineProperty(player.media, 'muted', {
  4602. get: function get() {
  4603. return muted;
  4604. },
  4605. set: function set(input) {
  4606. var toggle = is$1.boolean(input) ? input : false;
  4607. player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () {
  4608. muted = toggle;
  4609. triggerEvent.call(player, player.media, 'volumechange');
  4610. });
  4611. }
  4612. }); // Loop
  4613. var loop = player.config.loop;
  4614. Object.defineProperty(player.media, 'loop', {
  4615. get: function get() {
  4616. return loop;
  4617. },
  4618. set: function set(input) {
  4619. var toggle = is$1.boolean(input) ? input : player.config.loop.active;
  4620. player.embed.setLoop(toggle).then(function () {
  4621. loop = toggle;
  4622. });
  4623. }
  4624. }); // Source
  4625. var currentSrc;
  4626. player.embed.getVideoUrl().then(function (value) {
  4627. currentSrc = value;
  4628. controls.setDownloadUrl.call(player);
  4629. }).catch(function (error) {
  4630. _this2.debug.warn(error);
  4631. });
  4632. Object.defineProperty(player.media, 'currentSrc', {
  4633. get: function get() {
  4634. return currentSrc;
  4635. }
  4636. }); // Ended
  4637. Object.defineProperty(player.media, 'ended', {
  4638. get: function get() {
  4639. return player.currentTime === player.duration;
  4640. }
  4641. }); // Set aspect ratio based on video size
  4642. Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) {
  4643. var _dimensions = _slicedToArray(dimensions, 2),
  4644. width = _dimensions[0],
  4645. height = _dimensions[1];
  4646. player.embed.ratio = [width, height];
  4647. setAspectRatio.call(_this2);
  4648. }); // Set autopause
  4649. player.embed.setAutopause(player.config.autopause).then(function (state) {
  4650. player.config.autopause = state;
  4651. }); // Get title
  4652. player.embed.getVideoTitle().then(function (title) {
  4653. player.config.title = title;
  4654. ui.setTitle.call(_this2);
  4655. }); // Get current time
  4656. player.embed.getCurrentTime().then(function (value) {
  4657. currentTime = value;
  4658. triggerEvent.call(player, player.media, 'timeupdate');
  4659. }); // Get duration
  4660. player.embed.getDuration().then(function (value) {
  4661. player.media.duration = value;
  4662. triggerEvent.call(player, player.media, 'durationchange');
  4663. }); // Get captions
  4664. player.embed.getTextTracks().then(function (tracks) {
  4665. player.media.textTracks = tracks;
  4666. captions.setup.call(player);
  4667. });
  4668. player.embed.on('cuechange', function (_ref) {
  4669. var _ref$cues = _ref.cues,
  4670. cues = _ref$cues === void 0 ? [] : _ref$cues;
  4671. var strippedCues = cues.map(function (cue) {
  4672. return stripHTML(cue.text);
  4673. });
  4674. captions.updateCues.call(player, strippedCues);
  4675. });
  4676. player.embed.on('loaded', function () {
  4677. // Assure state and events are updated on autoplay
  4678. player.embed.getPaused().then(function (paused) {
  4679. assurePlaybackState.call(player, !paused);
  4680. if (!paused) {
  4681. triggerEvent.call(player, player.media, 'playing');
  4682. }
  4683. });
  4684. if (is$1.element(player.embed.element) && player.supported.ui) {
  4685. var frame = player.embed.element; // Fix keyboard focus issues
  4686. // https://github.com/sampotts/plyr/issues/317
  4687. frame.setAttribute('tabindex', -1);
  4688. }
  4689. });
  4690. player.embed.on('play', function () {
  4691. assurePlaybackState.call(player, true);
  4692. triggerEvent.call(player, player.media, 'playing');
  4693. });
  4694. player.embed.on('pause', function () {
  4695. assurePlaybackState.call(player, false);
  4696. });
  4697. player.embed.on('timeupdate', function (data) {
  4698. player.media.seeking = false;
  4699. currentTime = data.seconds;
  4700. triggerEvent.call(player, player.media, 'timeupdate');
  4701. });
  4702. player.embed.on('progress', function (data) {
  4703. player.media.buffered = data.percent;
  4704. triggerEvent.call(player, player.media, 'progress'); // Check all loaded
  4705. if (parseInt(data.percent, 10) === 1) {
  4706. triggerEvent.call(player, player.media, 'canplaythrough');
  4707. } // Get duration as if we do it before load, it gives an incorrect value
  4708. // https://github.com/sampotts/plyr/issues/891
  4709. player.embed.getDuration().then(function (value) {
  4710. if (value !== player.media.duration) {
  4711. player.media.duration = value;
  4712. triggerEvent.call(player, player.media, 'durationchange');
  4713. }
  4714. });
  4715. });
  4716. player.embed.on('seeked', function () {
  4717. player.media.seeking = false;
  4718. triggerEvent.call(player, player.media, 'seeked');
  4719. });
  4720. player.embed.on('ended', function () {
  4721. player.media.paused = true;
  4722. triggerEvent.call(player, player.media, 'ended');
  4723. });
  4724. player.embed.on('error', function (detail) {
  4725. player.media.error = detail;
  4726. triggerEvent.call(player, player.media, 'error');
  4727. }); // Rebuild UI
  4728. setTimeout(function () {
  4729. return ui.build.call(player);
  4730. }, 0);
  4731. }
  4732. };
  4733. // ==========================================================================
  4734. function parseId$1(url) {
  4735. if (is$1.empty(url)) {
  4736. return null;
  4737. }
  4738. var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
  4739. return url.match(regex) ? RegExp.$2 : url;
  4740. } // Set playback state and trigger change (only on actual change)
  4741. function assurePlaybackState$1(play) {
  4742. if (play && !this.embed.hasPlayed) {
  4743. this.embed.hasPlayed = true;
  4744. }
  4745. if (this.media.paused === play) {
  4746. this.media.paused = !play;
  4747. triggerEvent.call(this, this.media, play ? 'play' : 'pause');
  4748. }
  4749. }
  4750. function getHost(config) {
  4751. if (config.noCookie) {
  4752. return 'https://www.youtube-nocookie.com';
  4753. }
  4754. if (window.location.protocol === 'http:') {
  4755. return 'http://www.youtube.com';
  4756. } // Use YouTube's default
  4757. return undefined;
  4758. }
  4759. var youtube = {
  4760. setup: function setup() {
  4761. var _this = this;
  4762. // Add embed class for responsive
  4763. toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Setup API
  4764. if (is$1.object(window.YT) && is$1.function(window.YT.Player)) {
  4765. youtube.ready.call(this);
  4766. } else {
  4767. // Load the API
  4768. loadScript(this.config.urls.youtube.sdk).catch(function (error) {
  4769. _this.debug.warn('YouTube API failed to load', error);
  4770. }); // Setup callback for the API
  4771. // YouTube has it's own system of course...
  4772. window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; // Add to queue
  4773. window.onYouTubeReadyCallbacks.push(function () {
  4774. youtube.ready.call(_this);
  4775. }); // Set callback to process queue
  4776. window.onYouTubeIframeAPIReady = function () {
  4777. window.onYouTubeReadyCallbacks.forEach(function (callback) {
  4778. callback();
  4779. });
  4780. };
  4781. }
  4782. },
  4783. // Get the media title
  4784. getTitle: function getTitle(videoId) {
  4785. var _this2 = this;
  4786. var url = format(this.config.urls.youtube.api, videoId);
  4787. fetch(url).then(function (data) {
  4788. if (is$1.object(data)) {
  4789. var title = data.title,
  4790. height = data.height,
  4791. width = data.width; // Set title
  4792. _this2.config.title = title;
  4793. ui.setTitle.call(_this2); // Set aspect ratio
  4794. _this2.embed.ratio = [width, height];
  4795. }
  4796. setAspectRatio.call(_this2);
  4797. }).catch(function () {
  4798. // Set aspect ratio
  4799. setAspectRatio.call(_this2);
  4800. });
  4801. },
  4802. // API ready
  4803. ready: function ready() {
  4804. var player = this; // Ignore already setup (race condition)
  4805. var currentId = player.media.getAttribute('id');
  4806. if (!is$1.empty(currentId) && currentId.startsWith('youtube-')) {
  4807. return;
  4808. } // Get the source URL or ID
  4809. var source = player.media.getAttribute('src'); // Get from <div> if needed
  4810. if (is$1.empty(source)) {
  4811. source = player.media.getAttribute(this.config.attributes.embed.id);
  4812. } // Replace the <iframe> with a <div> due to YouTube API issues
  4813. var videoId = parseId$1(source);
  4814. var id = generateId(player.provider); // Get poster, if already set
  4815. var poster = player.poster; // Replace media element
  4816. var container = createElement('div', {
  4817. id: id,
  4818. poster: poster
  4819. });
  4820. player.media = replaceElement(container, player.media); // Id to poster wrapper
  4821. var posterSrc = function posterSrc(format) {
  4822. return "https://i.ytimg.com/vi/".concat(videoId, "/").concat(format, "default.jpg");
  4823. }; // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
  4824. loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
  4825. .catch(function () {
  4826. return loadImage(posterSrc('sd'), 121);
  4827. }) // 480p padded 4:3
  4828. .catch(function () {
  4829. return loadImage(posterSrc('hq'));
  4830. }) // 360p padded 4:3. Always exists
  4831. .then(function (image) {
  4832. return ui.setPoster.call(player, image.src);
  4833. }).then(function (posterSrc) {
  4834. // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
  4835. if (!posterSrc.includes('maxres')) {
  4836. player.elements.poster.style.backgroundSize = 'cover';
  4837. }
  4838. }).catch(function () {});
  4839. var config = player.config.youtube; // Setup instance
  4840. // https://developers.google.com/youtube/iframe_api_reference
  4841. player.embed = new window.YT.Player(id, {
  4842. videoId: videoId,
  4843. host: getHost(config),
  4844. playerVars: extend({}, {
  4845. autoplay: player.config.autoplay ? 1 : 0,
  4846. // Autoplay
  4847. hl: player.config.hl,
  4848. // iframe interface language
  4849. controls: player.supported.ui ? 0 : 1,
  4850. // Only show controls if not fully supported
  4851. disablekb: 1,
  4852. // Disable keyboard as we handle it
  4853. playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
  4854. // Allow iOS inline playback
  4855. // Captions are flaky on YouTube
  4856. cc_load_policy: player.captions.active ? 1 : 0,
  4857. cc_lang_pref: player.config.captions.language,
  4858. // Tracking for stats
  4859. widget_referrer: window ? window.location.href : null
  4860. }, config),
  4861. events: {
  4862. onError: function onError(event) {
  4863. // YouTube may fire onError twice, so only handle it once
  4864. if (!player.media.error) {
  4865. var code = event.data; // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
  4866. var message = {
  4867. 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
  4868. 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
  4869. 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
  4870. 101: 'The owner of the requested video does not allow it to be played in embedded players.',
  4871. 150: 'The owner of the requested video does not allow it to be played in embedded players.'
  4872. }[code] || 'An unknown error occured';
  4873. player.media.error = {
  4874. code: code,
  4875. message: message
  4876. };
  4877. triggerEvent.call(player, player.media, 'error');
  4878. }
  4879. },
  4880. onPlaybackRateChange: function onPlaybackRateChange(event) {
  4881. // Get the instance
  4882. var instance = event.target; // Get current speed
  4883. player.media.playbackRate = instance.getPlaybackRate();
  4884. triggerEvent.call(player, player.media, 'ratechange');
  4885. },
  4886. onReady: function onReady(event) {
  4887. // Bail if onReady has already been called. See issue #1108
  4888. if (is$1.function(player.media.play)) {
  4889. return;
  4890. } // Get the instance
  4891. var instance = event.target; // Get the title
  4892. youtube.getTitle.call(player, videoId); // Create a faux HTML5 API using the YouTube API
  4893. player.media.play = function () {
  4894. assurePlaybackState$1.call(player, true);
  4895. instance.playVideo();
  4896. };
  4897. player.media.pause = function () {
  4898. assurePlaybackState$1.call(player, false);
  4899. instance.pauseVideo();
  4900. };
  4901. player.media.stop = function () {
  4902. instance.stopVideo();
  4903. };
  4904. player.media.duration = instance.getDuration();
  4905. player.media.paused = true; // Seeking
  4906. player.media.currentTime = 0;
  4907. Object.defineProperty(player.media, 'currentTime', {
  4908. get: function get() {
  4909. return Number(instance.getCurrentTime());
  4910. },
  4911. set: function set(time) {
  4912. // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
  4913. if (player.paused && !player.embed.hasPlayed) {
  4914. player.embed.mute();
  4915. } // Set seeking state and trigger event
  4916. player.media.seeking = true;
  4917. triggerEvent.call(player, player.media, 'seeking'); // Seek after events sent
  4918. instance.seekTo(time);
  4919. }
  4920. }); // Playback speed
  4921. Object.defineProperty(player.media, 'playbackRate', {
  4922. get: function get() {
  4923. return instance.getPlaybackRate();
  4924. },
  4925. set: function set(input) {
  4926. instance.setPlaybackRate(input);
  4927. }
  4928. }); // Volume
  4929. var volume = player.config.volume;
  4930. Object.defineProperty(player.media, 'volume', {
  4931. get: function get() {
  4932. return volume;
  4933. },
  4934. set: function set(input) {
  4935. volume = input;
  4936. instance.setVolume(volume * 100);
  4937. triggerEvent.call(player, player.media, 'volumechange');
  4938. }
  4939. }); // Muted
  4940. var muted = player.config.muted;
  4941. Object.defineProperty(player.media, 'muted', {
  4942. get: function get() {
  4943. return muted;
  4944. },
  4945. set: function set(input) {
  4946. var toggle = is$1.boolean(input) ? input : muted;
  4947. muted = toggle;
  4948. instance[toggle ? 'mute' : 'unMute']();
  4949. triggerEvent.call(player, player.media, 'volumechange');
  4950. }
  4951. }); // Source
  4952. Object.defineProperty(player.media, 'currentSrc', {
  4953. get: function get() {
  4954. return instance.getVideoUrl();
  4955. }
  4956. }); // Ended
  4957. Object.defineProperty(player.media, 'ended', {
  4958. get: function get() {
  4959. return player.currentTime === player.duration;
  4960. }
  4961. }); // Get available speeds
  4962. player.options.speed = instance.getAvailablePlaybackRates(); // Set the tabindex to avoid focus entering iframe
  4963. if (player.supported.ui) {
  4964. player.media.setAttribute('tabindex', -1);
  4965. }
  4966. triggerEvent.call(player, player.media, 'timeupdate');
  4967. triggerEvent.call(player, player.media, 'durationchange'); // Reset timer
  4968. clearInterval(player.timers.buffering); // Setup buffering
  4969. player.timers.buffering = setInterval(function () {
  4970. // Get loaded % from YouTube
  4971. player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something
  4972. if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
  4973. triggerEvent.call(player, player.media, 'progress');
  4974. } // Set last buffer point
  4975. player.media.lastBuffered = player.media.buffered; // Bail if we're at 100%
  4976. if (player.media.buffered === 1) {
  4977. clearInterval(player.timers.buffering); // Trigger event
  4978. triggerEvent.call(player, player.media, 'canplaythrough');
  4979. }
  4980. }, 200); // Rebuild UI
  4981. setTimeout(function () {
  4982. return ui.build.call(player);
  4983. }, 50);
  4984. },
  4985. onStateChange: function onStateChange(event) {
  4986. // Get the instance
  4987. var instance = event.target; // Reset timer
  4988. clearInterval(player.timers.playing);
  4989. var seeked = player.media.seeking && [1, 2].includes(event.data);
  4990. if (seeked) {
  4991. // Unset seeking and fire seeked event
  4992. player.media.seeking = false;
  4993. triggerEvent.call(player, player.media, 'seeked');
  4994. } // Handle events
  4995. // -1 Unstarted
  4996. // 0 Ended
  4997. // 1 Playing
  4998. // 2 Paused
  4999. // 3 Buffering
  5000. // 5 Video cued
  5001. switch (event.data) {
  5002. case -1:
  5003. // Update scrubber
  5004. triggerEvent.call(player, player.media, 'timeupdate'); // Get loaded % from YouTube
  5005. player.media.buffered = instance.getVideoLoadedFraction();
  5006. triggerEvent.call(player, player.media, 'progress');
  5007. break;
  5008. case 0:
  5009. assurePlaybackState$1.call(player, false); // YouTube doesn't support loop for a single video, so mimick it.
  5010. if (player.media.loop) {
  5011. // YouTube needs a call to `stopVideo` before playing again
  5012. instance.stopVideo();
  5013. instance.playVideo();
  5014. } else {
  5015. triggerEvent.call(player, player.media, 'ended');
  5016. }
  5017. break;
  5018. case 1:
  5019. // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
  5020. if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
  5021. player.media.pause();
  5022. } else {
  5023. assurePlaybackState$1.call(player, true);
  5024. triggerEvent.call(player, player.media, 'playing'); // Poll to get playback progress
  5025. player.timers.playing = setInterval(function () {
  5026. triggerEvent.call(player, player.media, 'timeupdate');
  5027. }, 50); // Check duration again due to YouTube bug
  5028. // https://github.com/sampotts/plyr/issues/374
  5029. // https://code.google.com/p/gdata-issues/issues/detail?id=8690
  5030. if (player.media.duration !== instance.getDuration()) {
  5031. player.media.duration = instance.getDuration();
  5032. triggerEvent.call(player, player.media, 'durationchange');
  5033. }
  5034. }
  5035. break;
  5036. case 2:
  5037. // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
  5038. if (!player.muted) {
  5039. player.embed.unMute();
  5040. }
  5041. assurePlaybackState$1.call(player, false);
  5042. break;
  5043. default:
  5044. break;
  5045. }
  5046. triggerEvent.call(player, player.elements.container, 'statechange', false, {
  5047. code: event.data
  5048. });
  5049. }
  5050. }
  5051. });
  5052. }
  5053. };
  5054. // ==========================================================================
  5055. var media = {
  5056. // Setup media
  5057. setup: function setup() {
  5058. // If there's no media, bail
  5059. if (!this.media) {
  5060. this.debug.warn('No media element found!');
  5061. return;
  5062. } // Add type class
  5063. toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); // Add provider class
  5064. toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); // Add video class for embeds
  5065. // This will require changes if audio embeds are added
  5066. if (this.isEmbed) {
  5067. toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
  5068. } // Inject the player wrapper
  5069. if (this.isVideo) {
  5070. // Create the wrapper div
  5071. this.elements.wrapper = createElement('div', {
  5072. class: this.config.classNames.video
  5073. }); // Wrap the video in a container
  5074. wrap(this.media, this.elements.wrapper); // Faux poster container
  5075. this.elements.poster = createElement('div', {
  5076. class: this.config.classNames.poster
  5077. });
  5078. this.elements.wrapper.appendChild(this.elements.poster);
  5079. }
  5080. if (this.isHTML5) {
  5081. html5.extend.call(this);
  5082. } else if (this.isYouTube) {
  5083. youtube.setup.call(this);
  5084. } else if (this.isVimeo) {
  5085. vimeo.setup.call(this);
  5086. }
  5087. }
  5088. };
  5089. var destroy = function destroy(instance) {
  5090. // Destroy our adsManager
  5091. if (instance.manager) {
  5092. instance.manager.destroy();
  5093. } // Destroy our adsManager
  5094. if (instance.elements.displayContainer) {
  5095. instance.elements.displayContainer.destroy();
  5096. }
  5097. instance.elements.container.remove();
  5098. };
  5099. var Ads =
  5100. /*#__PURE__*/
  5101. function () {
  5102. /**
  5103. * Ads constructor.
  5104. * @param {Object} player
  5105. * @return {Ads}
  5106. */
  5107. function Ads(player) {
  5108. var _this = this;
  5109. _classCallCheck(this, Ads);
  5110. this.player = player;
  5111. this.config = player.config.ads;
  5112. this.playing = false;
  5113. this.initialized = false;
  5114. this.elements = {
  5115. container: null,
  5116. displayContainer: null
  5117. };
  5118. this.manager = null;
  5119. this.loader = null;
  5120. this.cuePoints = null;
  5121. this.events = {};
  5122. this.safetyTimer = null;
  5123. this.countdownTimer = null; // Setup a promise to resolve when the IMA manager is ready
  5124. this.managerPromise = new Promise(function (resolve, reject) {
  5125. // The ad is loaded and ready
  5126. _this.on('loaded', resolve); // Ads failed
  5127. _this.on('error', reject);
  5128. });
  5129. this.load();
  5130. }
  5131. _createClass(Ads, [{
  5132. key: "load",
  5133. /**
  5134. * Load the IMA SDK
  5135. */
  5136. value: function load() {
  5137. var _this2 = this;
  5138. if (!this.enabled) {
  5139. return;
  5140. } // Check if the Google IMA3 SDK is loaded or load it ourselves
  5141. if (!is$1.object(window.google) || !is$1.object(window.google.ima)) {
  5142. loadScript(this.player.config.urls.googleIMA.sdk).then(function () {
  5143. _this2.ready();
  5144. }).catch(function () {
  5145. // Script failed to load or is blocked
  5146. _this2.trigger('error', new Error('Google IMA SDK failed to load'));
  5147. });
  5148. } else {
  5149. this.ready();
  5150. }
  5151. }
  5152. /**
  5153. * Get the ads instance ready
  5154. */
  5155. }, {
  5156. key: "ready",
  5157. value: function ready() {
  5158. var _this3 = this;
  5159. // Double check we're enabled
  5160. if (!this.enabled) {
  5161. destroy(this);
  5162. } // Start ticking our safety timer. If the whole advertisement
  5163. // thing doesn't resolve within our set time; we bail
  5164. this.startSafetyTimer(12000, 'ready()'); // Clear the safety timer
  5165. this.managerPromise.then(function () {
  5166. _this3.clearSafetyTimer('onAdsManagerLoaded()');
  5167. }); // Set listeners on the Plyr instance
  5168. this.listeners(); // Setup the IMA SDK
  5169. this.setupIMA();
  5170. } // Build the tag URL
  5171. }, {
  5172. key: "setupIMA",
  5173. /**
  5174. * In order for the SDK to display ads for our video, we need to tell it where to put them,
  5175. * so here we define our ad container. This div is set up to render on top of the video player.
  5176. * Using the code below, we tell the SDK to render ads within that div. We also provide a
  5177. * handle to the content video player - the SDK will poll the current time of our player to
  5178. * properly place mid-rolls. After we create the ad display container, we initialize it. On
  5179. * mobile devices, this initialization is done as the result of a user action.
  5180. */
  5181. value: function setupIMA() {
  5182. // Create the container for our advertisements
  5183. this.elements.container = createElement('div', {
  5184. class: this.player.config.classNames.ads
  5185. });
  5186. this.player.elements.container.appendChild(this.elements.container); // So we can run VPAID2
  5187. google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); // Set language
  5188. google.ima.settings.setLocale(this.player.config.ads.language); // Set playback for iOS10+
  5189. google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads
  5190. this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Request video ads to be pre-loaded
  5191. this.requestAds();
  5192. }
  5193. /**
  5194. * Request advertisements
  5195. */
  5196. }, {
  5197. key: "requestAds",
  5198. value: function requestAds() {
  5199. var _this4 = this;
  5200. var container = this.player.elements.container;
  5201. try {
  5202. // Create ads loader
  5203. this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
  5204. this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
  5205. return _this4.onAdsManagerLoaded(event);
  5206. }, false);
  5207. this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
  5208. return _this4.onAdError(error);
  5209. }, false); // Request video ads
  5210. var request = new google.ima.AdsRequest();
  5211. request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
  5212. // to select the correct creative if multiple are returned
  5213. request.linearAdSlotWidth = container.offsetWidth;
  5214. request.linearAdSlotHeight = container.offsetHeight;
  5215. request.nonLinearAdSlotWidth = container.offsetWidth;
  5216. request.nonLinearAdSlotHeight = container.offsetHeight; // We only overlay ads as we only support video.
  5217. request.forceNonLinearFullSlot = false; // Mute based on current state
  5218. request.setAdWillPlayMuted(!this.player.muted);
  5219. this.loader.requestAds(request);
  5220. } catch (e) {
  5221. this.onAdError(e);
  5222. }
  5223. }
  5224. /**
  5225. * Update the ad countdown
  5226. * @param {Boolean} start
  5227. */
  5228. }, {
  5229. key: "pollCountdown",
  5230. value: function pollCountdown() {
  5231. var _this5 = this;
  5232. var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  5233. if (!start) {
  5234. clearInterval(this.countdownTimer);
  5235. this.elements.container.removeAttribute('data-badge-text');
  5236. return;
  5237. }
  5238. var update = function update() {
  5239. var time = formatTime(Math.max(_this5.manager.getRemainingTime(), 0));
  5240. var label = "".concat(i18n.get('advertisement', _this5.player.config), " - ").concat(time);
  5241. _this5.elements.container.setAttribute('data-badge-text', label);
  5242. };
  5243. this.countdownTimer = setInterval(update, 100);
  5244. }
  5245. /**
  5246. * This method is called whenever the ads are ready inside the AdDisplayContainer
  5247. * @param {Event} adsManagerLoadedEvent
  5248. */
  5249. }, {
  5250. key: "onAdsManagerLoaded",
  5251. value: function onAdsManagerLoaded(event) {
  5252. var _this6 = this;
  5253. // Load could occur after a source change (race condition)
  5254. if (!this.enabled) {
  5255. return;
  5256. } // Get the ads manager
  5257. var settings = new google.ima.AdsRenderingSettings(); // Tell the SDK to save and restore content video state on our behalf
  5258. settings.restoreCustomPlaybackStateOnAdBreakComplete = true;
  5259. settings.enablePreloading = true; // The SDK is polling currentTime on the contentPlayback. And needs a duration
  5260. // so it can determine when to start the mid- and post-roll
  5261. this.manager = event.getAdsManager(this.player, settings); // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
  5262. this.cuePoints = this.manager.getCuePoints(); // Add listeners to the required events
  5263. // Advertisement error events
  5264. this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
  5265. return _this6.onAdError(error);
  5266. }); // Advertisement regular events
  5267. Object.keys(google.ima.AdEvent.Type).forEach(function (type) {
  5268. _this6.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) {
  5269. return _this6.onAdEvent(event);
  5270. });
  5271. }); // Resolve our adsManager
  5272. this.trigger('loaded');
  5273. }
  5274. }, {
  5275. key: "addCuePoints",
  5276. value: function addCuePoints() {
  5277. var _this7 = this;
  5278. // Add advertisement cue's within the time line if available
  5279. if (!is$1.empty(this.cuePoints)) {
  5280. this.cuePoints.forEach(function (cuePoint) {
  5281. if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < _this7.player.duration) {
  5282. var seekElement = _this7.player.elements.progress;
  5283. if (is$1.element(seekElement)) {
  5284. var cuePercentage = 100 / _this7.player.duration * cuePoint;
  5285. var cue = createElement('span', {
  5286. class: _this7.player.config.classNames.cues
  5287. });
  5288. cue.style.left = "".concat(cuePercentage.toString(), "%");
  5289. seekElement.appendChild(cue);
  5290. }
  5291. }
  5292. });
  5293. }
  5294. }
  5295. /**
  5296. * This is where all the event handling takes place. Retrieve the ad from the event. Some
  5297. * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated
  5298. * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type
  5299. * @param {Event} event
  5300. */
  5301. }, {
  5302. key: "onAdEvent",
  5303. value: function onAdEvent(event) {
  5304. var _this8 = this;
  5305. var container = this.player.elements.container; // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
  5306. // don't have ad object associated
  5307. var ad = event.getAd();
  5308. var adData = event.getAdData(); // Proxy event
  5309. var dispatchEvent = function dispatchEvent(type) {
  5310. var event = "ads".concat(type.replace(/_/g, '').toLowerCase());
  5311. triggerEvent.call(_this8.player, _this8.player.media, event);
  5312. }; // Bubble the event
  5313. dispatchEvent(event.type);
  5314. switch (event.type) {
  5315. case google.ima.AdEvent.Type.LOADED:
  5316. // This is the first event sent for an ad - it is possible to determine whether the
  5317. // ad is a video ad or an overlay
  5318. this.trigger('loaded'); // Start countdown
  5319. this.pollCountdown(true);
  5320. if (!ad.isLinear()) {
  5321. // Position AdDisplayContainer correctly for overlay
  5322. ad.width = container.offsetWidth;
  5323. ad.height = container.offsetHeight;
  5324. } // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
  5325. // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
  5326. break;
  5327. case google.ima.AdEvent.Type.STARTED:
  5328. // Set volume to match player
  5329. this.manager.setVolume(this.player.volume);
  5330. break;
  5331. case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
  5332. // All ads for the current videos are done. We can now request new advertisements
  5333. // in case the video is re-played
  5334. // TODO: Example for what happens when a next video in a playlist would be loaded.
  5335. // So here we load a new video when all ads are done.
  5336. // Then we load new ads within a new adsManager. When the video
  5337. // Is started - after - the ads are loaded, then we get ads.
  5338. // You can also easily test cancelling and reloading by running
  5339. // player.ads.cancel() and player.ads.play from the console I guess.
  5340. // this.player.source = {
  5341. // type: 'video',
  5342. // title: 'View From A Blue Moon',
  5343. // sources: [{
  5344. // src:
  5345. // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:
  5346. // 'video/mp4', }], poster:
  5347. // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:
  5348. // [ { kind: 'captions', label: 'English', srclang: 'en', src:
  5349. // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
  5350. // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:
  5351. // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],
  5352. // };
  5353. // TODO: So there is still this thing where a video should only be allowed to start
  5354. // playing when the IMA SDK is ready or has failed
  5355. this.loadAds();
  5356. break;
  5357. case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
  5358. // This event indicates the ad has started - the video player can adjust the UI,
  5359. // for example display a pause button and remaining time. Fired when content should
  5360. // be paused. This usually happens right before an ad is about to cover the content
  5361. this.pauseContent();
  5362. break;
  5363. case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
  5364. // This event indicates the ad has finished - the video player can perform
  5365. // appropriate UI actions, such as removing the timer for remaining time detection.
  5366. // Fired when content should be resumed. This usually happens when an ad finishes
  5367. // or collapses
  5368. this.pollCountdown();
  5369. this.resumeContent();
  5370. break;
  5371. case google.ima.AdEvent.Type.LOG:
  5372. if (adData.adError) {
  5373. this.player.debug.warn("Non-fatal ad error: ".concat(adData.adError.getMessage()));
  5374. }
  5375. break;
  5376. default:
  5377. break;
  5378. }
  5379. }
  5380. /**
  5381. * Any ad error handling comes through here
  5382. * @param {Event} event
  5383. */
  5384. }, {
  5385. key: "onAdError",
  5386. value: function onAdError(event) {
  5387. this.cancel();
  5388. this.player.debug.warn('Ads error', event);
  5389. }
  5390. /**
  5391. * Setup hooks for Plyr and window events. This ensures
  5392. * the mid- and post-roll launch at the correct time. And
  5393. * resize the advertisement when the player resizes
  5394. */
  5395. }, {
  5396. key: "listeners",
  5397. value: function listeners() {
  5398. var _this9 = this;
  5399. var container = this.player.elements.container;
  5400. var time;
  5401. this.player.on('canplay', function () {
  5402. _this9.addCuePoints();
  5403. });
  5404. this.player.on('ended', function () {
  5405. _this9.loader.contentComplete();
  5406. });
  5407. this.player.on('timeupdate', function () {
  5408. time = _this9.player.currentTime;
  5409. });
  5410. this.player.on('seeked', function () {
  5411. var seekedTime = _this9.player.currentTime;
  5412. if (is$1.empty(_this9.cuePoints)) {
  5413. return;
  5414. }
  5415. _this9.cuePoints.forEach(function (cuePoint, index) {
  5416. if (time < cuePoint && cuePoint < seekedTime) {
  5417. _this9.manager.discardAdBreak();
  5418. _this9.cuePoints.splice(index, 1);
  5419. }
  5420. });
  5421. }); // Listen to the resizing of the window. And resize ad accordingly
  5422. // TODO: eventually implement ResizeObserver
  5423. window.addEventListener('resize', function () {
  5424. if (_this9.manager) {
  5425. _this9.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
  5426. }
  5427. });
  5428. }
  5429. /**
  5430. * Initialize the adsManager and start playing advertisements
  5431. */
  5432. }, {
  5433. key: "play",
  5434. value: function play() {
  5435. var _this10 = this;
  5436. var container = this.player.elements.container;
  5437. if (!this.managerPromise) {
  5438. this.resumeContent();
  5439. } // Play the requested advertisement whenever the adsManager is ready
  5440. this.managerPromise.then(function () {
  5441. // Set volume to match player
  5442. _this10.manager.setVolume(_this10.player.volume); // Initialize the container. Must be done via a user action on mobile devices
  5443. _this10.elements.displayContainer.initialize();
  5444. try {
  5445. if (!_this10.initialized) {
  5446. // Initialize the ads manager. Ad rules playlist will start at this time
  5447. _this10.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will
  5448. // start at this time; the call will be ignored for ad rules
  5449. _this10.manager.start();
  5450. }
  5451. _this10.initialized = true;
  5452. } catch (adError) {
  5453. // An error may be thrown if there was a problem with the
  5454. // VAST response
  5455. _this10.onAdError(adError);
  5456. }
  5457. }).catch(function () {});
  5458. }
  5459. /**
  5460. * Resume our video
  5461. */
  5462. }, {
  5463. key: "resumeContent",
  5464. value: function resumeContent() {
  5465. // Hide the advertisement container
  5466. this.elements.container.style.zIndex = ''; // Ad is stopped
  5467. this.playing = false; // Play video
  5468. this.player.media.play();
  5469. }
  5470. /**
  5471. * Pause our video
  5472. */
  5473. }, {
  5474. key: "pauseContent",
  5475. value: function pauseContent() {
  5476. // Show the advertisement container
  5477. this.elements.container.style.zIndex = 3; // Ad is playing
  5478. this.playing = true; // Pause our video.
  5479. this.player.media.pause();
  5480. }
  5481. /**
  5482. * Destroy the adsManager so we can grab new ads after this. If we don't then we're not
  5483. * allowed to call new ads based on google policies, as they interpret this as an accidental
  5484. * video requests. https://developers.google.com/interactive-
  5485. * media-ads/docs/sdks/android/faq#8
  5486. */
  5487. }, {
  5488. key: "cancel",
  5489. value: function cancel() {
  5490. // Pause our video
  5491. if (this.initialized) {
  5492. this.resumeContent();
  5493. } // Tell our instance that we're done for now
  5494. this.trigger('error'); // Re-create our adsManager
  5495. this.loadAds();
  5496. }
  5497. /**
  5498. * Re-create our adsManager
  5499. */
  5500. }, {
  5501. key: "loadAds",
  5502. value: function loadAds() {
  5503. var _this11 = this;
  5504. // Tell our adsManager to go bye bye
  5505. this.managerPromise.then(function () {
  5506. // Destroy our adsManager
  5507. if (_this11.manager) {
  5508. _this11.manager.destroy();
  5509. } // Re-set our adsManager promises
  5510. _this11.managerPromise = new Promise(function (resolve) {
  5511. _this11.on('loaded', resolve);
  5512. _this11.player.debug.log(_this11.manager);
  5513. }); // Now request some new advertisements
  5514. _this11.requestAds();
  5515. }).catch(function () {});
  5516. }
  5517. /**
  5518. * Handles callbacks after an ad event was invoked
  5519. * @param {String} event - Event type
  5520. */
  5521. }, {
  5522. key: "trigger",
  5523. value: function trigger(event) {
  5524. var _this12 = this;
  5525. for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  5526. args[_key - 1] = arguments[_key];
  5527. }
  5528. var handlers = this.events[event];
  5529. if (is$1.array(handlers)) {
  5530. handlers.forEach(function (handler) {
  5531. if (is$1.function(handler)) {
  5532. handler.apply(_this12, args);
  5533. }
  5534. });
  5535. }
  5536. }
  5537. /**
  5538. * Add event listeners
  5539. * @param {String} event - Event type
  5540. * @param {Function} callback - Callback for when event occurs
  5541. * @return {Ads}
  5542. */
  5543. }, {
  5544. key: "on",
  5545. value: function on(event, callback) {
  5546. if (!is$1.array(this.events[event])) {
  5547. this.events[event] = [];
  5548. }
  5549. this.events[event].push(callback);
  5550. return this;
  5551. }
  5552. /**
  5553. * Setup a safety timer for when the ad network doesn't respond for whatever reason.
  5554. * The advertisement has 12 seconds to get its things together. We stop this timer when the
  5555. * advertisement is playing, or when a user action is required to start, then we clear the
  5556. * timer on ad ready
  5557. * @param {Number} time
  5558. * @param {String} from
  5559. */
  5560. }, {
  5561. key: "startSafetyTimer",
  5562. value: function startSafetyTimer(time, from) {
  5563. var _this13 = this;
  5564. this.player.debug.log("Safety timer invoked from: ".concat(from));
  5565. this.safetyTimer = setTimeout(function () {
  5566. _this13.cancel();
  5567. _this13.clearSafetyTimer('startSafetyTimer()');
  5568. }, time);
  5569. }
  5570. /**
  5571. * Clear our safety timer(s)
  5572. * @param {String} from
  5573. */
  5574. }, {
  5575. key: "clearSafetyTimer",
  5576. value: function clearSafetyTimer(from) {
  5577. if (!is$1.nullOrUndefined(this.safetyTimer)) {
  5578. this.player.debug.log("Safety timer cleared from: ".concat(from));
  5579. clearTimeout(this.safetyTimer);
  5580. this.safetyTimer = null;
  5581. }
  5582. }
  5583. }, {
  5584. key: "enabled",
  5585. get: function get() {
  5586. var config = this.config;
  5587. return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is$1.empty(config.publisherId) || is$1.url(config.tagUrl));
  5588. }
  5589. }, {
  5590. key: "tagUrl",
  5591. get: function get() {
  5592. var config = this.config;
  5593. if (is$1.url(config.tagUrl)) {
  5594. return config.tagUrl;
  5595. }
  5596. var params = {
  5597. AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
  5598. AV_CHANNELID: '5a0458dc28a06145e4519d21',
  5599. AV_URL: window.location.hostname,
  5600. cb: Date.now(),
  5601. AV_WIDTH: 640,
  5602. AV_HEIGHT: 480,
  5603. AV_CDIM2: this.publisherId
  5604. };
  5605. var base = 'https://go.aniview.com/api/adserver6/vast/';
  5606. return "".concat(base, "?").concat(buildUrlParams(params));
  5607. }
  5608. }]);
  5609. return Ads;
  5610. }();
  5611. var parseVtt = function parseVtt(vttDataString) {
  5612. var processedList = [];
  5613. var frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
  5614. frames.forEach(function (frame) {
  5615. var result = {};
  5616. var lines = frame.split(/\r\n|\n|\r/);
  5617. lines.forEach(function (line) {
  5618. if (!is$1.number(result.startTime)) {
  5619. // The line with start and end times on it is the first line of interest
  5620. var matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
  5621. if (matchTimes) {
  5622. result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number("0.".concat(matchTimes[4]));
  5623. result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number("0.".concat(matchTimes[9]));
  5624. }
  5625. } else if (!is$1.empty(line.trim()) && is$1.empty(result.text)) {
  5626. // If we already have the startTime, then we're definitely up to the text line(s)
  5627. var lineSplit = line.trim().split('#xywh=');
  5628. var _lineSplit = _slicedToArray(lineSplit, 1);
  5629. result.text = _lineSplit[0];
  5630. // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
  5631. if (lineSplit[1]) {
  5632. var _lineSplit$1$split = lineSplit[1].split(',');
  5633. var _lineSplit$1$split2 = _slicedToArray(_lineSplit$1$split, 4);
  5634. result.x = _lineSplit$1$split2[0];
  5635. result.y = _lineSplit$1$split2[1];
  5636. result.w = _lineSplit$1$split2[2];
  5637. result.h = _lineSplit$1$split2[3];
  5638. }
  5639. }
  5640. });
  5641. if (result.text) {
  5642. processedList.push(result);
  5643. }
  5644. });
  5645. return processedList;
  5646. };
  5647. /**
  5648. * Preview thumbnails for seek hover and scrubbing
  5649. * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
  5650. * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
  5651. *
  5652. * Notes:
  5653. * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
  5654. * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
  5655. * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
  5656. */
  5657. var PreviewThumbnails =
  5658. /*#__PURE__*/
  5659. function () {
  5660. /**
  5661. * PreviewThumbnails constructor.
  5662. * @param {Plyr} player
  5663. * @return {PreviewThumbnails}
  5664. */
  5665. function PreviewThumbnails(player) {
  5666. _classCallCheck(this, PreviewThumbnails);
  5667. this.player = player;
  5668. this.thumbnails = [];
  5669. this.loaded = false;
  5670. this.lastMouseMoveTime = Date.now();
  5671. this.mouseDown = false;
  5672. this.loadedImages = [];
  5673. this.elements = {
  5674. thumb: {},
  5675. scrubbing: {}
  5676. };
  5677. this.load();
  5678. }
  5679. _createClass(PreviewThumbnails, [{
  5680. key: "load",
  5681. value: function load() {
  5682. var _this = this;
  5683. // Togglethe regular seek tooltip
  5684. if (this.player.elements.display.seekTooltip) {
  5685. this.player.elements.display.seekTooltip.hidden = this.enabled;
  5686. }
  5687. if (!this.enabled) {
  5688. return;
  5689. }
  5690. this.getThumbnails().then(function () {
  5691. // Render DOM elements
  5692. _this.render(); // Check to see if thumb container size was specified manually in CSS
  5693. _this.determineContainerAutoSizing();
  5694. _this.loaded = true;
  5695. });
  5696. } // Download VTT files and parse them
  5697. }, {
  5698. key: "getThumbnails",
  5699. value: function getThumbnails() {
  5700. var _this2 = this;
  5701. return new Promise(function (resolve) {
  5702. var src = _this2.player.config.previewThumbnails.src;
  5703. if (is$1.empty(src)) {
  5704. throw new Error('Missing previewThumbnails.src config attribute');
  5705. } // If string, convert into single-element list
  5706. var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
  5707. var promises = urls.map(function (u) {
  5708. return _this2.getThumbnail(u);
  5709. });
  5710. Promise.all(promises).then(function () {
  5711. // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
  5712. _this2.thumbnails.sort(function (x, y) {
  5713. return x.height - y.height;
  5714. });
  5715. _this2.player.debug.log('Preview thumbnails', _this2.thumbnails);
  5716. resolve();
  5717. });
  5718. });
  5719. } // Process individual VTT file
  5720. }, {
  5721. key: "getThumbnail",
  5722. value: function getThumbnail(url) {
  5723. var _this3 = this;
  5724. return new Promise(function (resolve) {
  5725. fetch(url).then(function (response) {
  5726. var thumbnail = {
  5727. frames: parseVtt(response),
  5728. height: null,
  5729. urlPrefix: ''
  5730. }; // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
  5731. // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
  5732. // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
  5733. if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {
  5734. thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
  5735. } // Download the first frame, so that we can determine/set the height of this thumbnailsDef
  5736. var tempImage = new Image();
  5737. tempImage.onload = function () {
  5738. thumbnail.height = tempImage.naturalHeight;
  5739. thumbnail.width = tempImage.naturalWidth;
  5740. _this3.thumbnails.push(thumbnail);
  5741. resolve();
  5742. };
  5743. tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
  5744. });
  5745. });
  5746. }
  5747. }, {
  5748. key: "startMove",
  5749. value: function startMove(event) {
  5750. if (!this.loaded) {
  5751. return;
  5752. }
  5753. if (!is$1.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
  5754. return;
  5755. } // Wait until media has a duration
  5756. if (!this.player.media.duration) {
  5757. return;
  5758. }
  5759. if (event.type === 'touchmove') {
  5760. // Calculate seek hover position as approx video seconds
  5761. this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
  5762. } else {
  5763. // Calculate seek hover position as approx video seconds
  5764. var clientRect = this.player.elements.progress.getBoundingClientRect();
  5765. var percentage = 100 / clientRect.width * (event.pageX - clientRect.left);
  5766. this.seekTime = this.player.media.duration * (percentage / 100);
  5767. if (this.seekTime < 0) {
  5768. // The mousemove fires for 10+px out to the left
  5769. this.seekTime = 0;
  5770. }
  5771. if (this.seekTime > this.player.media.duration - 1) {
  5772. // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
  5773. this.seekTime = this.player.media.duration - 1;
  5774. }
  5775. this.mousePosX = event.pageX; // Set time text inside image container
  5776. this.elements.thumb.time.innerText = formatTime(this.seekTime);
  5777. } // Download and show image
  5778. this.showImageAtCurrentTime();
  5779. }
  5780. }, {
  5781. key: "endMove",
  5782. value: function endMove() {
  5783. this.toggleThumbContainer(false, true);
  5784. }
  5785. }, {
  5786. key: "startScrubbing",
  5787. value: function startScrubbing(event) {
  5788. // Only act on left mouse button (0), or touch device (event.button is false)
  5789. if (event.button === false || event.button === 0) {
  5790. this.mouseDown = true; // Wait until media has a duration
  5791. if (this.player.media.duration) {
  5792. this.toggleScrubbingContainer(true);
  5793. this.toggleThumbContainer(false, true); // Download and show image
  5794. this.showImageAtCurrentTime();
  5795. }
  5796. }
  5797. }
  5798. }, {
  5799. key: "endScrubbing",
  5800. value: function endScrubbing() {
  5801. var _this4 = this;
  5802. this.mouseDown = false; // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
  5803. if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
  5804. // The video was already seeked/loaded at the chosen time - hide immediately
  5805. this.toggleScrubbingContainer(false);
  5806. } else {
  5807. // The video hasn't seeked yet. Wait for that
  5808. once.call(this.player, this.player.media, 'timeupdate', function () {
  5809. // Re-check mousedown - we might have already started scrubbing again
  5810. if (!_this4.mouseDown) {
  5811. _this4.toggleScrubbingContainer(false);
  5812. }
  5813. });
  5814. }
  5815. }
  5816. /**
  5817. * Setup hooks for Plyr and window events
  5818. */
  5819. }, {
  5820. key: "listeners",
  5821. value: function listeners() {
  5822. var _this5 = this;
  5823. // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
  5824. this.player.on('play', function () {
  5825. _this5.toggleThumbContainer(false, true);
  5826. });
  5827. this.player.on('seeked', function () {
  5828. _this5.toggleThumbContainer(false);
  5829. });
  5830. this.player.on('timeupdate', function () {
  5831. _this5.lastTime = _this5.player.media.currentTime;
  5832. });
  5833. }
  5834. /**
  5835. * Create HTML elements for image containers
  5836. */
  5837. }, {
  5838. key: "render",
  5839. value: function render() {
  5840. // Create HTML element: plyr__preview-thumbnail-container
  5841. this.elements.thumb.container = createElement('div', {
  5842. class: this.player.config.classNames.previewThumbnails.thumbContainer
  5843. }); // Wrapper for the image for styling
  5844. this.elements.thumb.imageContainer = createElement('div', {
  5845. class: this.player.config.classNames.previewThumbnails.imageContainer
  5846. });
  5847. this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer); // Create HTML element, parent+span: time text (e.g., 01:32:00)
  5848. var timeContainer = createElement('div', {
  5849. class: this.player.config.classNames.previewThumbnails.timeContainer
  5850. });
  5851. this.elements.thumb.time = createElement('span', {}, '00:00');
  5852. timeContainer.appendChild(this.elements.thumb.time);
  5853. this.elements.thumb.container.appendChild(timeContainer); // Inject the whole thumb
  5854. if (is$1.element(this.player.elements.progress)) {
  5855. this.player.elements.progress.appendChild(this.elements.thumb.container);
  5856. } // Create HTML element: plyr__preview-scrubbing-container
  5857. this.elements.scrubbing.container = createElement('div', {
  5858. class: this.player.config.classNames.previewThumbnails.scrubbingContainer
  5859. });
  5860. this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
  5861. }
  5862. }, {
  5863. key: "showImageAtCurrentTime",
  5864. value: function showImageAtCurrentTime() {
  5865. var _this6 = this;
  5866. if (this.mouseDown) {
  5867. this.setScrubbingContainerSize();
  5868. } else {
  5869. this.setThumbContainerSizeAndPos();
  5870. } // Find the desired thumbnail index
  5871. // TODO: Handle a video longer than the thumbs where thumbNum is null
  5872. var thumbNum = this.thumbnails[0].frames.findIndex(function (frame) {
  5873. return _this6.seekTime >= frame.startTime && _this6.seekTime <= frame.endTime;
  5874. });
  5875. var hasThumb = thumbNum >= 0;
  5876. var qualityIndex = 0; // Show the thumb container if we're not scrubbing
  5877. if (!this.mouseDown) {
  5878. this.toggleThumbContainer(hasThumb);
  5879. } // No matching thumb found
  5880. if (!hasThumb) {
  5881. return;
  5882. } // Check to see if we've already downloaded higher quality versions of this image
  5883. this.thumbnails.forEach(function (thumbnail, index) {
  5884. if (_this6.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
  5885. qualityIndex = index;
  5886. }
  5887. }); // Only proceed if either thumbnum or thumbfilename has changed
  5888. if (thumbNum !== this.showingThumb) {
  5889. this.showingThumb = thumbNum;
  5890. this.loadImage(qualityIndex);
  5891. }
  5892. } // Show the image that's currently specified in this.showingThumb
  5893. }, {
  5894. key: "loadImage",
  5895. value: function loadImage() {
  5896. var _this7 = this;
  5897. var qualityIndex = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
  5898. var thumbNum = this.showingThumb;
  5899. var thumbnail = this.thumbnails[qualityIndex];
  5900. var urlPrefix = thumbnail.urlPrefix;
  5901. var frame = thumbnail.frames[thumbNum];
  5902. var thumbFilename = thumbnail.frames[thumbNum].text;
  5903. var thumbUrl = urlPrefix + thumbFilename;
  5904. if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
  5905. // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
  5906. // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
  5907. if (this.loadingImage && this.usingSprites) {
  5908. this.loadingImage.onload = null;
  5909. } // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
  5910. // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
  5911. // images causes a flicker. Putting a new image over the top does not
  5912. var previewImage = new Image();
  5913. previewImage.src = thumbUrl;
  5914. previewImage.dataset.index = thumbNum;
  5915. previewImage.dataset.filename = thumbFilename;
  5916. this.showingThumbFilename = thumbFilename;
  5917. this.player.debug.log("Loading image: ".concat(thumbUrl)); // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
  5918. previewImage.onload = function () {
  5919. return _this7.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
  5920. };
  5921. this.loadingImage = previewImage;
  5922. this.removeOldImages(previewImage);
  5923. } else {
  5924. // Update the existing image
  5925. this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
  5926. this.currentImageElement.dataset.index = thumbNum;
  5927. this.removeOldImages(this.currentImageElement);
  5928. }
  5929. }
  5930. }, {
  5931. key: "showImage",
  5932. value: function showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename) {
  5933. var newImage = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;
  5934. this.player.debug.log("Showing thumb: ".concat(thumbFilename, ". num: ").concat(thumbNum, ". qual: ").concat(qualityIndex, ". newimg: ").concat(newImage));
  5935. this.setImageSizeAndOffset(previewImage, frame);
  5936. if (newImage) {
  5937. this.currentImageContainer.appendChild(previewImage);
  5938. this.currentImageElement = previewImage;
  5939. if (!this.loadedImages.includes(thumbFilename)) {
  5940. this.loadedImages.push(thumbFilename);
  5941. }
  5942. } // Preload images before and after the current one
  5943. // Show higher quality of the same frame
  5944. // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
  5945. this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
  5946. } // Remove all preview images that aren't the designated current image
  5947. }, {
  5948. key: "removeOldImages",
  5949. value: function removeOldImages(currentImage) {
  5950. var _this8 = this;
  5951. // Get a list of all images, convert it from a DOM list to an array
  5952. Array.from(this.currentImageContainer.children).forEach(function (image) {
  5953. if (image.tagName.toLowerCase() !== 'img') {
  5954. return;
  5955. }
  5956. var removeDelay = _this8.usingSprites ? 500 : 1000;
  5957. if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
  5958. // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
  5959. // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
  5960. image.dataset.deleting = true; // This has to be set before the timeout - to prevent issues switching between hover and scrub
  5961. var currentImageContainer = _this8.currentImageContainer;
  5962. setTimeout(function () {
  5963. currentImageContainer.removeChild(image);
  5964. _this8.player.debug.log("Removing thumb: ".concat(image.dataset.filename));
  5965. }, removeDelay);
  5966. }
  5967. });
  5968. } // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
  5969. // This will only preload the lowest quality
  5970. }, {
  5971. key: "preloadNearby",
  5972. value: function preloadNearby(thumbNum) {
  5973. var _this9 = this;
  5974. var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  5975. return new Promise(function (resolve) {
  5976. setTimeout(function () {
  5977. var oldThumbFilename = _this9.thumbnails[0].frames[thumbNum].text;
  5978. if (_this9.showingThumbFilename === oldThumbFilename) {
  5979. // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
  5980. var thumbnailsClone;
  5981. if (forward) {
  5982. thumbnailsClone = _this9.thumbnails[0].frames.slice(thumbNum);
  5983. } else {
  5984. thumbnailsClone = _this9.thumbnails[0].frames.slice(0, thumbNum).reverse();
  5985. }
  5986. var foundOne = false;
  5987. thumbnailsClone.forEach(function (frame) {
  5988. var newThumbFilename = frame.text;
  5989. if (newThumbFilename !== oldThumbFilename) {
  5990. // Found one with a different filename. Make sure it hasn't already been loaded on this page visit
  5991. if (!_this9.loadedImages.includes(newThumbFilename)) {
  5992. foundOne = true;
  5993. _this9.player.debug.log("Preloading thumb filename: ".concat(newThumbFilename));
  5994. var urlPrefix = _this9.thumbnails[0].urlPrefix;
  5995. var thumbURL = urlPrefix + newThumbFilename;
  5996. var previewImage = new Image();
  5997. previewImage.src = thumbURL;
  5998. previewImage.onload = function () {
  5999. _this9.player.debug.log("Preloaded thumb filename: ".concat(newThumbFilename));
  6000. if (!_this9.loadedImages.includes(newThumbFilename)) _this9.loadedImages.push(newThumbFilename); // We don't resolve until the thumb is loaded
  6001. resolve();
  6002. };
  6003. }
  6004. }
  6005. }); // If there are none to preload then we want to resolve immediately
  6006. if (!foundOne) {
  6007. resolve();
  6008. }
  6009. }
  6010. }, 300);
  6011. });
  6012. } // If user has been hovering current image for half a second, look for a higher quality one
  6013. }, {
  6014. key: "getHigherQuality",
  6015. value: function getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) {
  6016. var _this10 = this;
  6017. if (currentQualityIndex < this.thumbnails.length - 1) {
  6018. // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
  6019. var previewImageHeight = previewImage.naturalHeight;
  6020. if (this.usingSprites) {
  6021. previewImageHeight = frame.h;
  6022. }
  6023. if (previewImageHeight < this.thumbContainerHeight) {
  6024. // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
  6025. setTimeout(function () {
  6026. // Make sure the mouse hasn't already moved on and started hovering at another image
  6027. if (_this10.showingThumbFilename === thumbFilename) {
  6028. _this10.player.debug.log("Showing higher quality thumb for: ".concat(thumbFilename));
  6029. _this10.loadImage(currentQualityIndex + 1);
  6030. }
  6031. }, 300);
  6032. }
  6033. }
  6034. }
  6035. }, {
  6036. key: "toggleThumbContainer",
  6037. value: function toggleThumbContainer() {
  6038. var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  6039. var clearShowing = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  6040. var className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
  6041. this.elements.thumb.container.classList.toggle(className, toggle);
  6042. if (!toggle && clearShowing) {
  6043. this.showingThumb = null;
  6044. this.showingThumbFilename = null;
  6045. }
  6046. }
  6047. }, {
  6048. key: "toggleScrubbingContainer",
  6049. value: function toggleScrubbingContainer() {
  6050. var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  6051. var className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
  6052. this.elements.scrubbing.container.classList.toggle(className, toggle);
  6053. if (!toggle) {
  6054. this.showingThumb = null;
  6055. this.showingThumbFilename = null;
  6056. }
  6057. }
  6058. }, {
  6059. key: "determineContainerAutoSizing",
  6060. value: function determineContainerAutoSizing() {
  6061. if (this.elements.thumb.imageContainer.clientHeight > 20) {
  6062. // This will prevent auto sizing in this.setThumbContainerSizeAndPos()
  6063. this.sizeSpecifiedInCSS = true;
  6064. }
  6065. } // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
  6066. }, {
  6067. key: "setThumbContainerSizeAndPos",
  6068. value: function setThumbContainerSizeAndPos() {
  6069. if (!this.sizeSpecifiedInCSS) {
  6070. var thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
  6071. this.elements.thumb.imageContainer.style.height = "".concat(this.thumbContainerHeight, "px");
  6072. this.elements.thumb.imageContainer.style.width = "".concat(thumbWidth, "px");
  6073. }
  6074. this.setThumbContainerPos();
  6075. }
  6076. }, {
  6077. key: "setThumbContainerPos",
  6078. value: function setThumbContainerPos() {
  6079. var seekbarRect = this.player.elements.progress.getBoundingClientRect();
  6080. var plyrRect = this.player.elements.container.getBoundingClientRect();
  6081. var container = this.elements.thumb.container; // Find the lowest and highest desired left-position, so we don't slide out the side of the video container
  6082. var minVal = plyrRect.left - seekbarRect.left + 10;
  6083. var maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10; // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
  6084. var previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
  6085. if (previewPos < minVal) {
  6086. previewPos = minVal;
  6087. }
  6088. if (previewPos > maxVal) {
  6089. previewPos = maxVal;
  6090. }
  6091. container.style.left = "".concat(previewPos, "px");
  6092. } // Can't use 100% width, in case the video is a different aspect ratio to the video container
  6093. }, {
  6094. key: "setScrubbingContainerSize",
  6095. value: function setScrubbingContainerSize() {
  6096. this.elements.scrubbing.container.style.width = "".concat(this.player.media.clientWidth, "px"); // Can't use media.clientHeight - html5 video goes big and does black bars above and below
  6097. this.elements.scrubbing.container.style.height = "".concat(this.player.media.clientWidth / this.thumbAspectRatio, "px");
  6098. } // Sprites need to be offset to the correct location
  6099. }, {
  6100. key: "setImageSizeAndOffset",
  6101. value: function setImageSizeAndOffset(previewImage, frame) {
  6102. if (!this.usingSprites) {
  6103. return;
  6104. } // Find difference between height and preview container height
  6105. var multiplier = this.thumbContainerHeight / frame.h;
  6106. previewImage.style.height = "".concat(Math.floor(previewImage.naturalHeight * multiplier), "px");
  6107. previewImage.style.width = "".concat(Math.floor(previewImage.naturalWidth * multiplier), "px");
  6108. previewImage.style.left = "-".concat(frame.x * multiplier, "px");
  6109. previewImage.style.top = "-".concat(frame.y * multiplier, "px");
  6110. }
  6111. }, {
  6112. key: "enabled",
  6113. get: function get() {
  6114. return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
  6115. }
  6116. }, {
  6117. key: "currentImageContainer",
  6118. get: function get() {
  6119. if (this.mouseDown) {
  6120. return this.elements.scrubbing.container;
  6121. }
  6122. return this.elements.thumb.imageContainer;
  6123. }
  6124. }, {
  6125. key: "usingSprites",
  6126. get: function get() {
  6127. return Object.keys(this.thumbnails[0].frames[0]).includes('w');
  6128. }
  6129. }, {
  6130. key: "thumbAspectRatio",
  6131. get: function get() {
  6132. if (this.usingSprites) {
  6133. return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
  6134. }
  6135. return this.thumbnails[0].width / this.thumbnails[0].height;
  6136. }
  6137. }, {
  6138. key: "thumbContainerHeight",
  6139. get: function get() {
  6140. if (this.mouseDown) {
  6141. // Can't use media.clientHeight - HTML5 video goes big and does black bars above and below
  6142. return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio);
  6143. }
  6144. return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
  6145. }
  6146. }, {
  6147. key: "currentImageElement",
  6148. get: function get() {
  6149. if (this.mouseDown) {
  6150. return this.currentScrubbingImageElement;
  6151. }
  6152. return this.currentThumbnailImageElement;
  6153. },
  6154. set: function set(element) {
  6155. if (this.mouseDown) {
  6156. this.currentScrubbingImageElement = element;
  6157. } else {
  6158. this.currentThumbnailImageElement = element;
  6159. }
  6160. }
  6161. }]);
  6162. return PreviewThumbnails;
  6163. }();
  6164. var source = {
  6165. // Add elements to HTML5 media (source, tracks, etc)
  6166. insertElements: function insertElements(type, attributes) {
  6167. var _this = this;
  6168. if (is$1.string(attributes)) {
  6169. insertElement(type, this.media, {
  6170. src: attributes
  6171. });
  6172. } else if (is$1.array(attributes)) {
  6173. attributes.forEach(function (attribute) {
  6174. insertElement(type, _this.media, attribute);
  6175. });
  6176. }
  6177. },
  6178. // Update source
  6179. // Sources are not checked for support so be careful
  6180. change: function change(input) {
  6181. var _this2 = this;
  6182. if (!getDeep(input, 'sources.length')) {
  6183. this.debug.warn('Invalid source format');
  6184. return;
  6185. } // Cancel current network requests
  6186. html5.cancelRequests.call(this); // Destroy instance and re-setup
  6187. this.destroy.call(this, function () {
  6188. // Reset quality options
  6189. _this2.options.quality = []; // Remove elements
  6190. removeElement(_this2.media);
  6191. _this2.media = null; // Reset class name
  6192. if (is$1.element(_this2.elements.container)) {
  6193. _this2.elements.container.removeAttribute('class');
  6194. } // Set the type and provider
  6195. var sources = input.sources,
  6196. type = input.type;
  6197. var _sources = _slicedToArray(sources, 1),
  6198. _sources$ = _sources[0],
  6199. _sources$$provider = _sources$.provider,
  6200. provider = _sources$$provider === void 0 ? providers.html5 : _sources$$provider,
  6201. src = _sources$.src;
  6202. var tagName = provider === 'html5' ? type : 'div';
  6203. var attributes = provider === 'html5' ? {} : {
  6204. src: src
  6205. };
  6206. Object.assign(_this2, {
  6207. provider: provider,
  6208. type: type,
  6209. // Check for support
  6210. supported: support.check(type, provider, _this2.config.playsinline),
  6211. // Create new element
  6212. media: createElement(tagName, attributes)
  6213. }); // Inject the new element
  6214. _this2.elements.container.appendChild(_this2.media); // Autoplay the new source?
  6215. if (is$1.boolean(input.autoplay)) {
  6216. _this2.config.autoplay = input.autoplay;
  6217. } // Set attributes for audio and video
  6218. if (_this2.isHTML5) {
  6219. if (_this2.config.crossorigin) {
  6220. _this2.media.setAttribute('crossorigin', '');
  6221. }
  6222. if (_this2.config.autoplay) {
  6223. _this2.media.setAttribute('autoplay', '');
  6224. }
  6225. if (!is$1.empty(input.poster)) {
  6226. _this2.poster = input.poster;
  6227. }
  6228. if (_this2.config.loop.active) {
  6229. _this2.media.setAttribute('loop', '');
  6230. }
  6231. if (_this2.config.muted) {
  6232. _this2.media.setAttribute('muted', '');
  6233. }
  6234. if (_this2.config.playsinline) {
  6235. _this2.media.setAttribute('playsinline', '');
  6236. }
  6237. } // Restore class hook
  6238. ui.addStyleHook.call(_this2); // Set new sources for html5
  6239. if (_this2.isHTML5) {
  6240. source.insertElements.call(_this2, 'source', sources);
  6241. } // Set video title
  6242. _this2.config.title = input.title; // Set up from scratch
  6243. media.setup.call(_this2); // HTML5 stuff
  6244. if (_this2.isHTML5) {
  6245. // Setup captions
  6246. if (Object.keys(input).includes('tracks')) {
  6247. source.insertElements.call(_this2, 'track', input.tracks);
  6248. }
  6249. } // If HTML5 or embed but not fully supported, setupInterface and call ready now
  6250. if (_this2.isHTML5 || _this2.isEmbed && !_this2.supported.ui) {
  6251. // Setup interface
  6252. ui.build.call(_this2);
  6253. } // Load HTML5 sources
  6254. if (_this2.isHTML5) {
  6255. _this2.media.load();
  6256. } // Reload thumbnails
  6257. if (_this2.previewThumbnails) {
  6258. _this2.previewThumbnails.load();
  6259. } // Update the fullscreen support
  6260. _this2.fullscreen.update();
  6261. }, true);
  6262. }
  6263. };
  6264. /**
  6265. * Returns a number whose value is limited to the given range.
  6266. *
  6267. * Example: limit the output of this computation to between 0 and 255
  6268. * (x * 255).clamp(0, 255)
  6269. *
  6270. * @param {Number} input
  6271. * @param {Number} min The lower boundary of the output range
  6272. * @param {Number} max The upper boundary of the output range
  6273. * @returns A number in the range [min, max]
  6274. * @type Number
  6275. */
  6276. function clamp() {
  6277. var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
  6278. var min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  6279. var max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 255;
  6280. return Math.min(Math.max(input, min), max);
  6281. }
  6282. // TODO: Use a WeakMap for private globals
  6283. // const globals = new WeakMap();
  6284. // Plyr instance
  6285. var Plyr =
  6286. /*#__PURE__*/
  6287. function () {
  6288. function Plyr(target, options) {
  6289. var _this = this;
  6290. _classCallCheck(this, Plyr);
  6291. this.timers = {}; // State
  6292. this.ready = false;
  6293. this.loading = false;
  6294. this.failed = false; // Touch device
  6295. this.touch = support.touch; // Set the media element
  6296. this.media = target; // String selector passed
  6297. if (is$1.string(this.media)) {
  6298. this.media = document.querySelectorAll(this.media);
  6299. } // jQuery, NodeList or Array passed, use first element
  6300. if (window.jQuery && this.media instanceof jQuery || is$1.nodeList(this.media) || is$1.array(this.media)) {
  6301. // eslint-disable-next-line
  6302. this.media = this.media[0];
  6303. } // Set config
  6304. this.config = extend({}, defaults$1, Plyr.defaults, options || {}, function () {
  6305. try {
  6306. return JSON.parse(_this.media.getAttribute('data-plyr-config'));
  6307. } catch (e) {
  6308. return {};
  6309. }
  6310. }()); // Elements cache
  6311. this.elements = {
  6312. container: null,
  6313. captions: null,
  6314. buttons: {},
  6315. display: {},
  6316. progress: {},
  6317. inputs: {},
  6318. settings: {
  6319. popup: null,
  6320. menu: null,
  6321. panels: {},
  6322. buttons: {}
  6323. }
  6324. }; // Captions
  6325. this.captions = {
  6326. active: null,
  6327. currentTrack: -1,
  6328. meta: new WeakMap()
  6329. }; // Fullscreen
  6330. this.fullscreen = {
  6331. active: false
  6332. }; // Options
  6333. this.options = {
  6334. speed: [],
  6335. quality: []
  6336. }; // Debugging
  6337. // TODO: move to globals
  6338. this.debug = new Console(this.config.debug); // Log config options and support
  6339. this.debug.log('Config', this.config);
  6340. this.debug.log('Support', support); // We need an element to setup
  6341. if (is$1.nullOrUndefined(this.media) || !is$1.element(this.media)) {
  6342. this.debug.error('Setup failed: no suitable element passed');
  6343. return;
  6344. } // Bail if the element is initialized
  6345. if (this.media.plyr) {
  6346. this.debug.warn('Target already setup');
  6347. return;
  6348. } // Bail if not enabled
  6349. if (!this.config.enabled) {
  6350. this.debug.error('Setup failed: disabled by config');
  6351. return;
  6352. } // Bail if disabled or no basic support
  6353. // You may want to disable certain UAs etc
  6354. if (!support.check().api) {
  6355. this.debug.error('Setup failed: no support');
  6356. return;
  6357. } // Cache original element state for .destroy()
  6358. var clone = this.media.cloneNode(true);
  6359. clone.autoplay = false;
  6360. this.elements.original = clone; // Set media type based on tag or data attribute
  6361. // Supported: video, audio, vimeo, youtube
  6362. var type = this.media.tagName.toLowerCase(); // Embed properties
  6363. var iframe = null;
  6364. var url = null; // Different setup based on type
  6365. switch (type) {
  6366. case 'div':
  6367. // Find the frame
  6368. iframe = this.media.querySelector('iframe'); // <iframe> type
  6369. if (is$1.element(iframe)) {
  6370. // Detect provider
  6371. url = parseUrl(iframe.getAttribute('src'));
  6372. this.provider = getProviderByUrl(url.toString()); // Rework elements
  6373. this.elements.container = this.media;
  6374. this.media = iframe; // Reset classname
  6375. this.elements.container.className = ''; // Get attributes from URL and set config
  6376. if (url.search.length) {
  6377. var truthy = ['1', 'true'];
  6378. if (truthy.includes(url.searchParams.get('autoplay'))) {
  6379. this.config.autoplay = true;
  6380. }
  6381. if (truthy.includes(url.searchParams.get('loop'))) {
  6382. this.config.loop.active = true;
  6383. } // TODO: replace fullscreen.iosNative with this playsinline config option
  6384. // YouTube requires the playsinline in the URL
  6385. if (this.isYouTube) {
  6386. this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
  6387. this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
  6388. } else {
  6389. this.config.playsinline = true;
  6390. }
  6391. }
  6392. } else {
  6393. // <div> with attributes
  6394. this.provider = this.media.getAttribute(this.config.attributes.embed.provider); // Remove attribute
  6395. this.media.removeAttribute(this.config.attributes.embed.provider);
  6396. } // Unsupported or missing provider
  6397. if (is$1.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
  6398. this.debug.error('Setup failed: Invalid provider');
  6399. return;
  6400. } // Audio will come later for external providers
  6401. this.type = types.video;
  6402. break;
  6403. case 'video':
  6404. case 'audio':
  6405. this.type = type;
  6406. this.provider = providers.html5; // Get config from attributes
  6407. if (this.media.hasAttribute('crossorigin')) {
  6408. this.config.crossorigin = true;
  6409. }
  6410. if (this.media.hasAttribute('autoplay')) {
  6411. this.config.autoplay = true;
  6412. }
  6413. if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
  6414. this.config.playsinline = true;
  6415. }
  6416. if (this.media.hasAttribute('muted')) {
  6417. this.config.muted = true;
  6418. }
  6419. if (this.media.hasAttribute('loop')) {
  6420. this.config.loop.active = true;
  6421. }
  6422. break;
  6423. default:
  6424. this.debug.error('Setup failed: unsupported type');
  6425. return;
  6426. } // Check for support again but with type
  6427. this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail
  6428. if (!this.supported.api) {
  6429. this.debug.error('Setup failed: no support');
  6430. return;
  6431. }
  6432. this.eventListeners = []; // Create listeners
  6433. this.listeners = new Listeners(this); // Setup local storage for user settings
  6434. this.storage = new Storage(this); // Store reference
  6435. this.media.plyr = this; // Wrap media
  6436. if (!is$1.element(this.elements.container)) {
  6437. this.elements.container = createElement('div', {
  6438. tabindex: 0
  6439. });
  6440. wrap(this.media, this.elements.container);
  6441. } // Add style hook
  6442. ui.addStyleHook.call(this); // Setup media
  6443. media.setup.call(this); // Listen for events if debugging
  6444. if (this.config.debug) {
  6445. on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
  6446. _this.debug.log("event: ".concat(event.type));
  6447. });
  6448. } // Setup interface
  6449. // If embed but not fully supported, build interface now to avoid flash of controls
  6450. if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
  6451. ui.build.call(this);
  6452. } // Container listeners
  6453. this.listeners.container(); // Global listeners
  6454. this.listeners.global(); // Setup fullscreen
  6455. this.fullscreen = new Fullscreen(this); // Setup ads if provided
  6456. if (this.config.ads.enabled) {
  6457. this.ads = new Ads(this);
  6458. } // Autoplay if required
  6459. if (this.isHTML5 && this.config.autoplay) {
  6460. setTimeout(function () {
  6461. return _this.play();
  6462. }, 10);
  6463. } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
  6464. this.lastSeekTime = 0; // Setup preview thumbnails if enabled
  6465. if (this.config.previewThumbnails.enabled) {
  6466. this.previewThumbnails = new PreviewThumbnails(this);
  6467. }
  6468. } // ---------------------------------------
  6469. // API
  6470. // ---------------------------------------
  6471. /**
  6472. * Types and provider helpers
  6473. */
  6474. _createClass(Plyr, [{
  6475. key: "play",
  6476. /**
  6477. * Play the media, or play the advertisement (if they are not blocked)
  6478. */
  6479. value: function play() {
  6480. var _this2 = this;
  6481. if (!is$1.function(this.media.play)) {
  6482. return null;
  6483. } // Intecept play with ads
  6484. if (this.ads && this.ads.enabled) {
  6485. this.ads.managerPromise.then(function () {
  6486. return _this2.ads.play();
  6487. }).catch(function () {
  6488. return _this2.media.play();
  6489. });
  6490. } // Return the promise (for HTML5)
  6491. return this.media.play();
  6492. }
  6493. /**
  6494. * Pause the media
  6495. */
  6496. }, {
  6497. key: "pause",
  6498. value: function pause() {
  6499. if (!this.playing || !is$1.function(this.media.pause)) {
  6500. return;
  6501. }
  6502. this.media.pause();
  6503. }
  6504. /**
  6505. * Get playing state
  6506. */
  6507. }, {
  6508. key: "togglePlay",
  6509. /**
  6510. * Toggle playback based on current status
  6511. * @param {Boolean} input
  6512. */
  6513. value: function togglePlay(input) {
  6514. // Toggle based on current state if nothing passed
  6515. var toggle = is$1.boolean(input) ? input : !this.playing;
  6516. if (toggle) {
  6517. this.play();
  6518. } else {
  6519. this.pause();
  6520. }
  6521. }
  6522. /**
  6523. * Stop playback
  6524. */
  6525. }, {
  6526. key: "stop",
  6527. value: function stop() {
  6528. if (this.isHTML5) {
  6529. this.pause();
  6530. this.restart();
  6531. } else if (is$1.function(this.media.stop)) {
  6532. this.media.stop();
  6533. }
  6534. }
  6535. /**
  6536. * Restart playback
  6537. */
  6538. }, {
  6539. key: "restart",
  6540. value: function restart() {
  6541. this.currentTime = 0;
  6542. }
  6543. /**
  6544. * Rewind
  6545. * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
  6546. */
  6547. }, {
  6548. key: "rewind",
  6549. value: function rewind(seekTime) {
  6550. this.currentTime = this.currentTime - (is$1.number(seekTime) ? seekTime : this.config.seekTime);
  6551. }
  6552. /**
  6553. * Fast forward
  6554. * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
  6555. */
  6556. }, {
  6557. key: "forward",
  6558. value: function forward(seekTime) {
  6559. this.currentTime = this.currentTime + (is$1.number(seekTime) ? seekTime : this.config.seekTime);
  6560. }
  6561. /**
  6562. * Seek to a time
  6563. * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)
  6564. */
  6565. }, {
  6566. key: "increaseVolume",
  6567. /**
  6568. * Increase volume
  6569. * @param {Boolean} step - How much to decrease by (between 0 and 1)
  6570. */
  6571. value: function increaseVolume(step) {
  6572. var volume = this.media.muted ? 0 : this.volume;
  6573. this.volume = volume + (is$1.number(step) ? step : 0);
  6574. }
  6575. /**
  6576. * Decrease volume
  6577. * @param {Boolean} step - How much to decrease by (between 0 and 1)
  6578. */
  6579. }, {
  6580. key: "decreaseVolume",
  6581. value: function decreaseVolume(step) {
  6582. this.increaseVolume(-step);
  6583. }
  6584. /**
  6585. * Set muted state
  6586. * @param {Boolean} mute
  6587. */
  6588. }, {
  6589. key: "toggleCaptions",
  6590. /**
  6591. * Toggle captions
  6592. * @param {Boolean} input - Whether to enable captions
  6593. */
  6594. value: function toggleCaptions(input) {
  6595. captions.toggle.call(this, input, false);
  6596. }
  6597. /**
  6598. * Set the caption track by index
  6599. * @param {Number} - Caption index
  6600. */
  6601. }, {
  6602. key: "airplay",
  6603. /**
  6604. * Trigger the airplay dialog
  6605. * TODO: update player with state, support, enabled
  6606. */
  6607. value: function airplay() {
  6608. // Show dialog if supported
  6609. if (support.airplay) {
  6610. this.media.webkitShowPlaybackTargetPicker();
  6611. }
  6612. }
  6613. /**
  6614. * Toggle the player controls
  6615. * @param {Boolean} [toggle] - Whether to show the controls
  6616. */
  6617. }, {
  6618. key: "toggleControls",
  6619. value: function toggleControls(toggle) {
  6620. // Don't toggle if missing UI support or if it's audio
  6621. if (this.supported.ui && !this.isAudio) {
  6622. // Get state before change
  6623. var isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); // Negate the argument if not undefined since adding the class to hides the controls
  6624. var force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state
  6625. var hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); // Close menu
  6626. if (hiding && this.config.controls.includes('settings') && !is$1.empty(this.config.settings)) {
  6627. controls.toggleMenu.call(this, false);
  6628. } // Trigger event on change
  6629. if (hiding !== isHidden) {
  6630. var eventName = hiding ? 'controlshidden' : 'controlsshown';
  6631. triggerEvent.call(this, this.media, eventName);
  6632. }
  6633. return !hiding;
  6634. }
  6635. return false;
  6636. }
  6637. /**
  6638. * Add event listeners
  6639. * @param {String} event - Event type
  6640. * @param {Function} callback - Callback for when event occurs
  6641. */
  6642. }, {
  6643. key: "on",
  6644. value: function on$1(event, callback) {
  6645. on.call(this, this.elements.container, event, callback);
  6646. }
  6647. /**
  6648. * Add event listeners once
  6649. * @param {String} event - Event type
  6650. * @param {Function} callback - Callback for when event occurs
  6651. */
  6652. }, {
  6653. key: "once",
  6654. value: function once$1(event, callback) {
  6655. once.call(this, this.elements.container, event, callback);
  6656. }
  6657. /**
  6658. * Remove event listeners
  6659. * @param {String} event - Event type
  6660. * @param {Function} callback - Callback for when event occurs
  6661. */
  6662. }, {
  6663. key: "off",
  6664. value: function off$1(event, callback) {
  6665. off(this.elements.container, event, callback);
  6666. }
  6667. /**
  6668. * Destroy an instance
  6669. * Event listeners are removed when elements are removed
  6670. * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
  6671. * @param {Function} callback - Callback for when destroy is complete
  6672. * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)
  6673. */
  6674. }, {
  6675. key: "destroy",
  6676. value: function destroy(callback) {
  6677. var _this3 = this;
  6678. var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  6679. if (!this.ready) {
  6680. return;
  6681. }
  6682. var done = function done() {
  6683. // Reset overflow (incase destroyed while in fullscreen)
  6684. document.body.style.overflow = ''; // GC for embed
  6685. _this3.embed = null; // If it's a soft destroy, make minimal changes
  6686. if (soft) {
  6687. if (Object.keys(_this3.elements).length) {
  6688. // Remove elements
  6689. removeElement(_this3.elements.buttons.play);
  6690. removeElement(_this3.elements.captions);
  6691. removeElement(_this3.elements.controls);
  6692. removeElement(_this3.elements.wrapper); // Clear for GC
  6693. _this3.elements.buttons.play = null;
  6694. _this3.elements.captions = null;
  6695. _this3.elements.controls = null;
  6696. _this3.elements.wrapper = null;
  6697. } // Callback
  6698. if (is$1.function(callback)) {
  6699. callback();
  6700. }
  6701. } else {
  6702. // Unbind listeners
  6703. unbindListeners.call(_this3); // Replace the container with the original element provided
  6704. replaceElement(_this3.elements.original, _this3.elements.container); // Event
  6705. triggerEvent.call(_this3, _this3.elements.original, 'destroyed', true); // Callback
  6706. if (is$1.function(callback)) {
  6707. callback.call(_this3.elements.original);
  6708. } // Reset state
  6709. _this3.ready = false; // Clear for garbage collection
  6710. setTimeout(function () {
  6711. _this3.elements = null;
  6712. _this3.media = null;
  6713. }, 200);
  6714. }
  6715. }; // Stop playback
  6716. this.stop(); // Clear timeouts
  6717. clearTimeout(this.timers.loading);
  6718. clearTimeout(this.timers.controls);
  6719. clearTimeout(this.timers.resized); // Provider specific stuff
  6720. if (this.isHTML5) {
  6721. // Restore native video controls
  6722. ui.toggleNativeControls.call(this, true); // Clean up
  6723. done();
  6724. } else if (this.isYouTube) {
  6725. // Clear timers
  6726. clearInterval(this.timers.buffering);
  6727. clearInterval(this.timers.playing); // Destroy YouTube API
  6728. if (this.embed !== null && is$1.function(this.embed.destroy)) {
  6729. this.embed.destroy();
  6730. } // Clean up
  6731. done();
  6732. } else if (this.isVimeo) {
  6733. // Destroy Vimeo API
  6734. // then clean up (wait, to prevent postmessage errors)
  6735. if (this.embed !== null) {
  6736. this.embed.unload().then(done);
  6737. } // Vimeo does not always return
  6738. setTimeout(done, 200);
  6739. }
  6740. }
  6741. /**
  6742. * Check for support for a mime type (HTML5 only)
  6743. * @param {String} type - Mime type
  6744. */
  6745. }, {
  6746. key: "supports",
  6747. value: function supports(type) {
  6748. return support.mime.call(this, type);
  6749. }
  6750. /**
  6751. * Check for support
  6752. * @param {String} type - Player type (audio/video)
  6753. * @param {String} provider - Provider (html5/youtube/vimeo)
  6754. * @param {Boolean} inline - Where player has `playsinline` sttribute
  6755. */
  6756. }, {
  6757. key: "isHTML5",
  6758. get: function get() {
  6759. return Boolean(this.provider === providers.html5);
  6760. }
  6761. }, {
  6762. key: "isEmbed",
  6763. get: function get() {
  6764. return Boolean(this.isYouTube || this.isVimeo);
  6765. }
  6766. }, {
  6767. key: "isYouTube",
  6768. get: function get() {
  6769. return Boolean(this.provider === providers.youtube);
  6770. }
  6771. }, {
  6772. key: "isVimeo",
  6773. get: function get() {
  6774. return Boolean(this.provider === providers.vimeo);
  6775. }
  6776. }, {
  6777. key: "isVideo",
  6778. get: function get() {
  6779. return Boolean(this.type === types.video);
  6780. }
  6781. }, {
  6782. key: "isAudio",
  6783. get: function get() {
  6784. return Boolean(this.type === types.audio);
  6785. }
  6786. }, {
  6787. key: "playing",
  6788. get: function get() {
  6789. return Boolean(this.ready && !this.paused && !this.ended);
  6790. }
  6791. /**
  6792. * Get paused state
  6793. */
  6794. }, {
  6795. key: "paused",
  6796. get: function get() {
  6797. return Boolean(this.media.paused);
  6798. }
  6799. /**
  6800. * Get stopped state
  6801. */
  6802. }, {
  6803. key: "stopped",
  6804. get: function get() {
  6805. return Boolean(this.paused && this.currentTime === 0);
  6806. }
  6807. /**
  6808. * Get ended state
  6809. */
  6810. }, {
  6811. key: "ended",
  6812. get: function get() {
  6813. return Boolean(this.media.ended);
  6814. }
  6815. }, {
  6816. key: "currentTime",
  6817. set: function set(input) {
  6818. // Bail if media duration isn't available yet
  6819. if (!this.duration) {
  6820. return;
  6821. } // Validate input
  6822. var inputIsValid = is$1.number(input) && input > 0; // Set
  6823. this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging
  6824. this.debug.log("Seeking to ".concat(this.currentTime, " seconds"));
  6825. }
  6826. /**
  6827. * Get current time
  6828. */
  6829. ,
  6830. get: function get() {
  6831. return Number(this.media.currentTime);
  6832. }
  6833. /**
  6834. * Get buffered
  6835. */
  6836. }, {
  6837. key: "buffered",
  6838. get: function get() {
  6839. var buffered = this.media.buffered; // YouTube / Vimeo return a float between 0-1
  6840. if (is$1.number(buffered)) {
  6841. return buffered;
  6842. } // HTML5
  6843. // TODO: Handle buffered chunks of the media
  6844. // (i.e. seek to another section buffers only that section)
  6845. if (buffered && buffered.length && this.duration > 0) {
  6846. return buffered.end(0) / this.duration;
  6847. }
  6848. return 0;
  6849. }
  6850. /**
  6851. * Get seeking status
  6852. */
  6853. }, {
  6854. key: "seeking",
  6855. get: function get() {
  6856. return Boolean(this.media.seeking);
  6857. }
  6858. /**
  6859. * Get the duration of the current media
  6860. */
  6861. }, {
  6862. key: "duration",
  6863. get: function get() {
  6864. // Faux duration set via config
  6865. var fauxDuration = parseFloat(this.config.duration); // Media duration can be NaN or Infinity before the media has loaded
  6866. var realDuration = (this.media || {}).duration;
  6867. var duration = !is$1.number(realDuration) || realDuration === Infinity ? 0 : realDuration; // If config duration is funky, use regular duration
  6868. return fauxDuration || duration;
  6869. }
  6870. /**
  6871. * Set the player volume
  6872. * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
  6873. */
  6874. }, {
  6875. key: "volume",
  6876. set: function set(value) {
  6877. var volume = value;
  6878. var max = 1;
  6879. var min = 0;
  6880. if (is$1.string(volume)) {
  6881. volume = Number(volume);
  6882. } // Load volume from storage if no value specified
  6883. if (!is$1.number(volume)) {
  6884. volume = this.storage.get('volume');
  6885. } // Use config if all else fails
  6886. if (!is$1.number(volume)) {
  6887. volume = this.config.volume;
  6888. } // Maximum is volumeMax
  6889. if (volume > max) {
  6890. volume = max;
  6891. } // Minimum is volumeMin
  6892. if (volume < min) {
  6893. volume = min;
  6894. } // Update config
  6895. this.config.volume = volume; // Set the player volume
  6896. this.media.volume = volume; // If muted, and we're increasing volume manually, reset muted state
  6897. if (!is$1.empty(value) && this.muted && volume > 0) {
  6898. this.muted = false;
  6899. }
  6900. }
  6901. /**
  6902. * Get the current player volume
  6903. */
  6904. ,
  6905. get: function get() {
  6906. return Number(this.media.volume);
  6907. }
  6908. }, {
  6909. key: "muted",
  6910. set: function set(mute) {
  6911. var toggle = mute; // Load muted state from storage
  6912. if (!is$1.boolean(toggle)) {
  6913. toggle = this.storage.get('muted');
  6914. } // Use config if all else fails
  6915. if (!is$1.boolean(toggle)) {
  6916. toggle = this.config.muted;
  6917. } // Update config
  6918. this.config.muted = toggle; // Set mute on the player
  6919. this.media.muted = toggle;
  6920. }
  6921. /**
  6922. * Get current muted state
  6923. */
  6924. ,
  6925. get: function get() {
  6926. return Boolean(this.media.muted);
  6927. }
  6928. /**
  6929. * Check if the media has audio
  6930. */
  6931. }, {
  6932. key: "hasAudio",
  6933. get: function get() {
  6934. // Assume yes for all non HTML5 (as we can't tell...)
  6935. if (!this.isHTML5) {
  6936. return true;
  6937. }
  6938. if (this.isAudio) {
  6939. return true;
  6940. } // Get audio tracks
  6941. return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
  6942. }
  6943. /**
  6944. * Set playback speed
  6945. * @param {Number} speed - the speed of playback (0.5-2.0)
  6946. */
  6947. }, {
  6948. key: "speed",
  6949. set: function set(input) {
  6950. var _this4 = this;
  6951. var speed = null;
  6952. if (is$1.number(input)) {
  6953. speed = input;
  6954. }
  6955. if (!is$1.number(speed)) {
  6956. speed = this.storage.get('speed');
  6957. }
  6958. if (!is$1.number(speed)) {
  6959. speed = this.config.speed.selected;
  6960. } // Clamp to min/max
  6961. var min = this.minimumSpeed,
  6962. max = this.maximumSpeed;
  6963. speed = clamp(speed, min, max); // Update config
  6964. this.config.speed.selected = speed; // Set media speed
  6965. setTimeout(function () {
  6966. _this4.media.playbackRate = speed;
  6967. }, 0);
  6968. }
  6969. /**
  6970. * Get current playback speed
  6971. */
  6972. ,
  6973. get: function get() {
  6974. return Number(this.media.playbackRate);
  6975. }
  6976. /**
  6977. * Get the minimum allowed speed
  6978. */
  6979. }, {
  6980. key: "minimumSpeed",
  6981. get: function get() {
  6982. if (this.isYouTube) {
  6983. // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
  6984. return Math.min.apply(Math, _toConsumableArray(this.options.speed));
  6985. }
  6986. if (this.isVimeo) {
  6987. // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
  6988. return 0.5;
  6989. } // https://stackoverflow.com/a/32320020/1191319
  6990. return 0.0625;
  6991. }
  6992. /**
  6993. * Get the maximum allowed speed
  6994. */
  6995. }, {
  6996. key: "maximumSpeed",
  6997. get: function get() {
  6998. if (this.isYouTube) {
  6999. // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
  7000. return Math.max.apply(Math, _toConsumableArray(this.options.speed));
  7001. }
  7002. if (this.isVimeo) {
  7003. // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
  7004. return 2;
  7005. } // https://stackoverflow.com/a/32320020/1191319
  7006. return 16;
  7007. }
  7008. /**
  7009. * Set playback quality
  7010. * Currently HTML5 & YouTube only
  7011. * @param {Number} input - Quality level
  7012. */
  7013. }, {
  7014. key: "quality",
  7015. set: function set(input) {
  7016. var config = this.config.quality;
  7017. var options = this.options.quality;
  7018. if (!options.length) {
  7019. return;
  7020. }
  7021. var quality = [!is$1.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is$1.number);
  7022. var updateStorage = true;
  7023. if (!options.includes(quality)) {
  7024. var value = closest(options, quality);
  7025. this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
  7026. quality = value; // Don't update storage if quality is not supported
  7027. updateStorage = false;
  7028. } // Update config
  7029. config.selected = quality; // Set quality
  7030. this.media.quality = quality; // Save to storage
  7031. if (updateStorage) {
  7032. this.storage.set({
  7033. quality: quality
  7034. });
  7035. }
  7036. }
  7037. /**
  7038. * Get current quality level
  7039. */
  7040. ,
  7041. get: function get() {
  7042. return this.media.quality;
  7043. }
  7044. /**
  7045. * Toggle loop
  7046. * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
  7047. * @param {Boolean} input - Whether to loop or not
  7048. */
  7049. }, {
  7050. key: "loop",
  7051. set: function set(input) {
  7052. var toggle = is$1.boolean(input) ? input : this.config.loop.active;
  7053. this.config.loop.active = toggle;
  7054. this.media.loop = toggle; // Set default to be a true toggle
  7055. /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
  7056. switch (type) {
  7057. case 'start':
  7058. if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
  7059. this.config.loop.end = null;
  7060. }
  7061. this.config.loop.start = this.currentTime;
  7062. // this.config.loop.indicator.start = this.elements.display.played.value;
  7063. break;
  7064. case 'end':
  7065. if (this.config.loop.start >= this.currentTime) {
  7066. return this;
  7067. }
  7068. this.config.loop.end = this.currentTime;
  7069. // this.config.loop.indicator.end = this.elements.display.played.value;
  7070. break;
  7071. case 'all':
  7072. this.config.loop.start = 0;
  7073. this.config.loop.end = this.duration - 2;
  7074. this.config.loop.indicator.start = 0;
  7075. this.config.loop.indicator.end = 100;
  7076. break;
  7077. case 'toggle':
  7078. if (this.config.loop.active) {
  7079. this.config.loop.start = 0;
  7080. this.config.loop.end = null;
  7081. } else {
  7082. this.config.loop.start = 0;
  7083. this.config.loop.end = this.duration - 2;
  7084. }
  7085. break;
  7086. default:
  7087. this.config.loop.start = 0;
  7088. this.config.loop.end = null;
  7089. break;
  7090. } */
  7091. }
  7092. /**
  7093. * Get current loop state
  7094. */
  7095. ,
  7096. get: function get() {
  7097. return Boolean(this.media.loop);
  7098. }
  7099. /**
  7100. * Set new media source
  7101. * @param {Object} input - The new source object (see docs)
  7102. */
  7103. }, {
  7104. key: "source",
  7105. set: function set(input) {
  7106. source.change.call(this, input);
  7107. }
  7108. /**
  7109. * Get current source
  7110. */
  7111. ,
  7112. get: function get() {
  7113. return this.media.currentSrc;
  7114. }
  7115. /**
  7116. * Get a download URL (either source or custom)
  7117. */
  7118. }, {
  7119. key: "download",
  7120. get: function get() {
  7121. var download = this.config.urls.download;
  7122. return is$1.url(download) ? download : this.source;
  7123. }
  7124. /**
  7125. * Set the download URL
  7126. */
  7127. ,
  7128. set: function set(input) {
  7129. if (!is$1.url(input)) {
  7130. return;
  7131. }
  7132. this.config.urls.download = input;
  7133. controls.setDownloadUrl.call(this);
  7134. }
  7135. /**
  7136. * Set the poster image for a video
  7137. * @param {String} input - the URL for the new poster image
  7138. */
  7139. }, {
  7140. key: "poster",
  7141. set: function set(input) {
  7142. if (!this.isVideo) {
  7143. this.debug.warn('Poster can only be set for video');
  7144. return;
  7145. }
  7146. ui.setPoster.call(this, input, false).catch(function () {});
  7147. }
  7148. /**
  7149. * Get the current poster image
  7150. */
  7151. ,
  7152. get: function get() {
  7153. if (!this.isVideo) {
  7154. return null;
  7155. }
  7156. return this.media.getAttribute('poster');
  7157. }
  7158. /**
  7159. * Get the current aspect ratio in use
  7160. */
  7161. }, {
  7162. key: "ratio",
  7163. get: function get() {
  7164. if (!this.isVideo) {
  7165. return null;
  7166. }
  7167. var ratio = reduceAspectRatio(getAspectRatio.call(this));
  7168. return is$1.array(ratio) ? ratio.join(':') : ratio;
  7169. }
  7170. /**
  7171. * Set video aspect ratio
  7172. */
  7173. ,
  7174. set: function set(input) {
  7175. if (!this.isVideo) {
  7176. this.debug.warn('Aspect ratio can only be set for video');
  7177. return;
  7178. }
  7179. if (!is$1.string(input) || !validateRatio(input)) {
  7180. this.debug.error("Invalid aspect ratio specified (".concat(input, ")"));
  7181. return;
  7182. }
  7183. this.config.ratio = input;
  7184. setAspectRatio.call(this);
  7185. }
  7186. /**
  7187. * Set the autoplay state
  7188. * @param {Boolean} input - Whether to autoplay or not
  7189. */
  7190. }, {
  7191. key: "autoplay",
  7192. set: function set(input) {
  7193. var toggle = is$1.boolean(input) ? input : this.config.autoplay;
  7194. this.config.autoplay = toggle;
  7195. }
  7196. /**
  7197. * Get the current autoplay state
  7198. */
  7199. ,
  7200. get: function get() {
  7201. return Boolean(this.config.autoplay);
  7202. }
  7203. }, {
  7204. key: "currentTrack",
  7205. set: function set(input) {
  7206. captions.set.call(this, input, false);
  7207. }
  7208. /**
  7209. * Get the current caption track index (-1 if disabled)
  7210. */
  7211. ,
  7212. get: function get() {
  7213. var _this$captions = this.captions,
  7214. toggled = _this$captions.toggled,
  7215. currentTrack = _this$captions.currentTrack;
  7216. return toggled ? currentTrack : -1;
  7217. }
  7218. /**
  7219. * Set the wanted language for captions
  7220. * Since tracks can be added later it won't update the actual caption track until there is a matching track
  7221. * @param {String} - Two character ISO language code (e.g. EN, FR, PT, etc)
  7222. */
  7223. }, {
  7224. key: "language",
  7225. set: function set(input) {
  7226. captions.setLanguage.call(this, input, false);
  7227. }
  7228. /**
  7229. * Get the current track's language
  7230. */
  7231. ,
  7232. get: function get() {
  7233. return (captions.getCurrentTrack.call(this) || {}).language;
  7234. }
  7235. /**
  7236. * Toggle picture-in-picture playback on WebKit/MacOS
  7237. * TODO: update player with state, support, enabled
  7238. * TODO: detect outside changes
  7239. */
  7240. }, {
  7241. key: "pip",
  7242. set: function set(input) {
  7243. // Bail if no support
  7244. if (!support.pip) {
  7245. return;
  7246. } // Toggle based on current state if not passed
  7247. var toggle = is$1.boolean(input) ? input : !this.pip; // Toggle based on current state
  7248. // Safari
  7249. if (is$1.function(this.media.webkitSetPresentationMode)) {
  7250. this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
  7251. } // Chrome
  7252. if (is$1.function(this.media.requestPictureInPicture)) {
  7253. if (!this.pip && toggle) {
  7254. this.media.requestPictureInPicture();
  7255. } else if (this.pip && !toggle) {
  7256. document.exitPictureInPicture();
  7257. }
  7258. }
  7259. }
  7260. /**
  7261. * Get the current picture-in-picture state
  7262. */
  7263. ,
  7264. get: function get() {
  7265. if (!support.pip) {
  7266. return null;
  7267. } // Safari
  7268. if (!is$1.empty(this.media.webkitPresentationMode)) {
  7269. return this.media.webkitPresentationMode === pip.active;
  7270. } // Chrome
  7271. return this.media === document.pictureInPictureElement;
  7272. }
  7273. }], [{
  7274. key: "supported",
  7275. value: function supported(type, provider, inline) {
  7276. return support.check(type, provider, inline);
  7277. }
  7278. /**
  7279. * Load an SVG sprite into the page
  7280. * @param {String} url - URL for the SVG sprite
  7281. * @param {String} [id] - Unique ID
  7282. */
  7283. }, {
  7284. key: "loadSprite",
  7285. value: function loadSprite$1(url, id) {
  7286. return loadSprite(url, id);
  7287. }
  7288. /**
  7289. * Setup multiple instances
  7290. * @param {*} selector
  7291. * @param {Object} options
  7292. */
  7293. }, {
  7294. key: "setup",
  7295. value: function setup(selector) {
  7296. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  7297. var targets = null;
  7298. if (is$1.string(selector)) {
  7299. targets = Array.from(document.querySelectorAll(selector));
  7300. } else if (is$1.nodeList(selector)) {
  7301. targets = Array.from(selector);
  7302. } else if (is$1.array(selector)) {
  7303. targets = selector.filter(is$1.element);
  7304. }
  7305. if (is$1.empty(targets)) {
  7306. return null;
  7307. }
  7308. return targets.map(function (t) {
  7309. return new Plyr(t, options);
  7310. });
  7311. }
  7312. }]);
  7313. return Plyr;
  7314. }();
  7315. Plyr.defaults = cloneDeep(defaults$1);
  7316. return Plyr;
  7317. }));