| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455 |
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.jessibuca = factory());
- })(this, (function () { 'use strict';
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
- function unwrapExports (x) {
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
- }
- function createCommonjsModule(fn, module) {
- return module = { exports: {} }, fn(module, module.exports), module.exports;
- }
- var defineProperty = createCommonjsModule(function (module) {
- function _defineProperty(obj, key, value) {
- if (key in obj) {
- Object.defineProperty(obj, key, {
- value: value,
- enumerable: true,
- configurable: true,
- writable: true
- });
- } else {
- obj[key] = value;
- }
- return obj;
- }
- module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports;
- });
- var _defineProperty = unwrapExports(defineProperty);
- // 播放协议
- const PLAYER_PLAY_PROTOCOL = {
- websocket: 0,
- fetch: 1,
- webrtc: 2
- };
- const DEMUX_TYPE = {
- flv: 'flv',
- m7s: 'm7s'
- }; // default player options
- const DEFAULT_PLAYER_OPTIONS = {
- videoBuffer: 1000,
- //1000ms == 1 second
- videoBufferDelay: 1000,
- // 1000ms
- isResize: true,
- isFullResize: false,
- //
- isFlv: false,
- debug: false,
- hotKey: false,
- // 快捷键
- loadingTimeout: 10,
- // loading timeout
- heartTimeout: 5,
- // heart timeout
- timeout: 10,
- // second
- loadingTimeoutReplay: false,
- // loading timeout replay
- heartTimeoutReplay: false,
- // heart timeout replay。
- loadingTimeoutReplayTimes: 3,
- // loading timeout replay fail times
- heartTimeoutReplayTimes: 3,
- // heart timeout replay fail times
- supportDblclickFullscreen: false,
- showBandwidth: false,
- //
- keepScreenOn: false,
- isNotMute: false,
- hasAudio: true,
- hasVideo: true,
- operateBtns: {
- fullscreen: false,
- screenshot: false,
- play: false,
- audio: false,
- record: false
- },
- controlAutoHide: false,
- hasControl: false,
- loadingText: '',
- background: '',
- decoder: 'decoder.js',
- url: '',
- //
- rotate: 0,
- // text: '',
- forceNoOffscreen: true,
- // 默认是不采用
- hiddenAutoPause: false,
- protocol: PLAYER_PLAY_PROTOCOL.fetch,
- demuxType: DEMUX_TYPE.flv,
- //
- useWCS: false,
- //
- wcsUseVideoRender: false,
- useMSE: false,
- //
- useOffscreen: false,
- //
- autoWasm: true,
- // 自动降级到 wasm 模式
- wasmDecodeErrorReplay: true,
- // 解码失败重新播放。
- openWebglAlignment: false // https://github.com/langhuihui/jessibuca/issues/152
- };
- const WORKER_CMD_TYPE = {
- init: 'init',
- initVideo: 'initVideo',
- render: 'render',
- playAudio: 'playAudio',
- initAudio: 'initAudio',
- kBps: 'kBps',
- decode: 'decode',
- audioCode: 'audioCode',
- videoCode: 'videoCode',
- wasmError: 'wasmError'
- };
- const WASM_ERROR = {
- invalidNalUnitSize: 'Invalid NAL unit size' // errorSplittingTheInputIntoNALUnits: 'Error splitting the input into NAL units'
- };
- const MEDIA_TYPE = {
- audio: 1,
- video: 2
- };
- const FLV_MEDIA_TYPE = {
- audio: 8,
- video: 9
- };
- const WORKER_SEND_TYPE = {
- init: 'init',
- decode: 'decode',
- audioDecode: 'audioDecode',
- videoDecode: 'videoDecode',
- close: 'close',
- updateConfig: 'updateConfig'
- }; //
- const EVENTS = {
- fullscreen: 'fullscreen$2',
- webFullscreen: 'webFullscreen',
- decoderWorkerInit: 'decoderWorkerInit',
- play: 'play',
- playing: 'playing',
- pause: 'pause',
- mute: 'mute',
- load: 'load',
- loading: 'loading',
- videoInfo: 'videoInfo',
- timeUpdate: 'timeUpdate',
- audioInfo: "audioInfo",
- log: 'log',
- error: "error",
- kBps: 'kBps',
- timeout: 'timeout',
- delayTimeout: 'delayTimeout',
- loadingTimeout: 'loadingTimeout',
- stats: 'stats',
- performance: "performance",
- record: 'record',
- recording: 'recording',
- recordingTimestamp: 'recordingTimestamp',
- recordStart: 'recordStart',
- recordEnd: 'recordEnd',
- recordCreateError: 'recordCreateError',
- buffer: 'buffer',
- videoFrame: 'videoFrame',
- start: 'start',
- metadata: 'metadata',
- resize: 'resize',
- streamEnd: 'streamEnd',
- streamSuccess: 'streamSuccess',
- streamMessage: 'streamMessage',
- streamError: 'streamError',
- volumechange: 'volumechange',
- destroy: 'destroy',
- mseSourceOpen: 'mseSourceOpen',
- mseSourceClose: 'mseSourceClose',
- mseSourceBufferError: 'mseSourceBufferError',
- mseSourceBufferBusy: 'mseSourceBufferBusy',
- videoWaiting: 'videoWaiting',
- videoTimeUpdate: 'videoTimeUpdate',
- videoSyncAudio: 'videoSyncAudio',
- playToRenderTimes: 'playToRenderTimes'
- };
- const JESSIBUCA_EVENTS = {
- load: EVENTS.load,
- timeUpdate: EVENTS.timeUpdate,
- videoInfo: EVENTS.videoInfo,
- audioInfo: EVENTS.audioInfo,
- error: EVENTS.error,
- kBps: EVENTS.kBps,
- log: EVENTS.log,
- start: EVENTS.start,
- timeout: EVENTS.timeout,
- loadingTimeout: EVENTS.loadingTimeout,
- delayTimeout: EVENTS.delayTimeout,
- fullscreen: 'fullscreen',
- play: EVENTS.play,
- pause: EVENTS.pause,
- mute: EVENTS.mute,
- stats: EVENTS.stats,
- performance: EVENTS.performance,
- recordingTimestamp: EVENTS.recordingTimestamp,
- recordStart: EVENTS.recordStart,
- recordEnd: EVENTS.recordEnd,
- playToRenderTimes: EVENTS.playToRenderTimes
- };
- const EVENTS_ERROR = {
- playError: 'playIsNotPauseOrUrlIsNull',
- fetchError: "fetchError",
- websocketError: 'websocketError',
- webcodecsH265NotSupport: 'webcodecsH265NotSupport',
- mediaSourceH265NotSupport: 'mediaSourceH265NotSupport',
- wasmDecodeError: 'wasmDecodeError'
- };
- const WEBSOCKET_STATUS = {
- notConnect: 'notConnect',
- open: 'open',
- close: 'close',
- error: 'error'
- };
- const SCREENSHOT_TYPE = {
- download: 'download',
- base64: 'base64',
- blob: 'blob'
- };
- const VIDEO_ENC_TYPE = {
- 7: 'H264(AVC)',
- //
- 12: 'H265(HEVC)' //
- };
- const VIDEO_ENC_CODE = {
- h264: 7,
- h265: 12
- };
- const AUDIO_ENC_TYPE = {
- 10: 'AAC',
- 7: 'ALAW',
- 8: 'MULAW'
- };
- const H265_NAL_TYPE = {
- vps: 32,
- sps: 33,
- pps: 34
- };
- const CONTROL_HEIGHT = 38;
- const SCALE_MODE_TYPE = {
- full: 0,
- // 视频画面完全填充canvas区域,画面会被拉伸
- auto: 1,
- // 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边
- fullAuto: 2 // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
- };
- const FILE_SUFFIX = {
- mp4: 'mp4',
- webm: 'webm'
- };
- const CANVAS_RENDER_TYPE = {
- webcodecs: 'webcodecs',
- webgl: 'webgl',
- offscreen: 'offscreen'
- };
- const ENCODED_VIDEO_TYPE = {
- key: 'key',
- delta: 'delta'
- };
- const MP4_CODECS = {
- avc: 'video/mp4; codecs="avc1.64002A"',
- hev: 'video/mp4; codecs="hev1.1.6.L123.b0"'
- };
- const MEDIA_SOURCE_STATE = {
- ended: 'ended',
- open: 'open',
- closed: 'closed'
- }; // frag duration
- const AUDIO_SYNC_VIDEO_DIFF = 1000;
- const HOT_KEY = {
- esc: 27,
- //
- arrowUp: 38,
- //
- arrowDown: 40 //
- };
- class Debug {
- constructor(master) {
- this.log = function (name) {
- if (master._opt.debug) {
- for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
- args[_key - 1] = arguments[_key];
- }
- // console.log(`Jessibuca: [${name}]`, ...args);
- }
- };
- this.warn = function (name) {
- if (master._opt.debug) {
- for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
- args[_key2 - 1] = arguments[_key2];
- }
- // console.warn(`Jessibuca: [${name}]`, ...args);
- }
- };
- this.error = function (name) {
- if (master._opt.debug) {
- for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
- args[_key3 - 1] = arguments[_key3];
- }
- // console.error(`Jessibuca: [${name}]`, ...args);
- }
- };
- }
- }
- class Events {
- constructor(master) {
- this.destroys = [];
- this.proxy = this.proxy.bind(this);
- this.master = master;
- }
- proxy(target, name, callback) {
- let option = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
- if (!target) {
- return;
- }
- if (Array.isArray(name)) {
- return name.map(item => this.proxy(target, item, callback, option));
- }
- target.addEventListener(name, callback, option);
- const destroy = () => target.removeEventListener(name, callback, option);
- this.destroys.push(destroy);
- return destroy;
- }
- destroy() {
- this.master.debug && this.master.debug.log(`Events`, 'destroy');
- this.destroys.forEach(event => event());
- }
- }
- var property$1 = (player => {
- Object.defineProperty(player, 'rect', {
- get: () => {
- const clientRect = player.$container.getBoundingClientRect();
- clientRect.width = Math.max(clientRect.width, player.$container.clientWidth);
- clientRect.height = Math.max(clientRect.height, player.$container.clientHeight);
- return clientRect;
- }
- });
- ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(key => {
- Object.defineProperty(player, key, {
- get: () => {
- return player.rect[key];
- }
- });
- });
- });
- var screenfull = createCommonjsModule(function (module) {
- /*!
- * screenfull
- * v5.1.0 - 2020-12-24
- * (c) Sindre Sorhus; MIT License
- */
- (function () {
- var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
- var isCommonjs = module.exports;
- var fn = (function () {
- var val;
- var fnMap = [
- [
- 'requestFullscreen',
- 'exitFullscreen',
- 'fullscreenElement',
- 'fullscreenEnabled',
- 'fullscreenchange',
- 'fullscreenerror'
- ],
- // New WebKit
- [
- 'webkitRequestFullscreen',
- 'webkitExitFullscreen',
- 'webkitFullscreenElement',
- 'webkitFullscreenEnabled',
- 'webkitfullscreenchange',
- 'webkitfullscreenerror'
- ],
- // Old WebKit
- [
- 'webkitRequestFullScreen',
- 'webkitCancelFullScreen',
- 'webkitCurrentFullScreenElement',
- 'webkitCancelFullScreen',
- 'webkitfullscreenchange',
- 'webkitfullscreenerror'
- ],
- [
- 'mozRequestFullScreen',
- 'mozCancelFullScreen',
- 'mozFullScreenElement',
- 'mozFullScreenEnabled',
- 'mozfullscreenchange',
- 'mozfullscreenerror'
- ],
- [
- 'msRequestFullscreen',
- 'msExitFullscreen',
- 'msFullscreenElement',
- 'msFullscreenEnabled',
- 'MSFullscreenChange',
- 'MSFullscreenError'
- ]
- ];
- var i = 0;
- var l = fnMap.length;
- var ret = {};
- for (; i < l; i++) {
- val = fnMap[i];
- if (val && val[1] in document) {
- for (i = 0; i < val.length; i++) {
- ret[fnMap[0][i]] = val[i];
- }
- return ret;
- }
- }
- return false;
- })();
- var eventNameMap = {
- change: fn.fullscreenchange,
- error: fn.fullscreenerror
- };
- var screenfull = {
- request: function (element, options) {
- return new Promise(function (resolve, reject) {
- var onFullScreenEntered = function () {
- this.off('change', onFullScreenEntered);
- resolve();
- }.bind(this);
- this.on('change', onFullScreenEntered);
- element = element || document.documentElement;
- var returnPromise = element[fn.requestFullscreen](options);
- if (returnPromise instanceof Promise) {
- returnPromise.then(onFullScreenEntered).catch(reject);
- }
- }.bind(this));
- },
- exit: function () {
- return new Promise(function (resolve, reject) {
- if (!this.isFullscreen) {
- resolve();
- return;
- }
- var onFullScreenExit = function () {
- this.off('change', onFullScreenExit);
- resolve();
- }.bind(this);
- this.on('change', onFullScreenExit);
- var returnPromise = document[fn.exitFullscreen]();
- if (returnPromise instanceof Promise) {
- returnPromise.then(onFullScreenExit).catch(reject);
- }
- }.bind(this));
- },
- toggle: function (element, options) {
- return this.isFullscreen ? this.exit() : this.request(element, options);
- },
- onchange: function (callback) {
- this.on('change', callback);
- },
- onerror: function (callback) {
- this.on('error', callback);
- },
- on: function (event, callback) {
- var eventName = eventNameMap[event];
- if (eventName) {
- document.addEventListener(eventName, callback, false);
- }
- },
- off: function (event, callback) {
- var eventName = eventNameMap[event];
- if (eventName) {
- document.removeEventListener(eventName, callback, false);
- }
- },
- raw: fn
- };
- if (!fn) {
- if (isCommonjs) {
- module.exports = {isEnabled: false};
- } else {
- window.screenfull = {isEnabled: false};
- }
- return;
- }
- Object.defineProperties(screenfull, {
- isFullscreen: {
- get: function () {
- return Boolean(document[fn.fullscreenElement]);
- }
- },
- element: {
- enumerable: true,
- get: function () {
- return document[fn.fullscreenElement];
- }
- },
- isEnabled: {
- enumerable: true,
- get: function () {
- // Coerce to boolean in case of old WebKit
- return Boolean(document[fn.fullscreenEnabled]);
- }
- }
- });
- if (isCommonjs) {
- module.exports = screenfull;
- } else {
- window.screenfull = screenfull;
- }
- })();
- });
- screenfull.isEnabled;
- function noop() {}
- function supportOffscreen($canvas) {
- return typeof $canvas.transferControlToOffscreen === 'function';
- }
- function supportOffscreenV2() {
- return typeof OffscreenCanvas !== "undefined";
- }
- function createContextGL($canvas) {
- let gl = null;
- const validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"];
- let nameIndex = 0;
- while (!gl && nameIndex < validContextNames.length) {
- const contextName = validContextNames[nameIndex];
- try {
- let contextOptions = {
- preserveDrawingBuffer: true
- };
- gl = $canvas.getContext(contextName, contextOptions);
- } catch (e) {
- gl = null;
- }
- if (!gl || typeof gl.getParameter !== "function") {
- gl = null;
- }
- ++nameIndex;
- }
- return gl;
- }
- function dataURLToFile() {
- let dataURL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
- const arr = dataURL.split(",");
- const bstr = atob(arr[1]);
- const type = arr[0].replace("data:", "").replace(";base64", "");
- let n = bstr.length,
- u8arr = new Uint8Array(n);
- while (n--) {
- u8arr[n] = bstr.charCodeAt(n);
- }
- return new File([u8arr], 'file', {
- type
- });
- }
- function downloadImg(content, fileName) {
- const aLink = document.createElement("a");
- aLink.download = fileName;
- const href = URL.createObjectURL(content);
- aLink.href = href;
- aLink.click();
- setTimeout(() => {
- URL.revokeObjectURL(href);
- }, isIOS() ? 1000 : 0);
- }
- function now() {
- return new Date().getTime();
- }
- (() => {
- try {
- if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
- const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
- if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
- }
- } catch (e) {}
- return false;
- })();
- function clamp(num, a, b) {
- return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
- }
- function setStyle(element, key, value) {
- if (!element) {
- return;
- }
- if (typeof key === 'object') {
- Object.keys(key).forEach(item => {
- setStyle(element, item, key[item]);
- });
- }
- element.style[key] = value;
- return element;
- }
- function getStyle(element, key) {
- let numberType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
- if (!element) {
- return 0;
- }
- const value = getComputedStyle(element, null).getPropertyValue(key);
- return numberType ? parseFloat(value) : value;
- }
- function getNowTime() {
- if (performance && typeof performance.now === 'function') {
- return performance.now();
- }
- return Date.now();
- }
- function calculationRate(callback) {
- let totalSize = 0;
- let lastTime = getNowTime();
- return size => {
- totalSize += size;
- const thisTime = getNowTime();
- const diffTime = thisTime - lastTime;
- if (diffTime >= 1000) {
- callback(totalSize / diffTime * 1000);
- lastTime = thisTime;
- totalSize = 0;
- }
- };
- }
- function downloadRecord(blob, name, suffix) {
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = (name || now()) + '.' + (suffix || FILE_SUFFIX.webm);
- a.click();
- setTimeout(() => {
- window.URL.revokeObjectURL(url);
- }, isIOS() ? 1000 : 0);
- }
- function isMobile() {
- return /iphone|ipod|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase());
- }
- function isIOS() {
- const UA = window.navigator.userAgent.toLowerCase();
- return UA && /iphone|ipad|ipod|ios/.test(UA);
- }
- function supportWCS() {
- return "VideoEncoder" in window;
- }
- function formatVideoDecoderConfigure(avcC) {
- let codecArray = avcC.subarray(1, 4);
- let codecString = "avc1.";
- for (let j = 0; j < 3; j++) {
- let h = codecArray[j].toString(16);
- if (h.length < 2) {
- h = "0" + h;
- }
- codecString += h;
- }
- return {
- codec: codecString,
- description: avcC
- };
- }
- function isFullScreen() {
- return document.isFullScreen || document.mozIsFullScreen || document.webkitIsFullScreen;
- }
- function bpsSize(value) {
- if (null == value || value === '') {
- return "0kb/s";
- }
- let size = parseFloat(value);
- size = size.toFixed(2);
- return size + 'kb/s';
- }
- function fpsStatus(fps) {
- let result = 0;
- if (fps >= 24) {
- result = 2;
- } else if (fps >= 15) {
- result = 1;
- }
- return result;
- }
- function createEmptyImageBitmap(width, height) {
- const $canvasElement = document.createElement("canvas");
- $canvasElement.width = width;
- $canvasElement.height = height;
- return window.createImageBitmap($canvasElement, 0, 0, width, height);
- }
- function supportMSE() {
- return window.MediaSource && window.MediaSource.isTypeSupported(MP4_CODECS.avc);
- }
- function supportMediaStreamTrack() {
- return window.MediaStreamTrackGenerator && typeof window.MediaStreamTrackGenerator === 'function';
- }
- function isEmpty(value) {
- return value === null || value === undefined;
- }
- function isBoolean(value) {
- return value === true || value === false;
- }
- function isNotEmpty(value) {
- return !isEmpty(value);
- }
- function initPlayTimes() {
- return {
- playInitStart: '',
- //1
- playStart: '',
- // 2
- streamStart: '',
- //3
- streamResponse: '',
- // 4
- demuxStart: '',
- // 5
- decodeStart: '',
- // 6
- videoStart: '',
- // 7
- playTimestamp: '',
- // playStart- playInitStart
- streamTimestamp: '',
- // streamStart - playStart
- streamResponseTimestamp: '',
- // streamResponse - streamStart
- demuxTimestamp: '',
- // demuxStart - streamResponse
- decodeTimestamp: '',
- // decodeStart - demuxStart
- videoTimestamp: '',
- // videoStart - decodeStart
- allTimestamp: '' // videoStart - playInitStart
- };
- } // create watermark
- function formatTimeTips(time) {
- var result; //
- if (time > -1) {
- var hour = Math.floor(time / 3600);
- var min = Math.floor(time / 60) % 60;
- var sec = time % 60;
- sec = Math.round(sec);
- if (hour < 10) {
- result = '0' + hour + ":";
- } else {
- result = hour + ":";
- }
- if (min < 10) {
- result += "0";
- }
- result += min + ":";
- if (sec < 10) {
- result += "0";
- }
- result += sec.toFixed(0);
- }
- return result;
- }
- var events$1 = (player => {
- try {
- const screenfullChange = () => {
- player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen); // 如果不是fullscreen,则触发下 resize 方法
- if (!player.fullscreen) {
- player.resize();
- } else {
- if (player._opt.useMSE) {
- player.resize();
- }
- }
- };
- screenfull.on('change', screenfullChange);
- player.events.destroys.push(() => {
- screenfull.off('change', screenfullChange);
- });
- } catch (error) {//
- } //
- player.on(EVENTS.decoderWorkerInit, () => {
- player.debug.log('player', 'has loaded');
- player.loaded = true;
- }); //
- player.on(EVENTS.play, () => {
- player.loading = false;
- }); //
- player.on(EVENTS.fullscreen, value => {
- if (value) {
- try {
- screenfull.request(player.$container).then(() => {}).catch(e => {
- player.webFullscreen = true;
- });
- } catch (e) {
- player.webFullscreen = true;
- }
- } else {
- try {
- screenfull.exit().then(() => {}).catch(() => {
- player.webFullscreen = false;
- });
- } catch (e) {
- player.webFullscreen = false;
- }
- }
- });
- player.on(EVENTS.webFullscreen, value => {
- if (value) {
- player.$container.classList.add('jessibuca-fullscreen-web');
- } else {
- player.$container.classList.remove('jessibuca-fullscreen-web');
- }
- }); //
- player.on(EVENTS.resize, () => {
- player.video.resize();
- });
- if (player._opt.debug) {
- const ignoreList = [EVENTS.timeUpdate];
- Object.keys(EVENTS).forEach(key => {
- player.on(EVENTS[key], value => {
- if (ignoreList.includes(key)) {
- return;
- }
- player.debug.log('player events', EVENTS[key], value);
- });
- });
- Object.keys(EVENTS_ERROR).forEach(key => {
- player.on(EVENTS_ERROR[key], value => {
- player.debug.log('player event error', EVENTS_ERROR[key], value);
- });
- });
- }
- });
- class Emitter {
- on(name, fn, ctx) {
- const e = this.e || (this.e = {});
- (e[name] || (e[name] = [])).push({
- fn,
- ctx
- });
- return this;
- }
- once(name, fn, ctx) {
- const self = this;
- function listener() {
- self.off(name, listener);
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
- args[_key] = arguments[_key];
- }
- fn.apply(ctx, args);
- }
- listener._ = fn;
- return this.on(name, listener, ctx);
- }
- emit(name) {
- const evtArr = ((this.e || (this.e = {}))[name] || []).slice();
- for (var _len2 = arguments.length, data = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
- data[_key2 - 1] = arguments[_key2];
- }
- for (let i = 0; i < evtArr.length; i += 1) {
- evtArr[i].fn.apply(evtArr[i].ctx, data);
- }
- return this;
- }
- off(name, callback) {
- const e = this.e || (this.e = {});
- if (!name) {
- Object.keys(e).forEach(key => {
- delete e[key];
- });
- delete this.e;
- return;
- }
- const evts = e[name];
- const liveEvents = [];
- if (evts && callback) {
- for (let i = 0, len = evts.length; i < len; i += 1) {
- if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i]);
- }
- }
- if (liveEvents.length) {
- e[name] = liveEvents;
- } else {
- delete e[name];
- }
- return this;
- }
- }
- var createWebGL = ((gl, openWebglAlignment) => {
- var vertexShaderScript = ['attribute vec4 vertexPos;', 'attribute vec4 texturePos;', 'varying vec2 textureCoord;', 'void main()', '{', 'gl_Position = vertexPos;', 'textureCoord = texturePos.xy;', '}'].join('\n');
- var fragmentShaderScript = ['precision highp float;', 'varying highp vec2 textureCoord;', 'uniform sampler2D ySampler;', 'uniform sampler2D uSampler;', 'uniform sampler2D vSampler;', 'const mat4 YUV2RGB = mat4', '(', '1.1643828125, 0, 1.59602734375, -.87078515625,', '1.1643828125, -.39176171875, -.81296875, .52959375,', '1.1643828125, 2.017234375, 0, -1.081390625,', '0, 0, 0, 1', ');', 'void main(void) {', 'highp float y = texture2D(ySampler, textureCoord).r;', 'highp float u = texture2D(uSampler, textureCoord).r;', 'highp float v = texture2D(vSampler, textureCoord).r;', 'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', '}'].join('\n');
- if (openWebglAlignment) {
- gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
- }
- var vertexShader = gl.createShader(gl.VERTEX_SHADER);
- gl.shaderSource(vertexShader, vertexShaderScript);
- gl.compileShader(vertexShader);
- if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
- // console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader));
- }
- var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
- gl.shaderSource(fragmentShader, fragmentShaderScript);
- gl.compileShader(fragmentShader);
- if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
- // console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader));
- }
- var program = gl.createProgram();
- gl.attachShader(program, vertexShader);
- gl.attachShader(program, fragmentShader);
- gl.linkProgram(program);
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
- // console.log('Program failed to compile: ' + gl.getProgramInfoLog(program));
- }
- gl.useProgram(program); // initBuffers
- var vertexPosBuffer = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);
- var vertexPosRef = gl.getAttribLocation(program, 'vertexPos');
- gl.enableVertexAttribArray(vertexPosRef);
- gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0);
- var texturePosBuffer = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
- var texturePosRef = gl.getAttribLocation(program, 'texturePos');
- gl.enableVertexAttribArray(texturePosRef);
- gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0);
- function _initTexture(name, index) {
- var textureRef = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, textureRef);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- gl.bindTexture(gl.TEXTURE_2D, null);
- gl.uniform1i(gl.getUniformLocation(program, name), index);
- return textureRef;
- }
- var yTextureRef = _initTexture('ySampler', 0);
- var uTextureRef = _initTexture('uSampler', 1);
- var vTextureRef = _initTexture('vSampler', 2);
- return {
- render: function (w, h, y, u, v) {
- gl.viewport(0, 0, w, h);
- gl.activeTexture(gl.TEXTURE0);
- gl.bindTexture(gl.TEXTURE_2D, yTextureRef);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, y);
- gl.activeTexture(gl.TEXTURE1);
- gl.bindTexture(gl.TEXTURE_2D, uTextureRef);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, u);
- gl.activeTexture(gl.TEXTURE2);
- gl.bindTexture(gl.TEXTURE_2D, vTextureRef);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, v);
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
- },
- destroy: function () {
- try {
- gl.deleteProgram(program);
- gl.deleteBuffer(vertexPosBuffer);
- gl.deleteBuffer(texturePosBuffer);
- gl.deleteTexture(yTextureRef);
- gl.deleteTexture(uTextureRef);
- gl.deleteBuffer(vTextureRef);
- } catch (e) {//// console.error(e);
- }
- }
- };
- });
- class CommonLoader$1 extends Emitter {
- constructor() {
- super();
- this.init = false;
- }
- resetInit() {
- this.init = false;
- this.videoInfo = {
- width: '',
- height: '',
- encType: '',
- encTypeCode: ''
- };
- }
- destroy() {
- this.resetInit();
- this.player.$container.removeChild(this.$videoElement);
- this.off();
- } //
- updateVideoInfo(data) {
- if (data.encTypeCode) {
- this.videoInfo.encType = VIDEO_ENC_TYPE[data.encTypeCode];
- }
- if (data.width) {
- this.videoInfo.width = data.width;
- }
- if (data.height) {
- this.videoInfo.height = data.height;
- } // video 基本信息
- if (this.videoInfo.encType && this.videoInfo.height && this.videoInfo.width && !this.init) {
- this.player.emit(EVENTS.videoInfo, this.videoInfo);
- this.init = true;
- }
- }
- play() {}
- pause() {}
- clearView() {}
- }
- class CanvasVideoLoader extends CommonLoader$1 {
- constructor(player) {
- super();
- this.player = player;
- const $canvasElement = document.createElement("canvas");
- $canvasElement.style.position = "absolute";
- $canvasElement.style.top = 0;
- $canvasElement.style.left = 0;
- this.$videoElement = $canvasElement;
- player.$container.appendChild(this.$videoElement);
- this.context2D = null;
- this.contextGl = null;
- this.contextGlRender = null;
- this.contextGlDestroy = null;
- this.bitmaprenderer = null;
- this.renderType = null;
- this.videoInfo = {
- width: '',
- height: '',
- encType: ''
- }; //
- this._initCanvasRender();
- this.player.debug.log('CanvasVideo', 'init');
- }
- destroy() {
- super.destroy();
- if (this.contextGl) {
- this.contextGl = null;
- }
- if (this.context2D) {
- this.context2D = null;
- }
- if (this.contextGlRender) {
- this.contextGlDestroy && this.contextGlDestroy();
- this.contextGlDestroy = null;
- this.contextGlRender = null;
- }
- if (this.bitmaprenderer) {
- this.bitmaprenderer = null;
- }
- this.renderType = null;
- this.player.debug.log(`CanvasVideoLoader`, 'destroy');
- }
- _initContextGl() {
- this.contextGl = createContextGL(this.$videoElement);
- const webgl = createWebGL(this.contextGl, this.player._opt.openWebglAlignment);
- this.contextGlRender = webgl.render;
- this.contextGlDestroy = webgl.destroy;
- }
- _initContext2D() {
- this.context2D = this.$videoElement.getContext('2d');
- } // 渲染类型
- _initCanvasRender() {
- if (this.player._opt.useWCS && !this._supportOffscreen()) {
- this.renderType = CANVAS_RENDER_TYPE.webcodecs;
- this._initContext2D();
- } else if (this._supportOffscreen()) {
- this.renderType = CANVAS_RENDER_TYPE.offscreen;
- this._bindOffscreen();
- } else {
- this.renderType = CANVAS_RENDER_TYPE.webgl;
- this._initContextGl();
- }
- }
- _supportOffscreen() {
- return supportOffscreen(this.$videoElement) && this.player._opt.useOffscreen;
- } //
- _bindOffscreen() {
- this.bitmaprenderer = this.$videoElement.getContext('bitmaprenderer');
- }
- initCanvasViewSize() {
- this.$videoElement.width = this.videoInfo.width;
- this.$videoElement.height = this.videoInfo.height;
- this.resize();
- } //
- render(msg) {
- this.player.videoTimestamp = msg.ts;
- switch (this.renderType) {
- case CANVAS_RENDER_TYPE.offscreen:
- this.bitmaprenderer.transferFromImageBitmap(msg.buffer);
- break;
- case CANVAS_RENDER_TYPE.webgl:
- this.contextGlRender(this.$videoElement.width, this.$videoElement.height, msg.output[0], msg.output[1], msg.output[2]);
- break;
- case CANVAS_RENDER_TYPE.webcodecs:
- this.context2D.drawImage(msg.videoFrame, 0, 0, this.$videoElement.width, this.$videoElement.height);
- break;
- }
- }
- screenshot(filename, format, quality, type) {
- filename = filename || now();
- type = type || SCREENSHOT_TYPE.download;
- const formatType = {
- png: 'image/png',
- jpeg: 'image/jpeg',
- webp: 'image/webp'
- };
- let encoderOptions = 0.92;
- if (!formatType[format] && SCREENSHOT_TYPE[format]) {
- type = format;
- format = 'png';
- quality = undefined;
- }
- if (typeof quality === "string") {
- type = quality;
- quality = undefined;
- }
- if (typeof quality !== 'undefined') {
- encoderOptions = Number(quality);
- }
- const dataURL = this.$videoElement.toDataURL(formatType[format] || formatType.png, encoderOptions);
- const file = dataURLToFile(dataURL);
- if (type === SCREENSHOT_TYPE.base64) {
- return dataURL;
- } else if (type === SCREENSHOT_TYPE.blob) {
- return file;
- } else if (type === SCREENSHOT_TYPE.download) {
- downloadImg(file, filename);
- }
- } //
- clearView() {
- switch (this.renderType) {
- case CANVAS_RENDER_TYPE.offscreen:
- createEmptyImageBitmap(this.$videoElement.width, this.$videoElement.height).then(imageBitMap => {
- this.bitmaprenderer.transferFromImageBitmap(imageBitMap);
- });
- break;
- case CANVAS_RENDER_TYPE.webgl:
- this.contextGl.clear(this.contextGl.COLOR_BUFFER_BIT);
- break;
- case CANVAS_RENDER_TYPE.webcodecs:
- this.context2D.clearRect(0, 0, this.$videoElement.width, this.$videoElement.height);
- break;
- }
- }
- resize() {
- this.player.debug.log('canvasVideo', 'resize');
- const option = this.player._opt;
- let width = this.player.width;
- let height = this.player.height;
- if (option.hasControl && !option.controlAutoHide) {
- if (isMobile() && this.player.fullscreen) {
- width -= CONTROL_HEIGHT;
- } else {
- height -= CONTROL_HEIGHT;
- }
- }
- let resizeWidth = this.$videoElement.width;
- let resizeHeight = this.$videoElement.height;
- const rotate = option.rotate;
- let left = (width - resizeWidth) / 2;
- let top = (height - resizeHeight) / 2;
- if (rotate === 270 || rotate === 90) {
- resizeWidth = this.$videoElement.height;
- resizeHeight = this.$videoElement.width;
- }
- const wScale = width / resizeWidth;
- const hScale = height / resizeHeight;
- let scale = wScale > hScale ? hScale : wScale; //
- if (!option.isResize) {
- if (wScale !== hScale) {
- scale = wScale + ',' + hScale;
- }
- } //
- if (option.isFullResize) {
- scale = wScale > hScale ? wScale : hScale;
- }
- let transform = "scale(" + scale + ")";
- if (rotate) {
- transform += ' rotate(' + rotate + 'deg)';
- }
- this.$videoElement.style.transform = transform;
- this.$videoElement.style.left = left + "px";
- this.$videoElement.style.top = top + "px";
- }
- }
- class VideoLoader extends CommonLoader$1 {
- constructor(player) {
- super();
- this.player = player;
- const $videoElement = document.createElement('video');
- $videoElement.muted = true;
- $videoElement.style.position = "absolute";
- $videoElement.style.top = 0;
- $videoElement.style.left = 0;
- player.$container.appendChild($videoElement);
- this.$videoElement = $videoElement;
- this.videoInfo = {
- width: '',
- height: '',
- encType: ''
- };
- const _opt = this.player._opt;
- if (_opt.useWCS && _opt.wcsUseVideoRender) {
- this.trackGenerator = new MediaStreamTrackGenerator({
- kind: 'video'
- });
- $videoElement.srcObject = new MediaStream([this.trackGenerator]);
- this.vwriter = this.trackGenerator.writable.getWriter();
- }
- this.resize();
- const {
- proxy
- } = this.player.events;
- proxy(this.$videoElement, 'canplay', () => {
- this.player.debug.log('Video', 'canplay');
- });
- proxy(this.$videoElement, 'waiting', () => {
- this.player.emit(EVENTS.videoWaiting);
- });
- proxy(this.$videoElement, 'timeupdate', event => {// this.player.emit(EVENTS.videoTimeUpdate, event.timeStamp);
- });
- this.player.debug.log('Video', 'init');
- }
- destroy() {
- super.destroy();
- if (this.$videoElement) {
- this.$videoElement.src = '';
- this.$videoElement = null;
- }
- if (this.trackGenerator) {
- this.trackGenerator = null;
- }
- if (this.vwriter) {
- this.trackGenerator = null;
- }
- this.player.debug.log('Video', 'destroy');
- }
- play() {
- // this.$videoElement.autoplay = true;
- this.$videoElement.play();
- }
- clearView() {}
- screenshot(filename, format, quality, type) {
- filename = filename || now();
- type = type || SCREENSHOT_TYPE.download;
- const formatType = {
- png: 'image/png',
- jpeg: 'image/jpeg',
- webp: 'image/webp'
- };
- let encoderOptions = 0.92;
- if (!formatType[format] && SCREENSHOT_TYPE[format]) {
- type = format;
- format = 'png';
- quality = undefined;
- }
- if (typeof quality === "string") {
- type = quality;
- quality = undefined;
- }
- if (typeof quality !== 'undefined') {
- encoderOptions = Number(quality);
- }
- const $video = this.$videoElement;
- let canvas = document.createElement('canvas');
- canvas.width = $video.videoWidth;
- canvas.height = $video.videoHeight;
- const context = canvas.getContext('2d');
- context.drawImage($video, 0, 0, canvas.width, canvas.height);
- const dataURL = canvas.toDataURL(SCREENSHOT_TYPE[format] || SCREENSHOT_TYPE.png, encoderOptions);
- const file = dataURLToFile(dataURL);
- if (type === SCREENSHOT_TYPE.base64) {
- return dataURL;
- } else if (type === SCREENSHOT_TYPE.blob) {
- return file;
- } else if (type === SCREENSHOT_TYPE.download) {
- downloadImg(file, filename);
- }
- }
- initCanvasViewSize() {
- this.resize();
- } //
- render(msg) {
- if (this.vwriter) {
- this.vwriter.write(msg.videoFrame);
- }
- }
- resize() {
- let width = this.player.width;
- let height = this.player.height;
- const option = this.player._opt;
- const rotate = option.rotate;
- if (option.hasControl && !option.controlAutoHide) {
- if (isMobile() && this.player.fullscreen) {
- width -= CONTROL_HEIGHT;
- } else {
- height -= CONTROL_HEIGHT;
- }
- }
- this.$videoElement.width = width;
- this.$videoElement.height = height;
- if (rotate === 270 || rotate === 90) {
- this.$videoElement.width = height;
- this.$videoElement.height = width;
- }
- let resizeWidth = this.$videoElement.width;
- let resizeHeight = this.$videoElement.height;
- let left = (width - resizeWidth) / 2;
- let top = (height - resizeHeight) / 2;
- let objectFill = 'contain'; // 默认是true
- // 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边
- // 视频画面完全填充canvas区域,画面会被拉伸
- if (!option.isResize) {
- objectFill = 'fill';
- } // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
- if (option.isFullResize) {
- objectFill = 'none';
- }
- this.$videoElement.style.objectFit = objectFill;
- this.$videoElement.style.transform = 'rotate(' + rotate + 'deg)';
- this.$videoElement.style.left = left + "px";
- this.$videoElement.style.top = top + "px";
- }
- }
- class Video {
- constructor(player) {
- const Loader = Video.getLoaderFactory(player._opt);
- return new Loader(player);
- }
- static getLoaderFactory(opt) {
- if (opt.useMSE || opt.useWCS && !opt.useOffscreen && opt.wcsUseVideoRender) {
- return VideoLoader;
- } else {
- return CanvasVideoLoader;
- }
- }
- }
- class AudioContextLoader extends Emitter {
- constructor(player) {
- super();
- this.bufferList = [];
- this.player = player;
- this.scriptNode = null;
- this.hasInitScriptNode = false;
- this.audioContextChannel = null;
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); //
- this.gainNode = this.audioContext.createGain(); // Get an AudioBufferSourceNode.
- // This is the AudioNode to use when we want to play an AudioBuffer
- const source = this.audioContext.createBufferSource(); // set the buffer in the AudioBufferSourceNode
- source.buffer = this.audioContext.createBuffer(1, 1, 22050); // connect the AudioBufferSourceNode to the
- // destination so we can hear the sound
- source.connect(this.audioContext.destination); // noteOn as start
- // start the source playing
- if (source.noteOn) {
- source.noteOn(0);
- } else {
- source.start(0);
- }
- this.audioBufferSourceNode = source; //
- this.mediaStreamAudioDestinationNode = this.audioContext.createMediaStreamDestination(); //
- this.audioEnabled(true); // default setting 0
- this.gainNode.gain.value = 0;
- this.playing = false; //
- this.audioSyncVideoOption = {
- diff: null
- };
- this.audioInfo = {
- encType: '',
- channels: '',
- sampleRate: ''
- };
- this.init = false;
- this.hasAudio = false; // update
- this.on(EVENTS.videoSyncAudio, options => {
- // this.player.debug.log('AudioContext', `videoSyncAudio , audioTimestamp: ${options.audioTimestamp},videoTimestamp: ${options.videoTimestamp},diff:${options.diff}`)
- this.audioSyncVideoOption = options;
- });
- this.player.debug.log('AudioContext', 'init');
- }
- resetInit() {
- this.init = false;
- this.audioInfo = {
- encType: '',
- channels: '',
- sampleRate: ''
- };
- }
- destroy() {
- this.closeAudio();
- this.resetInit();
- this.audioContext.close();
- this.audioContext = null;
- this.gainNode = null;
- this.hasAudio = false;
- this.playing = false;
- if (this.scriptNode) {
- this.scriptNode.onaudioprocess = noop;
- this.scriptNode = null;
- }
- this.audioBufferSourceNode = null;
- this.mediaStreamAudioDestinationNode = null;
- this.hasInitScriptNode = false;
- this.audioSyncVideoOption = {
- diff: null
- };
- this.off();
- this.player.debug.log('AudioContext', 'destroy');
- }
- updateAudioInfo(data) {
- if (data.encTypeCode) {
- this.audioInfo.encType = AUDIO_ENC_TYPE[data.encTypeCode];
- }
- if (data.channels) {
- this.audioInfo.channels = data.channels;
- }
- if (data.sampleRate) {
- this.audioInfo.sampleRate = data.sampleRate;
- } // audio 基本信息
- if (this.audioInfo.sampleRate && this.audioInfo.channels && this.audioInfo.encType && !this.init) {
- this.player.emit(EVENTS.audioInfo, this.audioInfo);
- this.init = true;
- }
- } //
- get isPlaying() {
- return this.playing;
- }
- get isMute() {
- return this.gainNode.gain.value === 0 || this.isStateSuspended();
- }
- get volume() {
- return this.gainNode.gain.value;
- }
- get bufferSize() {
- return this.bufferList.length;
- }
- initScriptNode() {
- this.playing = true;
- if (this.hasInitScriptNode) {
- return;
- }
- const channels = this.audioInfo.channels;
- const scriptNode = this.audioContext.createScriptProcessor(1024, 0, channels); // tips: if audio isStateSuspended onaudioprocess method not working
- scriptNode.onaudioprocess = audioProcessingEvent => {
- const outputBuffer = audioProcessingEvent.outputBuffer;
- if (this.bufferList.length && this.playing) {
- // just for wasm
- if (!this.player._opt.useWCS && !this.player._opt.useMSE) {
- // audio > video
- // wait
- if (this.audioSyncVideoOption.diff > AUDIO_SYNC_VIDEO_DIFF) {
- this.player.debug.warn('AudioContext', `audioSyncVideoOption more than diff :${this.audioSyncVideoOption.diff}, waiting`); // wait
- return;
- } // audio < video
- // throw away then chase video
- else if (this.audioSyncVideoOption.diff < -AUDIO_SYNC_VIDEO_DIFF) {
- this.player.debug.warn('AudioContext', `audioSyncVideoOption less than diff :${this.audioSyncVideoOption.diff}, dropping`); //
- let bufferItem = this.bufferList.shift(); //
- while (bufferItem.ts - this.player.videoTimestamp < -AUDIO_SYNC_VIDEO_DIFF && this.bufferList.length > 0) {
- // this.player.debug.warn('AudioContext', `audioSyncVideoOption less than inner ts is:${bufferItem.ts}, videoTimestamp is ${this.player.videoTimestamp},diff:${bufferItem.ts - this.player.videoTimestamp}`)
- bufferItem = this.bufferList.shift();
- }
- if (this.bufferList.length === 0) {
- return;
- }
- }
- }
- if (this.bufferList.length === 0) {
- return;
- }
- const bufferItem = this.bufferList.shift(); // update audio time stamp
- if (bufferItem && bufferItem.ts) {
- this.player.audioTimestamp = bufferItem.ts;
- }
- for (let channel = 0; channel < channels; channel++) {
- const b = bufferItem.buffer[channel];
- const nowBuffering = outputBuffer.getChannelData(channel);
- for (let i = 0; i < 1024; i++) {
- nowBuffering[i] = b[i] || 0;
- }
- }
- }
- };
- scriptNode.connect(this.gainNode);
- this.scriptNode = scriptNode;
- this.gainNode.connect(this.audioContext.destination);
- this.gainNode.connect(this.mediaStreamAudioDestinationNode);
- this.hasInitScriptNode = true;
- }
- mute(flag) {
- if (flag) {
- if (!this.isMute) {
- this.player.emit(EVENTS.mute, flag);
- }
- this.setVolume(0);
- this.audioEnabled(false);
- this.clear();
- } else {
- if (this.isMute) {
- this.player.emit(EVENTS.mute, flag);
- }
- this.setVolume(0.5);
- this.audioEnabled(true);
- }
- }
- setVolume(volume) {
- volume = parseFloat(volume).toFixed(2);
- if (isNaN(volume)) {
- return;
- }
- this.audioEnabled(true);
- volume = clamp(volume, 0, 1);
- this.gainNode.gain.value = volume;
- this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
- this.player.emit(EVENTS.volumechange, this.player.volume);
- }
- closeAudio() {
- if (this.hasInitScriptNode) {
- this.scriptNode && this.scriptNode.disconnect(this.gainNode);
- this.gainNode && this.gainNode.disconnect(this.audioContext.destination);
- this.gainNode && this.gainNode.disconnect(this.mediaStreamAudioDestinationNode);
- }
- this.clear();
- } // 是否播放。。。
- audioEnabled(flag) {
- if (flag) {
- if (this.audioContext.state === 'suspended') {
- // resume
- this.audioContext.resume();
- }
- } else {
- if (this.audioContext.state === 'running') {
- // suspend
- this.audioContext.suspend();
- }
- }
- }
- isStateRunning() {
- return this.audioContext.state === 'running';
- }
- isStateSuspended() {
- return this.audioContext.state === 'suspended';
- }
- clear() {
- this.bufferList = [];
- }
- play(buffer, ts) {
- // if is mute
- if (this.isMute) {
- return;
- }
- this.hasAudio = true;
- this.bufferList.push({
- buffer,
- ts
- });
- if (this.bufferList.length > 20) {
- this.player.debug.warn('AudioContext', `bufferList is large: ${this.bufferList.length}`); // out of memory
- if (this.bufferList.length > 50) {
- this.bufferList.shift();
- }
- } // this.player.debug.log('AudioContext', `bufferList is ${this.bufferList.length}`)
- }
- pause() {
- this.audioSyncVideoOption = {
- diff: null
- };
- this.playing = false;
- this.clear();
- }
- resume() {
- this.playing = true;
- }
- }
- class Audio {
- constructor(player) {
- const Loader = Audio.getLoaderFactory();
- return new Loader(player);
- }
- static getLoaderFactory() {
- return AudioContextLoader;
- }
- }
- class FetchLoader extends Emitter {
- constructor(player) {
- super();
- this.player = player;
- this.playing = false;
- this.abortController = new AbortController(); //
- this.streamRate = calculationRate(rate => {
- player.emit(EVENTS.kBps, (rate / 1024).toFixed(2));
- });
- player.debug.log('FetchStream', 'init');
- }
- destroy() {
- this.abort();
- this.off();
- this.streamRate = null;
- this.player.debug.log('FetchStream', 'destroy');
- }
- fetchStream(url) {
- const {
- demux
- } = this.player;
- this.player._times.streamStart = now();
- fetch(url, {
- signal: this.abortController.signal
- }).then(res => {
- const reader = res.body.getReader();
- this.emit(EVENTS.streamSuccess);
- const fetchNext = () => {
- reader.read().then(_ref => {
- let {
- done,
- value
- } = _ref;
- if (done) {
- demux.close();
- } else {
- this.streamRate && this.streamRate(value.byteLength);
- demux.dispatch(value);
- fetchNext();
- }
- }).catch(e => {
- demux.close(); // 这边会报用户 aborted a request 错误。
- this.emit(EVENTS_ERROR.fetchError, e);
- this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError);
- this.abort();
- });
- };
- fetchNext();
- }).catch(e => {
- this.abort();
- this.emit(EVENTS_ERROR.fetchError, e);
- this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError);
- });
- }
- abort() {
- if (this.abortController) {
- this.abortController.abort();
- this.abortController = null;
- }
- }
- }
- class WebsocketLoader extends Emitter {
- constructor(player) {
- super();
- this.player = player;
- this.socket = null;
- this.socketStatus = WEBSOCKET_STATUS.notConnect;
- this.wsUrl = null; //
- this.streamRate = calculationRate(rate => {
- player.emit(EVENTS.kBps, (rate / 1024).toFixed(2));
- });
- player.debug.log('WebsocketLoader', 'init');
- }
- destroy() {
- if (this.socket) {
- this.socket.close();
- this.socket = null;
- }
- this.socketStatus = WEBSOCKET_STATUS.notConnect;
- this.streamRate = null;
- this.wsUrl = null;
- this.off();
- this.player.debug.log('websocketLoader', 'destroy');
- }
- _createWebSocket() {
- const player = this.player;
- const {
- debug,
- events: {
- proxy
- },
- demux
- } = player;
- this.socket = new WebSocket(this.wsUrl);
- this.socket.binaryType = 'arraybuffer';
- proxy(this.socket, 'open', () => {
- this.emit(EVENTS.streamSuccess);
- debug.log('websocketLoader', 'socket open');
- this.socketStatus = WEBSOCKET_STATUS.open;
- });
- proxy(this.socket, 'message', event => {
- this.streamRate && this.streamRate(event.data.byteLength);
- this._handleMessage(event.data);
- });
- proxy(this.socket, 'close', () => {
- debug.log('websocketLoader', 'socket close');
- this.emit(EVENTS.streamEnd);
- this.socketStatus = WEBSOCKET_STATUS.close;
- });
- proxy(this.socket, 'error', error => {
- debug.log('websocketLoader', 'socket error');
- this.emit(EVENTS_ERROR.websocketError, error);
- this.player.emit(EVENTS.error, EVENTS_ERROR.websocketError);
- this.socketStatus = WEBSOCKET_STATUS.error;
- demux.close();
- debug.log('websocketLoader', `socket error:`, error);
- });
- } //
- _handleMessage(message) {
- const {
- demux
- } = this.player;
- if (!demux) {
- this.player.debug.warn('websocketLoader', 'websocket handle message demux is null');
- return;
- }
- demux.dispatch(message);
- }
- fetchStream(url) {
- this.player._times.streamStart = now();
- this.wsUrl = url;
- this._createWebSocket();
- }
- }
- class Stream {
- constructor(player) {
- const Loader = Stream.getLoaderFactory(player._opt.protocol);
- return new Loader(player);
- }
- static getLoaderFactory(protocol) {
- if (protocol === PLAYER_PLAY_PROTOCOL.fetch) {
- return FetchLoader;
- } else if (protocol === PLAYER_PLAY_PROTOCOL.websocket) {
- return WebsocketLoader;
- }
- }
- }
- var RecordRTC_1 = createCommonjsModule(function (module) {
- // Last time updated: 2021-03-09 3:20:22 AM UTC
- // ________________
- // RecordRTC v5.6.2
- // Open-Sourced: https://github.com/muaz-khan/RecordRTC
- // --------------------------------------------------
- // Muaz Khan - www.MuazKhan.com
- // MIT License - www.WebRTC-Experiment.com/licence
- // --------------------------------------------------
- // ____________
- // RecordRTC.js
- /**
- * {@link https://github.com/muaz-khan/RecordRTC|RecordRTC} is a WebRTC JavaScript library for audio/video as well as screen activity recording. It supports Chrome, Firefox, Opera, Android, and Microsoft Edge. Platforms: Linux, Mac and Windows.
- * @summary Record audio, video or screen inside the browser.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef RecordRTC
- * @class
- * @example
- * var recorder = RecordRTC(mediaStream or [arrayOfMediaStream], {
- * type: 'video', // audio or video or gif or canvas
- * recorderType: MediaStreamRecorder || CanvasRecorder || StereoAudioRecorder || Etc
- * });
- * recorder.startRecording();
- * @see For further information:
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc.
- * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, desiredSampRate: 16000, video: HTMLVideoElement, etc.}
- */
- function RecordRTC(mediaStream, config) {
- if (!mediaStream) {
- throw 'First parameter is required.';
- }
- config = config || {
- type: 'video'
- };
- config = new RecordRTCConfiguration(mediaStream, config);
- // a reference to user's recordRTC object
- var self = this;
- function startRecording(config2) {
- if (!config.disableLogs) {
- // console.log('RecordRTC version: ', self.version);
- }
- if (!!config2) {
- // allow users to set options using startRecording method
- // config2 is similar to main "config" object (second parameter over RecordRTC constructor)
- config = new RecordRTCConfiguration(mediaStream, config2);
- }
- if (!config.disableLogs) {
- // console.log('started recording ' + config.type + ' stream.');
- }
- if (mediaRecorder) {
- mediaRecorder.clearRecordedData();
- mediaRecorder.record();
- setState('recording');
- if (self.recordingDuration) {
- handleRecordingDuration();
- }
- return self;
- }
- initRecorder(function() {
- if (self.recordingDuration) {
- handleRecordingDuration();
- }
- });
- return self;
- }
- function initRecorder(initCallback) {
- if (initCallback) {
- config.initCallback = function() {
- initCallback();
- initCallback = config.initCallback = null; // recorder.initRecorder should be call-backed once.
- };
- }
- var Recorder = new GetRecorderType(mediaStream, config);
- mediaRecorder = new Recorder(mediaStream, config);
- mediaRecorder.record();
- setState('recording');
- if (!config.disableLogs) {
- // console.log('Initialized recorderType:', mediaRecorder.constructor.name, 'for output-type:', config.type);
- }
- }
- function stopRecording(callback) {
- callback = callback || function() {};
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- if (self.state === 'paused') {
- self.resumeRecording();
- setTimeout(function() {
- stopRecording(callback);
- }, 1);
- return;
- }
- if (self.state !== 'recording' && !config.disableLogs) {
- // console.warn('Recording state should be: "recording", however current state is: ', self.state);
- }
- if (!config.disableLogs) {
- // console.log('Stopped recording ' + config.type + ' stream.');
- }
- if (config.type !== 'gif') {
- mediaRecorder.stop(_callback);
- } else {
- mediaRecorder.stop();
- _callback();
- }
- setState('stopped');
- function _callback(__blob) {
- if (!mediaRecorder) {
- if (typeof callback.call === 'function') {
- callback.call(self, '');
- } else {
- callback('');
- }
- return;
- }
- Object.keys(mediaRecorder).forEach(function(key) {
- if (typeof mediaRecorder[key] === 'function') {
- return;
- }
- self[key] = mediaRecorder[key];
- });
- var blob = mediaRecorder.blob;
- if (!blob) {
- if (__blob) {
- mediaRecorder.blob = blob = __blob;
- } else {
- throw 'Recording failed.';
- }
- }
- if (blob && !config.disableLogs) {
- // console.log(blob.type, '->', bytesToSize(blob.size));
- }
- if (callback) {
- var url;
- try {
- url = URL.createObjectURL(blob);
- } catch (e) {}
- if (typeof callback.call === 'function') {
- callback.call(self, url);
- } else {
- callback(url);
- }
- }
- if (!config.autoWriteToDisk) {
- return;
- }
- getDataURL(function(dataURL) {
- var parameter = {};
- parameter[config.type + 'Blob'] = dataURL;
- DiskStorage.Store(parameter);
- });
- }
- }
- function pauseRecording() {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- if (self.state !== 'recording') {
- if (!config.disableLogs) {
- // console.warn('Unable to pause the recording. Recording state: ', self.state);
- }
- return;
- }
- setState('paused');
- mediaRecorder.pause();
- if (!config.disableLogs) {
- // console.log('Paused recording.');
- }
- }
- function resumeRecording() {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- if (self.state !== 'paused') {
- if (!config.disableLogs) {
- // console.warn('Unable to resume the recording. Recording state: ', self.state);
- }
- return;
- }
- setState('recording');
- // not all libs have this method yet
- mediaRecorder.resume();
- if (!config.disableLogs) {
- // console.log('Resumed recording.');
- }
- }
- function readFile(_blob) {
- postMessage(new FileReaderSync().readAsDataURL(_blob));
- }
- function getDataURL(callback, _mediaRecorder) {
- if (!callback) {
- throw 'Pass a callback function over getDataURL.';
- }
- var blob = _mediaRecorder ? _mediaRecorder.blob : (mediaRecorder || {}).blob;
- if (!blob) {
- if (!config.disableLogs) {
- // console.warn('Blob encoder did not finish its job yet.');
- }
- setTimeout(function() {
- getDataURL(callback, _mediaRecorder);
- }, 1000);
- return;
- }
- if (typeof Worker !== 'undefined' && !navigator.mozGetUserMedia) {
- var webWorker = processInWebWorker(readFile);
- webWorker.onmessage = function(event) {
- callback(event.data);
- };
- webWorker.postMessage(blob);
- } else {
- var reader = new FileReader();
- reader.readAsDataURL(blob);
- reader.onload = function(event) {
- callback(event.target.result);
- };
- }
- function processInWebWorker(_function) {
- try {
- var blob = URL.createObjectURL(new Blob([_function.toString(),
- 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
- ], {
- type: 'application/javascript'
- }));
- var worker = new Worker(blob);
- URL.revokeObjectURL(blob);
- return worker;
- } catch (e) {}
- }
- }
- function handleRecordingDuration(counter) {
- counter = counter || 0;
- if (self.state === 'paused') {
- setTimeout(function() {
- handleRecordingDuration(counter);
- }, 1000);
- return;
- }
- if (self.state === 'stopped') {
- return;
- }
- if (counter >= self.recordingDuration) {
- stopRecording(self.onRecordingStopped);
- return;
- }
- counter += 1000; // 1-second
- setTimeout(function() {
- handleRecordingDuration(counter);
- }, 1000);
- }
- function setState(state) {
- if (!self) {
- return;
- }
- self.state = state;
- if (typeof self.onStateChanged.call === 'function') {
- self.onStateChanged.call(self, state);
- } else {
- self.onStateChanged(state);
- }
- }
- var WARNING = 'It seems that recorder is destroyed or "startRecording" is not invoked for ' + config.type + ' recorder.';
- function warningLog() {
- if (config.disableLogs === true) {
- return;
- }
- // console.warn(WARNING);
- }
- var mediaRecorder;
- var returnObject = {
- /**
- * This method starts the recording.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * var recorder = RecordRTC(mediaStream, {
- * type: 'video'
- * });
- * recorder.startRecording();
- */
- startRecording: startRecording,
- /**
- * This method stops the recording. It is strongly recommended to get "blob" or "URI" inside the callback to make sure all recorders finished their job.
- * @param {function} callback - Callback to get the recorded blob.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.stopRecording(function() {
- * // use either "this" or "recorder" object; both are identical
- * video.src = this.toURL();
- * var blob = this.getBlob();
- * });
- */
- stopRecording: stopRecording,
- /**
- * This method pauses the recording. You can resume recording using "resumeRecording" method.
- * @method
- * @memberof RecordRTC
- * @instance
- * @todo Firefox is unable to pause the recording. Fix it.
- * @example
- * recorder.pauseRecording(); // pause the recording
- * recorder.resumeRecording(); // resume again
- */
- pauseRecording: pauseRecording,
- /**
- * This method resumes the recording.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.pauseRecording(); // first of all, pause the recording
- * recorder.resumeRecording(); // now resume it
- */
- resumeRecording: resumeRecording,
- /**
- * This method initializes the recording.
- * @method
- * @memberof RecordRTC
- * @instance
- * @todo This method should be deprecated.
- * @example
- * recorder.initRecorder();
- */
- initRecorder: initRecorder,
- /**
- * Ask RecordRTC to auto-stop the recording after 5 minutes.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * var fiveMinutes = 5 * 1000 * 60;
- * recorder.setRecordingDuration(fiveMinutes, function() {
- * var blob = this.getBlob();
- * video.src = this.toURL();
- * });
- *
- * // or otherwise
- * recorder.setRecordingDuration(fiveMinutes).onRecordingStopped(function() {
- * var blob = this.getBlob();
- * video.src = this.toURL();
- * });
- */
- setRecordingDuration: function(recordingDuration, callback) {
- if (typeof recordingDuration === 'undefined') {
- throw 'recordingDuration is required.';
- }
- if (typeof recordingDuration !== 'number') {
- throw 'recordingDuration must be a number.';
- }
- self.recordingDuration = recordingDuration;
- self.onRecordingStopped = callback || function() {};
- return {
- onRecordingStopped: function(callback) {
- self.onRecordingStopped = callback;
- }
- };
- },
- /**
- * This method can be used to clear/reset all the recorded data.
- * @method
- * @memberof RecordRTC
- * @instance
- * @todo Figure out the difference between "reset" and "clearRecordedData" methods.
- * @example
- * recorder.clearRecordedData();
- */
- clearRecordedData: function() {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- mediaRecorder.clearRecordedData();
- if (!config.disableLogs) {
- // console.log('Cleared old recorded data.');
- }
- },
- /**
- * Get the recorded blob. Use this method inside the "stopRecording" callback.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.stopRecording(function() {
- * var blob = this.getBlob();
- *
- * var file = new File([blob], 'filename.webm', {
- * type: 'video/webm'
- * });
- *
- * var formData = new FormData();
- * formData.append('file', file); // upload "File" object rather than a "Blob"
- * uploadToServer(formData);
- * });
- * @returns {Blob} Returns recorded data as "Blob" object.
- */
- getBlob: function() {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- return mediaRecorder.blob;
- },
- /**
- * Get data-URI instead of Blob.
- * @param {function} callback - Callback to get the Data-URI.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.stopRecording(function() {
- * recorder.getDataURL(function(dataURI) {
- * video.src = dataURI;
- * });
- * });
- */
- getDataURL: getDataURL,
- /**
- * Get virtual/temporary URL. Usage of this URL is limited to current tab.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.stopRecording(function() {
- * video.src = this.toURL();
- * });
- * @returns {String} Returns a virtual/temporary URL for the recorded "Blob".
- */
- toURL: function() {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- return URL.createObjectURL(mediaRecorder.blob);
- },
- /**
- * Get internal recording object (i.e. internal module) e.g. MutliStreamRecorder, MediaStreamRecorder, StereoAudioRecorder or WhammyRecorder etc.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * var internalRecorder = recorder.getInternalRecorder();
- * if(internalRecorder instanceof MultiStreamRecorder) {
- * internalRecorder.addStreams([newAudioStream]);
- * internalRecorder.resetVideoStreams([screenStream]);
- * }
- * @returns {Object} Returns internal recording object.
- */
- getInternalRecorder: function() {
- return mediaRecorder;
- },
- /**
- * Invoke save-as dialog to save the recorded blob into your disk.
- * @param {string} fileName - Set your own file name.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.stopRecording(function() {
- * this.save('file-name');
- *
- * // or manually:
- * invokeSaveAsDialog(this.getBlob(), 'filename.webm');
- * });
- */
- save: function(fileName) {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- invokeSaveAsDialog(mediaRecorder.blob, fileName);
- },
- /**
- * This method gets a blob from indexed-DB storage.
- * @param {function} callback - Callback to get the recorded blob.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.getFromDisk(function(dataURL) {
- * video.src = dataURL;
- * });
- */
- getFromDisk: function(callback) {
- if (!mediaRecorder) {
- warningLog();
- return;
- }
- RecordRTC.getFromDisk(config.type, callback);
- },
- /**
- * This method appends an array of webp images to the recorded video-blob. It takes an "array" object.
- * @type {Array.<Array>}
- * @param {Array} arrayOfWebPImages - Array of webp images.
- * @method
- * @memberof RecordRTC
- * @instance
- * @todo This method should be deprecated.
- * @example
- * var arrayOfWebPImages = [];
- * arrayOfWebPImages.push({
- * duration: index,
- * image: 'data:image/webp;base64,...'
- * });
- * recorder.setAdvertisementArray(arrayOfWebPImages);
- */
- setAdvertisementArray: function(arrayOfWebPImages) {
- config.advertisement = [];
- var length = arrayOfWebPImages.length;
- for (var i = 0; i < length; i++) {
- config.advertisement.push({
- duration: i,
- image: arrayOfWebPImages[i]
- });
- }
- },
- /**
- * It is equivalent to <code class="str">"recorder.getBlob()"</code> method. Usage of "getBlob" is recommended, though.
- * @property {Blob} blob - Recorded Blob can be accessed using this property.
- * @memberof RecordRTC
- * @instance
- * @readonly
- * @example
- * recorder.stopRecording(function() {
- * var blob = this.blob;
- *
- * // below one is recommended
- * var blob = this.getBlob();
- * });
- */
- blob: null,
- /**
- * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates.
- * @property {number} bufferSize - Buffer-size used to encode the WAV container
- * @memberof RecordRTC
- * @instance
- * @readonly
- * @example
- * recorder.stopRecording(function() {
- * alert('Recorder used this buffer-size: ' + this.bufferSize);
- * });
- */
- bufferSize: 0,
- /**
- * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates.
- * @property {number} sampleRate - Sample-rates used to encode the WAV container
- * @memberof RecordRTC
- * @instance
- * @readonly
- * @example
- * recorder.stopRecording(function() {
- * alert('Recorder used these sample-rates: ' + this.sampleRate);
- * });
- */
- sampleRate: 0,
- /**
- * {recorderType:StereoAudioRecorder} returns ArrayBuffer object.
- * @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome.
- * @memberof RecordRTC
- * @instance
- * @readonly
- * @example
- * recorder.stopRecording(function() {
- * var arrayBuffer = this.buffer;
- * alert(arrayBuffer.byteLength);
- * });
- */
- buffer: null,
- /**
- * This method resets the recorder. So that you can reuse single recorder instance many times.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.reset();
- * recorder.startRecording();
- */
- reset: function() {
- if (self.state === 'recording' && !config.disableLogs) {
- // console.warn('Stop an active recorder.');
- }
- if (mediaRecorder && typeof mediaRecorder.clearRecordedData === 'function') {
- mediaRecorder.clearRecordedData();
- }
- mediaRecorder = null;
- setState('inactive');
- self.blob = null;
- },
- /**
- * This method is called whenever recorder's state changes. Use this as an "event".
- * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive.
- * @method
- * @memberof RecordRTC
- * @instance
- * @example
- * recorder.onStateChanged = function(state) {
- * // console.log('Recorder state: ', state);
- * };
- */
- onStateChanged: function(state) {
- if (!config.disableLogs) {
- // console.log('Recorder state changed:', state);
- }
- },
- /**
- * A recorder can have inactive, recording, paused or stopped states.
- * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive.
- * @memberof RecordRTC
- * @static
- * @readonly
- * @example
- * // this looper function will keep you updated about the recorder's states.
- * (function looper() {
- * document.querySelector('h1').innerHTML = 'Recorder\'s state is: ' + recorder.state;
- * if(recorder.state === 'stopped') return; // ignore+stop
- * setTimeout(looper, 1000); // update after every 3-seconds
- * })();
- * recorder.startRecording();
- */
- state: 'inactive',
- /**
- * Get recorder's readonly state.
- * @method
- * @memberof RecordRTC
- * @example
- * var state = recorder.getState();
- * @returns {String} Returns recording state.
- */
- getState: function() {
- return self.state;
- },
- /**
- * Destroy RecordRTC instance. Clear all recorders and objects.
- * @method
- * @memberof RecordRTC
- * @example
- * recorder.destroy();
- */
- destroy: function() {
- var disableLogsCache = config.disableLogs;
- config = {
- disableLogs: true
- };
- self.reset();
- setState('destroyed');
- returnObject = self = null;
- if (Storage.AudioContextConstructor) {
- Storage.AudioContextConstructor.close();
- Storage.AudioContextConstructor = null;
- }
- config.disableLogs = disableLogsCache;
- if (!config.disableLogs) {
- // console.log('RecordRTC is destroyed.');
- }
- },
- /**
- * RecordRTC version number
- * @property {String} version - Release version number.
- * @memberof RecordRTC
- * @static
- * @readonly
- * @example
- * alert(recorder.version);
- */
- version: '5.6.2'
- };
- if (!this) {
- self = returnObject;
- return returnObject;
- }
- // if someone wants to use RecordRTC with the "new" keyword.
- for (var prop in returnObject) {
- this[prop] = returnObject[prop];
- }
- self = this;
- return returnObject;
- }
- RecordRTC.version = '5.6.2';
- {
- module.exports = RecordRTC;
- }
- RecordRTC.getFromDisk = function(type, callback) {
- if (!callback) {
- throw 'callback is mandatory.';
- }
- // console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!');
- DiskStorage.Fetch(function(dataURL, _type) {
- if (type !== 'all' && _type === type + 'Blob' && callback) {
- callback(dataURL);
- }
- if (type === 'all' && callback) {
- callback(dataURL, _type.replace('Blob', ''));
- }
- });
- };
- /**
- * This method can be used to store recorded blobs into IndexedDB storage.
- * @param {object} options - {audio: Blob, video: Blob, gif: Blob}
- * @method
- * @memberof RecordRTC
- * @example
- * RecordRTC.writeToDisk({
- * audio: audioBlob,
- * video: videoBlob,
- * gif : gifBlob
- * });
- */
- RecordRTC.writeToDisk = function(options) {
- // console.log('Writing recorded blob(s) to disk!');
- options = options || {};
- if (options.audio && options.video && options.gif) {
- options.audio.getDataURL(function(audioDataURL) {
- options.video.getDataURL(function(videoDataURL) {
- options.gif.getDataURL(function(gifDataURL) {
- DiskStorage.Store({
- audioBlob: audioDataURL,
- videoBlob: videoDataURL,
- gifBlob: gifDataURL
- });
- });
- });
- });
- } else if (options.audio && options.video) {
- options.audio.getDataURL(function(audioDataURL) {
- options.video.getDataURL(function(videoDataURL) {
- DiskStorage.Store({
- audioBlob: audioDataURL,
- videoBlob: videoDataURL
- });
- });
- });
- } else if (options.audio && options.gif) {
- options.audio.getDataURL(function(audioDataURL) {
- options.gif.getDataURL(function(gifDataURL) {
- DiskStorage.Store({
- audioBlob: audioDataURL,
- gifBlob: gifDataURL
- });
- });
- });
- } else if (options.video && options.gif) {
- options.video.getDataURL(function(videoDataURL) {
- options.gif.getDataURL(function(gifDataURL) {
- DiskStorage.Store({
- videoBlob: videoDataURL,
- gifBlob: gifDataURL
- });
- });
- });
- } else if (options.audio) {
- options.audio.getDataURL(function(audioDataURL) {
- DiskStorage.Store({
- audioBlob: audioDataURL
- });
- });
- } else if (options.video) {
- options.video.getDataURL(function(videoDataURL) {
- DiskStorage.Store({
- videoBlob: videoDataURL
- });
- });
- } else if (options.gif) {
- options.gif.getDataURL(function(gifDataURL) {
- DiskStorage.Store({
- gifBlob: gifDataURL
- });
- });
- }
- };
- // __________________________
- // RecordRTC-Configuration.js
- /**
- * {@link RecordRTCConfiguration} is an inner/private helper for {@link RecordRTC}.
- * @summary It configures the 2nd parameter passed over {@link RecordRTC} and returns a valid "config" object.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef RecordRTCConfiguration
- * @class
- * @example
- * var options = RecordRTCConfiguration(mediaStream, options);
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, getNativeBlob:true, etc.}
- */
- function RecordRTCConfiguration(mediaStream, config) {
- if (!config.recorderType && !config.type) {
- if (!!config.audio && !!config.video) {
- config.type = 'video';
- } else if (!!config.audio && !config.video) {
- config.type = 'audio';
- }
- }
- if (config.recorderType && !config.type) {
- if (config.recorderType === WhammyRecorder || config.recorderType === CanvasRecorder || (typeof WebAssemblyRecorder !== 'undefined' && config.recorderType === WebAssemblyRecorder)) {
- config.type = 'video';
- } else if (config.recorderType === GifRecorder) {
- config.type = 'gif';
- } else if (config.recorderType === StereoAudioRecorder) {
- config.type = 'audio';
- } else if (config.recorderType === MediaStreamRecorder) {
- if (getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) {
- config.type = 'video';
- } else if (!getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) {
- config.type = 'video';
- } else if (getTracks(mediaStream, 'audio').length && !getTracks(mediaStream, 'video').length) {
- config.type = 'audio';
- } else ;
- }
- }
- if (typeof MediaStreamRecorder !== 'undefined' && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) {
- if (!config.mimeType) {
- config.mimeType = 'video/webm';
- }
- if (!config.type) {
- config.type = config.mimeType.split('/')[0];
- }
- if (!config.bitsPerSecond) ;
- }
- // consider default type=audio
- if (!config.type) {
- if (config.mimeType) {
- config.type = config.mimeType.split('/')[0];
- }
- if (!config.type) {
- config.type = 'audio';
- }
- }
- return config;
- }
- // __________________
- // GetRecorderType.js
- /**
- * {@link GetRecorderType} is an inner/private helper for {@link RecordRTC}.
- * @summary It returns best recorder-type available for your browser.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef GetRecorderType
- * @class
- * @example
- * var RecorderType = GetRecorderType(options);
- * var recorder = new RecorderType(options);
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.}
- */
- function GetRecorderType(mediaStream, config) {
- var recorder;
- // StereoAudioRecorder can work with all three: Edge, Firefox and Chrome
- // todo: detect if it is Edge, then auto use: StereoAudioRecorder
- if (isChrome || isEdge || isOpera) {
- // Media Stream Recording API has not been implemented in chrome yet;
- // That's why using WebAudio API to record stereo audio in WAV format
- recorder = StereoAudioRecorder;
- }
- if (typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype && !isChrome) {
- recorder = MediaStreamRecorder;
- }
- // video recorder (in WebM format)
- if (config.type === 'video' && (isChrome || isOpera)) {
- recorder = WhammyRecorder;
- if (typeof WebAssemblyRecorder !== 'undefined' && typeof ReadableStream !== 'undefined') {
- recorder = WebAssemblyRecorder;
- }
- }
- // video recorder (in Gif format)
- if (config.type === 'gif') {
- recorder = GifRecorder;
- }
- // html2canvas recording!
- if (config.type === 'canvas') {
- recorder = CanvasRecorder;
- }
- if (isMediaRecorderCompatible() && recorder !== CanvasRecorder && recorder !== GifRecorder && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) {
- if (getTracks(mediaStream, 'video').length || getTracks(mediaStream, 'audio').length) {
- // audio-only recording
- if (config.type === 'audio') {
- if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('audio/webm')) {
- recorder = MediaStreamRecorder;
- }
- // else recorder = StereoAudioRecorder;
- } else {
- // video or screen tracks
- if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('video/webm')) {
- recorder = MediaStreamRecorder;
- }
- }
- }
- }
- if (mediaStream instanceof Array && mediaStream.length) {
- recorder = MultiStreamRecorder;
- }
- if (config.recorderType) {
- recorder = config.recorderType;
- }
- if (!config.disableLogs && !!recorder && !!recorder.name) {
- // console.log('Using recorderType:', recorder.name || recorder.constructor.name);
- }
- if (!recorder && isSafari) {
- recorder = MediaStreamRecorder;
- }
- return recorder;
- }
- // _____________
- // MRecordRTC.js
- /**
- * MRecordRTC runs on top of {@link RecordRTC} to bring multiple recordings in a single place, by providing simple API.
- * @summary MRecordRTC stands for "Multiple-RecordRTC".
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef MRecordRTC
- * @class
- * @example
- * var recorder = new MRecordRTC();
- * recorder.addStream(MediaStream);
- * recorder.mediaType = {
- * audio: true, // or StereoAudioRecorder or MediaStreamRecorder
- * video: true, // or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder
- * gif: true // or GifRecorder
- * };
- * // mimeType is optional and should be set only in advance cases.
- * recorder.mimeType = {
- * audio: 'audio/wav',
- * video: 'video/webm',
- * gif: 'image/gif'
- * };
- * recorder.startRecording();
- * @see For further information:
- * @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @requires {@link RecordRTC}
- */
- function MRecordRTC(mediaStream) {
- /**
- * This method attaches MediaStream object to {@link MRecordRTC}.
- * @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.addStream(MediaStream);
- */
- this.addStream = function(_mediaStream) {
- if (_mediaStream) {
- mediaStream = _mediaStream;
- }
- };
- /**
- * This property can be used to set the recording type e.g. audio, or video, or gif, or canvas.
- * @property {object} mediaType - {audio: true, video: true, gif: true}
- * @memberof MRecordRTC
- * @example
- * var recorder = new MRecordRTC();
- * recorder.mediaType = {
- * audio: true, // TRUE or StereoAudioRecorder or MediaStreamRecorder
- * video: true, // TRUE or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder
- * gif : true // TRUE or GifRecorder
- * };
- */
- this.mediaType = {
- audio: true,
- video: true
- };
- /**
- * This method starts recording.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.startRecording();
- */
- this.startRecording = function() {
- var mediaType = this.mediaType;
- var recorderType;
- var mimeType = this.mimeType || {
- audio: null,
- video: null,
- gif: null
- };
- if (typeof mediaType.audio !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'audio').length) {
- mediaType.audio = false;
- }
- if (typeof mediaType.video !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) {
- mediaType.video = false;
- }
- if (typeof mediaType.gif !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) {
- mediaType.gif = false;
- }
- if (!mediaType.audio && !mediaType.video && !mediaType.gif) {
- throw 'MediaStream must have either audio or video tracks.';
- }
- if (!!mediaType.audio) {
- recorderType = null;
- if (typeof mediaType.audio === 'function') {
- recorderType = mediaType.audio;
- }
- this.audioRecorder = new RecordRTC(mediaStream, {
- type: 'audio',
- bufferSize: this.bufferSize,
- sampleRate: this.sampleRate,
- numberOfAudioChannels: this.numberOfAudioChannels || 2,
- disableLogs: this.disableLogs,
- recorderType: recorderType,
- mimeType: mimeType.audio,
- timeSlice: this.timeSlice,
- onTimeStamp: this.onTimeStamp
- });
- if (!mediaType.video) {
- this.audioRecorder.startRecording();
- }
- }
- if (!!mediaType.video) {
- recorderType = null;
- if (typeof mediaType.video === 'function') {
- recorderType = mediaType.video;
- }
- var newStream = mediaStream;
- if (isMediaRecorderCompatible() && !!mediaType.audio && typeof mediaType.audio === 'function') {
- var videoTrack = getTracks(mediaStream, 'video')[0];
- if (isFirefox) {
- newStream = new MediaStream();
- newStream.addTrack(videoTrack);
- if (recorderType && recorderType === WhammyRecorder) {
- // Firefox does NOT supports webp-encoding yet
- // But Firefox do supports WebAssemblyRecorder
- recorderType = MediaStreamRecorder;
- }
- } else {
- newStream = new MediaStream();
- newStream.addTrack(videoTrack);
- }
- }
- this.videoRecorder = new RecordRTC(newStream, {
- type: 'video',
- video: this.video,
- canvas: this.canvas,
- frameInterval: this.frameInterval || 10,
- disableLogs: this.disableLogs,
- recorderType: recorderType,
- mimeType: mimeType.video,
- timeSlice: this.timeSlice,
- onTimeStamp: this.onTimeStamp,
- workerPath: this.workerPath,
- webAssemblyPath: this.webAssemblyPath,
- frameRate: this.frameRate, // used by WebAssemblyRecorder; values: usually 30; accepts any.
- bitrate: this.bitrate // used by WebAssemblyRecorder; values: 0 to 1000+
- });
- if (!mediaType.audio) {
- this.videoRecorder.startRecording();
- }
- }
- if (!!mediaType.audio && !!mediaType.video) {
- var self = this;
- var isSingleRecorder = isMediaRecorderCompatible() === true;
- if (mediaType.audio instanceof StereoAudioRecorder && !!mediaType.video) {
- isSingleRecorder = false;
- } else if (mediaType.audio !== true && mediaType.video !== true && mediaType.audio !== mediaType.video) {
- isSingleRecorder = false;
- }
- if (isSingleRecorder === true) {
- self.audioRecorder = null;
- self.videoRecorder.startRecording();
- } else {
- self.videoRecorder.initRecorder(function() {
- self.audioRecorder.initRecorder(function() {
- // Both recorders are ready to record things accurately
- self.videoRecorder.startRecording();
- self.audioRecorder.startRecording();
- });
- });
- }
- }
- if (!!mediaType.gif) {
- recorderType = null;
- if (typeof mediaType.gif === 'function') {
- recorderType = mediaType.gif;
- }
- this.gifRecorder = new RecordRTC(mediaStream, {
- type: 'gif',
- frameRate: this.frameRate || 200,
- quality: this.quality || 10,
- disableLogs: this.disableLogs,
- recorderType: recorderType,
- mimeType: mimeType.gif
- });
- this.gifRecorder.startRecording();
- }
- };
- /**
- * This method stops recording.
- * @param {function} callback - Callback function is invoked when all encoders finished their jobs.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.stopRecording(function(recording){
- * var audioBlob = recording.audio;
- * var videoBlob = recording.video;
- * var gifBlob = recording.gif;
- * });
- */
- this.stopRecording = function(callback) {
- callback = callback || function() {};
- if (this.audioRecorder) {
- this.audioRecorder.stopRecording(function(blobURL) {
- callback(blobURL, 'audio');
- });
- }
- if (this.videoRecorder) {
- this.videoRecorder.stopRecording(function(blobURL) {
- callback(blobURL, 'video');
- });
- }
- if (this.gifRecorder) {
- this.gifRecorder.stopRecording(function(blobURL) {
- callback(blobURL, 'gif');
- });
- }
- };
- /**
- * This method pauses recording.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.pauseRecording();
- */
- this.pauseRecording = function() {
- if (this.audioRecorder) {
- this.audioRecorder.pauseRecording();
- }
- if (this.videoRecorder) {
- this.videoRecorder.pauseRecording();
- }
- if (this.gifRecorder) {
- this.gifRecorder.pauseRecording();
- }
- };
- /**
- * This method resumes recording.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.resumeRecording();
- */
- this.resumeRecording = function() {
- if (this.audioRecorder) {
- this.audioRecorder.resumeRecording();
- }
- if (this.videoRecorder) {
- this.videoRecorder.resumeRecording();
- }
- if (this.gifRecorder) {
- this.gifRecorder.resumeRecording();
- }
- };
- /**
- * This method can be used to manually get all recorded blobs.
- * @param {function} callback - All recorded blobs are passed back to the "callback" function.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.getBlob(function(recording){
- * var audioBlob = recording.audio;
- * var videoBlob = recording.video;
- * var gifBlob = recording.gif;
- * });
- * // or
- * var audioBlob = recorder.getBlob().audio;
- * var videoBlob = recorder.getBlob().video;
- */
- this.getBlob = function(callback) {
- var output = {};
- if (this.audioRecorder) {
- output.audio = this.audioRecorder.getBlob();
- }
- if (this.videoRecorder) {
- output.video = this.videoRecorder.getBlob();
- }
- if (this.gifRecorder) {
- output.gif = this.gifRecorder.getBlob();
- }
- if (callback) {
- callback(output);
- }
- return output;
- };
- /**
- * Destroy all recorder instances.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.destroy();
- */
- this.destroy = function() {
- if (this.audioRecorder) {
- this.audioRecorder.destroy();
- this.audioRecorder = null;
- }
- if (this.videoRecorder) {
- this.videoRecorder.destroy();
- this.videoRecorder = null;
- }
- if (this.gifRecorder) {
- this.gifRecorder.destroy();
- this.gifRecorder = null;
- }
- };
- /**
- * This method can be used to manually get all recorded blobs' DataURLs.
- * @param {function} callback - All recorded blobs' DataURLs are passed back to the "callback" function.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.getDataURL(function(recording){
- * var audioDataURL = recording.audio;
- * var videoDataURL = recording.video;
- * var gifDataURL = recording.gif;
- * });
- */
- this.getDataURL = function(callback) {
- this.getBlob(function(blob) {
- if (blob.audio && blob.video) {
- getDataURL(blob.audio, function(_audioDataURL) {
- getDataURL(blob.video, function(_videoDataURL) {
- callback({
- audio: _audioDataURL,
- video: _videoDataURL
- });
- });
- });
- } else if (blob.audio) {
- getDataURL(blob.audio, function(_audioDataURL) {
- callback({
- audio: _audioDataURL
- });
- });
- } else if (blob.video) {
- getDataURL(blob.video, function(_videoDataURL) {
- callback({
- video: _videoDataURL
- });
- });
- }
- });
- function getDataURL(blob, callback00) {
- if (typeof Worker !== 'undefined') {
- var webWorker = processInWebWorker(function readFile(_blob) {
- postMessage(new FileReaderSync().readAsDataURL(_blob));
- });
- webWorker.onmessage = function(event) {
- callback00(event.data);
- };
- webWorker.postMessage(blob);
- } else {
- var reader = new FileReader();
- reader.readAsDataURL(blob);
- reader.onload = function(event) {
- callback00(event.target.result);
- };
- }
- }
- function processInWebWorker(_function) {
- var blob = URL.createObjectURL(new Blob([_function.toString(),
- 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
- ], {
- type: 'application/javascript'
- }));
- var worker = new Worker(blob);
- var url;
- if (typeof URL !== 'undefined') {
- url = URL;
- } else if (typeof webkitURL !== 'undefined') {
- url = webkitURL;
- } else {
- throw 'Neither URL nor webkitURL detected.';
- }
- url.revokeObjectURL(blob);
- return worker;
- }
- };
- /**
- * This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage.
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.writeToDisk();
- */
- this.writeToDisk = function() {
- RecordRTC.writeToDisk({
- audio: this.audioRecorder,
- video: this.videoRecorder,
- gif: this.gifRecorder
- });
- };
- /**
- * This method can be used to invoke a save-as dialog for all recorded blobs.
- * @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'}
- * @method
- * @memberof MRecordRTC
- * @example
- * recorder.save({
- * audio: 'audio-file-name',
- * video: 'video-file-name',
- * gif : 'gif-file-name'
- * });
- */
- this.save = function(args) {
- args = args || {
- audio: true,
- video: true,
- gif: true
- };
- if (!!args.audio && this.audioRecorder) {
- this.audioRecorder.save(typeof args.audio === 'string' ? args.audio : '');
- }
- if (!!args.video && this.videoRecorder) {
- this.videoRecorder.save(typeof args.video === 'string' ? args.video : '');
- }
- if (!!args.gif && this.gifRecorder) {
- this.gifRecorder.save(typeof args.gif === 'string' ? args.gif : '');
- }
- };
- }
- /**
- * This method can be used to get all recorded blobs from IndexedDB storage.
- * @param {string} type - 'all' or 'audio' or 'video' or 'gif'
- * @param {function} callback - Callback function to get all stored blobs.
- * @method
- * @memberof MRecordRTC
- * @example
- * MRecordRTC.getFromDisk('all', function(dataURL, type){
- * if(type === 'audio') { }
- * if(type === 'video') { }
- * if(type === 'gif') { }
- * });
- */
- MRecordRTC.getFromDisk = RecordRTC.getFromDisk;
- /**
- * This method can be used to store recorded blobs into IndexedDB storage.
- * @param {object} options - {audio: Blob, video: Blob, gif: Blob}
- * @method
- * @memberof MRecordRTC
- * @example
- * MRecordRTC.writeToDisk({
- * audio: audioBlob,
- * video: videoBlob,
- * gif : gifBlob
- * });
- */
- MRecordRTC.writeToDisk = RecordRTC.writeToDisk;
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.MRecordRTC = MRecordRTC;
- }
- var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
- (function(that) {
- if (!that) {
- return;
- }
- if (typeof window !== 'undefined') {
- return;
- }
- if (typeof commonjsGlobal === 'undefined') {
- return;
- }
- commonjsGlobal.navigator = {
- userAgent: browserFakeUserAgent,
- getUserMedia: function() {}
- };
- if (!commonjsGlobal.console) {
- commonjsGlobal.console = {};
- }
- if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') {
- commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function() {
- // console.log(arguments);
- };
- }
- if (typeof document === 'undefined') {
- /*global document:true */
- that.document = {
- documentElement: {
- appendChild: function() {
- return '';
- }
- }
- };
- document.createElement = document.captureStream = document.mozCaptureStream = function() {
- var obj = {
- getContext: function() {
- return obj;
- },
- play: function() {},
- pause: function() {},
- drawImage: function() {},
- toDataURL: function() {
- return '';
- },
- style: {}
- };
- return obj;
- };
- that.HTMLVideoElement = function() {};
- }
- if (typeof location === 'undefined') {
- /*global location:true */
- that.location = {
- protocol: 'file:',
- href: '',
- hash: ''
- };
- }
- if (typeof screen === 'undefined') {
- /*global screen:true */
- that.screen = {
- width: 0,
- height: 0
- };
- }
- if (typeof URL === 'undefined') {
- /*global screen:true */
- that.URL = {
- createObjectURL: function() {
- return '';
- },
- revokeObjectURL: function() {
- return '';
- }
- };
- }
- /*global window:true */
- that.window = commonjsGlobal;
- })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null);
- // _____________________________
- // Cross-Browser-Declarations.js
- // animation-frame used in WebM recording
- /*jshint -W079 */
- var requestAnimationFrame = window.requestAnimationFrame;
- if (typeof requestAnimationFrame === 'undefined') {
- if (typeof webkitRequestAnimationFrame !== 'undefined') {
- /*global requestAnimationFrame:true */
- requestAnimationFrame = webkitRequestAnimationFrame;
- } else if (typeof mozRequestAnimationFrame !== 'undefined') {
- /*global requestAnimationFrame:true */
- requestAnimationFrame = mozRequestAnimationFrame;
- } else if (typeof msRequestAnimationFrame !== 'undefined') {
- /*global requestAnimationFrame:true */
- requestAnimationFrame = msRequestAnimationFrame;
- } else if (typeof requestAnimationFrame === 'undefined') {
- // via: https://gist.github.com/paulirish/1579671
- var lastTime = 0;
- /*global requestAnimationFrame:true */
- requestAnimationFrame = function(callback, element) {
- var currTime = new Date().getTime();
- var timeToCall = Math.max(0, 16 - (currTime - lastTime));
- var id = setTimeout(function() {
- callback(currTime + timeToCall);
- }, timeToCall);
- lastTime = currTime + timeToCall;
- return id;
- };
- }
- }
- /*jshint -W079 */
- var cancelAnimationFrame = window.cancelAnimationFrame;
- if (typeof cancelAnimationFrame === 'undefined') {
- if (typeof webkitCancelAnimationFrame !== 'undefined') {
- /*global cancelAnimationFrame:true */
- cancelAnimationFrame = webkitCancelAnimationFrame;
- } else if (typeof mozCancelAnimationFrame !== 'undefined') {
- /*global cancelAnimationFrame:true */
- cancelAnimationFrame = mozCancelAnimationFrame;
- } else if (typeof msCancelAnimationFrame !== 'undefined') {
- /*global cancelAnimationFrame:true */
- cancelAnimationFrame = msCancelAnimationFrame;
- } else if (typeof cancelAnimationFrame === 'undefined') {
- /*global cancelAnimationFrame:true */
- cancelAnimationFrame = function(id) {
- clearTimeout(id);
- };
- }
- }
- // WebAudio API representer
- var AudioContext = window.AudioContext;
- if (typeof AudioContext === 'undefined') {
- if (typeof webkitAudioContext !== 'undefined') {
- /*global AudioContext:true */
- AudioContext = webkitAudioContext;
- }
- if (typeof mozAudioContext !== 'undefined') {
- /*global AudioContext:true */
- AudioContext = mozAudioContext;
- }
- }
- /*jshint -W079 */
- var URL = window.URL;
- if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
- /*global URL:true */
- URL = webkitURL;
- }
- if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator?
- if (typeof navigator.webkitGetUserMedia !== 'undefined') {
- navigator.getUserMedia = navigator.webkitGetUserMedia;
- }
- if (typeof navigator.mozGetUserMedia !== 'undefined') {
- navigator.getUserMedia = navigator.mozGetUserMedia;
- }
- }
- var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
- var isOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1;
- var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && ('netscape' in window) && / rv:/.test(navigator.userAgent);
- var isChrome = (!isOpera && !isEdge && !!navigator.webkitGetUserMedia) || isElectron() || navigator.userAgent.toLowerCase().indexOf('chrome/') !== -1;
- var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
- if (isSafari && !isChrome && navigator.userAgent.indexOf('CriOS') !== -1) {
- isSafari = false;
- isChrome = true;
- }
- var MediaStream = window.MediaStream;
- if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
- MediaStream = webkitMediaStream;
- }
- /*global MediaStream:true */
- if (typeof MediaStream !== 'undefined') {
- // override "stop" method for all browsers
- if (typeof MediaStream.prototype.stop === 'undefined') {
- MediaStream.prototype.stop = function() {
- this.getTracks().forEach(function(track) {
- track.stop();
- });
- };
- }
- }
- // below function via: http://goo.gl/B3ae8c
- /**
- * Return human-readable file size.
- * @param {number} bytes - Pass bytes and get formatted string.
- * @returns {string} - formatted string
- * @example
- * bytesToSize(1024*1024*5) === '5 GB'
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- */
- function bytesToSize(bytes) {
- var k = 1000;
- var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
- if (bytes === 0) {
- return '0 Bytes';
- }
- var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
- return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
- }
- /**
- * @param {Blob} file - File or Blob object. This parameter is required.
- * @param {string} fileName - Optional file name e.g. "Recorded-Video.webm"
- * @example
- * invokeSaveAsDialog(blob or file, [optional] fileName);
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- */
- function invokeSaveAsDialog(file, fileName) {
- if (!file) {
- throw 'Blob object is required.';
- }
- if (!file.type) {
- try {
- file.type = 'video/webm';
- } catch (e) {}
- }
- var fileExtension = (file.type || 'video/webm').split('/')[1];
- if (fileExtension.indexOf(';') !== -1) {
- // extended mimetype, e.g. 'video/webm;codecs=vp8,opus'
- fileExtension = fileExtension.split(';')[0];
- }
- if (fileName && fileName.indexOf('.') !== -1) {
- var splitted = fileName.split('.');
- fileName = splitted[0];
- fileExtension = splitted[1];
- }
- var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension;
- if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
- return navigator.msSaveOrOpenBlob(file, fileFullName);
- } else if (typeof navigator.msSaveBlob !== 'undefined') {
- return navigator.msSaveBlob(file, fileFullName);
- }
- var hyperlink = document.createElement('a');
- hyperlink.href = URL.createObjectURL(file);
- hyperlink.download = fileFullName;
- hyperlink.style = 'display:none;opacity:0;color:transparent;';
- (document.body || document.documentElement).appendChild(hyperlink);
- if (typeof hyperlink.click === 'function') {
- hyperlink.click();
- } else {
- hyperlink.target = '_blank';
- hyperlink.dispatchEvent(new MouseEvent('click', {
- view: window,
- bubbles: true,
- cancelable: true
- }));
- }
- URL.revokeObjectURL(hyperlink.href);
- }
- /**
- * from: https://github.com/cheton/is-electron/blob/master/index.js
- **/
- function isElectron() {
- // Renderer process
- if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {
- return true;
- }
- // Main process
- if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) {
- return true;
- }
- // Detect the user agent when the `nodeIntegration` option is set to true
- if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {
- return true;
- }
- return false;
- }
- function getTracks(stream, kind) {
- if (!stream || !stream.getTracks) {
- return [];
- }
- return stream.getTracks().filter(function(t) {
- return t.kind === (kind || 'audio');
- });
- }
- function setSrcObject(stream, element) {
- if ('srcObject' in element) {
- element.srcObject = stream;
- } else if ('mozSrcObject' in element) {
- element.mozSrcObject = stream;
- } else {
- element.srcObject = stream;
- }
- }
- /**
- * @param {Blob} file - File or Blob object.
- * @param {function} callback - Callback function.
- * @example
- * getSeekableBlob(blob or file, callback);
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- */
- function getSeekableBlob(inputBlob, callback) {
- // EBML.js copyrights goes to: https://github.com/legokichi/ts-ebml
- if (typeof EBML === 'undefined') {
- throw new Error('Please link: https://www.webrtc-experiment.com/EBML.js');
- }
- var reader = new EBML.Reader();
- var decoder = new EBML.Decoder();
- var tools = EBML.tools;
- var fileReader = new FileReader();
- fileReader.onload = function(e) {
- var ebmlElms = decoder.decode(this.result);
- ebmlElms.forEach(function(element) {
- reader.read(element);
- });
- reader.stop();
- var refinedMetadataBuf = tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues);
- var body = this.result.slice(reader.metadataSize);
- var newBlob = new Blob([refinedMetadataBuf, body], {
- type: 'video/webm'
- });
- callback(newBlob);
- };
- fileReader.readAsArrayBuffer(inputBlob);
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.invokeSaveAsDialog = invokeSaveAsDialog;
- RecordRTC.getTracks = getTracks;
- RecordRTC.getSeekableBlob = getSeekableBlob;
- RecordRTC.bytesToSize = bytesToSize;
- RecordRTC.isElectron = isElectron;
- }
- // __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129
- // Storage.js
- /**
- * Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext".
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @example
- * Storage.AudioContext === webkitAudioContext
- * @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object.
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- */
- var Storage = {};
- if (typeof AudioContext !== 'undefined') {
- Storage.AudioContext = AudioContext;
- } else if (typeof webkitAudioContext !== 'undefined') {
- Storage.AudioContext = webkitAudioContext;
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.Storage = Storage;
- }
- function isMediaRecorderCompatible() {
- if (isFirefox || isSafari || isEdge) {
- return true;
- }
- var nAgt = navigator.userAgent;
- var fullVersion = '' + parseFloat(navigator.appVersion);
- var majorVersion = parseInt(navigator.appVersion, 10);
- var verOffset, ix;
- if (isChrome || isOpera) {
- verOffset = nAgt.indexOf('Chrome');
- fullVersion = nAgt.substring(verOffset + 7);
- }
- // trim the fullVersion string at semicolon/space if present
- if ((ix = fullVersion.indexOf(';')) !== -1) {
- fullVersion = fullVersion.substring(0, ix);
- }
- if ((ix = fullVersion.indexOf(' ')) !== -1) {
- fullVersion = fullVersion.substring(0, ix);
- }
- majorVersion = parseInt('' + fullVersion, 10);
- if (isNaN(majorVersion)) {
- fullVersion = '' + parseFloat(navigator.appVersion);
- majorVersion = parseInt(navigator.appVersion, 10);
- }
- return majorVersion >= 49;
- }
- // ______________________
- // MediaStreamRecorder.js
- /**
- * MediaStreamRecorder is an abstraction layer for {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. It is used by {@link RecordRTC} to record MediaStream(s) in both Chrome and Firefox.
- * @summary Runs top over {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://github.com/muaz-khan|Muaz Khan}
- * @typedef MediaStreamRecorder
- * @class
- * @example
- * var config = {
- * mimeType: 'video/webm', // vp8, vp9, h264, mkv, opus/vorbis
- * audioBitsPerSecond : 256 * 8 * 1024,
- * videoBitsPerSecond : 256 * 8 * 1024,
- * bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two
- * checkForInactiveTracks: true,
- * timeSlice: 1000, // concatenate intervals based blobs
- * ondataavailable: function() {} // get intervals based blobs
- * }
- * var recorder = new MediaStreamRecorder(mediaStream, config);
- * recorder.record();
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- *
- * // or
- * var blob = recorder.blob;
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @param {object} config - {disableLogs:true, initCallback: function, mimeType: "video/webm", timeSlice: 1000}
- * @throws Will throw an error if first argument "MediaStream" is missing. Also throws error if "MediaRecorder API" are not supported by the browser.
- */
- function MediaStreamRecorder(mediaStream, config) {
- var self = this;
- if (typeof mediaStream === 'undefined') {
- throw 'First argument "MediaStream" is required.';
- }
- if (typeof MediaRecorder === 'undefined') {
- throw 'Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder.';
- }
- config = config || {
- // bitsPerSecond: 256 * 8 * 1024,
- mimeType: 'video/webm'
- };
- if (config.type === 'audio') {
- if (getTracks(mediaStream, 'video').length && getTracks(mediaStream, 'audio').length) {
- var stream;
- if (!!navigator.mozGetUserMedia) {
- stream = new MediaStream();
- stream.addTrack(getTracks(mediaStream, 'audio')[0]);
- } else {
- // webkitMediaStream
- stream = new MediaStream(getTracks(mediaStream, 'audio'));
- }
- mediaStream = stream;
- }
- if (!config.mimeType || config.mimeType.toString().toLowerCase().indexOf('audio') === -1) {
- config.mimeType = isChrome ? 'audio/webm' : 'audio/ogg';
- }
- if (config.mimeType && config.mimeType.toString().toLowerCase() !== 'audio/ogg' && !!navigator.mozGetUserMedia) {
- // forcing better codecs on Firefox (via #166)
- config.mimeType = 'audio/ogg';
- }
- }
- var arrayOfBlobs = [];
- /**
- * This method returns array of blobs. Use only with "timeSlice". Its useful to preview recording anytime, without using the "stop" method.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * var arrayOfBlobs = recorder.getArrayOfBlobs();
- * @returns {Array} Returns array of recorded blobs.
- */
- this.getArrayOfBlobs = function() {
- return arrayOfBlobs;
- };
- /**
- * This method records MediaStream.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- // set defaults
- self.blob = null;
- self.clearRecordedData();
- self.timestamps = [];
- allStates = [];
- arrayOfBlobs = [];
- var recorderHints = config;
- if (!config.disableLogs) {
- // console.log('Passing following config over MediaRecorder API.', recorderHints);
- }
- if (mediaRecorder) {
- // mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page.
- mediaRecorder = null;
- }
- if (isChrome && !isMediaRecorderCompatible()) {
- // to support video-only recording on stable
- recorderHints = 'video/vp8';
- }
- if (typeof MediaRecorder.isTypeSupported === 'function' && recorderHints.mimeType) {
- if (!MediaRecorder.isTypeSupported(recorderHints.mimeType)) {
- if (!config.disableLogs) {
- // console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType);
- }
- recorderHints.mimeType = config.type === 'audio' ? 'audio/webm' : 'video/webm';
- }
- }
- // using MediaRecorder API here
- try {
- mediaRecorder = new MediaRecorder(mediaStream, recorderHints);
- // reset
- config.mimeType = recorderHints.mimeType;
- } catch (e) {
- // chrome-based fallback
- mediaRecorder = new MediaRecorder(mediaStream);
- }
- // old hack?
- if (recorderHints.mimeType && !MediaRecorder.isTypeSupported && 'canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(recorderHints.mimeType) === false) {
- if (!config.disableLogs) {
- // console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType);
- }
- }
- // Dispatching OnDataAvailable Handler
- mediaRecorder.ondataavailable = function(e) {
- if (e.data) {
- allStates.push('ondataavailable: ' + bytesToSize(e.data.size));
- }
- if (typeof config.timeSlice === 'number') {
- if (e.data && e.data.size) {
- arrayOfBlobs.push(e.data);
- updateTimeStamp();
- if (typeof config.ondataavailable === 'function') {
- // intervals based blobs
- var blob = config.getNativeBlob ? e.data : new Blob([e.data], {
- type: getMimeType(recorderHints)
- });
- config.ondataavailable(blob);
- }
- }
- return;
- }
- if (!e.data || !e.data.size || e.data.size < 100 || self.blob) {
- // make sure that stopRecording always getting fired
- // even if there is invalid data
- if (self.recordingCallback) {
- self.recordingCallback(new Blob([], {
- type: getMimeType(recorderHints)
- }));
- self.recordingCallback = null;
- }
- return;
- }
- self.blob = config.getNativeBlob ? e.data : new Blob([e.data], {
- type: getMimeType(recorderHints)
- });
- if (self.recordingCallback) {
- self.recordingCallback(self.blob);
- self.recordingCallback = null;
- }
- };
- mediaRecorder.onstart = function() {
- allStates.push('started');
- };
- mediaRecorder.onpause = function() {
- allStates.push('paused');
- };
- mediaRecorder.onresume = function() {
- allStates.push('resumed');
- };
- mediaRecorder.onstop = function() {
- allStates.push('stopped');
- };
- mediaRecorder.onerror = function(error) {
- if (!error) {
- return;
- }
- if (!error.name) {
- error.name = 'UnknownError';
- }
- allStates.push('error: ' + error);
- if (!config.disableLogs) {
- // via: https://w3c.github.io/mediacapture-record/MediaRecorder.html#exception-summary
- if (error.name.toString().toLowerCase().indexOf('invalidstate') !== -1) {
- // console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.', error);
- } else if (error.name.toString().toLowerCase().indexOf('notsupported') !== -1) {
- // console.error('MIME type (', recorderHints.mimeType, ') is not supported.', error);
- } else if (error.name.toString().toLowerCase().indexOf('security') !== -1) {
- // console.error('MediaRecorder security error', error);
- }
- // older code below
- else if (error.name === 'OutOfMemory') {
- // console.error('The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.', error);
- } else if (error.name === 'IllegalStreamModification') {
- // console.error('A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.', error);
- } else if (error.name === 'OtherRecordingError') {
- // console.error('Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.', error);
- } else if (error.name === 'GenericError') {
- // console.error('The UA cannot provide the codec or recording option that has been requested.', error);
- } else {
- // console.error('MediaRecorder Error', error);
- }
- }
- (function(looper) {
- if (!self.manuallyStopped && mediaRecorder && mediaRecorder.state === 'inactive') {
- delete config.timeslice;
- // 10 minutes, enough?
- mediaRecorder.start(10 * 60 * 1000);
- return;
- }
- setTimeout(looper, 1000);
- })();
- if (mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') {
- mediaRecorder.stop();
- }
- };
- if (typeof config.timeSlice === 'number') {
- updateTimeStamp();
- mediaRecorder.start(config.timeSlice);
- } else {
- // default is 60 minutes; enough?
- // use config => {timeSlice: 1000} otherwise
- mediaRecorder.start(3.6e+6);
- }
- if (config.initCallback) {
- config.initCallback(); // old code
- }
- };
- /**
- * @property {Array} timestamps - Array of time stamps
- * @memberof MediaStreamRecorder
- * @example
- * console.log(recorder.timestamps);
- */
- this.timestamps = [];
- function updateTimeStamp() {
- self.timestamps.push(new Date().getTime());
- if (typeof config.onTimeStamp === 'function') {
- config.onTimeStamp(self.timestamps[self.timestamps.length - 1], self.timestamps);
- }
- }
- function getMimeType(secondObject) {
- if (mediaRecorder && mediaRecorder.mimeType) {
- return mediaRecorder.mimeType;
- }
- return secondObject.mimeType || 'video/webm';
- }
- /**
- * This method stops recording MediaStream.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- callback = callback || function() {};
- self.manuallyStopped = true; // used inside the mediaRecorder.onerror
- if (!mediaRecorder) {
- return;
- }
- this.recordingCallback = callback;
- if (mediaRecorder.state === 'recording') {
- mediaRecorder.stop();
- }
- if (typeof config.timeSlice === 'number') {
- setTimeout(function() {
- self.blob = new Blob(arrayOfBlobs, {
- type: getMimeType(config)
- });
- self.recordingCallback(self.blob);
- }, 100);
- }
- };
- /**
- * This method pauses the recording process.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- if (!mediaRecorder) {
- return;
- }
- if (mediaRecorder.state === 'recording') {
- mediaRecorder.pause();
- }
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- if (!mediaRecorder) {
- return;
- }
- if (mediaRecorder.state === 'paused') {
- mediaRecorder.resume();
- }
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- if (mediaRecorder && mediaRecorder.state === 'recording') {
- self.stop(clearRecordedDataCB);
- }
- clearRecordedDataCB();
- };
- function clearRecordedDataCB() {
- arrayOfBlobs = [];
- mediaRecorder = null;
- self.timestamps = [];
- }
- // Reference to "MediaRecorder" object
- var mediaRecorder;
- /**
- * Access to native MediaRecorder API
- * @method
- * @memberof MediaStreamRecorder
- * @instance
- * @example
- * var internal = recorder.getInternalRecorder();
- * internal.ondataavailable = function() {}; // override
- * internal.stream, internal.onpause, internal.onstop, etc.
- * @returns {Object} Returns internal recording object.
- */
- this.getInternalRecorder = function() {
- return mediaRecorder;
- };
- function isMediaStreamActive() {
- if ('active' in mediaStream) {
- if (!mediaStream.active) {
- return false;
- }
- } else if ('ended' in mediaStream) { // old hack
- if (mediaStream.ended) {
- return false;
- }
- }
- return true;
- }
- /**
- * @property {Blob} blob - Recorded data as "Blob" object.
- * @memberof MediaStreamRecorder
- * @example
- * recorder.stop(function() {
- * var blob = recorder.blob;
- * });
- */
- this.blob = null;
- /**
- * Get MediaRecorder readonly state.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * var state = recorder.getState();
- * @returns {String} Returns recording state.
- */
- this.getState = function() {
- if (!mediaRecorder) {
- return 'inactive';
- }
- return mediaRecorder.state || 'inactive';
- };
- // list of all recording states
- var allStates = [];
- /**
- * Get MediaRecorder all recording states.
- * @method
- * @memberof MediaStreamRecorder
- * @example
- * var state = recorder.getAllStates();
- * @returns {Array} Returns all recording states
- */
- this.getAllStates = function() {
- return allStates;
- };
- // if any Track within the MediaStream is muted or not enabled at any time,
- // the browser will only record black frames
- // or silence since that is the content produced by the Track
- // so we need to stopRecording as soon as any single track ends.
- if (typeof config.checkForInactiveTracks === 'undefined') {
- config.checkForInactiveTracks = false; // disable to minimize CPU usage
- }
- var self = this;
- // this method checks if media stream is stopped
- // or if any track is ended.
- (function looper() {
- if (!mediaRecorder || config.checkForInactiveTracks === false) {
- return;
- }
- if (isMediaStreamActive() === false) {
- if (!config.disableLogs) {
- // console.log('MediaStream seems stopped.');
- }
- self.stop();
- return;
- }
- setTimeout(looper, 1000); // check every second
- })();
- // for debugging
- this.name = 'MediaStreamRecorder';
- this.toString = function() {
- return this.name;
- };
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.MediaStreamRecorder = MediaStreamRecorder;
- }
- // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js
- // https://github.com/mattdiamond/Recorderjs#license-mit
- // ______________________
- // StereoAudioRecorder.js
- /**
- * StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome.
- * @summary JavaScript standalone object for stereo audio recording.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef StereoAudioRecorder
- * @class
- * @example
- * var recorder = new StereoAudioRecorder(MediaStream, {
- * sampleRate: 44100,
- * bufferSize: 4096
- * });
- * recorder.record();
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @param {object} config - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.}
- */
- function StereoAudioRecorder(mediaStream, config) {
- if (!getTracks(mediaStream, 'audio').length) {
- throw 'Your stream has no audio tracks.';
- }
- config = config || {};
- var self = this;
- // variables
- var leftchannel = [];
- var rightchannel = [];
- var recording = false;
- var recordingLength = 0;
- var jsAudioNode;
- var numberOfAudioChannels = 2;
- /**
- * Set sample rates such as 8K or 16K. Reference: http://stackoverflow.com/a/28977136/552182
- * @property {number} desiredSampRate - Desired Bits per sample * 1000
- * @memberof StereoAudioRecorder
- * @instance
- * @example
- * var recorder = StereoAudioRecorder(mediaStream, {
- * desiredSampRate: 16 * 1000 // bits-per-sample * 1000
- * });
- */
- var desiredSampRate = config.desiredSampRate;
- // backward compatibility
- if (config.leftChannel === true) {
- numberOfAudioChannels = 1;
- }
- if (config.numberOfAudioChannels === 1) {
- numberOfAudioChannels = 1;
- }
- if (!numberOfAudioChannels || numberOfAudioChannels < 1) {
- numberOfAudioChannels = 2;
- }
- if (!config.disableLogs) {
- // console.log('StereoAudioRecorder is set to record number of channels: ' + numberOfAudioChannels);
- }
- // if any Track within the MediaStream is muted or not enabled at any time,
- // the browser will only record black frames
- // or silence since that is the content produced by the Track
- // so we need to stopRecording as soon as any single track ends.
- if (typeof config.checkForInactiveTracks === 'undefined') {
- config.checkForInactiveTracks = true;
- }
- function isMediaStreamActive() {
- if (config.checkForInactiveTracks === false) {
- // always return "true"
- return true;
- }
- if ('active' in mediaStream) {
- if (!mediaStream.active) {
- return false;
- }
- } else if ('ended' in mediaStream) { // old hack
- if (mediaStream.ended) {
- return false;
- }
- }
- return true;
- }
- /**
- * This method records MediaStream.
- * @method
- * @memberof StereoAudioRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- if (isMediaStreamActive() === false) {
- throw 'Please make sure MediaStream is active.';
- }
- resetVariables();
- isAudioProcessStarted = isPaused = false;
- recording = true;
- if (typeof config.timeSlice !== 'undefined') {
- looper();
- }
- };
- function mergeLeftRightBuffers(config, callback) {
- function mergeAudioBuffers(config, cb) {
- var numberOfAudioChannels = config.numberOfAudioChannels;
- // todo: "slice(0)" --- is it causes loop? Should be removed?
- var leftBuffers = config.leftBuffers.slice(0);
- var rightBuffers = config.rightBuffers.slice(0);
- var sampleRate = config.sampleRate;
- var internalInterleavedLength = config.internalInterleavedLength;
- var desiredSampRate = config.desiredSampRate;
- if (numberOfAudioChannels === 2) {
- leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength);
- rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength);
- if (desiredSampRate) {
- leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate);
- rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate);
- }
- }
- if (numberOfAudioChannels === 1) {
- leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength);
- if (desiredSampRate) {
- leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate);
- }
- }
- // set sample rate as desired sample rate
- if (desiredSampRate) {
- sampleRate = desiredSampRate;
- }
- // for changing the sampling rate, reference:
- // http://stackoverflow.com/a/28977136/552182
- function interpolateArray(data, newSampleRate, oldSampleRate) {
- var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate));
- var newData = [];
- var springFactor = Number((data.length - 1) / (fitCount - 1));
- newData[0] = data[0];
- for (var i = 1; i < fitCount - 1; i++) {
- var tmp = i * springFactor;
- var before = Number(Math.floor(tmp)).toFixed();
- var after = Number(Math.ceil(tmp)).toFixed();
- var atPoint = tmp - before;
- newData[i] = linearInterpolate(data[before], data[after], atPoint);
- }
- newData[fitCount - 1] = data[data.length - 1];
- return newData;
- }
- function linearInterpolate(before, after, atPoint) {
- return before + (after - before) * atPoint;
- }
- function mergeBuffers(channelBuffer, rLength) {
- var result = new Float64Array(rLength);
- var offset = 0;
- var lng = channelBuffer.length;
- for (var i = 0; i < lng; i++) {
- var buffer = channelBuffer[i];
- result.set(buffer, offset);
- offset += buffer.length;
- }
- return result;
- }
- function interleave(leftChannel, rightChannel) {
- var length = leftChannel.length + rightChannel.length;
- var result = new Float64Array(length);
- var inputIndex = 0;
- for (var index = 0; index < length;) {
- result[index++] = leftChannel[inputIndex];
- result[index++] = rightChannel[inputIndex];
- inputIndex++;
- }
- return result;
- }
- function writeUTFBytes(view, offset, string) {
- var lng = string.length;
- for (var i = 0; i < lng; i++) {
- view.setUint8(offset + i, string.charCodeAt(i));
- }
- }
- // interleave both channels together
- var interleaved;
- if (numberOfAudioChannels === 2) {
- interleaved = interleave(leftBuffers, rightBuffers);
- }
- if (numberOfAudioChannels === 1) {
- interleaved = leftBuffers;
- }
- var interleavedLength = interleaved.length;
- // create wav file
- var resultingBufferLength = 44 + interleavedLength * 2;
- var buffer = new ArrayBuffer(resultingBufferLength);
- var view = new DataView(buffer);
- // RIFF chunk descriptor/identifier
- writeUTFBytes(view, 0, 'RIFF');
- // RIFF chunk length
- // changed "44" to "36" via #401
- view.setUint32(4, 36 + interleavedLength * 2, true);
- // RIFF type
- writeUTFBytes(view, 8, 'WAVE');
- // format chunk identifier
- // FMT sub-chunk
- writeUTFBytes(view, 12, 'fmt ');
- // format chunk length
- view.setUint32(16, 16, true);
- // sample format (raw)
- view.setUint16(20, 1, true);
- // stereo (2 channels)
- view.setUint16(22, numberOfAudioChannels, true);
- // sample rate
- view.setUint32(24, sampleRate, true);
- // byte rate (sample rate * block align)
- view.setUint32(28, sampleRate * numberOfAudioChannels * 2, true);
- // block align (channel count * bytes per sample)
- view.setUint16(32, numberOfAudioChannels * 2, true);
- // bits per sample
- view.setUint16(34, 16, true);
- // data sub-chunk
- // data chunk identifier
- writeUTFBytes(view, 36, 'data');
- // data chunk length
- view.setUint32(40, interleavedLength * 2, true);
- // write the PCM samples
- var lng = interleavedLength;
- var index = 44;
- var volume = 1;
- for (var i = 0; i < lng; i++) {
- view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
- index += 2;
- }
- if (cb) {
- return cb({
- buffer: buffer,
- view: view
- });
- }
- postMessage({
- buffer: buffer,
- view: view
- });
- }
- if (config.noWorker) {
- mergeAudioBuffers(config, function(data) {
- callback(data.buffer, data.view);
- });
- return;
- }
- var webWorker = processInWebWorker(mergeAudioBuffers);
- webWorker.onmessage = function(event) {
- callback(event.data.buffer, event.data.view);
- // release memory
- URL.revokeObjectURL(webWorker.workerURL);
- // kill webworker (or Chrome will kill your page after ~25 calls)
- webWorker.terminate();
- };
- webWorker.postMessage(config);
- }
- function processInWebWorker(_function) {
- var workerURL = URL.createObjectURL(new Blob([_function.toString(),
- ';this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
- ], {
- type: 'application/javascript'
- }));
- var worker = new Worker(workerURL);
- worker.workerURL = workerURL;
- return worker;
- }
- /**
- * This method stops recording MediaStream.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof StereoAudioRecorder
- * @example
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- callback = callback || function() {};
- // stop recording
- recording = false;
- mergeLeftRightBuffers({
- desiredSampRate: desiredSampRate,
- sampleRate: sampleRate,
- numberOfAudioChannels: numberOfAudioChannels,
- internalInterleavedLength: recordingLength,
- leftBuffers: leftchannel,
- rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel,
- noWorker: config.noWorker
- }, function(buffer, view) {
- /**
- * @property {Blob} blob - The recorded blob object.
- * @memberof StereoAudioRecorder
- * @example
- * recorder.stop(function(){
- * var blob = recorder.blob;
- * });
- */
- self.blob = new Blob([view], {
- type: 'audio/wav'
- });
- /**
- * @property {ArrayBuffer} buffer - The recorded buffer object.
- * @memberof StereoAudioRecorder
- * @example
- * recorder.stop(function(){
- * var buffer = recorder.buffer;
- * });
- */
- self.buffer = new ArrayBuffer(view.buffer.byteLength);
- /**
- * @property {DataView} view - The recorded data-view object.
- * @memberof StereoAudioRecorder
- * @example
- * recorder.stop(function(){
- * var view = recorder.view;
- * });
- */
- self.view = view;
- self.sampleRate = desiredSampRate || sampleRate;
- self.bufferSize = bufferSize;
- // recorded audio length
- self.length = recordingLength;
- isAudioProcessStarted = false;
- if (callback) {
- callback(self.blob);
- }
- });
- };
- if (typeof RecordRTC.Storage === 'undefined') {
- RecordRTC.Storage = {
- AudioContextConstructor: null,
- AudioContext: window.AudioContext || window.webkitAudioContext
- };
- }
- if (!RecordRTC.Storage.AudioContextConstructor || RecordRTC.Storage.AudioContextConstructor.state === 'closed') {
- RecordRTC.Storage.AudioContextConstructor = new RecordRTC.Storage.AudioContext();
- }
- var context = RecordRTC.Storage.AudioContextConstructor;
- // creates an audio node from the microphone incoming stream
- var audioInput = context.createMediaStreamSource(mediaStream);
- var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384];
- /**
- * From the spec: This value controls how frequently the audioprocess event is
- * dispatched and how many sample-frames need to be processed each call.
- * Lower values for buffer size will result in a lower (better) latency.
- * Higher values will be necessary to avoid audio breakup and glitches
- * The size of the buffer (in sample-frames) which needs to
- * be processed each time onprocessaudio is called.
- * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384).
- * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched.
- * @memberof StereoAudioRecorder
- * @example
- * recorder = new StereoAudioRecorder(mediaStream, {
- * bufferSize: 4096
- * });
- */
- // "0" means, let chrome decide the most accurate buffer-size for current platform.
- var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize;
- if (legalBufferValues.indexOf(bufferSize) === -1) {
- if (!config.disableLogs) {
- // console.log('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t'));
- }
- }
- if (context.createJavaScriptNode) {
- jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels);
- } else if (context.createScriptProcessor) {
- jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels);
- } else {
- throw 'WebAudio API has no support on this browser.';
- }
- // connect the stream to the script processor
- audioInput.connect(jsAudioNode);
- if (!config.bufferSize) {
- bufferSize = jsAudioNode.bufferSize; // device buffer-size
- }
- /**
- * The sample rate (in sample-frames per second) at which the
- * AudioContext handles audio. It is assumed that all AudioNodes
- * in the context run at this rate. In making this assumption,
- * sample-rate converters or "varispeed" processors are not supported
- * in real-time processing.
- * The sampleRate parameter describes the sample-rate of the
- * linear PCM audio data in the buffer in sample-frames per second.
- * An implementation must support sample-rates in at least
- * the range 22050 to 96000.
- * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched.
- * @memberof StereoAudioRecorder
- * @example
- * recorder = new StereoAudioRecorder(mediaStream, {
- * sampleRate: 44100
- * });
- */
- var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100;
- if (sampleRate < 22050 || sampleRate > 96000) {
- // Ref: http://stackoverflow.com/a/26303918/552182
- if (!config.disableLogs) {
- // console.log('sample-rate must be under range 22050 and 96000.');
- }
- }
- if (!config.disableLogs) {
- if (config.desiredSampRate) {
- // console.log('Desired sample-rate: ' + config.desiredSampRate);
- }
- }
- var isPaused = false;
- /**
- * This method pauses the recording process.
- * @method
- * @memberof StereoAudioRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- isPaused = true;
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof StereoAudioRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- if (isMediaStreamActive() === false) {
- throw 'Please make sure MediaStream is active.';
- }
- if (!recording) {
- if (!config.disableLogs) {
- // console.log('Seems recording has been restarted.');
- }
- this.record();
- return;
- }
- isPaused = false;
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof StereoAudioRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- config.checkForInactiveTracks = false;
- if (recording) {
- this.stop(clearRecordedDataCB);
- }
- clearRecordedDataCB();
- };
- function resetVariables() {
- leftchannel = [];
- rightchannel = [];
- recordingLength = 0;
- isAudioProcessStarted = false;
- recording = false;
- isPaused = false;
- context = null;
- self.leftchannel = leftchannel;
- self.rightchannel = rightchannel;
- self.numberOfAudioChannels = numberOfAudioChannels;
- self.desiredSampRate = desiredSampRate;
- self.sampleRate = sampleRate;
- self.recordingLength = recordingLength;
- intervalsBasedBuffers = {
- left: [],
- right: [],
- recordingLength: 0
- };
- }
- function clearRecordedDataCB() {
- if (jsAudioNode) {
- jsAudioNode.onaudioprocess = null;
- jsAudioNode.disconnect();
- jsAudioNode = null;
- }
- if (audioInput) {
- audioInput.disconnect();
- audioInput = null;
- }
- resetVariables();
- }
- // for debugging
- this.name = 'StereoAudioRecorder';
- this.toString = function() {
- return this.name;
- };
- var isAudioProcessStarted = false;
- function onAudioProcessDataAvailable(e) {
- if (isPaused) {
- return;
- }
- if (isMediaStreamActive() === false) {
- if (!config.disableLogs) {
- // console.log('MediaStream seems stopped.');
- }
- jsAudioNode.disconnect();
- recording = false;
- }
- if (!recording) {
- if (audioInput) {
- audioInput.disconnect();
- audioInput = null;
- }
- return;
- }
- /**
- * This method is called on "onaudioprocess" event's first invocation.
- * @method {function} onAudioProcessStarted
- * @memberof StereoAudioRecorder
- * @example
- * recorder.onAudioProcessStarted: function() { };
- */
- if (!isAudioProcessStarted) {
- isAudioProcessStarted = true;
- if (config.onAudioProcessStarted) {
- config.onAudioProcessStarted();
- }
- if (config.initCallback) {
- config.initCallback();
- }
- }
- var left = e.inputBuffer.getChannelData(0);
- // we clone the samples
- var chLeft = new Float32Array(left);
- leftchannel.push(chLeft);
- if (numberOfAudioChannels === 2) {
- var right = e.inputBuffer.getChannelData(1);
- var chRight = new Float32Array(right);
- rightchannel.push(chRight);
- }
- recordingLength += bufferSize;
- // export raw PCM
- self.recordingLength = recordingLength;
- if (typeof config.timeSlice !== 'undefined') {
- intervalsBasedBuffers.recordingLength += bufferSize;
- intervalsBasedBuffers.left.push(chLeft);
- if (numberOfAudioChannels === 2) {
- intervalsBasedBuffers.right.push(chRight);
- }
- }
- }
- jsAudioNode.onaudioprocess = onAudioProcessDataAvailable;
- // to prevent self audio to be connected with speakers
- if (context.createMediaStreamDestination) {
- jsAudioNode.connect(context.createMediaStreamDestination());
- } else {
- jsAudioNode.connect(context.destination);
- }
- // export raw PCM
- this.leftchannel = leftchannel;
- this.rightchannel = rightchannel;
- this.numberOfAudioChannels = numberOfAudioChannels;
- this.desiredSampRate = desiredSampRate;
- this.sampleRate = sampleRate;
- self.recordingLength = recordingLength;
- // helper for intervals based blobs
- var intervalsBasedBuffers = {
- left: [],
- right: [],
- recordingLength: 0
- };
- // this looper is used to support intervals based blobs (via timeSlice+ondataavailable)
- function looper() {
- if (!recording || typeof config.ondataavailable !== 'function' || typeof config.timeSlice === 'undefined') {
- return;
- }
- if (intervalsBasedBuffers.left.length) {
- mergeLeftRightBuffers({
- desiredSampRate: desiredSampRate,
- sampleRate: sampleRate,
- numberOfAudioChannels: numberOfAudioChannels,
- internalInterleavedLength: intervalsBasedBuffers.recordingLength,
- leftBuffers: intervalsBasedBuffers.left,
- rightBuffers: numberOfAudioChannels === 1 ? [] : intervalsBasedBuffers.right
- }, function(buffer, view) {
- var blob = new Blob([view], {
- type: 'audio/wav'
- });
- config.ondataavailable(blob);
- setTimeout(looper, config.timeSlice);
- });
- intervalsBasedBuffers = {
- left: [],
- right: [],
- recordingLength: 0
- };
- } else {
- setTimeout(looper, config.timeSlice);
- }
- }
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.StereoAudioRecorder = StereoAudioRecorder;
- }
- // _________________
- // CanvasRecorder.js
- /**
- * CanvasRecorder is a standalone class used by {@link RecordRTC} to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}.
- * @summary HTML2Canvas recording into video WebM.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef CanvasRecorder
- * @class
- * @example
- * var recorder = new CanvasRecorder(htmlElement, { disableLogs: true, useWhammyRecorder: true });
- * recorder.record();
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc.
- * @param {object} config - {disableLogs:true, initCallback: function}
- */
- function CanvasRecorder(htmlElement, config) {
- if (typeof html2canvas === 'undefined') {
- throw 'Please link: https://www.webrtc-experiment.com/screenshot.js';
- }
- config = config || {};
- if (!config.frameInterval) {
- config.frameInterval = 10;
- }
- // via DetectRTC.js
- var isCanvasSupportsStreamCapturing = false;
- ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) {
- if (item in document.createElement('canvas')) {
- isCanvasSupportsStreamCapturing = true;
- }
- });
- var _isChrome = (!!window.webkitRTCPeerConnection || !!window.webkitGetUserMedia) && !!window.chrome;
- var chromeVersion = 50;
- var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
- if (_isChrome && matchArray && matchArray[2]) {
- chromeVersion = parseInt(matchArray[2], 10);
- }
- if (_isChrome && chromeVersion < 52) {
- isCanvasSupportsStreamCapturing = false;
- }
- if (config.useWhammyRecorder) {
- isCanvasSupportsStreamCapturing = false;
- }
- var globalCanvas, mediaStreamRecorder;
- if (isCanvasSupportsStreamCapturing) {
- if (!config.disableLogs) {
- // console.log('Your browser supports both MediRecorder API and canvas.captureStream!');
- }
- if (htmlElement instanceof HTMLCanvasElement) {
- globalCanvas = htmlElement;
- } else if (htmlElement instanceof CanvasRenderingContext2D) {
- globalCanvas = htmlElement.canvas;
- } else {
- throw 'Please pass either HTMLCanvasElement or CanvasRenderingContext2D.';
- }
- } else if (!!navigator.mozGetUserMedia) {
- if (!config.disableLogs) {
- // console.error('Canvas recording is NOT supported in Firefox.');
- }
- }
- var isRecording;
- /**
- * This method records Canvas.
- * @method
- * @memberof CanvasRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- isRecording = true;
- if (isCanvasSupportsStreamCapturing && !config.useWhammyRecorder) {
- // CanvasCaptureMediaStream
- var canvasMediaStream;
- if ('captureStream' in globalCanvas) {
- canvasMediaStream = globalCanvas.captureStream(25); // 25 FPS
- } else if ('mozCaptureStream' in globalCanvas) {
- canvasMediaStream = globalCanvas.mozCaptureStream(25);
- } else if ('webkitCaptureStream' in globalCanvas) {
- canvasMediaStream = globalCanvas.webkitCaptureStream(25);
- }
- try {
- var mdStream = new MediaStream();
- mdStream.addTrack(getTracks(canvasMediaStream, 'video')[0]);
- canvasMediaStream = mdStream;
- } catch (e) {}
- if (!canvasMediaStream) {
- throw 'captureStream API are NOT available.';
- }
- // Note: Jan 18, 2016 status is that,
- // Firefox MediaRecorder API can't record CanvasCaptureMediaStream object.
- mediaStreamRecorder = new MediaStreamRecorder(canvasMediaStream, {
- mimeType: config.mimeType || 'video/webm'
- });
- mediaStreamRecorder.record();
- } else {
- whammy.frames = [];
- lastTime = new Date().getTime();
- drawCanvasFrame();
- }
- if (config.initCallback) {
- config.initCallback();
- }
- };
- this.getWebPImages = function(callback) {
- if (htmlElement.nodeName.toLowerCase() !== 'canvas') {
- callback();
- return;
- }
- var framesLength = whammy.frames.length;
- whammy.frames.forEach(function(frame, idx) {
- var framesRemaining = framesLength - idx;
- if (!config.disableLogs) {
- // console.log(framesRemaining + '/' + framesLength + ' frames remaining');
- }
- if (config.onEncodingCallback) {
- config.onEncodingCallback(framesRemaining, framesLength);
- }
- var webp = frame.image.toDataURL('image/webp', 1);
- whammy.frames[idx].image = webp;
- });
- if (!config.disableLogs) {
- // console.log('Generating WebM');
- }
- callback();
- };
- /**
- * This method stops recording Canvas.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof CanvasRecorder
- * @example
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- isRecording = false;
- var that = this;
- if (isCanvasSupportsStreamCapturing && mediaStreamRecorder) {
- mediaStreamRecorder.stop(callback);
- return;
- }
- this.getWebPImages(function() {
- /**
- * @property {Blob} blob - Recorded frames in video/webm blob.
- * @memberof CanvasRecorder
- * @example
- * recorder.stop(function() {
- * var blob = recorder.blob;
- * });
- */
- whammy.compile(function(blob) {
- if (!config.disableLogs) {
- // console.log('Recording finished!');
- }
- that.blob = blob;
- if (that.blob.forEach) {
- that.blob = new Blob([], {
- type: 'video/webm'
- });
- }
- if (callback) {
- callback(that.blob);
- }
- whammy.frames = [];
- });
- });
- };
- var isPausedRecording = false;
- /**
- * This method pauses the recording process.
- * @method
- * @memberof CanvasRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- isPausedRecording = true;
- if (mediaStreamRecorder instanceof MediaStreamRecorder) {
- mediaStreamRecorder.pause();
- return;
- }
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof CanvasRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- isPausedRecording = false;
- if (mediaStreamRecorder instanceof MediaStreamRecorder) {
- mediaStreamRecorder.resume();
- return;
- }
- if (!isRecording) {
- this.record();
- }
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof CanvasRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- if (isRecording) {
- this.stop(clearRecordedDataCB);
- }
- clearRecordedDataCB();
- };
- function clearRecordedDataCB() {
- whammy.frames = [];
- isRecording = false;
- isPausedRecording = false;
- }
- // for debugging
- this.name = 'CanvasRecorder';
- this.toString = function() {
- return this.name;
- };
- function cloneCanvas() {
- //create a new canvas
- var newCanvas = document.createElement('canvas');
- var context = newCanvas.getContext('2d');
- //set dimensions
- newCanvas.width = htmlElement.width;
- newCanvas.height = htmlElement.height;
- //apply the old canvas to the new one
- context.drawImage(htmlElement, 0, 0);
- //return the new canvas
- return newCanvas;
- }
- function drawCanvasFrame() {
- if (isPausedRecording) {
- lastTime = new Date().getTime();
- return setTimeout(drawCanvasFrame, 500);
- }
- if (htmlElement.nodeName.toLowerCase() === 'canvas') {
- var duration = new Date().getTime() - lastTime;
- // via #206, by Jack i.e. @Seymourr
- lastTime = new Date().getTime();
- whammy.frames.push({
- image: cloneCanvas(),
- duration: duration
- });
- if (isRecording) {
- setTimeout(drawCanvasFrame, config.frameInterval);
- }
- return;
- }
- html2canvas(htmlElement, {
- grabMouse: typeof config.showMousePointer === 'undefined' || config.showMousePointer,
- onrendered: function(canvas) {
- var duration = new Date().getTime() - lastTime;
- if (!duration) {
- return setTimeout(drawCanvasFrame, config.frameInterval);
- }
- // via #206, by Jack i.e. @Seymourr
- lastTime = new Date().getTime();
- whammy.frames.push({
- image: canvas.toDataURL('image/webp', 1),
- duration: duration
- });
- if (isRecording) {
- setTimeout(drawCanvasFrame, config.frameInterval);
- }
- }
- });
- }
- var lastTime = new Date().getTime();
- var whammy = new Whammy.Video(100);
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.CanvasRecorder = CanvasRecorder;
- }
- // _________________
- // WhammyRecorder.js
- /**
- * WhammyRecorder is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It runs top over {@link Whammy}.
- * @summary Video recording feature in Chrome.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef WhammyRecorder
- * @class
- * @example
- * var recorder = new WhammyRecorder(mediaStream);
- * recorder.record();
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @param {object} config - {disableLogs: true, initCallback: function, video: HTMLVideoElement, etc.}
- */
- function WhammyRecorder(mediaStream, config) {
- config = config || {};
- if (!config.frameInterval) {
- config.frameInterval = 10;
- }
- if (!config.disableLogs) {
- // console.log('Using frames-interval:', config.frameInterval);
- }
- /**
- * This method records video.
- * @method
- * @memberof WhammyRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- if (!config.width) {
- config.width = 320;
- }
- if (!config.height) {
- config.height = 240;
- }
- if (!config.video) {
- config.video = {
- width: config.width,
- height: config.height
- };
- }
- if (!config.canvas) {
- config.canvas = {
- width: config.width,
- height: config.height
- };
- }
- canvas.width = config.canvas.width || 320;
- canvas.height = config.canvas.height || 240;
- context = canvas.getContext('2d');
- // setting defaults
- if (config.video && config.video instanceof HTMLVideoElement) {
- video = config.video.cloneNode();
- if (config.initCallback) {
- config.initCallback();
- }
- } else {
- video = document.createElement('video');
- setSrcObject(mediaStream, video);
- video.onloadedmetadata = function() { // "onloadedmetadata" may NOT work in FF?
- if (config.initCallback) {
- config.initCallback();
- }
- };
- video.width = config.video.width;
- video.height = config.video.height;
- }
- video.muted = true;
- video.play();
- lastTime = new Date().getTime();
- whammy = new Whammy.Video();
- if (!config.disableLogs) {
- // console.log('canvas resolutions', canvas.width, '*', canvas.height);
- // console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height);
- }
- drawFrames(config.frameInterval);
- };
- /**
- * Draw and push frames to Whammy
- * @param {integer} frameInterval - set minimum interval (in milliseconds) between each time we push a frame to Whammy
- */
- function drawFrames(frameInterval) {
- frameInterval = typeof frameInterval !== 'undefined' ? frameInterval : 10;
- var duration = new Date().getTime() - lastTime;
- if (!duration) {
- return setTimeout(drawFrames, frameInterval, frameInterval);
- }
- if (isPausedRecording) {
- lastTime = new Date().getTime();
- return setTimeout(drawFrames, 100);
- }
- // via #206, by Jack i.e. @Seymourr
- lastTime = new Date().getTime();
- if (video.paused) {
- // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316
- // Tweak for Android Chrome
- video.play();
- }
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
- whammy.frames.push({
- duration: duration,
- image: canvas.toDataURL('image/webp')
- });
- if (!isStopDrawing) {
- setTimeout(drawFrames, frameInterval, frameInterval);
- }
- }
- function asyncLoop(o) {
- var i = -1,
- length = o.length;
- (function loop() {
- i++;
- if (i === length) {
- o.callback();
- return;
- }
- // "setTimeout" added by Jim McLeod
- setTimeout(function() {
- o.functionToLoop(loop, i);
- }, 1);
- })();
- }
- /**
- * remove black frames from the beginning to the specified frame
- * @param {Array} _frames - array of frames to be checked
- * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found)
- * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all
- * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all
- * @returns {Array} - array of frames
- */
- // pull#293 by @volodalexey
- function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance, callback) {
- var localCanvas = document.createElement('canvas');
- localCanvas.width = canvas.width;
- localCanvas.height = canvas.height;
- var context2d = localCanvas.getContext('2d');
- var resultFrames = [];
- var checkUntilNotBlack = _framesToCheck === -1;
- var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ?
- _framesToCheck : _frames.length;
- var sampleColor = {
- r: 0,
- g: 0,
- b: 0
- };
- var maxColorDifference = Math.sqrt(
- Math.pow(255, 2) +
- Math.pow(255, 2) +
- Math.pow(255, 2)
- );
- var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0;
- var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0;
- var doNotCheckNext = false;
- asyncLoop({
- length: endCheckFrame,
- functionToLoop: function(loop, f) {
- var matchPixCount, endPixCheck, maxPixCount;
- var finishImage = function() {
- if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) ; else {
- //// console.log('frame is passed : ' + f);
- if (checkUntilNotBlack) {
- doNotCheckNext = true;
- }
- resultFrames.push(_frames[f]);
- }
- loop();
- };
- if (!doNotCheckNext) {
- var image = new Image();
- image.onload = function() {
- context2d.drawImage(image, 0, 0, canvas.width, canvas.height);
- var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height);
- matchPixCount = 0;
- endPixCheck = imageData.data.length;
- maxPixCount = imageData.data.length / 4;
- for (var pix = 0; pix < endPixCheck; pix += 4) {
- var currentColor = {
- r: imageData.data[pix],
- g: imageData.data[pix + 1],
- b: imageData.data[pix + 2]
- };
- var colorDifference = Math.sqrt(
- Math.pow(currentColor.r - sampleColor.r, 2) +
- Math.pow(currentColor.g - sampleColor.g, 2) +
- Math.pow(currentColor.b - sampleColor.b, 2)
- );
- // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2)
- if (colorDifference <= maxColorDifference * pixTolerance) {
- matchPixCount++;
- }
- }
- finishImage();
- };
- image.src = _frames[f].image;
- } else {
- finishImage();
- }
- },
- callback: function() {
- resultFrames = resultFrames.concat(_frames.slice(endCheckFrame));
- if (resultFrames.length <= 0) {
- // at least one last frame should be available for next manipulation
- // if total duration of all frames will be < 1000 than ffmpeg doesn't work well...
- resultFrames.push(_frames[_frames.length - 1]);
- }
- callback(resultFrames);
- }
- });
- }
- var isStopDrawing = false;
- /**
- * This method stops recording video.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof WhammyRecorder
- * @example
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- callback = callback || function() {};
- isStopDrawing = true;
- var _this = this;
- // analyse of all frames takes some time!
- setTimeout(function() {
- // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames
- // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames
- // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color
- dropBlackFrames(whammy.frames, -1, null, null, function(frames) {
- whammy.frames = frames;
- // to display advertisement images!
- if (config.advertisement && config.advertisement.length) {
- whammy.frames = config.advertisement.concat(whammy.frames);
- }
- /**
- * @property {Blob} blob - Recorded frames in video/webm blob.
- * @memberof WhammyRecorder
- * @example
- * recorder.stop(function() {
- * var blob = recorder.blob;
- * });
- */
- whammy.compile(function(blob) {
- _this.blob = blob;
- if (_this.blob.forEach) {
- _this.blob = new Blob([], {
- type: 'video/webm'
- });
- }
- if (callback) {
- callback(_this.blob);
- }
- });
- });
- }, 10);
- };
- var isPausedRecording = false;
- /**
- * This method pauses the recording process.
- * @method
- * @memberof WhammyRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- isPausedRecording = true;
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof WhammyRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- isPausedRecording = false;
- if (isStopDrawing) {
- this.record();
- }
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof WhammyRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- if (!isStopDrawing) {
- this.stop(clearRecordedDataCB);
- }
- clearRecordedDataCB();
- };
- function clearRecordedDataCB() {
- whammy.frames = [];
- isStopDrawing = true;
- isPausedRecording = false;
- }
- // for debugging
- this.name = 'WhammyRecorder';
- this.toString = function() {
- return this.name;
- };
- var canvas = document.createElement('canvas');
- var context = canvas.getContext('2d');
- var video;
- var lastTime;
- var whammy;
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.WhammyRecorder = WhammyRecorder;
- }
- // https://github.com/antimatter15/whammy/blob/master/LICENSE
- // _________
- // Whammy.js
- // todo: Firefox now supports webp for webm containers!
- // their MediaRecorder implementation works well!
- // should we provide an option to record via Whammy.js or MediaRecorder API is a better solution?
- /**
- * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15}
- * @summary A real time javascript webm encoder based on a canvas hack.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef Whammy
- * @class
- * @example
- * var recorder = new Whammy().Video(15);
- * recorder.add(context || canvas || dataURL);
- * var output = recorder.compile();
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- */
- var Whammy = (function() {
- // a more abstract-ish API
- function WhammyVideo(duration) {
- this.frames = [];
- this.duration = duration || 1;
- this.quality = 0.8;
- }
- /**
- * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder.
- * @method
- * @memberof Whammy
- * @example
- * recorder = new Whammy().Video(0.8, 100);
- * recorder.add(canvas || context || 'image/webp');
- * @param {string} frame - Canvas || Context || image/webp
- * @param {number} duration - Stick a duration (in milliseconds)
- */
- WhammyVideo.prototype.add = function(frame, duration) {
- if ('canvas' in frame) { //CanvasRenderingContext2D
- frame = frame.canvas;
- }
- if ('toDataURL' in frame) {
- frame = frame.toDataURL('image/webp', this.quality);
- }
- if (!(/^data:image\/webp;base64,/ig).test(frame)) {
- throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp';
- }
- this.frames.push({
- image: frame,
- duration: duration || this.duration
- });
- };
- function processInWebWorker(_function) {
- var blob = URL.createObjectURL(new Blob([_function.toString(),
- 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
- ], {
- type: 'application/javascript'
- }));
- var worker = new Worker(blob);
- URL.revokeObjectURL(blob);
- return worker;
- }
- function whammyInWebWorker(frames) {
- function ArrayToWebM(frames) {
- var info = checkFrames(frames);
- if (!info) {
- return [];
- }
- var clusterMaxDuration = 30000;
- var EBML = [{
- 'id': 0x1a45dfa3, // EBML
- 'data': [{
- 'data': 1,
- 'id': 0x4286 // EBMLVersion
- }, {
- 'data': 1,
- 'id': 0x42f7 // EBMLReadVersion
- }, {
- 'data': 4,
- 'id': 0x42f2 // EBMLMaxIDLength
- }, {
- 'data': 8,
- 'id': 0x42f3 // EBMLMaxSizeLength
- }, {
- 'data': 'webm',
- 'id': 0x4282 // DocType
- }, {
- 'data': 2,
- 'id': 0x4287 // DocTypeVersion
- }, {
- 'data': 2,
- 'id': 0x4285 // DocTypeReadVersion
- }]
- }, {
- 'id': 0x18538067, // Segment
- 'data': [{
- 'id': 0x1549a966, // Info
- 'data': [{
- 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale)
- 'id': 0x2ad7b1 // TimecodeScale
- }, {
- 'data': 'whammy',
- 'id': 0x4d80 // MuxingApp
- }, {
- 'data': 'whammy',
- 'id': 0x5741 // WritingApp
- }, {
- 'data': doubleToString(info.duration),
- 'id': 0x4489 // Duration
- }]
- }, {
- 'id': 0x1654ae6b, // Tracks
- 'data': [{
- 'id': 0xae, // TrackEntry
- 'data': [{
- 'data': 1,
- 'id': 0xd7 // TrackNumber
- }, {
- 'data': 1,
- 'id': 0x73c5 // TrackUID
- }, {
- 'data': 0,
- 'id': 0x9c // FlagLacing
- }, {
- 'data': 'und',
- 'id': 0x22b59c // Language
- }, {
- 'data': 'V_VP8',
- 'id': 0x86 // CodecID
- }, {
- 'data': 'VP8',
- 'id': 0x258688 // CodecName
- }, {
- 'data': 1,
- 'id': 0x83 // TrackType
- }, {
- 'id': 0xe0, // Video
- 'data': [{
- 'data': info.width,
- 'id': 0xb0 // PixelWidth
- }, {
- 'data': info.height,
- 'id': 0xba // PixelHeight
- }]
- }]
- }]
- }]
- }];
- //Generate clusters (max duration)
- var frameNumber = 0;
- var clusterTimecode = 0;
- while (frameNumber < frames.length) {
- var clusterFrames = [];
- var clusterDuration = 0;
- do {
- clusterFrames.push(frames[frameNumber]);
- clusterDuration += frames[frameNumber].duration;
- frameNumber++;
- } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration);
- var clusterCounter = 0;
- var cluster = {
- 'id': 0x1f43b675, // Cluster
- 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames)
- }; //Add cluster to segment
- EBML[1].data.push(cluster);
- clusterTimecode += clusterDuration;
- }
- return generateEBML(EBML);
- }
- function getClusterData(clusterTimecode, clusterCounter, clusterFrames) {
- return [{
- 'data': clusterTimecode,
- 'id': 0xe7 // Timecode
- }].concat(clusterFrames.map(function(webp) {
- var block = makeSimpleBlock({
- discardable: 0,
- frame: webp.data.slice(4),
- invisible: 0,
- keyframe: 1,
- lacing: 0,
- trackNum: 1,
- timecode: Math.round(clusterCounter)
- });
- clusterCounter += webp.duration;
- return {
- data: block,
- id: 0xa3
- };
- }));
- }
- // sums the lengths of all the frames and gets the duration
- function checkFrames(frames) {
- if (!frames[0]) {
- postMessage({
- error: 'Something went wrong. Maybe WebP format is not supported in the current browser.'
- });
- return;
- }
- var width = frames[0].width,
- height = frames[0].height,
- duration = frames[0].duration;
- for (var i = 1; i < frames.length; i++) {
- duration += frames[i].duration;
- }
- return {
- duration: duration,
- width: width,
- height: height
- };
- }
- function numToBuffer(num) {
- var parts = [];
- while (num > 0) {
- parts.push(num & 0xff);
- num = num >> 8;
- }
- return new Uint8Array(parts.reverse());
- }
- function strToBuffer(str) {
- return new Uint8Array(str.split('').map(function(e) {
- return e.charCodeAt(0);
- }));
- }
- function bitsToBuffer(bits) {
- var data = [];
- var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
- bits = pad + bits;
- for (var i = 0; i < bits.length; i += 8) {
- data.push(parseInt(bits.substr(i, 8), 2));
- }
- return new Uint8Array(data);
- }
- function generateEBML(json) {
- var ebml = [];
- for (var i = 0; i < json.length; i++) {
- var data = json[i].data;
- if (typeof data === 'object') {
- data = generateEBML(data);
- }
- if (typeof data === 'number') {
- data = bitsToBuffer(data.toString(2));
- }
- if (typeof data === 'string') {
- data = strToBuffer(data);
- }
- var len = data.size || data.byteLength || data.length;
- var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
- var sizeToString = len.toString(2);
- var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString;
- var size = (new Array(zeroes)).join('0') + '1' + padded;
- ebml.push(numToBuffer(json[i].id));
- ebml.push(bitsToBuffer(size));
- ebml.push(data);
- }
- return new Blob(ebml, {
- type: 'video/webm'
- });
- }
- function makeSimpleBlock(data) {
- var flags = 0;
- if (data.keyframe) {
- flags |= 128;
- }
- if (data.invisible) {
- flags |= 8;
- }
- if (data.lacing) {
- flags |= (data.lacing << 1);
- }
- if (data.discardable) {
- flags |= 1;
- }
- if (data.trackNum > 127) {
- throw 'TrackNumber > 127 not supported';
- }
- var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) {
- return String.fromCharCode(e);
- }).join('') + data.frame;
- return out;
- }
- function parseWebP(riff) {
- var VP8 = riff.RIFF[0].WEBP[0];
- var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header
- for (var i = 0, c = []; i < 4; i++) {
- c[i] = VP8.charCodeAt(frameStart + 3 + i);
- }
- var width, height, tmp;
- //the code below is literally copied verbatim from the bitstream spec
- tmp = (c[1] << 8) | c[0];
- width = tmp & 0x3FFF;
- tmp = (c[3] << 8) | c[2];
- height = tmp & 0x3FFF;
- return {
- width: width,
- height: height,
- data: VP8,
- riff: riff
- };
- }
- function getStrLength(string, offset) {
- return parseInt(string.substr(offset + 4, 4).split('').map(function(i) {
- var unpadded = i.charCodeAt(0).toString(2);
- return (new Array(8 - unpadded.length + 1)).join('0') + unpadded;
- }).join(''), 2);
- }
- function parseRIFF(string) {
- var offset = 0;
- var chunks = {};
- while (offset < string.length) {
- var id = string.substr(offset, 4);
- var len = getStrLength(string, offset);
- var data = string.substr(offset + 4 + 4, len);
- offset += 4 + 4 + len;
- chunks[id] = chunks[id] || [];
- if (id === 'RIFF' || id === 'LIST') {
- chunks[id].push(parseRIFF(data));
- } else {
- chunks[id].push(data);
- }
- }
- return chunks;
- }
- function doubleToString(num) {
- return [].slice.call(
- new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) {
- return String.fromCharCode(e);
- }).reverse().join('');
- }
- var webm = new ArrayToWebM(frames.map(function(frame) {
- var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
- webp.duration = frame.duration;
- return webp;
- }));
- postMessage(webm);
- }
- /**
- * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof Whammy
- * @example
- * recorder = new Whammy().Video(0.8, 100);
- * recorder.compile(function(blob) {
- * // blob.size - blob.type
- * });
- */
- WhammyVideo.prototype.compile = function(callback) {
- var webWorker = processInWebWorker(whammyInWebWorker);
- webWorker.onmessage = function(event) {
- if (event.data.error) {
- // console.error(event.data.error);
- return;
- }
- callback(event.data);
- };
- webWorker.postMessage(this.frames);
- };
- return {
- /**
- * A more abstract-ish API.
- * @method
- * @memberof Whammy
- * @example
- * recorder = new Whammy().Video(0.8, 100);
- * @param {?number} speed - 0.8
- * @param {?number} quality - 100
- */
- Video: WhammyVideo
- };
- })();
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.Whammy = Whammy;
- }
- // ______________ (indexed-db)
- // DiskStorage.js
- /**
- * DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage.
- * @summary Writing blobs into IndexedDB.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @example
- * DiskStorage.Store({
- * audioBlob: yourAudioBlob,
- * videoBlob: yourVideoBlob,
- * gifBlob : yourGifBlob
- * });
- * DiskStorage.Fetch(function(dataURL, type) {
- * if(type === 'audioBlob') { }
- * if(type === 'videoBlob') { }
- * if(type === 'gifBlob') { }
- * });
- * // DiskStorage.dataStoreName = 'recordRTC';
- * // DiskStorage.onError = function(error) { };
- * @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally.
- * @property {function} Fetch - This method fetches stored blobs from IndexedDB.
- * @property {function} Store - This method stores blobs in IndexedDB.
- * @property {function} onError - This function is invoked for any known/unknown error.
- * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage.
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- */
- var DiskStorage = {
- /**
- * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally.
- * @method
- * @memberof DiskStorage
- * @internal
- * @example
- * DiskStorage.init();
- */
- init: function() {
- var self = this;
- if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') {
- // console.error('IndexedDB API are not available in this browser.');
- return;
- }
- var dbVersion = 1;
- var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''),
- db;
- var request = indexedDB.open(dbName, dbVersion);
- function createObjectStore(dataBase) {
- dataBase.createObjectStore(self.dataStoreName);
- }
- function putInDB() {
- var transaction = db.transaction([self.dataStoreName], 'readwrite');
- if (self.videoBlob) {
- transaction.objectStore(self.dataStoreName).put(self.videoBlob, 'videoBlob');
- }
- if (self.gifBlob) {
- transaction.objectStore(self.dataStoreName).put(self.gifBlob, 'gifBlob');
- }
- if (self.audioBlob) {
- transaction.objectStore(self.dataStoreName).put(self.audioBlob, 'audioBlob');
- }
- function getFromStore(portionName) {
- transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function(event) {
- if (self.callback) {
- self.callback(event.target.result, portionName);
- }
- };
- }
- getFromStore('audioBlob');
- getFromStore('videoBlob');
- getFromStore('gifBlob');
- }
- request.onerror = self.onError;
- request.onsuccess = function() {
- db = request.result;
- db.onerror = self.onError;
- if (db.setVersion) {
- if (db.version !== dbVersion) {
- var setVersion = db.setVersion(dbVersion);
- setVersion.onsuccess = function() {
- createObjectStore(db);
- putInDB();
- };
- } else {
- putInDB();
- }
- } else {
- putInDB();
- }
- };
- request.onupgradeneeded = function(event) {
- createObjectStore(event.target.result);
- };
- },
- /**
- * This method fetches stored blobs from IndexedDB.
- * @method
- * @memberof DiskStorage
- * @internal
- * @example
- * DiskStorage.Fetch(function(dataURL, type) {
- * if(type === 'audioBlob') { }
- * if(type === 'videoBlob') { }
- * if(type === 'gifBlob') { }
- * });
- */
- Fetch: function(callback) {
- this.callback = callback;
- this.init();
- return this;
- },
- /**
- * This method stores blobs in IndexedDB.
- * @method
- * @memberof DiskStorage
- * @internal
- * @example
- * DiskStorage.Store({
- * audioBlob: yourAudioBlob,
- * videoBlob: yourVideoBlob,
- * gifBlob : yourGifBlob
- * });
- */
- Store: function(config) {
- this.audioBlob = config.audioBlob;
- this.videoBlob = config.videoBlob;
- this.gifBlob = config.gifBlob;
- this.init();
- return this;
- },
- /**
- * This function is invoked for any known/unknown error.
- * @method
- * @memberof DiskStorage
- * @internal
- * @example
- * DiskStorage.onError = function(error){
- * alerot( JSON.stringify(error) );
- * };
- */
- onError: function(error) {
- // console.error(JSON.stringify(error, null, '\t'));
- },
- /**
- * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage.
- * @memberof DiskStorage
- * @internal
- * @example
- * DiskStorage.dataStoreName = 'recordRTC';
- */
- dataStoreName: 'recordRTC',
- dbName: null
- };
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.DiskStorage = DiskStorage;
- }
- // ______________
- // GifRecorder.js
- /**
- * GifRecorder is standalone calss used by {@link RecordRTC} to record video or canvas into animated gif.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef GifRecorder
- * @class
- * @example
- * var recorder = new GifRecorder(mediaStream || canvas || context, { onGifPreview: function, onGifRecordingStarted: function, width: 1280, height: 720, frameRate: 200, quality: 10 });
- * recorder.record();
- * recorder.stop(function(blob) {
- * img.src = URL.createObjectURL(blob);
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object or HTMLCanvasElement or CanvasRenderingContext2D.
- * @param {object} config - {disableLogs:true, initCallback: function, width: 320, height: 240, frameRate: 200, quality: 10}
- */
- function GifRecorder(mediaStream, config) {
- if (typeof GIFEncoder === 'undefined') {
- var script = document.createElement('script');
- script.src = 'https://www.webrtc-experiment.com/gif-recorder.js';
- (document.body || document.documentElement).appendChild(script);
- }
- config = config || {};
- var isHTMLObject = mediaStream instanceof CanvasRenderingContext2D || mediaStream instanceof HTMLCanvasElement;
- /**
- * This method records MediaStream.
- * @method
- * @memberof GifRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- if (typeof GIFEncoder === 'undefined') {
- setTimeout(self.record, 1000);
- return;
- }
- if (!isLoadedMetaData) {
- setTimeout(self.record, 1000);
- return;
- }
- if (!isHTMLObject) {
- if (!config.width) {
- config.width = video.offsetWidth || 320;
- }
- if (!config.height) {
- config.height = video.offsetHeight || 240;
- }
- if (!config.video) {
- config.video = {
- width: config.width,
- height: config.height
- };
- }
- if (!config.canvas) {
- config.canvas = {
- width: config.width,
- height: config.height
- };
- }
- canvas.width = config.canvas.width || 320;
- canvas.height = config.canvas.height || 240;
- video.width = config.video.width || 320;
- video.height = config.video.height || 240;
- }
- // external library to record as GIF images
- gifEncoder = new GIFEncoder();
- // void setRepeat(int iter)
- // Sets the number of times the set of GIF frames should be played.
- // Default is 1; 0 means play indefinitely.
- gifEncoder.setRepeat(0);
- // void setFrameRate(Number fps)
- // Sets frame rate in frames per second.
- // Equivalent to setDelay(1000/fps).
- // Using "setDelay" instead of "setFrameRate"
- gifEncoder.setDelay(config.frameRate || 200);
- // void setQuality(int quality)
- // Sets quality of color quantization (conversion of images to the
- // maximum 256 colors allowed by the GIF specification).
- // Lower values (minimum = 1) produce better colors,
- // but slow processing significantly. 10 is the default,
- // and produces good color mapping at reasonable speeds.
- // Values greater than 20 do not yield significant improvements in speed.
- gifEncoder.setQuality(config.quality || 10);
- // Boolean start()
- // This writes the GIF Header and returns false if it fails.
- gifEncoder.start();
- if (typeof config.onGifRecordingStarted === 'function') {
- config.onGifRecordingStarted();
- }
- function drawVideoFrame(time) {
- if (self.clearedRecordedData === true) {
- return;
- }
- if (isPausedRecording) {
- return setTimeout(function() {
- drawVideoFrame(time);
- }, 100);
- }
- lastAnimationFrame = requestAnimationFrame(drawVideoFrame);
- if (typeof lastFrameTime === undefined) {
- lastFrameTime = time;
- }
- // ~10 fps
- if (time - lastFrameTime < 90) {
- return;
- }
- if (!isHTMLObject && video.paused) {
- // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316
- // Tweak for Android Chrome
- video.play();
- }
- if (!isHTMLObject) {
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
- }
- if (config.onGifPreview) {
- config.onGifPreview(canvas.toDataURL('image/png'));
- }
- gifEncoder.addFrame(context);
- lastFrameTime = time;
- }
- lastAnimationFrame = requestAnimationFrame(drawVideoFrame);
- if (config.initCallback) {
- config.initCallback();
- }
- };
- /**
- * This method stops recording MediaStream.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof GifRecorder
- * @example
- * recorder.stop(function(blob) {
- * img.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- callback = callback || function() {};
- if (lastAnimationFrame) {
- cancelAnimationFrame(lastAnimationFrame);
- }
- /**
- * @property {Blob} blob - The recorded blob object.
- * @memberof GifRecorder
- * @example
- * recorder.stop(function(){
- * var blob = recorder.blob;
- * });
- */
- this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], {
- type: 'image/gif'
- });
- callback(this.blob);
- // bug: find a way to clear old recorded blobs
- gifEncoder.stream().bin = [];
- };
- var isPausedRecording = false;
- /**
- * This method pauses the recording process.
- * @method
- * @memberof GifRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- isPausedRecording = true;
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof GifRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- isPausedRecording = false;
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof GifRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- self.clearedRecordedData = true;
- clearRecordedDataCB();
- };
- function clearRecordedDataCB() {
- if (gifEncoder) {
- gifEncoder.stream().bin = [];
- }
- }
- // for debugging
- this.name = 'GifRecorder';
- this.toString = function() {
- return this.name;
- };
- var canvas = document.createElement('canvas');
- var context = canvas.getContext('2d');
- if (isHTMLObject) {
- if (mediaStream instanceof CanvasRenderingContext2D) {
- context = mediaStream;
- canvas = context.canvas;
- } else if (mediaStream instanceof HTMLCanvasElement) {
- context = mediaStream.getContext('2d');
- canvas = mediaStream;
- }
- }
- var isLoadedMetaData = true;
- if (!isHTMLObject) {
- var video = document.createElement('video');
- video.muted = true;
- video.autoplay = true;
- video.playsInline = true;
- isLoadedMetaData = false;
- video.onloadedmetadata = function() {
- isLoadedMetaData = true;
- };
- setSrcObject(mediaStream, video);
- video.play();
- }
- var lastAnimationFrame = null;
- var lastFrameTime;
- var gifEncoder;
- var self = this;
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.GifRecorder = GifRecorder;
- }
- // Last time updated: 2019-06-21 4:09:42 AM UTC
- // ________________________
- // MultiStreamsMixer v1.2.2
- // Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer
- // --------------------------------------------------
- // Muaz Khan - www.MuazKhan.com
- // MIT License - www.WebRTC-Experiment.com/licence
- // --------------------------------------------------
- function MultiStreamsMixer(arrayOfMediaStreams, elementClass) {
- var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
- (function(that) {
- if (typeof RecordRTC !== 'undefined') {
- return;
- }
- if (!that) {
- return;
- }
- if (typeof window !== 'undefined') {
- return;
- }
- if (typeof commonjsGlobal === 'undefined') {
- return;
- }
- commonjsGlobal.navigator = {
- userAgent: browserFakeUserAgent,
- getUserMedia: function() {}
- };
- if (!commonjsGlobal.console) {
- commonjsGlobal.console = {};
- }
- if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') {
- commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function() {
- // console.log(arguments);
- };
- }
- if (typeof document === 'undefined') {
- /*global document:true */
- that.document = {
- documentElement: {
- appendChild: function() {
- return '';
- }
- }
- };
- document.createElement = document.captureStream = document.mozCaptureStream = function() {
- var obj = {
- getContext: function() {
- return obj;
- },
- play: function() {},
- pause: function() {},
- drawImage: function() {},
- toDataURL: function() {
- return '';
- },
- style: {}
- };
- return obj;
- };
- that.HTMLVideoElement = function() {};
- }
- if (typeof location === 'undefined') {
- /*global location:true */
- that.location = {
- protocol: 'file:',
- href: '',
- hash: ''
- };
- }
- if (typeof screen === 'undefined') {
- /*global screen:true */
- that.screen = {
- width: 0,
- height: 0
- };
- }
- if (typeof URL === 'undefined') {
- /*global screen:true */
- that.URL = {
- createObjectURL: function() {
- return '';
- },
- revokeObjectURL: function() {
- return '';
- }
- };
- }
- /*global window:true */
- that.window = commonjsGlobal;
- })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null);
- // requires: chrome://flags/#enable-experimental-web-platform-features
- elementClass = elementClass || 'multi-streams-mixer';
- var videos = [];
- var isStopDrawingFrames = false;
- var canvas = document.createElement('canvas');
- var context = canvas.getContext('2d');
- canvas.style.opacity = 0;
- canvas.style.position = 'absolute';
- canvas.style.zIndex = -1;
- canvas.style.top = '-1000em';
- canvas.style.left = '-1000em';
- canvas.className = elementClass;
- (document.body || document.documentElement).appendChild(canvas);
- this.disableLogs = false;
- this.frameInterval = 10;
- this.width = 360;
- this.height = 240;
- // use gain node to prevent echo
- this.useGainNode = true;
- var self = this;
- // _____________________________
- // Cross-Browser-Declarations.js
- // WebAudio API representer
- var AudioContext = window.AudioContext;
- if (typeof AudioContext === 'undefined') {
- if (typeof webkitAudioContext !== 'undefined') {
- /*global AudioContext:true */
- AudioContext = webkitAudioContext;
- }
- if (typeof mozAudioContext !== 'undefined') {
- /*global AudioContext:true */
- AudioContext = mozAudioContext;
- }
- }
- /*jshint -W079 */
- var URL = window.URL;
- if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
- /*global URL:true */
- URL = webkitURL;
- }
- if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator?
- if (typeof navigator.webkitGetUserMedia !== 'undefined') {
- navigator.getUserMedia = navigator.webkitGetUserMedia;
- }
- if (typeof navigator.mozGetUserMedia !== 'undefined') {
- navigator.getUserMedia = navigator.mozGetUserMedia;
- }
- }
- var MediaStream = window.MediaStream;
- if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
- MediaStream = webkitMediaStream;
- }
- /*global MediaStream:true */
- if (typeof MediaStream !== 'undefined') {
- // override "stop" method for all browsers
- if (typeof MediaStream.prototype.stop === 'undefined') {
- MediaStream.prototype.stop = function() {
- this.getTracks().forEach(function(track) {
- track.stop();
- });
- };
- }
- }
- var Storage = {};
- if (typeof AudioContext !== 'undefined') {
- Storage.AudioContext = AudioContext;
- } else if (typeof webkitAudioContext !== 'undefined') {
- Storage.AudioContext = webkitAudioContext;
- }
- function setSrcObject(stream, element) {
- if ('srcObject' in element) {
- element.srcObject = stream;
- } else if ('mozSrcObject' in element) {
- element.mozSrcObject = stream;
- } else {
- element.srcObject = stream;
- }
- }
- this.startDrawingFrames = function() {
- drawVideosToCanvas();
- };
- function drawVideosToCanvas() {
- if (isStopDrawingFrames) {
- return;
- }
- var videosLength = videos.length;
- var fullcanvas = false;
- var remaining = [];
- videos.forEach(function(video) {
- if (!video.stream) {
- video.stream = {};
- }
- if (video.stream.fullcanvas) {
- fullcanvas = video;
- } else {
- // todo: video.stream.active or video.stream.live to fix blank frames issues?
- remaining.push(video);
- }
- });
- if (fullcanvas) {
- canvas.width = fullcanvas.stream.width;
- canvas.height = fullcanvas.stream.height;
- } else if (remaining.length) {
- canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width;
- var height = 1;
- if (videosLength === 3 || videosLength === 4) {
- height = 2;
- }
- if (videosLength === 5 || videosLength === 6) {
- height = 3;
- }
- if (videosLength === 7 || videosLength === 8) {
- height = 4;
- }
- if (videosLength === 9 || videosLength === 10) {
- height = 5;
- }
- canvas.height = remaining[0].height * height;
- } else {
- canvas.width = self.width || 360;
- canvas.height = self.height || 240;
- }
- if (fullcanvas && fullcanvas instanceof HTMLVideoElement) {
- drawImage(fullcanvas);
- }
- remaining.forEach(function(video, idx) {
- drawImage(video, idx);
- });
- setTimeout(drawVideosToCanvas, self.frameInterval);
- }
- function drawImage(video, idx) {
- if (isStopDrawingFrames) {
- return;
- }
- var x = 0;
- var y = 0;
- var width = video.width;
- var height = video.height;
- if (idx === 1) {
- x = video.width;
- }
- if (idx === 2) {
- y = video.height;
- }
- if (idx === 3) {
- x = video.width;
- y = video.height;
- }
- if (idx === 4) {
- y = video.height * 2;
- }
- if (idx === 5) {
- x = video.width;
- y = video.height * 2;
- }
- if (idx === 6) {
- y = video.height * 3;
- }
- if (idx === 7) {
- x = video.width;
- y = video.height * 3;
- }
- if (typeof video.stream.left !== 'undefined') {
- x = video.stream.left;
- }
- if (typeof video.stream.top !== 'undefined') {
- y = video.stream.top;
- }
- if (typeof video.stream.width !== 'undefined') {
- width = video.stream.width;
- }
- if (typeof video.stream.height !== 'undefined') {
- height = video.stream.height;
- }
- context.drawImage(video, x, y, width, height);
- if (typeof video.stream.onRender === 'function') {
- video.stream.onRender(context, x, y, width, height, idx);
- }
- }
- function getMixedStream() {
- isStopDrawingFrames = false;
- var mixedVideoStream = getMixedVideoStream();
- var mixedAudioStream = getMixedAudioStream();
- if (mixedAudioStream) {
- mixedAudioStream.getTracks().filter(function(t) {
- return t.kind === 'audio';
- }).forEach(function(track) {
- mixedVideoStream.addTrack(track);
- });
- }
- arrayOfMediaStreams.forEach(function(stream) {
- if (stream.fullcanvas) ;
- });
- // mixedVideoStream.prototype.appendStreams = appendStreams;
- // mixedVideoStream.prototype.resetVideoStreams = resetVideoStreams;
- // mixedVideoStream.prototype.clearRecordedData = clearRecordedData;
- return mixedVideoStream;
- }
- function getMixedVideoStream() {
- resetVideoStreams();
- var capturedStream;
- if ('captureStream' in canvas) {
- capturedStream = canvas.captureStream();
- } else if ('mozCaptureStream' in canvas) {
- capturedStream = canvas.mozCaptureStream();
- } else if (!self.disableLogs) {
- // console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features');
- }
- var videoStream = new MediaStream();
- capturedStream.getTracks().filter(function(t) {
- return t.kind === 'video';
- }).forEach(function(track) {
- videoStream.addTrack(track);
- });
- canvas.stream = videoStream;
- return videoStream;
- }
- function getMixedAudioStream() {
- // via: @pehrsons
- if (!Storage.AudioContextConstructor) {
- Storage.AudioContextConstructor = new Storage.AudioContext();
- }
- self.audioContext = Storage.AudioContextConstructor;
- self.audioSources = [];
- if (self.useGainNode === true) {
- self.gainNode = self.audioContext.createGain();
- self.gainNode.connect(self.audioContext.destination);
- self.gainNode.gain.value = 0; // don't hear self
- }
- var audioTracksLength = 0;
- arrayOfMediaStreams.forEach(function(stream) {
- if (!stream.getTracks().filter(function(t) {
- return t.kind === 'audio';
- }).length) {
- return;
- }
- audioTracksLength++;
- var audioSource = self.audioContext.createMediaStreamSource(stream);
- if (self.useGainNode === true) {
- audioSource.connect(self.gainNode);
- }
- self.audioSources.push(audioSource);
- });
- if (!audioTracksLength) {
- // because "self.audioContext" is not initialized
- // that's why we've to ignore rest of the code
- return;
- }
- self.audioDestination = self.audioContext.createMediaStreamDestination();
- self.audioSources.forEach(function(audioSource) {
- audioSource.connect(self.audioDestination);
- });
- return self.audioDestination.stream;
- }
- function getVideo(stream) {
- var video = document.createElement('video');
- setSrcObject(stream, video);
- video.className = elementClass;
- video.muted = true;
- video.volume = 0;
- video.width = stream.width || self.width || 360;
- video.height = stream.height || self.height || 240;
- video.play();
- return video;
- }
- this.appendStreams = function(streams) {
- if (!streams) {
- throw 'First parameter is required.';
- }
- if (!(streams instanceof Array)) {
- streams = [streams];
- }
- streams.forEach(function(stream) {
- var newStream = new MediaStream();
- if (stream.getTracks().filter(function(t) {
- return t.kind === 'video';
- }).length) {
- var video = getVideo(stream);
- video.stream = stream;
- videos.push(video);
- newStream.addTrack(stream.getTracks().filter(function(t) {
- return t.kind === 'video';
- })[0]);
- }
- if (stream.getTracks().filter(function(t) {
- return t.kind === 'audio';
- }).length) {
- var audioSource = self.audioContext.createMediaStreamSource(stream);
- self.audioDestination = self.audioContext.createMediaStreamDestination();
- audioSource.connect(self.audioDestination);
- newStream.addTrack(self.audioDestination.stream.getTracks().filter(function(t) {
- return t.kind === 'audio';
- })[0]);
- }
- arrayOfMediaStreams.push(newStream);
- });
- };
- this.releaseStreams = function() {
- videos = [];
- isStopDrawingFrames = true;
- if (self.gainNode) {
- self.gainNode.disconnect();
- self.gainNode = null;
- }
- if (self.audioSources.length) {
- self.audioSources.forEach(function(source) {
- source.disconnect();
- });
- self.audioSources = [];
- }
- if (self.audioDestination) {
- self.audioDestination.disconnect();
- self.audioDestination = null;
- }
- if (self.audioContext) {
- self.audioContext.close();
- }
- self.audioContext = null;
- context.clearRect(0, 0, canvas.width, canvas.height);
- if (canvas.stream) {
- canvas.stream.stop();
- canvas.stream = null;
- }
- };
- this.resetVideoStreams = function(streams) {
- if (streams && !(streams instanceof Array)) {
- streams = [streams];
- }
- resetVideoStreams(streams);
- };
- function resetVideoStreams(streams) {
- videos = [];
- streams = streams || arrayOfMediaStreams;
- // via: @adrian-ber
- streams.forEach(function(stream) {
- if (!stream.getTracks().filter(function(t) {
- return t.kind === 'video';
- }).length) {
- return;
- }
- var video = getVideo(stream);
- video.stream = stream;
- videos.push(video);
- });
- }
- // for debugging
- this.name = 'MultiStreamsMixer';
- this.toString = function() {
- return this.name;
- };
- this.getMixedStream = getMixedStream;
- }
- if (typeof RecordRTC === 'undefined') {
- {
- module.exports = MultiStreamsMixer;
- }
- }
- // ______________________
- // MultiStreamRecorder.js
- /*
- * Video conference recording, using captureStream API along with WebAudio and Canvas2D API.
- */
- /**
- * MultiStreamRecorder can record multiple videos in single container.
- * @summary Multi-videos recorder.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef MultiStreamRecorder
- * @class
- * @example
- * var options = {
- * mimeType: 'video/webm'
- * }
- * var recorder = new MultiStreamRecorder(ArrayOfMediaStreams, options);
- * recorder.record();
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- *
- * // or
- * var blob = recorder.blob;
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStreams} mediaStreams - Array of MediaStreams.
- * @param {object} config - {disableLogs:true, frameInterval: 1, mimeType: "video/webm"}
- */
- function MultiStreamRecorder(arrayOfMediaStreams, options) {
- arrayOfMediaStreams = arrayOfMediaStreams || [];
- var self = this;
- var mixer;
- var mediaRecorder;
- options = options || {
- elementClass: 'multi-streams-mixer',
- mimeType: 'video/webm',
- video: {
- width: 360,
- height: 240
- }
- };
- if (!options.frameInterval) {
- options.frameInterval = 10;
- }
- if (!options.video) {
- options.video = {};
- }
- if (!options.video.width) {
- options.video.width = 360;
- }
- if (!options.video.height) {
- options.video.height = 240;
- }
- /**
- * This method records all MediaStreams.
- * @method
- * @memberof MultiStreamRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- // github/muaz-khan/MultiStreamsMixer
- mixer = new MultiStreamsMixer(arrayOfMediaStreams, options.elementClass || 'multi-streams-mixer');
- if (getAllVideoTracks().length) {
- mixer.frameInterval = options.frameInterval || 10;
- mixer.width = options.video.width || 360;
- mixer.height = options.video.height || 240;
- mixer.startDrawingFrames();
- }
- if (options.previewStream && typeof options.previewStream === 'function') {
- options.previewStream(mixer.getMixedStream());
- }
- // record using MediaRecorder API
- mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream(), options);
- mediaRecorder.record();
- };
- function getAllVideoTracks() {
- var tracks = [];
- arrayOfMediaStreams.forEach(function(stream) {
- getTracks(stream, 'video').forEach(function(track) {
- tracks.push(track);
- });
- });
- return tracks;
- }
- /**
- * This method stops recording MediaStream.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof MultiStreamRecorder
- * @example
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- if (!mediaRecorder) {
- return;
- }
- mediaRecorder.stop(function(blob) {
- self.blob = blob;
- callback(blob);
- self.clearRecordedData();
- });
- };
- /**
- * This method pauses the recording process.
- * @method
- * @memberof MultiStreamRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- if (mediaRecorder) {
- mediaRecorder.pause();
- }
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof MultiStreamRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- if (mediaRecorder) {
- mediaRecorder.resume();
- }
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof MultiStreamRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- if (mediaRecorder) {
- mediaRecorder.clearRecordedData();
- mediaRecorder = null;
- }
- if (mixer) {
- mixer.releaseStreams();
- mixer = null;
- }
- };
- /**
- * Add extra media-streams to existing recordings.
- * @method
- * @memberof MultiStreamRecorder
- * @param {MediaStreams} mediaStreams - Array of MediaStreams
- * @example
- * recorder.addStreams([newAudioStream, newVideoStream]);
- */
- this.addStreams = function(streams) {
- if (!streams) {
- throw 'First parameter is required.';
- }
- if (!(streams instanceof Array)) {
- streams = [streams];
- }
- arrayOfMediaStreams.concat(streams);
- if (!mediaRecorder || !mixer) {
- return;
- }
- mixer.appendStreams(streams);
- if (options.previewStream && typeof options.previewStream === 'function') {
- options.previewStream(mixer.getMixedStream());
- }
- };
- /**
- * Reset videos during live recording. Replace old videos e.g. replace cameras with full-screen.
- * @method
- * @memberof MultiStreamRecorder
- * @param {MediaStreams} mediaStreams - Array of MediaStreams
- * @example
- * recorder.resetVideoStreams([newVideo1, newVideo2]);
- */
- this.resetVideoStreams = function(streams) {
- if (!mixer) {
- return;
- }
- if (streams && !(streams instanceof Array)) {
- streams = [streams];
- }
- mixer.resetVideoStreams(streams);
- };
- /**
- * Returns MultiStreamsMixer
- * @method
- * @memberof MultiStreamRecorder
- * @example
- * let mixer = recorder.getMixer();
- * mixer.appendStreams([newStream]);
- */
- this.getMixer = function() {
- return mixer;
- };
- // for debugging
- this.name = 'MultiStreamRecorder';
- this.toString = function() {
- return this.name;
- };
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.MultiStreamRecorder = MultiStreamRecorder;
- }
- // _____________________
- // RecordRTC.promises.js
- /**
- * RecordRTCPromisesHandler adds promises support in {@link RecordRTC}. Try a {@link https://github.com/muaz-khan/RecordRTC/blob/master/simple-demos/RecordRTCPromisesHandler.html|demo here}
- * @summary Promises for {@link RecordRTC}
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef RecordRTCPromisesHandler
- * @class
- * @example
- * var recorder = new RecordRTCPromisesHandler(mediaStream, options);
- * recorder.startRecording()
- * .then(successCB)
- * .catch(errorCB);
- * // Note: You can access all RecordRTC API using "recorder.recordRTC" e.g.
- * recorder.recordRTC.onStateChanged = function(state) {};
- * recorder.recordRTC.setRecordingDuration(5000);
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc.
- * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.}
- * @throws Will throw an error if "new" keyword is not used to initiate "RecordRTCPromisesHandler". Also throws error if first argument "MediaStream" is missing.
- * @requires {@link RecordRTC}
- */
- function RecordRTCPromisesHandler(mediaStream, options) {
- if (!this) {
- throw 'Use "new RecordRTCPromisesHandler()"';
- }
- if (typeof mediaStream === 'undefined') {
- throw 'First argument "MediaStream" is required.';
- }
- var self = this;
- /**
- * @property {Blob} blob - Access/reach the native {@link RecordRTC} object.
- * @memberof RecordRTCPromisesHandler
- * @example
- * let internal = recorder.recordRTC.getInternalRecorder();
- * alert(internal instanceof MediaStreamRecorder);
- * recorder.recordRTC.onStateChanged = function(state) {};
- */
- self.recordRTC = new RecordRTC(mediaStream, options);
- /**
- * This method records MediaStream.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.startRecording()
- * .then(successCB)
- * .catch(errorCB);
- */
- this.startRecording = function() {
- return new Promise(function(resolve, reject) {
- try {
- self.recordRTC.startRecording();
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method stops the recording.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.stopRecording().then(function() {
- * var blob = recorder.getBlob();
- * }).catch(errorCB);
- */
- this.stopRecording = function() {
- return new Promise(function(resolve, reject) {
- try {
- self.recordRTC.stopRecording(function(url) {
- self.blob = self.recordRTC.getBlob();
- if (!self.blob || !self.blob.size) {
- reject('Empty blob.', self.blob);
- return;
- }
- resolve(url);
- });
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method pauses the recording. You can resume recording using "resumeRecording" method.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.pauseRecording()
- * .then(successCB)
- * .catch(errorCB);
- */
- this.pauseRecording = function() {
- return new Promise(function(resolve, reject) {
- try {
- self.recordRTC.pauseRecording();
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method resumes the recording.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.resumeRecording()
- * .then(successCB)
- * .catch(errorCB);
- */
- this.resumeRecording = function() {
- return new Promise(function(resolve, reject) {
- try {
- self.recordRTC.resumeRecording();
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method returns data-url for the recorded blob.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.stopRecording().then(function() {
- * recorder.getDataURL().then(function(dataURL) {
- * window.open(dataURL);
- * }).catch(errorCB);;
- * }).catch(errorCB);
- */
- this.getDataURL = function(callback) {
- return new Promise(function(resolve, reject) {
- try {
- self.recordRTC.getDataURL(function(dataURL) {
- resolve(dataURL);
- });
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method returns the recorded blob.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.stopRecording().then(function() {
- * recorder.getBlob().then(function(blob) {})
- * }).catch(errorCB);
- */
- this.getBlob = function() {
- return new Promise(function(resolve, reject) {
- try {
- resolve(self.recordRTC.getBlob());
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method returns the internal recording object.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * let internalRecorder = await recorder.getInternalRecorder();
- * if(internalRecorder instanceof MultiStreamRecorder) {
- * internalRecorder.addStreams([newAudioStream]);
- * internalRecorder.resetVideoStreams([screenStream]);
- * }
- * @returns {Object}
- */
- this.getInternalRecorder = function() {
- return new Promise(function(resolve, reject) {
- try {
- resolve(self.recordRTC.getInternalRecorder());
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * This method resets the recorder. So that you can reuse single recorder instance many times.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * await recorder.reset();
- * recorder.startRecording(); // record again
- */
- this.reset = function() {
- return new Promise(function(resolve, reject) {
- try {
- resolve(self.recordRTC.reset());
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * Destroy RecordRTC instance. Clear all recorders and objects.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * recorder.destroy().then(successCB).catch(errorCB);
- */
- this.destroy = function() {
- return new Promise(function(resolve, reject) {
- try {
- resolve(self.recordRTC.destroy());
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * Get recorder's readonly state.
- * @method
- * @memberof RecordRTCPromisesHandler
- * @example
- * let state = await recorder.getState();
- * // or
- * recorder.getState().then(state => { console.log(state); })
- * @returns {String} Returns recording state.
- */
- this.getState = function() {
- return new Promise(function(resolve, reject) {
- try {
- resolve(self.recordRTC.getState());
- } catch (e) {
- reject(e);
- }
- });
- };
- /**
- * @property {Blob} blob - Recorded data as "Blob" object.
- * @memberof RecordRTCPromisesHandler
- * @example
- * await recorder.stopRecording();
- * let blob = recorder.getBlob(); // or "recorder.recordRTC.blob"
- * invokeSaveAsDialog(blob);
- */
- this.blob = null;
- /**
- * RecordRTC version number
- * @property {String} version - Release version number.
- * @memberof RecordRTCPromisesHandler
- * @static
- * @readonly
- * @example
- * alert(recorder.version);
- */
- this.version = '5.6.2';
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.RecordRTCPromisesHandler = RecordRTCPromisesHandler;
- }
- // ______________________
- // WebAssemblyRecorder.js
- /**
- * WebAssemblyRecorder lets you create webm videos in JavaScript via WebAssembly. The library consumes raw RGBA32 buffers (4 bytes per pixel) and turns them into a webm video with the given framerate and quality. This makes it compatible out-of-the-box with ImageData from a CANVAS. With realtime mode you can also use webm-wasm for streaming webm videos.
- * @summary Video recording feature in Chrome, Firefox and maybe Edge.
- * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
- * @author {@link https://MuazKhan.com|Muaz Khan}
- * @typedef WebAssemblyRecorder
- * @class
- * @example
- * var recorder = new WebAssemblyRecorder(mediaStream);
- * recorder.record();
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
- * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
- * @param {object} config - {webAssemblyPath:'webm-wasm.wasm',workerPath: 'webm-worker.js', frameRate: 30, width: 1920, height: 1080, bitrate: 1024, realtime: true}
- */
- function WebAssemblyRecorder(stream, config) {
- // based on: github.com/GoogleChromeLabs/webm-wasm
- if (typeof ReadableStream === 'undefined' || typeof WritableStream === 'undefined') {
- // because it fixes readable/writable streams issues
- // console.error('Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js');
- }
- config = config || {};
- config.width = config.width || 640;
- config.height = config.height || 480;
- config.frameRate = config.frameRate || 30;
- config.bitrate = config.bitrate || 1200;
- config.realtime = config.realtime || true;
- var finished;
- function cameraStream() {
- return new ReadableStream({
- start: function(controller) {
- var cvs = document.createElement('canvas');
- var video = document.createElement('video');
- var first = true;
- video.srcObject = stream;
- video.muted = true;
- video.height = config.height;
- video.width = config.width;
- video.volume = 0;
- video.onplaying = function() {
- cvs.width = config.width;
- cvs.height = config.height;
- var ctx = cvs.getContext('2d');
- var frameTimeout = 1000 / config.frameRate;
- var cameraTimer = setInterval(function f() {
- if (finished) {
- clearInterval(cameraTimer);
- controller.close();
- }
- if (first) {
- first = false;
- if (config.onVideoProcessStarted) {
- config.onVideoProcessStarted();
- }
- }
- ctx.drawImage(video, 0, 0);
- if (controller._controlledReadableStream.state !== 'closed') {
- try {
- controller.enqueue(
- ctx.getImageData(0, 0, config.width, config.height)
- );
- } catch (e) {}
- }
- }, frameTimeout);
- };
- video.play();
- }
- });
- }
- var worker;
- function startRecording(stream, buffer) {
- if (!config.workerPath && !buffer) {
- finished = false;
- // is it safe to use @latest ?
- fetch(
- 'https://unpkg.com/webm-wasm@latest/dist/webm-worker.js'
- ).then(function(r) {
- r.arrayBuffer().then(function(buffer) {
- startRecording(stream, buffer);
- });
- });
- return;
- }
- if (!config.workerPath && buffer instanceof ArrayBuffer) {
- var blob = new Blob([buffer], {
- type: 'text/javascript'
- });
- config.workerPath = URL.createObjectURL(blob);
- }
- if (!config.workerPath) {
- // console.error('workerPath parameter is missing.');
- }
- worker = new Worker(config.workerPath);
- worker.postMessage(config.webAssemblyPath || 'https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm');
- worker.addEventListener('message', function(event) {
- if (event.data === 'READY') {
- worker.postMessage({
- width: config.width,
- height: config.height,
- bitrate: config.bitrate || 1200,
- timebaseDen: config.frameRate || 30,
- realtime: config.realtime
- });
- cameraStream().pipeTo(new WritableStream({
- write: function(image) {
- if (finished) {
- // console.error('Got image, but recorder is finished!');
- return;
- }
- worker.postMessage(image.data.buffer, [image.data.buffer]);
- }
- }));
- } else if (!!event.data) {
- if (!isPaused) {
- arrayOfBuffers.push(event.data);
- }
- }
- });
- }
- /**
- * This method records video.
- * @method
- * @memberof WebAssemblyRecorder
- * @example
- * recorder.record();
- */
- this.record = function() {
- arrayOfBuffers = [];
- isPaused = false;
- this.blob = null;
- startRecording(stream);
- if (typeof config.initCallback === 'function') {
- config.initCallback();
- }
- };
- var isPaused;
- /**
- * This method pauses the recording process.
- * @method
- * @memberof WebAssemblyRecorder
- * @example
- * recorder.pause();
- */
- this.pause = function() {
- isPaused = true;
- };
- /**
- * This method resumes the recording process.
- * @method
- * @memberof WebAssemblyRecorder
- * @example
- * recorder.resume();
- */
- this.resume = function() {
- isPaused = false;
- };
- function terminate(callback) {
- if (!worker) {
- if (callback) {
- callback();
- }
- return;
- }
- // Wait for null event data to indicate that the encoding is complete
- worker.addEventListener('message', function(event) {
- if (event.data === null) {
- worker.terminate();
- worker = null;
- if (callback) {
- callback();
- }
- }
- });
- worker.postMessage(null);
- }
- var arrayOfBuffers = [];
- /**
- * This method stops recording video.
- * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
- * @method
- * @memberof WebAssemblyRecorder
- * @example
- * recorder.stop(function(blob) {
- * video.src = URL.createObjectURL(blob);
- * });
- */
- this.stop = function(callback) {
- finished = true;
- var recorder = this;
- terminate(function() {
- recorder.blob = new Blob(arrayOfBuffers, {
- type: 'video/webm'
- });
- callback(recorder.blob);
- });
- };
- // for debugging
- this.name = 'WebAssemblyRecorder';
- this.toString = function() {
- return this.name;
- };
- /**
- * This method resets currently recorded data.
- * @method
- * @memberof WebAssemblyRecorder
- * @example
- * recorder.clearRecordedData();
- */
- this.clearRecordedData = function() {
- arrayOfBuffers = [];
- isPaused = false;
- this.blob = null;
- // todo: if recording-ON then STOP it first
- };
- /**
- * @property {Blob} blob - The recorded blob object.
- * @memberof WebAssemblyRecorder
- * @example
- * recorder.stop(function(){
- * var blob = recorder.blob;
- * });
- */
- this.blob = null;
- }
- if (typeof RecordRTC !== 'undefined') {
- RecordRTC.WebAssemblyRecorder = WebAssemblyRecorder;
- }
- });
- class RecordRTCLoader extends Emitter {
- constructor(player) {
- super();
- this.player = player;
- this.fileName = '';
- this.fileType = FILE_SUFFIX.webm;
- this.isRecording = false;
- this.recordingTimestamp = 0;
- this.recordingInterval = null;
- player.debug.log('Recorder', 'init');
- }
- destroy() {
- this._reset();
- this.player.debug.log('Recorder', 'destroy');
- }
- setFileName(fileName, fileType) {
- this.fileName = fileName;
- if (FILE_SUFFIX.mp4 === fileType || FILE_SUFFIX.webm === fileType) {
- this.fileType = fileType;
- }
- }
- get recording() {
- return this.isRecording;
- }
- get recordTime() {
- return this.recordingTimestamp;
- }
- startRecord() {
- const debug = this.player.debug;
- const options = {
- type: 'video',
- mimeType: 'video/webm;codecs=h264',
- onTimeStamp: timestamp => {
- debug.log('Recorder', 'record timestamp :' + timestamp);
- },
- disableLogs: !this.player._opt.debug
- };
- try {
- const stream = this.player.video.$videoElement.captureStream(25);
- if (this.player.audio && this.player.audio.mediaStreamAudioDestinationNode && this.player.audio.mediaStreamAudioDestinationNode.stream && !this.player.audio.isStateSuspended() && this.player.audio.hasAudio && this.player._opt.hasAudio) {
- const audioStream = this.player.audio.mediaStreamAudioDestinationNode.stream;
- if (audioStream.getAudioTracks().length > 0) {
- const audioTrack = audioStream.getAudioTracks()[0];
- if (audioTrack && audioTrack.enabled) {
- stream.addTrack(audioTrack);
- }
- }
- }
- this.recorder = RecordRTC_1(stream, options);
- } catch (e) {
- debug.error('Recorder', e);
- this.emit(EVENTS.recordCreateError);
- }
- if (this.recorder) {
- this.isRecording = true;
- this.player.emit(EVENTS.recording, true);
- this.recorder.startRecording();
- debug.log('Recorder', 'start recording');
- this.player.emit(EVENTS.recordStart);
- this.recordingInterval = window.setInterval(() => {
- this.recordingTimestamp += 1;
- this.player.emit(EVENTS.recordingTimestamp, this.recordingTimestamp);
- }, 1000);
- }
- }
- stopRecordAndSave() {
- if (!this.recorder || !this.isRecording) {
- return;
- }
- this.recorder.stopRecording(() => {
- this.player.debug.log('Recorder', 'stop recording');
- this.player.emit(EVENTS.recordEnd);
- downloadRecord(this.recorder.getBlob(), this.fileName, this.fileType);
- this._reset();
- this.player.emit(EVENTS.recording, false);
- });
- }
- _reset() {
- this.isRecording = false;
- this.recordingTimestamp = 0;
- if (this.recorder) {
- this.recorder.destroy();
- this.recorder = null;
- }
- this.fileName = null;
- if (this.recordingInterval) {
- clearInterval(this.recordingInterval);
- }
- this.recordingInterval = null;
- }
- }
- class Recorder {
- constructor(player) {
- const Loader = Recorder.getLoaderFactory();
- return new Loader(player);
- }
- static getLoaderFactory() {
- return RecordRTCLoader;
- }
- }
- class DecoderWorker {
- constructor(player) {
- this.player = player;
- this.decoderWorker = new Worker(player._opt.decoder);
- this._initDecoderWorker();
- player.debug.log('decoderWorker', 'init');
- }
- destroy() {
- this.decoderWorker.postMessage({
- cmd: WORKER_SEND_TYPE.close
- });
- this.decoderWorker.terminate();
- this.decoderWorker = null;
- this.player.debug.log(`decoderWorker`, 'destroy');
- }
- _initDecoderWorker() {
- const {
- debug,
- events: {
- proxy
- }
- } = this.player;
- this.decoderWorker.onmessage = event => {
- const msg = event.data;
- switch (msg.cmd) {
- case WORKER_CMD_TYPE.init:
- debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.init);
- if (!this.player.loaded) {
- this.player.emit(EVENTS.load);
- }
- this.player.emit(EVENTS.decoderWorkerInit);
- this._initWork();
- break;
- case WORKER_CMD_TYPE.videoCode:
- debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.videoCode, msg.code);
- if (!this.player._times.decodeStart) {
- this.player._times.decodeStart = now();
- }
- this.player.video.updateVideoInfo({
- encTypeCode: msg.code
- });
- break;
- case WORKER_CMD_TYPE.audioCode:
- debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.audioCode, msg.code);
- this.player.audio && this.player.audio.updateAudioInfo({
- encTypeCode: msg.code
- });
- break;
- case WORKER_CMD_TYPE.initVideo:
- debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initVideo, `width:${msg.w},height:${msg.h}`);
- this.player.video.updateVideoInfo({
- width: msg.w,
- height: msg.h
- });
- this.player.video.initCanvasViewSize();
- break;
- case WORKER_CMD_TYPE.initAudio:
- debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initAudio, `channels:${msg.channels},sampleRate:${msg.sampleRate}`);
- if (this.player.audio) {
- this.player.audio.updateAudioInfo(msg);
- this.player.audio.initScriptNode(msg);
- }
- break;
- case WORKER_CMD_TYPE.render:
- // debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.render, `msg ts:${msg.ts}`);
- this.player.handleRender();
- this.player.video.render(msg);
- this.player.emit(EVENTS.timeUpdate, msg.ts);
- this.player.updateStats({
- fps: true,
- ts: msg.ts,
- buf: msg.delay
- });
- if (!this.player._times.videoStart) {
- this.player._times.videoStart = now();
- this.player.handlePlayToRenderTimes();
- }
- break;
- case WORKER_CMD_TYPE.playAudio:
- // debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.playAudio, `msg ts:${msg.ts}`);
- // 只有在 playing 的时候。
- if (this.player.playing && this.player.audio) {
- this.player.audio.play(msg.buffer, msg.ts);
- }
- break;
- case WORKER_CMD_TYPE.wasmError:
- if (msg.message) {
- if (msg.message.indexOf(WASM_ERROR.invalidNalUnitSize) !== -1) {
- this.player.emit(EVENTS.error, EVENTS_ERROR.wasmDecodeError);
- this.player.emit(EVENTS_ERROR.wasmDecodeError);
- }
- }
- break;
- default:
- this.player[msg.cmd] && this.player[msg.cmd](msg);
- }
- };
- }
- _initWork() {
- const opt = {
- debug: this.player._opt.debug,
- forceNoOffscreen: this.player._opt.forceNoOffscreen,
- useWCS: this.player._opt.useWCS,
- videoBuffer: this.player._opt.videoBuffer,
- videoBufferDelay: this.player._opt.videoBufferDelay,
- openWebglAlignment: this.player._opt.openWebglAlignment
- };
- this.decoderWorker.postMessage({
- cmd: WORKER_SEND_TYPE.init,
- opt: JSON.stringify(opt),
- sampleRate: this.player.audio && this.player.audio.audioContext.sampleRate || 0
- });
- }
- decodeVideo(arrayBuffer, ts, isIFrame) {
- const options = {
- type: MEDIA_TYPE.video,
- ts: Math.max(ts, 0),
- isIFrame
- }; // this.player.debug.log('decoderWorker', 'decodeVideo', options);
- this.decoderWorker.postMessage({
- cmd: WORKER_SEND_TYPE.decode,
- buffer: arrayBuffer,
- options
- }, [arrayBuffer.buffer]);
- }
- decodeAudio(arrayBuffer, ts) {
- if (this.player._opt.useWCS && !this.player._opt.useOffscreen) {
- this._decodeAudioNoDelay(arrayBuffer, ts);
- } else if (this.player._opt.useMSE) {
- this._decodeAudioNoDelay(arrayBuffer, ts);
- } else {
- this._decodeAudio(arrayBuffer, ts);
- }
- } //
- _decodeAudio(arrayBuffer, ts) {
- const options = {
- type: MEDIA_TYPE.audio,
- ts: Math.max(ts, 0)
- }; // this.player.debug.log('decoderWorker', 'decodeAudio',options);
- this.decoderWorker.postMessage({
- cmd: WORKER_SEND_TYPE.decode,
- buffer: arrayBuffer,
- options
- }, [arrayBuffer.buffer]);
- }
- _decodeAudioNoDelay(arrayBuffer, ts) {
- //// console.log('_decodeAudioNoDelay', arrayBuffer);
- this.decoderWorker.postMessage({
- cmd: WORKER_SEND_TYPE.audioDecode,
- buffer: arrayBuffer,
- ts: Math.max(ts, 0)
- }, [arrayBuffer.buffer]);
- }
- updateWorkConfig(config) {
- this.decoderWorker.postMessage({
- cmd: WORKER_SEND_TYPE.updateConfig,
- key: config.key,
- value: config.value
- });
- }
- }
- class CommonLoader extends Emitter {
- constructor(player) {
- super();
- this.player = player;
- this.stopId = null;
- this.firstTimestamp = null;
- this.startTimestamp = null;
- this.delay = -1;
- this.bufferList = [];
- this.dropping = false;
- this.initInterval();
- }
- destroy() {
- if (this.stopId) {
- clearInterval(this.stopId);
- this.stopId = null;
- }
- this.firstTimestamp = null;
- this.startTimestamp = null;
- this.delay = -1;
- this.bufferList = [];
- this.dropping = false;
- this.off();
- }
- getDelay(timestamp) {
- if (!timestamp) {
- return -1;
- }
- if (!this.firstTimestamp) {
- this.firstTimestamp = timestamp;
- this.startTimestamp = Date.now();
- this.delay = -1;
- } else {
- if (timestamp) {
- const localTimestamp = Date.now() - this.startTimestamp;
- const timeTimestamp = timestamp - this.firstTimestamp;
- if (localTimestamp >= timeTimestamp) {
- this.delay = localTimestamp - timeTimestamp;
- } else {
- this.delay = timeTimestamp - localTimestamp;
- }
- }
- }
- return this.delay;
- }
- resetDelay() {
- this.firstTimestamp = null;
- this.startTimestamp = null;
- this.delay = -1;
- this.dropping = false;
- } //
- initInterval() {
- this.player.debug.log('common dumex', `init Interval`);
- let _loop = () => {
- let data;
- const videoBuffer = this.player._opt.videoBuffer;
- const videoBufferDelay = this.player._opt.videoBufferDelay;
- if (this.bufferList.length) {
- if (this.dropping) {
- // this.player.debug.log('common dumex', `is dropping`);
- data = this.bufferList.shift();
- if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) {
- this._doDecoderDecode(data);
- }
- while (!data.isIFrame && this.bufferList.length) {
- data = this.bufferList.shift();
- if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) {
- this._doDecoderDecode(data);
- }
- } // i frame
- if (data.isIFrame) {
- this.dropping = false;
- this._doDecoderDecode(data);
- }
- } else {
- data = this.bufferList[0];
- if (this.getDelay(data.ts) === -1) {
- // this.player.debug.log('common dumex', `delay is -1`);
- this.bufferList.shift();
- this._doDecoderDecode(data);
- } else if (this.delay > videoBuffer + videoBufferDelay) {
- // this.player.debug.log('common dumex', `delay is ${this.delay}, set dropping is true`);
- this.resetDelay();
- this.dropping = true;
- } else {
- while (this.bufferList.length) {
- data = this.bufferList[0];
- if (this.getDelay(data.ts) > videoBuffer) {
- // drop frame
- this.bufferList.shift();
- this._doDecoderDecode(data);
- } else {
- // this.player.debug.log('common dumex', `delay is ${this.delay}`);
- break;
- }
- }
- }
- }
- }
- };
- _loop();
- this.stopId = setInterval(_loop, 10);
- }
- _doDecode(payload, type, ts, isIFrame) {
- const player = this.player;
- let options = {
- ts: ts,
- type: type,
- isIFrame: false
- }; // use offscreen
- if (player._opt.useWCS && !player._opt.useOffscreen) {
- if (type === MEDIA_TYPE.video) {
- options.isIFrame = isIFrame;
- }
- this.pushBuffer(payload, options);
- } else if (player._opt.useMSE) {
- // use mse
- if (type === MEDIA_TYPE.video) {
- options.isIFrame = isIFrame;
- }
- this.pushBuffer(payload, options);
- } else {
- //
- if (type === MEDIA_TYPE.video) {
- player.decoderWorker && player.decoderWorker.decodeVideo(payload, ts, isIFrame);
- } else if (type === MEDIA_TYPE.audio) {
- if (player._opt.hasAudio) {
- player.decoderWorker && player.decoderWorker.decodeAudio(payload, ts);
- }
- }
- }
- }
- _doDecoderDecode(data) {
- const player = this.player;
- const {
- webcodecsDecoder,
- mseDecoder
- } = player;
- if (data.type === MEDIA_TYPE.audio) {
- if (player._opt.hasAudio) {
- player.decoderWorker && player.decoderWorker.decodeAudio(data.payload, data.ts);
- }
- } else if (data.type === MEDIA_TYPE.video) {
- if (player._opt.useWCS && !player._opt.useOffscreen) {
- webcodecsDecoder.decodeVideo(data.payload, data.ts, data.isIFrame);
- } else if (player._opt.useMSE) {
- mseDecoder.decodeVideo(data.payload, data.ts, data.isIFrame);
- }
- }
- }
- pushBuffer(payload, options) {
- // 音频
- if (options.type === MEDIA_TYPE.audio) {
- this.bufferList.push({
- ts: options.ts,
- payload: payload,
- type: MEDIA_TYPE.audio
- });
- } else if (options.type === MEDIA_TYPE.video) {
- this.bufferList.push({
- ts: options.ts,
- payload: payload,
- type: MEDIA_TYPE.video,
- isIFrame: options.isIFrame
- });
- }
- }
- close() {}
- }
- class FlvLoader extends CommonLoader {
- constructor(player) {
- super(player);
- this.input = this._inputFlv();
- this.flvDemux = this.dispatchFlvData(this.input);
- player.debug.log('FlvDemux', 'init');
- }
- destroy() {
- super.destroy();
- this.input = null;
- this.flvDemux = null;
- this.player.debug.log('FlvDemux', 'destroy');
- }
- dispatch(data) {
- this.flvDemux(data);
- }
- *_inputFlv() {
- yield 9;
- const tmp = new ArrayBuffer(4);
- const tmp8 = new Uint8Array(tmp);
- const tmp32 = new Uint32Array(tmp);
- const player = this.player;
- while (true) {
- tmp8[3] = 0;
- const t = yield 15;
- const type = t[4];
- tmp8[0] = t[7];
- tmp8[1] = t[6];
- tmp8[2] = t[5];
- const length = tmp32[0];
- tmp8[0] = t[10];
- tmp8[1] = t[9];
- tmp8[2] = t[8];
- let ts = tmp32[0];
- if (ts === 0xFFFFFF) {
- tmp8[3] = t[11];
- ts = tmp32[0];
- }
- const payload = yield length;
- switch (type) {
- case FLV_MEDIA_TYPE.audio:
- if (player._opt.hasAudio) {
- player.updateStats({
- abps: payload.byteLength
- });
- if (payload.byteLength > 0) {
- this._doDecode(payload, MEDIA_TYPE.audio, ts);
- }
- }
- break;
- case FLV_MEDIA_TYPE.video:
- if (!player._times.demuxStart) {
- player._times.demuxStart = now();
- }
- if (player._opt.hasVideo) {
- player.updateStats({
- vbps: payload.byteLength
- });
- const isIFrame = payload[0] >> 4 === 1;
- if (payload.byteLength > 0) {
- this._doDecode(payload, MEDIA_TYPE.video, ts, isIFrame);
- }
- }
- break;
- }
- }
- }
- dispatchFlvData(input) {
- let need = input.next();
- let buffer = null;
- return value => {
- let data = new Uint8Array(value);
- if (buffer) {
- let combine = new Uint8Array(buffer.length + data.length);
- combine.set(buffer);
- combine.set(data, buffer.length);
- data = combine;
- buffer = null;
- }
- while (data.length >= need.value) {
- let remain = data.slice(need.value);
- need = input.next(data.slice(0, need.value));
- data = remain;
- }
- if (data.length > 0) {
- buffer = data;
- }
- };
- }
- close() {
- this.input && this.input.return(null);
- }
- }
- class M7sLoader extends CommonLoader {
- constructor(player) {
- super(player);
- player.debug.log('M7sDemux', 'init');
- }
- destroy() {
- super.destroy();
- this.player.debug.log('M7sDemux', 'destroy');
- }
- dispatch(data) {
- const player = this.player;
- const dv = new DataView(data);
- const type = dv.getUint8(0);
- const ts = dv.getUint32(1, false);
- switch (type) {
- case MEDIA_TYPE.audio:
- if (player._opt.hasAudio) {
- const payload = new Uint8Array(data, 5);
- player.updateStats({
- abps: payload.byteLength
- });
- if (payload.byteLength > 0) {
- this._doDecode(payload, type, ts);
- }
- }
- break;
- case MEDIA_TYPE.video:
- if (player._opt.hasVideo) {
- if (!player._times.demuxStart) {
- player._times.demuxStart = now();
- }
- if (dv.byteLength > 5) {
- const payload = new Uint8Array(data, 5);
- const isIframe = dv.getUint8(5) >> 4 === 1;
- player.updateStats({
- vbps: payload.byteLength
- });
- if (payload.byteLength > 0) {
- this._doDecode(payload, type, ts, isIframe);
- }
- }
- }
- break;
- }
- }
- }
- class Demux {
- constructor(player) {
- const Loader = Demux.getLoaderFactory(player._opt.demuxType);
- return new Loader(player);
- }
- static getLoaderFactory(type) {
- if (type === DEMUX_TYPE.m7s) {
- return M7sLoader;
- } else if (type === DEMUX_TYPE.flv) {
- return FlvLoader;
- }
- }
- }
- class WebcodecsDecoder extends Emitter {
- constructor(player) {
- super();
- this.player = player;
- this.hasInit = false;
- this.isDecodeFirstIIframe = false;
- this.isInitInfo = false;
- this.decoder = null;
- this.initDecoder();
- player.debug.log('Webcodecs', 'init');
- }
- destroy() {
- if (this.decoder) {
- this.decoder.close();
- this.decoder = null;
- }
- this.hasInit = false;
- this.isInitInfo = false;
- this.isDecodeFirstIIframe = false;
- this.off();
- this.player.debug.log('Webcodecs', 'destroy');
- }
- initDecoder() {
- const _this = this;
- this.decoder = new VideoDecoder({
- output(videoFrame) {
- _this.handleDecode(videoFrame);
- },
- error(error) {
- _this.handleError(error);
- }
- });
- }
- handleDecode(videoFrame) {
- if (!this.isInitInfo) {
- this.player.video.updateVideoInfo({
- width: videoFrame.codedWidth,
- height: videoFrame.codedHeight
- });
- this.player.video.initCanvasViewSize();
- this.isInitInfo = true;
- }
- if (!this.player._times.videoStart) {
- this.player._times.videoStart = now();
- this.player.handlePlayToRenderTimes();
- }
- this.player.handleRender();
- this.player.video.render({
- videoFrame
- });
- this.player.updateStats({
- fps: true,
- ts: 0,
- buf: this.player.demux.delay
- }); // release resource
- setTimeout(function () {
- if (videoFrame.close) {
- videoFrame.close();
- } else {
- videoFrame.destroy();
- }
- }, 100);
- }
- handleError(error) {
- this.player.debug.log('Webcodecs', 'VideoDecoder handleError', error);
- }
- decodeVideo(payload, ts, isIframe) {
- // this.player.debug.log('Webcodecs decoder', 'decodeVideo', ts, isIframe);
- if (!this.hasInit) {
- if (isIframe && payload[1] === 0) {
- const videoCodec = payload[0] & 0x0F;
- this.player.video.updateVideoInfo({
- encTypeCode: videoCodec
- }); // 如果解码出来的是
- if (videoCodec === VIDEO_ENC_CODE.h265) {
- this.emit(EVENTS_ERROR.webcodecsH265NotSupport);
- return;
- }
- if (!this.player._times.decodeStart) {
- this.player._times.decodeStart = now();
- }
- const config = formatVideoDecoderConfigure(payload.slice(5));
- this.decoder.configure(config);
- this.hasInit = true;
- }
- } else {
- // fix : Uncaught DOMException: Failed to execute 'decode' on 'VideoDecoder': A key frame is required after configure() or flush().
- if (!this.isDecodeFirstIIframe && isIframe) {
- this.isDecodeFirstIIframe = true;
- }
- if (this.isDecodeFirstIIframe) {
- const chunk = new EncodedVideoChunk({
- data: payload.slice(5),
- timestamp: ts,
- type: isIframe ? ENCODED_VIDEO_TYPE.key : ENCODED_VIDEO_TYPE.delta
- });
- this.decoder.decode(chunk);
- } else {
- this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeFirstIIframe false');
- }
- }
- }
- }
- const iconsMap = {
- play: '播放',
- pause: '暂停',
- audio: '',
- mute: '',
- screenshot: '截图',
- loading: '加载',
- fullscreen: '全屏',
- fullscreenExit: '退出全屏',
- record: '录制',
- recordStop: '停止录制'
- };
- var icons = Object.keys(iconsMap).reduce((icons, key) => {
- icons[key] = `
- <i class="jessibuca-icon jessibuca-icon-${key}"></i>
- ${iconsMap[key] ? `<span class="icon-title-tips"><span class="icon-title">${iconsMap[key]}</span></span>` : ''}
- `;
- return icons;
- }, {});
- var template = ((player, control) => {
- if (player._opt.hasControl && player._opt.controlAutoHide) {
- player.$container.classList.add('jessibuca-controls-show-auto-hide');
- } else {
- player.$container.classList.add('jessibuca-controls-show');
- }
- const options = player._opt;
- const operateBtns = options.operateBtns;
- player.$container.insertAdjacentHTML('beforeend', `
- ${options.background ? `<div class="jessibuca-poster" style="background-image: url(${options.background})"></div>` : ''}
- <div class="jessibuca-loading">
- ${icons.loading}
- ${options.loadingText ? `<div class="jessibuca-loading-text">${options.loadingText}</div>` : ''}
- </div>
- ${options.hasControl && operateBtns.play ? `<div class="jessibuca-play-big"></div>` : ''}
- ${options.hasControl ? `
- <div class="jessibuca-recording">
- <div class="jessibuca-recording-red-point"></div>
- <div class="jessibuca-recording-time">00:00:01</div>
- <div class="jessibuca-icon-recordStop jessibuca-recording-stop">${icons.recordStop}</div>
- </div>
- ` : ''}
- ${options.hasControl ? `
- <div class="jessibuca-controls">
- <div class="jessibuca-controls-bottom">
- <div class="jessibuca-controls-left">
- ${options.showBandwidth ? `<div class="jessibuca-controls-item jessibuca-speed"></div>` : ''}
- </div>
- <div class="jessibuca-controls-right">
- ${operateBtns.audio ? `
- <div class="jessibuca-controls-item jessibuca-volume">
- ${icons.audio}
- ${icons.mute}
- <div class="jessibuca-volume-panel-wrap">
- <div class="jessibuca-volume-panel">
- <div class="jessibuca-volume-panel-handle"></div>
- </div>
- <div class="jessibuca-volume-panel-text"></div>
- </div>
- </div>
- ` : ''}
- ${operateBtns.play ? `<div class="jessibuca-controls-item jessibuca-play">${icons.play}</div><div class="jessibuca-controls-item jessibuca-pause">${icons.pause}</div>` : ''}
- ${operateBtns.screenshot ? `<div class="jessibuca-controls-item jessibuca-screenshot">${icons.screenshot}</div>` : ''}
- ${operateBtns.record ? ` <div class="jessibuca-controls-item jessibuca-record">${icons.record}</div><div class="jessibuca-controls-item jessibuca-record-stop">${icons.recordStop}</div>` : ''}
- ${operateBtns.fullscreen ? `<div class="jessibuca-controls-item jessibuca-fullscreen">${icons.fullscreen}</div><div class="jessibuca-controls-item jessibuca-fullscreen-exit">${icons.fullscreenExit}</div>` : ''}
- </div>
- </div>
- </div>
- ` : ''}
- `);
- Object.defineProperty(control, '$poster', {
- value: player.$container.querySelector('.jessibuca-poster')
- });
- Object.defineProperty(control, '$loading', {
- value: player.$container.querySelector('.jessibuca-loading')
- });
- Object.defineProperty(control, '$play', {
- value: player.$container.querySelector('.jessibuca-play')
- });
- Object.defineProperty(control, '$playBig', {
- value: player.$container.querySelector('.jessibuca-play-big')
- });
- Object.defineProperty(control, '$recording', {
- value: player.$container.querySelector('.jessibuca-recording')
- });
- Object.defineProperty(control, '$recordingTime', {
- value: player.$container.querySelector('.jessibuca-recording-time')
- });
- Object.defineProperty(control, '$recordingStop', {
- value: player.$container.querySelector('.jessibuca-recording-stop')
- });
- Object.defineProperty(control, '$pause', {
- value: player.$container.querySelector('.jessibuca-pause')
- });
- Object.defineProperty(control, '$controls', {
- value: player.$container.querySelector('.jessibuca-controls')
- });
- Object.defineProperty(control, '$fullscreen', {
- value: player.$container.querySelector('.jessibuca-fullscreen')
- });
- Object.defineProperty(control, '$fullscreen', {
- value: player.$container.querySelector('.jessibuca-fullscreen')
- });
- Object.defineProperty(control, '$volume', {
- value: player.$container.querySelector('.jessibuca-volume')
- });
- Object.defineProperty(control, '$volumePanelWrap', {
- value: player.$container.querySelector('.jessibuca-volume-panel-wrap')
- });
- Object.defineProperty(control, '$volumePanelText', {
- value: player.$container.querySelector('.jessibuca-volume-panel-text')
- });
- Object.defineProperty(control, '$volumePanel', {
- value: player.$container.querySelector('.jessibuca-volume-panel')
- });
- Object.defineProperty(control, '$volumeHandle', {
- value: player.$container.querySelector('.jessibuca-volume-panel-handle')
- });
- Object.defineProperty(control, '$volumeOn', {
- value: player.$container.querySelector('.jessibuca-icon-audio')
- });
- Object.defineProperty(control, '$volumeOff', {
- value: player.$container.querySelector('.jessibuca-icon-mute')
- });
- Object.defineProperty(control, '$fullscreen', {
- value: player.$container.querySelector('.jessibuca-fullscreen')
- });
- Object.defineProperty(control, '$fullscreenExit', {
- value: player.$container.querySelector('.jessibuca-fullscreen-exit')
- });
- Object.defineProperty(control, '$record', {
- value: player.$container.querySelector('.jessibuca-record')
- });
- Object.defineProperty(control, '$recordStop', {
- value: player.$container.querySelector('.jessibuca-record-stop')
- });
- Object.defineProperty(control, '$screenshot', {
- value: player.$container.querySelector('.jessibuca-screenshot')
- });
- Object.defineProperty(control, '$speed', {
- value: player.$container.querySelector('.jessibuca-speed')
- });
- });
- var observer$1 = ((player, control) => {
- const {
- events: {
- proxy
- }
- } = player;
- const object = document.createElement('object');
- object.setAttribute('aria-hidden', 'true');
- object.setAttribute('tabindex', -1);
- object.type = 'text/html';
- object.data = 'about:blank';
- setStyle(object, {
- display: 'block',
- position: 'absolute',
- top: '0',
- left: '0',
- height: '100%',
- width: '100%',
- overflow: 'hidden',
- pointerEvents: 'none',
- zIndex: '-1'
- });
- let playerWidth = player.width;
- let playerHeight = player.height;
- proxy(object, 'load', () => {
- proxy(object.contentDocument.defaultView, 'resize', () => {
- if (player.width !== playerWidth || player.height !== playerHeight) {
- playerWidth = player.width;
- playerHeight = player.height;
- player.emit(EVENTS.resize);
- screenfullH5Control();
- }
- });
- });
- player.$container.appendChild(object);
- player.on(EVENTS.destroy, () => {
- player.$container.removeChild(object);
- });
- function setVolumeHandle(percentage) {
- if (percentage === 0) {
- setStyle(control.$volumeOn, 'display', 'none');
- setStyle(control.$volumeOff, 'display', 'flex');
- setStyle(control.$volumeHandle, 'top', `${48}px`);
- } else {
- if (control.$volumeHandle && control.$volumePanel) {
- const panelHeight = getStyle(control.$volumePanel, 'height') || 60;
- const handleHeight = getStyle(control.$volumeHandle, 'height');
- const top = panelHeight - (panelHeight - handleHeight) * percentage - handleHeight;
- setStyle(control.$volumeHandle, 'top', `${top}px`);
- setStyle(control.$volumeOn, 'display', 'flex');
- setStyle(control.$volumeOff, 'display', 'none');
- }
- }
- control.$volumePanelText && (control.$volumePanelText.innerHTML = parseInt(percentage * 100));
- }
- player.on(EVENTS.volumechange, () => {
- setVolumeHandle(player.volume);
- });
- player.on(EVENTS.loading, flag => {
- setStyle(control.$loading, 'display', flag ? 'flex' : 'none');
- setStyle(control.$poster, 'display', 'none');
- if (flag) {
- setStyle(control.$playBig, 'display', 'none');
- }
- });
- const screenfullChange = fullscreen => {
- let isFullScreen = isBoolean(fullscreen) ? fullscreen : player.fullscreen;
- setStyle(control.$fullscreenExit, 'display', isFullScreen ? 'flex' : 'none');
- setStyle(control.$fullscreen, 'display', isFullScreen ? 'none' : 'flex'); // control.autoSize();
- };
- const screenfullH5Control = () => {
- if (isMobile() && control.$controls) {
- setTimeout(() => {
- if (player.fullscreen) {
- // console.log(player.width, player.height);
- let translateX = player.height / 2 - player.width + CONTROL_HEIGHT / 2;
- let translateY = player.height / 2 - CONTROL_HEIGHT / 2;
- control.$controls.style.transform = `translateX(${-translateX}px) translateY(-${translateY}px) rotate(-90deg)`;
- } else {
- control.$controls.style.transform = `translateX(0) translateY(0) rotate(0)`;
- }
- }, 10);
- }
- };
- try {
- screenfull.on('change', screenfullChange);
- player.events.destroys.push(() => {
- screenfull.off('change', screenfullChange);
- });
- } catch (error) {//
- } //
- player.on(EVENTS.webFullscreen, value => {
- screenfullChange(value);
- screenfullH5Control();
- });
- player.on(EVENTS.recording, () => {
- setStyle(control.$record, 'display', player.recording ? 'none' : 'flex');
- setStyle(control.$recordStop, 'display', player.recording ? 'flex' : 'none');
- setStyle(control.$recording, 'display', player.recording ? 'flex' : 'none');
- }); //
- player.on(EVENTS.recordingTimestamp, timestamp => {
- //// console.log(timestamp);
- control.$recordingTime && (control.$recordingTime.innerHTML = formatTimeTips(timestamp));
- });
- player.on(EVENTS.playing, flag => {
- setStyle(control.$play, 'display', flag ? 'none' : 'flex');
- setStyle(control.$playBig, 'display', flag ? 'none' : 'block');
- setStyle(control.$pause, 'display', flag ? 'flex' : 'none');
- setStyle(control.$screenshot, 'display', flag ? 'flex' : 'none');
- setStyle(control.$record, 'display', flag ? 'flex' : 'none');
- setStyle(control.$fullscreen, 'display', flag ? 'flex' : 'none'); // 不在播放
- if (!flag) {
- control.$speed && (control.$speed.innerHTML = bpsSize(''));
- }
- });
- player.on(EVENTS.kBps, rate => {
- const bps = bpsSize(rate);
- control.$speed && (control.$speed.innerHTML = bps);
- });
- });
- var property = ((player, control) => {
- Object.defineProperty(control, 'controlsRect', {
- get: () => {
- return control.$controls.getBoundingClientRect();
- }
- });
- });
- var events = ((player, control) => {
- const {
- events: {
- proxy
- },
- debug
- } = player;
- function volumeChangeFromEvent(event) {
- const {
- bottom: panelBottom,
- height: panelHeight
- } = control.$volumePanel.getBoundingClientRect();
- const {
- height: handleHeight
- } = control.$volumeHandle.getBoundingClientRect();
- let moveLen = event.y; // if (isMobile() && player.fullscreen) {
- // moveLen = event.x;
- // }
- const percentage = clamp(panelBottom - moveLen - handleHeight / 2, 0, panelHeight - handleHeight / 2) / (panelHeight - handleHeight);
- return percentage;
- } //
- proxy(window, ['click', 'contextmenu'], event => {
- if (event.composedPath().indexOf(player.$container) > -1) {
- control.isFocus = true;
- } else {
- control.isFocus = false;
- }
- }); //
- proxy(window, 'orientationchange', () => {
- setTimeout(() => {
- player.resize();
- }, 300);
- });
- proxy(control.$controls, 'click', e => {
- e.stopPropagation();
- });
- proxy(control.$pause, 'click', e => {
- player.pause();
- }); // 监听 play 方法
- proxy(control.$play, 'click', e => {
- player.play();
- }); // 监听 play 方法
- proxy(control.$playBig, 'click', e => {
- player.play();
- });
- proxy(control.$volume, 'mouseover', () => {
- control.$volumePanelWrap.classList.add('jessibuca-volume-panel-wrap-show');
- });
- proxy(control.$volume, 'mouseout', () => {
- control.$volumePanelWrap.classList.remove('jessibuca-volume-panel-wrap-show');
- });
- proxy(control.$volumeOn, 'click', e => {
- e.stopPropagation();
- setStyle(control.$volumeOn, 'display', 'none');
- setStyle(control.$volumeOff, 'display', 'block');
- player.lastVolume = player.volume;
- player.volume = 0;
- });
- proxy(control.$volumeOff, 'click', e => {
- e.stopPropagation();
- setStyle(control.$volumeOn, 'display', 'block');
- setStyle(control.$volumeOff, 'display', 'none');
- player.volume = player.lastVolume || 0.5;
- });
- proxy(control.$screenshot, 'click', e => {
- e.stopPropagation();
- player.video.screenshot();
- });
- proxy(control.$volumePanel, 'click', event => {
- event.stopPropagation();
- player.volume = volumeChangeFromEvent(event);
- });
- proxy(control.$volumeHandle, 'mousedown', () => {
- control.isVolumeDroging = true;
- });
- proxy(control.$volumeHandle, 'mousemove', event => {
- if (control.isVolumeDroging) {
- player.volume = volumeChangeFromEvent(event);
- }
- });
- proxy(document, 'mouseup', () => {
- if (control.isVolumeDroging) {
- control.isVolumeDroging = false;
- }
- });
- proxy(control.$record, 'click', e => {
- e.stopPropagation();
- player.recording = true;
- });
- proxy(control.$recordStop, 'click', e => {
- e.stopPropagation();
- player.recording = false;
- });
- proxy(control.$recordingStop, 'click', e => {
- e.stopPropagation();
- player.recording = false;
- });
- proxy(control.$fullscreen, 'click', e => {
- e.stopPropagation();
- player.fullscreen = true;
- });
- proxy(control.$fullscreenExit, 'click', e => {
- e.stopPropagation();
- player.fullscreen = false;
- });
- if (player._opt.hasControl && player._opt.controlAutoHide) {
- //
- proxy(player.$container, 'mouseover', () => {
- if (!player.fullscreen) {
- setStyle(control.$controls, 'display', 'block');
- }
- });
- proxy(player.$container, 'mouseout', () => {
- setStyle(control.$controls, 'display', 'none');
- });
- }
- });
- function styleInject(css, ref) {
- if ( ref === void 0 ) ref = {};
- var insertAt = ref.insertAt;
- if (!css || typeof document === 'undefined') { return; }
- var head = document.head || document.getElementsByTagName('head')[0];
- var style = document.createElement('style');
- style.type = 'text/css';
- if (insertAt === 'top') {
- if (head.firstChild) {
- head.insertBefore(style, head.firstChild);
- } else {
- head.appendChild(style);
- }
- } else {
- head.appendChild(style);
- }
- if (style.styleSheet) {
- style.styleSheet.cssText = css;
- } else {
- style.appendChild(document.createTextNode(css));
- }
- }
- var css_248z$1 = "@keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes magentaPulse{0%{background-color:#630030;-webkit-box-shadow:0 0 9px #333}50%{background-color:#a9014b;-webkit-box-shadow:0 0 18px #a9014b}to{background-color:#630030;-webkit-box-shadow:0 0 9px #333}}.jessibuca-container .jessibuca-icon{cursor:pointer;width:16px;height:16px}.jessibuca-container .jessibuca-poster{position:absolute;z-index:10;left:0;top:0;right:0;bottom:0;height:100%;width:100%;background-position:50%;background-repeat:no-repeat;background-size:contain;pointer-events:none}.jessibuca-container .jessibuca-play-big{position:absolute;display:none;height:100%;width:100%;background:rgba(0,0,0,.4)}.jessibuca-container .jessibuca-play-big:after{cursor:pointer;content:\"\";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:block;width:48px;height:48px;background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\");background-repeat:no-repeat;background-position:50%}.jessibuca-container .jessibuca-play-big:hover:after{background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\")}.jessibuca-container .jessibuca-recording{display:none;position:absolute;left:50%;top:0;padding:0 3px;transform:translateX(-50%);justify-content:space-around;align-items:center;width:95px;height:20px;background:#000;opacity:1;border-radius:0 0 8px 8px;z-index:1}.jessibuca-container .jessibuca-recording .jessibuca-recording-red-point{width:8px;height:8px;background:#ff1f1f;border-radius:50%;animation:magentaPulse 1s linear infinite}.jessibuca-container .jessibuca-recording .jessibuca-recording-time{font-size:14px;font-weight:500;color:#ddd}.jessibuca-container .jessibuca-recording .jessibuca-icon-recordStop{width:16px;height:16px;cursor:pointer}.jessibuca-container .jessibuca-loading{display:none;flex-direction:column;justify-content:center;align-items:center;position:absolute;z-index:20;left:0;top:0;right:0;bottom:0;width:100%;height:100%;pointer-events:none}.jessibuca-container .jessibuca-loading-text{line-height:20px;font-size:13px;color:#fff;margin-top:10px}.jessibuca-container .jessibuca-controls{background-color:#161616;box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;position:absolute;z-index:40;left:0;right:0;bottom:0;height:38px;width:100%;padding-left:13px;padding-right:13px;font-size:14px;color:#fff;opacity:0;visibility:hidden;transition:all .2s ease-in-out;-webkit-user-select:none;user-select:none;transition:width .5s ease-in}.jessibuca-container .jessibuca-controls .jessibuca-controls-item{position:relative;display:flex;justify-content:center;padding:0 8px}.jessibuca-container .jessibuca-controls .jessibuca-controls-item:hover .icon-title-tips{visibility:visible;opacity:1}.jessibuca-container .jessibuca-controls .jessibuca-fullscreen,.jessibuca-container .jessibuca-controls .jessibuca-fullscreen-exit,.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-microphone-close,.jessibuca-container .jessibuca-controls .jessibuca-pause,.jessibuca-container .jessibuca-controls .jessibuca-play,.jessibuca-container .jessibuca-controls .jessibuca-record,.jessibuca-container .jessibuca-controls .jessibuca-record-stop,.jessibuca-container .jessibuca-controls .jessibuca-screenshot{display:none}.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-icon-mute{z-index:1}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom{display:flex;justify-content:space-between;height:100%}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-left,.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-right{display:flex;align-items:center}.jessibuca-container.jessibuca-controls-show .jessibuca-controls{opacity:1;visibility:visible}.jessibuca-container.jessibuca-controls-show-auto-hide .jessibuca-controls{opacity:.8;visibility:visible;display:none}.jessibuca-container.jessibuca-hide-cursor *{cursor:none!important}.jessibuca-container .jessibuca-icon-loading{width:50px;height:50px;background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%;animation:rotation 1s linear infinite}.jessibuca-container .jessibuca-icon-screenshot{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE5UlEQVRoQ+1YW2sdVRT+1s7JxbsoVkEUrIIX0ouz15zYNA+N1RdtQfCltlUfvLbqL/BCwZ8grbHtizQqPojgBSr0JkiMmT2nxgapqBURtPVCq7HxJCeZJVPmxDlzZubMmXOSEsnAvOy917fXt9e39tp7E5b4R0vcfywTuNgRbBgBx3HuJqLVzPzmYjprjHkcwAlmLqXNm4XAISLaSESPaq2HF4OE67rbRGRYRA7btn1fbgLGmKsA/Azg0gBkGzO/vZAkHMd5hIiqc5wHcCMz/5k0Z2oExsfHV1QqldPAf8lORNu11m8tBAljzFYAYWxRSl1vWdZvuQj4RsYYF4AVBlgIOVVlE55HRIxt23ZuCfmGjuOsJ6LPoiAistW27XfaEYmIbOYhPc9bXywWR1oiEJDYQkR1zrYjEjGyqfqbKd8a7kJVtLgQ+30i8pht2wfyRKIdmJkJBPkQTbILfudJ7CTZNBvVpggEcgpvc/ML38zESbLJsxBNE/A9biX0rdjGyTQXgbxyapdsarb0PMlXtWnGoXbKpm0Essqp3bJpK4E0OXmed3+hUBDP8w5FI91M0rdcyLLILElOCbaZilSWeXMncRx4klTCY1spfG3dhZJWx3GcDUR0EEB3ZMw0ET2gtT6SZWWzjmlrBIJCl0hAKfWgZVmHszqXZVxbCSxpCS2JJA6umIhe8ZKKVLPbaBJ+S9toqVRa53nedgAbAKwIwH4FcAzAa0R0l4i8F7PPz189k6RFRA+LyNcAXojDV0oNW5b1eW4Cxpg9AHZkSaaa6hhzb065uDSCH2LmRB8Sk9gY4293g43Qo/1pV80m8yQMfZSZ781cB1zXHRKRZ2IMpgD8A+DamL4ZItqitX4/jbQx5iEA7wLoihn3V/ACckWMJN/QWj9b1x5tGBsbW6uUOh5pPy0iL3Z2dn6ilJqanp5ep5TaJSLhF4NppdRNaU8gPmapVLrO87yfIoXuWyJ6uVKp+HmFjo6OQSJ6FcBtYT+UUmstyxqvkWuUgDFmP4AnQu2/e563qlgs+u9DNZ8xZhRAX7VRRPbath0XuXk7Y8xeAE+FgL6fnJzsHRwcLIfBR0ZGLunq6poAsDLUvp+Zw7b1r9PGmJMAbg8Z7WDmoThZuK67WkS+DD18fcPMdzSQUBR/EzN/nIC/SUQ+DPXV4dclsTHmHAD/SfHCNzc3t7Kvr++HJKeMMacA3BL0nyuXyzcPDAxMxo0fHR29slAo/Ajg6qD/fE9Pzw29vb1/x42fmJi4vFwu+5G/LOg/y8zXNJLQ2dAES5JANMQ7mfn1jBI6ycx3NiMhItqstf4oAX+ziHwQ6qvDj5NQNIn/ALCKmX+JSeIvABRD7fuY+ekGBPYBeDI05tTMzExvf3+/vz2Hk91/ET8RSeI6/DoCpVJpjed5fmKGvzMAXpqdnT3oed5Ud3d3v4jsAqBr9Ei0Rmv9VRqBBPzvROQVETnq2xJRdRu9tRF+bCVOKWT+Kvl/TSIFk6SW/LAjKfjV5K8rZABi8dOOEv7FI7Z8x6zwEWbemLbyMfJr5qiSiJ96oclymBOR3bZtP9+M89WxxpjdAHY2sN3DzM8ljWl4I3Nd9x7/OE1ENcdpETnmH3e11n41zv0l4J8RkU+J6AAz+xtF4teQQG7PFslwmcAiLfSyhC72Qv9/I/Avns2OT7QJskoAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-screenshot:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAED0lEQVRoQ+2ZycsdRRTFf2ejqHFAMQqiYBTUoElUHLNx3GgCgpuYRF2o0UT9CxwQ/BMkMSbZSKLiQgQHUDCJgjiAxiEiESdEcJbEedgcKaj3UV+/6q7u/jovPPkK3qbr1ql76p5bt6qemPKmKfefeQKHOoLFCNg+H1gi6fFJOmv7VmCvpD1N87Yh8ApwNXCzpB2TIGF7DRDm2inpmt4EbB8LfAMcGUHWSHryYJKwfRMwmuMP4BRJv9TN2RgB2wuB72BWsq+V9MTBIGF7NZBiGzhJ0o+9CIRBtt8FLqgADC6nRDbpVO9Iuqi3hCKB5cDrGZDVkp4aIhIV2aSQyyW9MScCkcQqIOfsnCORkc3I31b5VtyFRmg1IQ7dt0ja3icSQ2C2JhAjUU2ykd+dE7tBNp2i2olAJJFuc+nCt564QTadF6IzgUhiVGiqyinKaQjZpJP2ItBXTkPJZhACXeU0pGwGI9BWTkPLZlACBTldG4o5EA6E1dY66edcyNrs8Q36zg1vVaTazNs7iXPgDVJJzYs7VRvHRzaDEohyugJ4CTi84sg/wHWSdnVxsGQ7aQLXS9pZcqpL/6AEplpCU5HE8YpJ9YrXUKQ6baN1+HPaRm1fBqwFQnKGK2ZoPwCvAo8Ai4FnMpPMHMwapHUj8DFwbw3+Dklv9iZgexOwvktSRduxU2VDlErwmyXV+lCbxLbDdndlCT3TX3vV7JgnKfRuSVflfMkSsL0ZuDMz4E/gL+CETN+/wCpJzzaRtn0D8DRwWMbu1/gCcnSm7zFJd1W/jxGwvQx4r2IYnlbuA14GAomQFw8B6YtBKFSnNj2BxEJ3IvB1pdB9CjwQ8yqYhcg/DJxZ8WOZpA/SbzkC24DbEqOfgPMkBRKzmu23gEuSj1sk5SI3Y2J7C3BHMuZz4FxJf6fgto8APgIWJd+3SUrHjr9O294HnJUMWi8pSGqs2V4CvJ88fH0i6eyChKr4KyS9WIO/Ang+6RvDz0XgABCeFEdtkaQv65yy/QVweuwPY0+T9FuNQ8cAXwHHxf7wdHiypN9r7BfEl8GjYv9+SceXJLQ/mSDYTh2Baog3SHq0pYT2STqno4RWSnqhBn8l8FzSN4bfJol/jkn8bXUS228DFyfft0paVyCwFbg9sQkSDEkctueZZju8iO+tJPEYfo7A0piYKd73wP3xnB+20cvjNnphxdmlkj4sEMjhfwY8COyOY0fb6Bkl/K6FLKxS+M1KpDhJY8mvrG5doRwlf66QZfGbjhLh4pEt35kV3iUp/IvTunU8qtTil/7gaHOY2yjpntaez9b5RmBDYewmSXfX2RRvZLYvbThOh+NuqMa9Ww1+yLnXgO2SwkZR24oEens2oYHzBCa00PMSOtQL/f+NwH+Hg8hAnbrYgQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABA0lEQVRoQ+1YwQqCUBAcfWXXsLr2AXWTPXno8yVB8AP6Aa3oHI+kCDqYaawJljSe133uzO44bx0M/HEG/v1gAd9mkAyQgY4I/F8LJUlyrQFtD2AtIkcNoFEU+Z7n7QD4DfFHEVlocrVmgAUAIAOl3mILPcDgEFcUhyrUKMGUUcroc3NQRimj9XJBGaWMvvPydKN0o6/9QTdKN6rZANxj6EbpRulGuZnjYqs8BbyR8Ub2Izeys+u6yyAIDpo/ehzHM2NMDsA0xFsRmWhyfTIDWSXxCEBmrd2EYXjSHJqm6bQoii2AOYBL5Z0xgFxEVppcrQvQJO0zhgX0iXbdWWSADHRE4AZQ731AhEUeNwAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA7klEQVRoQ+2YSwrCQBBEX6HiVvxsPYDewfN7By/gD9ciQkvERQwJdBSiYs0mEDo96aruombEjy/9+P/jAj7NoBkwA28i8H8tFBFRA9oeWEo6ZgCNiDGwAYpn3TpKmmVytWbABQBmoNRbbqEHGB7iiuJYhRol2DJqGX1uDsuoZdRmLuNZSzGWUcuoZdRHSp/IylNgK2ErYSthK3FHwLcSvpXIjoLt9Jfa6TMwl3TIMBkRE2AH9BriL5KGmVyvWIltJXEfKN6tJJ0ym0bECFgDU+Ba+WZQFCdpkcnVuoBM0i5jXECXaNftZQbMwJsI3AAPN3dAQflHegAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC+UlEQVRoQ+1ZS2sTURT+zlDJYE3XSq219QHVuEjnJDT+Bff9Abqw2voAEfGxqygUqWhVFHGl/yMLu9BwByxk5SNI66ML6U7axjhHbmhgWiftncxoOiV3FcI53z3f/e65594zhIQPSnj86BBot4IdBToKRFyBnbeFlFIScVEiuYvIWC6Xe2YK8pcC7SYA4CMzH4mDQBXAqilQBDsLQLfPf9FxnF4i8kwwmypARI+Wl5dvmIBEsUmlUkNE9NaHsVCpVAZGR0d/m+A2JSAid3K53E0TkCg2pVKpz7KseR/GfKVSGYxMAMA0M1+JEpyJb6lUOm5ZVnkrAsVisaunp+esiByr1Wp3R0ZGvmifzZK4XQQWHMc52MgBpdQuAOcAXABwuB400ZTjONdaIjA7O5u2bVsnWU1EujzP+5nP5xdMVjvIJkCBD8x8VCm1G8AYgAkAAxt8Z5j5YmgCSqlTAJ4D2OcD/AXgATNfbYVEAIFPIvKKiE4D6GuCea8xX6gtpJT6DmBvECgRFRzHeROWRAABE4iWCbwHEFhkPM/L5vP5dyaz+23+KwHXdR3P854S0YG1ILSCuthNMfNM2OC1/RYENLY+ygcBnPfht6ZAA6BYLNr6dyqVokKhsGpaNQ2TWJstreXaE2aed133sojcj41AKyvdzCdAgSXLsk4MDw9/a/i4rntbRPxFNZoC/5jAV2be759DKTUJ4FZSFFi0bbs/k8noy2R9dAjEuWU2YgXkQOK3kD6BMsysi2Z9JC2Jdcw/ALzwPO+xvmcl7Rj177JVEbkO4BARjSflFDJJuW1dBxJPoCIiL4noDIB1BS0pW6j+oJmbm+uuVqvjRKQfLr0bZHnIzJf0f6HeAybahrUJqAPruhLlcnnPysqKfpXp11n/Gv62zoHAroS+AafT6QkiGrIsazKbzX7eVIHEt1US39gCkOzWYthkjNE+tuZujDGZQ8XRXn8N4KT5lLFZ6uaYPt+nwyDuvC80YdhvB9uOAu1WoaNAR4GIK/AHvdr+QAexB7EAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACfUlEQVRoQ+2ZSYsUQRCFvycK4nJXXEbHBdwO4kn/gv9CD467ICIutxEFkREdFUU86T/xojcPntyQcT2INw+uISFVkD1Wd2dWlU7nUHlqisiX+fJFZGREi8yHMt8/HYG5VrBToFOg4QnMPxcyM2t4KE2nT0i6EwvylwIjQOCFpE1tEPgGfI0FamC3AFgazP8IrJL0KwZzkAI3gLMxIA1ttgCPA4w3wHpJP2NwBxG4KOlcDEgTGzNbA8wEGP57vA0CU5JONtlczFwz2wY8HUbAzBYCB4CtwCVJb33OIAXmioC70LoyBsxsEXAQOApsLIhelnS6FgEzW+5BBvwA/FS+SPJFa40KBZ5L2mxmS4AJ4IjHxCzwaUnHkgmY2V7gLrAyAPwOXJN0qg6DCgIvgQfAPsDjo2pcKddLciEz+wCs6AO6W9KjVBIVBGIgahN4BvRLMjslPYlZPbT53wR2AbeBtcUmXEFPdh5U06mbd/shBBzbr/Jx4FCAX0+BEsDMFocEYrNmFcE+BD4XsXZL0oyZnQCutkagzkn3m1NBwDe/Q9L74MAuFEqUn5op8I8JvJO0elacTALnc1HAH3Njkvwx+WeYWUegTa/pwaqIgexdyIN4uyRPmqULZRXEvulPwD3gpr+zcrtGQxfzRHYG2AAczuUWiom3kc4D2RN4BdwH9gM9CS0XFyoLGu9UuN974eIFVDiuSzruH5LqgRhtU20q8kBPV8LMlhVVmVdnYwX+SMdAZVeieAF7eeltmElJr4cpkH1bJfvGVvatxdR4bMu+teZuWxtKxWncXn8I7EldtQV7vz79fp9KwZp//9CksB8F206BuVahU6BToOEJ/Ab7+KdABdTt8AAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAGDElEQVRoQ82ZaahVVRTHf//moKKggQawcmg0olGl0awvRoMVBRGFlQ1YQZIZqRVKmJmFgVk59EFQykYjgmajbJ7n2WiAbKKCBq0Vfznndd723Lvvve/5bMH9cvfaa63/2WuvaYteoIjYHDgEOAAYDOwIbA/4f9PvwHfAt8DbwGvAS5L8f49Ine6OCO89CTgFOBrYqU1Z3wBPAUskPdDm3i72jgBExCXAWGBQp4qTfR8CMyXd0a68tgBExEjgBmCfdhW1yP8eMFHS/S3y0xKAiNgQmA2MaUHwB8DnwNfAbwX/FsDOwG7Ani3I8ElcLOnvHG8WQET0Ax4C9msi7BHgbuAFSXaHhhQRewBDgZOBE5qwvuV1SSuayWsKICIcVZ4Atq4R8mdxKnMkfZT7UnXrEeE7dD7gO7VpDc/PwAhJrzaS3xBAROzrUFcJhVUZjhrjJX3cieHpnogYUNytUTXy/gAOlvROna5aABHhGG5f3qZmk33ztt4wvAbIBcCcBicxSNLKdK0RgNeB/RPmVcBxkp5eF8aXMiPiKODRGpd6XZJduhutBSAipgNX1Bg/tJkv9iao4u4tBzZJ5N4oaXz1v24AImIvwLE4peGSnDX7jCLC2f3JGoV7S3q//D8F8DJwULJpgiQnrz6niLgSmJYofkXSwWsBiIgRwGPNmPscARARDqGp7zu0Orz/l4kjYhlweGLk4Ebhq8oXEc6wGwH/tAhyA2C1JGfsphQRTqBvJkzLJB3ZBaBIKGkGXSqpWab013FWvacooXO21K07256WS4QRsRQ4PhHgsPrxmjsQEZOB6xKGIZJebGZVRDwOHNOJ5ZU9j0s6NqPnUJcpCc9kSVNKAA5ZQyoMn0gamDMsIj4rCrQca7P1zyT1zwmIiE+AKt9yScNUFGuuZaoxd7okR4Ccfzq997S0fleSy5acrjQ//QUMNADXH/cmu0dKcoWZE+r2MKs8I+YdSW5Dc7rcizycMI0ygKuA6ysLjiT9JX3RgtC+BLArYJet5q4JBuBG5aKKsV/ZryWt/p8BcJj2R3VjVNJsA1gEnFH5821JzZqXLtaI6LMTsNIafYsM4L6iOyoNe1FSNSI1PIj1AMCh1CG1pPsNYEkxGin/fFVSWg/VglgPAF4BDqwYs8QAFgDnVP78SJIzbJbWAwBXC9VRzgIDcLVXjfm/AP0kuR/NhbY+uwMR4e7QDf6WFaOmGYBHJbcnlh7USvPSlycQEXYdu1CVxhiARxzPJwsXSarrTbux9TEAh3qH/CqtKSU2Az5NZpsPSTqxBRdy49/SfWki60NJ2WFXTUXqwdmAsphbCJxZUeIGfltJvg8NKSIMfPcc0Mx6tpiLiK2AH4qeoxS3UNJZJYC6emicpJkZAOOAGT0EcLmkmzvQM8oz1BLAxsX8vjqBWynJ86FcJDoLGO4OC8jOMgthnrX696Qkn35Oh+dB21aYfgJ2kLSqqzCKiGuAaxNJkyRNzSlYl+sNmq2pkiZZbxWAJ8g/Aj6NksI+3kplui5AFL2271m1AvVJb1fmqXSsMhGYkhjznqSeNi0d4YsIz3/SCNXNK+omcy5ZPVKv0r2STu3Iig431dRolrRCkvuCLqoD4BlM3Th7nqTzOrSnrW0RcSdQp+tASX4gbAzAK8Ub2KwarQ8Cp0vy20CvU5FUFwN1SfRSSbemSpu9D9wCXFZjpacDoyU925sIIuIw4K5k8lCqmCWpzpbmb2QRMRc4t4GhfiOYJunLngCJiF2Aq4ELG8iZL6mRDflHvohwpnXGrSM/VM8DFkt6rh0gxRd3K3s24BBeRzMkpaP+bnzZR77iTvgLuOR29mxEDnmer7rk9dPT98CvBbNreGdSD8s8WT4i81rpjD5G0vzcR2kJQAHCs5ubgKZjwERhednrHvAa2eaPMFaSm6UstQyglBQRDm92qWwJnNXencGnZpdp67W+bQAVIKOLCz6sTUNTdjdTcyW5N2+bOgZQAeLHQLuV5/UeM6ZZPDXKfa1nqs/4QUXSG21bXdnQYwBV5RHhy2rXcmh0E+5GxOTGyCWwp34fSCovd09sX7P3X2uzPXCoLsVMAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAADd0lEQVRoQ+2Zz2sdVRTHv+fJBDW6anDVXen6wZszYxYBiYgtFGst3VSDunKjpS0GpUlqfjVpsVVs6aaL0or4YxMVFCJZ2ZLdPUP+gq5bQnTxtNAkfTnlhnnlkmQy9yV9780rudt77tzv5/y4v4bQ4Y06XD/2ANodwec/AiJygJnvtdvTWfPnRkBEJAiCN8rl8kMfiPn5+Ve7u7v3rays0Orq6lJfX99/PuN2auMDoAD+BvA2M6/mTWSMOUtE48D6AjHGzN/kjdlNvy+AnWOOmQ/lTSYiEwDOWzsimgrDcCRvzG76GwGw8/zJzO9sN6GInAMwbW1UdSSKoqndCMwb6wNwGsB39Q+p6h/M/C4R2dTa1AoHYBWKyCkA1+pqiWi2Wq0e7e/vf7yRoJAAKcQggMtuJKIoOtoxACnE0/xOi/SXMAxPuhCFjUBdpIjYVWXSEf0TM3/g9BeriDMKdSPEz8z8vrU1xgwT0YXCrEJZy1iSJKOqOub0/8jMA0mSfKKqNwoPkHp7ioiGHIhRIvpHVa93BEBa2JcAfOlALAHo6RgAKzRJkk9V1S6xL7kpV4idOM31taxaIKJHqmpPnMMA9hcOQES2PDJkAT1XAAC+ZebPfWB3auNzmLObVsNRUNUXVHUujuM7OxXnMy4XwOcj29mIyOuq+lapVGrYCelKpkEQ3CyXy4tbzdN0AGPMxr2iYZ+sra3FcRybtgCIiK2BKw2rdgaUSqWoUqlIkQAepFDdAF7cBq5ERI9rtdr1OI7tmE2t6SmUEYFHAEaexYW/1QC2EF+ru5GIvg7D0D2GNJxprQY4o6qv1I/b6SpzOYqiLxpWng5oOQAzXxWRWwA+dkRfYOb1p5hGW6sBJpn5KytSRG4D+KguWFXHoyhy7xdeLC0F2ChSRL4H8OFuINoKYIUbY34gogHH3eeZef1K6tPaDpCm068A3nMEDzHzxY4BUNWSiPxORO6z5aDPPlGICNQ9bYyZIaLjjudzIQoFkKbTbwCO+UI0HcB9J/LdeY0xs0R02IGYYObRrWqiFQCfEZEtSHsfmGZm+4qxbbM/hQD8BeBNa0hEM2EYnmgLgP3lFARBT1dXly4vL//b29tbzQNIU+llAHeJaLFSqRzJes5vegR8xGbZLCwsHKzVav8z8/0sm0ID+MDvAfh4qZk2exFopnd9vv0ELrXBQO7fD10AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC/ElEQVRoQ+2Zy49NQRCHvx+ReK6IlZ34E7CUiCAR4xEbTLCyQRATYswwb2IQZDYWgojHZpCQECts+ResiQwLj0RClNSkb9Lu3HtPz7mZc8+V6eXt6tP1VVV3VdcVbT7U5vozC9BqD/7/HjCzlZLet9rS9fbP9ICZvQPWSfqRAmFmS4ClMHm+JiR9S1mXVyYFwIBXwEZJv7I2MrPjQH8A6JN0OWtNM/OpAL7HS0mbsjYzswGgN8gNS+rJWtPM/HQAfJ9nkrY22tDMTgMjQaZH0nAzCmatTQE4ClyNPvQU2CbJQ2vKKB2Aa2hmR4DrkbbPgQ5Jv6sJSgkQILqA0dgTkjraBiBAxPHtPz2UtDuGKK0HKkqamd8qg5HS9yXtjebLdYjrHNRqiAeS9gQvnQGGSnML1bvGzOwc0BfN35PUaWYHgRulBwjW9ju+O4JwqM/AWFsABIgLwKkIYgJY1jYAAeJQuGIXVIVcKTKxh8WfBin9J+AVpx/eFWUEqFkyNACKp0rhgWYArkg6kQibSyylmPOklQdibijBX+fSLHFRJkDid+qKmdlaYENOI0zeEcBNSZ9qbVIEQHWuyGOTNZLetgrAz8ClPFpHa1ZL8rf5lFGEB2oBfAxQi4D5DeDmAP7mGJPka0oD4LnDr9imH/xFe8AP4vLIjBclxWXItCOtaIBjwOKo3HaFRyWdnLbmYUHhAJKumdkt4ECk9JCkSitmWixFAwxKOjt5uZvdBvZH2vZLit8XSSBFA/yjpJndAfY1A9FSgOCJu0BnBNErqfIkzfRCywECxCNgR6Rtt6TzmdqHBmyKXG4ZM4sTWc04NzNPWE+AuG3ZlZInSuGBinXMbBzYGVkrE6JUACGcHgPbUyGKAIj7REmZ18y897o5ghiQ5E/bltRChwE/kF7Xj0jyLkbDYWbzgBfA+iA4LmlXqwD8LydvszjAF0lfswBCKC0E3gBeP22p186f8RBKUbaejJmtAr5L+lBPptQAKfCzAClWmkmZWQ/MpHVTvv0X9iFAQGQyevIAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrUlEQVRoQ+2ZPYgTURCAZzbBXJnCeL2Cnb87b9MEtPBUrrMQFAtrtT5/ClGs9LBWWz0RtbBUFCF4oJDsbO68wsLA2YqQSmLlvpEHu7IuMdlLcus+yUKKhJfZ+ebnvZl5CJY/aLn+MAP41x7M1QPMfFtr/crzvHfTAs8FoNPp1LTWzwHgqIg0lFLvrQHwfX8BER8DwC6jNCIecF13wwoA3/dvIuKNpLJa60Oe560XGoCZd4rICiKeTCtaeABmPg4AJmRqg6xcaABmvg4At4aFRyEBhoVM4UMoCplHADCfJTEL5YEsIVNID5iQAYCHALCYxeq5b6PMfF5EBAAEESthGK7W6/XPRpFWq7W3VCqtZg2ZcT3g+/6i4zjzIlLSWn/yPO/DIGMNLCWY2Sj/+xGRK0qpZfNDEASnROTFVi0fr8+aA8z8Ld6KEfGt67oLYwMAwEUium8EREn7OgeAjwCwPyo/nrque3YSgAtE9GDaAM1mc65arc4Zuf1+P2w0Gt9jJZl5DQAORt+fENG5wgEw8zUAMB/zbBBRwyqAIAjuiMjlSOlNItpjFUCqWl0josMzgChR/9hGAWBbknjmAdPhDdqa0gfZzAMJKyVP4v8hhJYRcSni+0JEu63ahZj5anyQici6UuqIVQDdbrfS6/UqRulyufyTiH5sF8AlIro37VpoWEHIzGZ2tM+sEZFnSqkzk9RCS0R01wjIsZz+mug53hDRia0AnI4bGgDYISItz/M2jYC8Gpp2u30MEWuO4zha665Sqp0ZYFStX/iWchRAItFGzoHSsrJ2ZFl1mHg6bfVYJeGJv85CC++BpIJZ5kSFC6G0ha0e7mYJqcJ7IOkRay84UhD2XjHFIFZf8iW9YcYoYRi+tO6aNeupOs66iU/icV46zf/MAKZpzXFk/QL+JG1PUPhRiQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACSElEQVRoQ+2Zu4sUQRCHf5+C+gf4yBXMfMYHGvjCzEBQDIzV+HwEohipGKupD0QNDE8UEwUFTe68wEDhTMVUMFJ+0tArzbjs9u3Ojt0wBR0M9MzUV1XdXVWNKhcq1189wP/2YKcesH1d0nPgdVvgnQDY3iTpqaT9kuaAt9UA2D4o6aGkzVHpXcByFQC2r0q60lB2D7BUNIDtjZIeSDoyRNGyAWwfiiET4n6YlAtg+7Kka2PCozyAMSHT5CkLIIbMfUlbMhdmOQCZIVOeB2LI3JN0NNPq6bTZe8D2aUmOY72kN8DnoIXt7eF5FSEzkQdsB+OEsFwr6RPwbpixhqYStoPyqVwAbkaAY5KeTWD5wStZHrD9XdJgK34FhBP9H8kFOAvciQBhn3/RAcBHSTvjfx4DJ6cBOAPcbRvA9gZJYQT5DfwYKGl7UdLu+PwIOFUiwCVJYQRZBuZqA7gh6XxUegXYVhtAmq0uAnt7gLhQm9vorBZx74Hcc6D3QLKH/z2JGyVnlYs4pCfzEe4rsLW2XehicpAtAftqAwiZbhhBfgE/ZwVwDrjddi40KiG0HXpHO+KcJ8CJaXKheeBWBOgqnf6W1BwvgcOrATieFDTrJL0HViJAVwXNgVgPrJH0BfiQDTDKtREiNK7KLSnHASQLLacP1PxcVkWWq8PU3emq2yqJJ0b1Qsv2QKpdZp+orBBqmrfq5m5mSJXtgUZI1XnB0YCo94opCal6L/ka3ghtlIXqrllzT9VJ5k19Ek/y0zbf6QHatOYk3/oDujC8QMWgjf4AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAHsUlEQVRoQ+2Z969VVRCFv7H33nvvvfcSe2+xxJgY4z9j/Bs0/mABFQXBhl1sgNjQSCyoiL2BDaxs873MJsfDuZd7gfeQxJ3cvAfv3HP22rNmzZo5wRq+Yg3fP/8DWN0RXCYCpZSzgM2Br4GPgW8j4s9hNlpKWQfYETgUOB44GtgMmA1MBF4BFkdEGea+Xdd2AbgF2B2YD0wHZkbEZ4M+qJSyIbArcARwMnAUsC/wO/AscCfwQkT8Meg9+13XBeBx4EjgZ+ClPLGXI+KbfjcqpXivLYA9gWOA0/PnDsDGwOeA977bCAwb1V7P7gIwDpBG2wJfAg/nZ3oXiFLK2sD6ef0+uWlp48kbSddfwAfAVOB+YNZoRuBG4CLgbGDLpNLTwIPAjDaIUsomwM7A4cCJyfm9ga0Bwbn+Bt4fKwDyV+5eAZyayWgkHgGmmBdNEKUUk/U44DzgNGA/YN1WyBWBucATwH3Aq6MZgbXyRAVxMXABsFUrEi9GxILkvbQ5JwGfABiR9ho7APXJpRSTzxO9CjgF2ClBPJrJ+JYSm/Io2Mvyeq+r1Km3G3sAPrmUsktu3pyQItskiFkpiS8CnybfBXl+5sBu8K8qP3YASik+/DdgEaBWbw+cCVwHnJRF7gd5nJEwwT9JmglC2hmRZiRUoQ8HzYFSynrABhk+C17PQtolozcBC/Kklb7FwCHANbk5f3d5zZuAlDI5rdoqj/pvxMwHBaHKaE3ie5eXxKWU7QCjb6WeHxHfDVMH1GlV521AinyUSnR5Jqr6XhP1JzUdeKwBQpqdkSBUMf+tMAjA68YPAOBA4FhgSToBJbhzdUVADyQlrMKTgdfyZJVVE1qLYGWta2FGQpm1UPldT1AQl2ZhE4R2xGgZAetJT1qUUoyeVDQCUyJi5jAA/JJlX99iNF7OgnYl4EcKbdS64Y8JtNJpXoKwGJrYFjm9kPliBDRznq4GT+No3ZCqHoY/zaVr8xnjI+KFYQEojz7M05JGPsQICOCwVgTakdB6mBOCsEIrxdWamDMT0iSapAcBB+T99Vq6Vb8nTQWgqx23IgCMwDONCAhAOghAo9dVrARSI1Hp5H1UMUG4WekpODcqrQQm1aw5ioDfU920Ih6YHuuBiJAFA+fASOY3ABhuXeYljRzYtNcNkwavZ/4YRblvJExM5dTN+38aPTfpx9/nAHdlHgnI52nNJ0WEtn4oAIax5oBfHgaAD5LLJp72WRDSoyb+91ln9s8Dsb5owd8Bbk/gyrFSbK49FBEzxhpAs05IC/NIGbXH0JnKbQFIyeuBvRLAbW44VW+1A2jmxJMZjXd1odlD7JER0L7bsRkBAeh4zQ9ltEZgzCnUjLh0MicmJZ0+TBD2Gkbg5pTm94A7snmSQv8ZAIKR956iEjs1IlQczaJ14obsJ7xGibV4mnOVQpNXRxJ35Zx+Zhpwj5GIiIWlFOVSo6j5ky4WLBNflTMCqtBqS+IuEMqnfshEVe91vUqsYxddsImubJsDyqjFTgBD54AevymjtZDphbQF/epAnxIxYh+sMc9nsiqPUse2VOeqOZRednk2SNrqiREhqKHqwFdZyOxfNXUC0I0KwGFVr0rc6zkWMM2bG7Jbsy6oTEZC2pjo0sUiah/iWObqdLH3R4QyPBQA7fRz2YBXANWNCqBt5vqdun/7NTepadOpujykOu2QItoMI+RyuuFh6ZYnDGslPAHD7Mk4BvTmypoAPBXNXHvqsDwAUsND8aQtYvJeu2Ak9EZq/7SIEJTqdHCOdewjTHjtx8AReCP7XBsVT8gC45BLWfNUmg3N8jZe/24E5Lb38nAEoPrIfYE9VaOd0w6jZHGTbh9EhNcMDODWDKeKIPIvsh/Qo1+Ykqf5ks+DLtXG++lwjazfdRRzbgOENcIaYGLrar1GN/prRPj9gQHIP2lkuNVuGwzlzBOxU7LntSvTCph4gyyHAwLQF1mRPVGpaERteOq0w0hI26UTQGdP/abYXS2lmzWZlkSE6iEnvc7S76alkP2q2q2LtGrK1X6rjlWsATZJWguHZfYCqlvtCeoE0Eg4AbSx6rsGfkNTSnGTqo+8tYsyUsqdPt+mpV9iVwBWWVvEEXuccyersEWrTgAtdkZipHOLCOtEzzUwgHqHdJImtRs3Cs5F7bYsRBa4rnu2B1uO10ckszE8U+Xs3FSnnrPYNpKhATQoZUNu+bcyGwk/5ong2vdtA5DjTXqqSnUo1o5E51S8AlkhAI1oSBsfrm6b4OaGvyuDTZUSQHMyt8z7gVYk6lTc4uaoRoXSTiyMiF+aUVgpABkNtdpCZ16Y4OaGUbHLqnkxCABzzHFkOxLSyeT31dTciLCOLF0rDaARDVVKVXJq4Rsac0PV0ke57LOVUe207906B1sZCXPBnDDHlGpP325tTu0lVgmF2glVSlGlPEUT3Eg4DFbvBVdfVzl56PmOLNXOg/D7RtQa4YxW8PPaqrTKItBSKR8qCLksJWzgLWbaaOvASxFhgexcpRQrsAehSCgWTsOdj/7YfrOzygE0gFjgfN0kDaSVUbAaa6N9xaTB67nyXbP0UQxUrEVdtBtNACa3Rc9ISCOLne5Tdzt7eQBSIEzsukedwTIvxkcNQL/TXZV/W+MB/AMANfVPjBGemwAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-text{font-size:14px;width:30px}.jessibuca-container .jessibuca-speed{font-size:14px;color:#fff}.jessibuca-container .jessibuca-quality-menu-list{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .jessibuca-quality-menu-list.jessibuca-quality-menu-shown{visibility:visible;opacity:1}.jessibuca-container .icon-title-tips{pointer-events:none;position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s ease 0s,opacity .3s ease 0s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .icon-title{display:inline-block;padding:5px 10px;font-size:12px;white-space:nowrap;color:#fff}.jessibuca-container .jessibuca-quality-menu{padding:8px 0}.jessibuca-container .jessibuca-quality-menu-item{display:block;height:25px;margin:0;padding:0 10px;cursor:pointer;font-size:14px;text-align:center;width:50px;color:hsla(0,0%,100%,.5);transition:color .3s,background-color .3s}.jessibuca-container .jessibuca-quality-menu-item:hover{background-color:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-quality-menu-item:focus{outline:none}.jessibuca-container .jessibuca-quality-menu-item.jessibuca-quality-menu-item-active{color:#2298fc}.jessibuca-container .jessibuca-volume-panel-wrap{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%) translateY(22%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px;height:120px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-wrap.jessibuca-volume-panel-wrap-show{visibility:visible;opacity:1}.jessibuca-container .jessibuca-volume-panel{cursor:pointer;position:absolute;top:21px;height:60px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-text{position:absolute;left:0;top:0;width:50px;height:20px;line-height:20px;text-align:center;color:#fff;font-size:12px}.jessibuca-container .jessibuca-volume-panel-handle{position:absolute;top:48px;left:50%;width:12px;height:12px;border-radius:12px;margin-left:-6px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:before{bottom:-54px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:after{bottom:6px;background:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-volume-panel-handle:after,.jessibuca-container .jessibuca-volume-panel-handle:before{content:\"\";position:absolute;display:block;left:50%;width:3px;margin-left:-1px;height:60px}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-controls{width:100vh}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-play-big:after{transform:translate(-50%,-50%) rotate(270deg)}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading{flex-direction:row}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading-text{transform:rotate(270deg)}\n/*# sourceMappingURL=data:application/json;base64,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eWZyYW1lcyByb3RhdGlvbiB7XG4gIGZyb20ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7IH1cbiAgdG8ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMzYwZGVnKTsgfSB9XG5cbkBrZXlmcmFtZXMgbWFnZW50YVB1bHNlIHtcbiAgZnJvbSB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzYzMDAzMDtcbiAgICAtd2Via2l0LWJveC1zaGFkb3c6IDAgMCA5cHggIzMzMzsgfVxuICA1MCUge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICNhOTAxNGI7XG4gICAgLXdlYmtpdC1ib3gtc2hhZG93OiAwIDAgMThweCAjYTkwMTRiOyB9XG4gIHRvIHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjNjMwMDMwO1xuICAgIC13ZWJraXQtYm94LXNoYWRvdzogMCAwIDlweCAjMzMzOyB9IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICB3aWR0aDogMTZweDtcbiAgaGVpZ2h0OiAxNnB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcG9zdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAxMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHdpZHRoOiAxMDAlO1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXIgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIHBvaW50ZXItZXZlbnRzOiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWcge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIGhlaWdodDogMTAwJTtcbiAgd2lkdGg6IDEwMCU7XG4gIGJhY2tncm91bmQ6IHJnYmEoMCwgMCwgMCwgMC40KTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXBsYXktYmlnOmFmdGVyIHtcbiAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgY29udGVudDogJyc7XG4gICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgIGxlZnQ6IDUwJTtcbiAgICB0b3A6IDUwJTtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICB3aWR0aDogNDhweDtcbiAgICBoZWlnaHQ6IDQ4cHg7XG4gICAgYmFja2dyb3VuZC1pbWFnZTogdXJsKFwiLi4vYXNzZXRzL3BsYXkucG5nXCIpO1xuICAgIGJhY2tncm91bmQtcmVwZWF0OiBuby1yZXBlYXQ7XG4gICAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWc6aG92ZXI6YWZ0ZXIge1xuICAgIGJhY2tncm91bmQtaW1hZ2U6IHVybChcIi4uL2Fzc2V0cy9wbGF5LWhvdmVyLnBuZ1wiKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyB7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgbGVmdDogNTAlO1xuICB0b3A6IDA7XG4gIHBhZGRpbmc6IDAgM3B4O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIGp1c3RpZnktY29udGVudDogc3BhY2UtYXJvdW5kO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICB3aWR0aDogOTVweDtcbiAgaGVpZ2h0OiAyMHB4O1xuICBiYWNrZ3JvdW5kOiAjMDAwMDAwO1xuICBvcGFjaXR5OiAxO1xuICBib3JkZXItcmFkaXVzOiAwcHggMHB4IDhweCA4cHg7XG4gIHotaW5kZXg6IDE7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1yZWNvcmRpbmcgLmplc3NpYnVjYS1yZWNvcmRpbmctcmVkLXBvaW50IHtcbiAgICB3aWR0aDogOHB4O1xuICAgIGhlaWdodDogOHB4O1xuICAgIGJhY2tncm91bmQ6ICNGRjFGMUY7XG4gICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgIGFuaW1hdGlvbjogbWFnZW50YVB1bHNlIDFzIGxpbmVhciBpbmZpbml0ZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLXJlY29yZGluZy10aW1lIHtcbiAgICBmb250LXNpemU6IDE0cHg7XG4gICAgZm9udC13ZWlnaHQ6IDUwMDtcbiAgICBjb2xvcjogI0RERERERDsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gICAgd2lkdGg6IDE2cHg7XG4gICAgaGVpZ2h0OiAxNnB4O1xuICAgIGN1cnNvcjogcG9pbnRlcjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmcge1xuICBkaXNwbGF5OiBub25lO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAyMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmctdGV4dCB7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICBmb250LXNpemU6IDEzcHg7XG4gIGNvbG9yOiAjZmZmO1xuICBtYXJnaW4tdG9wOiAxMHB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTYxNjE2O1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBkaXNwbGF5OiBmbGV4O1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGZsZXgtZW5kO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHotaW5kZXg6IDQwO1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDM4cHg7XG4gIHdpZHRoOiAxMDAlO1xuICBwYWRkaW5nLWxlZnQ6IDEzcHg7XG4gIHBhZGRpbmctcmlnaHQ6IDEzcHg7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgY29sb3I6ICNmZmY7XG4gIG9wYWNpdHk6IDA7XG4gIHZpc2liaWxpdHk6IGhpZGRlbjtcbiAgdHJhbnNpdGlvbjogYWxsIDAuMnMgZWFzZS1pbi1vdXQ7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICB0cmFuc2l0aW9uOiB3aWR0aCAuNXMgZWFzZS1pbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbSB7XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gICAgcGFkZGluZzogMCA4cHg7IH1cbiAgICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbTpob3ZlciAuaWNvbi10aXRsZS10aXBzIHtcbiAgICAgIHZpc2liaWxpdHk6IHZpc2libGU7XG4gICAgICBvcGFjaXR5OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1taWNyb3Bob25lLWNsb3NlIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wbGF5IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wYXVzZSB7XG4gICAgZGlzcGxheTogbm9uZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi1leGl0IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1zY3JlZW5zaG90IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1yZWNvcmQge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWZ1bGxzY3JlZW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLXJlY29yZC1zdG9wIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvLCAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtaWNvbi1tdXRlIHtcbiAgICB6LWluZGV4OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20ge1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuO1xuICAgIGhlaWdodDogMTAwJTsgfVxuICAgIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20gLmplc3NpYnVjYS1jb250cm9scy1sZWZ0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG4gICAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWNvbnRyb2xzLWJvdHRvbSAuamVzc2lidWNhLWNvbnRyb2xzLXJpZ2h0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93IC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBvcGFjaXR5OiAxO1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93LWF1dG8taGlkZSAuamVzc2lidWNhLWNvbnRyb2xzIHtcbiAgb3BhY2l0eTogMC44O1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlO1xuICBkaXNwbGF5OiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1oaWRlLWN1cnNvciAqIHtcbiAgY3Vyc29yOiBub25lICFpbXBvcnRhbnQ7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWxvYWRpbmcge1xuICB3aWR0aDogNTBweDtcbiAgaGVpZ2h0OiA1MHB4O1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbG9hZGluZy5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7XG4gIGFuaW1hdGlvbjogcm90YXRpb24gMXMgbGluZWFyIGluZmluaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3NjcmVlbnNob3QucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvc2NyZWVuc2hvdC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheSB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wbGF5LnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheTpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BsYXktaG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXBhdXNlIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BhdXNlLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGF1c2U6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wYXVzZS1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZDpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZFN0b3A6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbiB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9mdWxsc2NyZWVuLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbjpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2Z1bGxzY3JlZW4taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2V4aXQtZnVsbHNjcmVlbi5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvZXhpdC1mdWxsc2NyZWVuLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1hdWRpbyB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9hdWRpby5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWF1ZGlvOmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvYXVkaW8taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGUge1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbXV0ZS5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGU6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9tdXRlLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi10ZXh0IHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICB3aWR0aDogMzBweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXNwZWVkIHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICBjb2xvcjogI2ZmZjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXF1YWxpdHktbWVudS1saXN0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMsIG9wYWNpdHkgMzAwbXM7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMC41KTtcbiAgYm9yZGVyLXJhZGl1czogNHB4OyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWxpc3QuamVzc2lidWNhLXF1YWxpdHktbWVudS1zaG93biB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5pY29uLXRpdGxlLXRpcHMge1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMgZWFzZSAwcywgb3BhY2l0eSAzMDBtcyBlYXNlIDBzO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuaWNvbi10aXRsZSB7XG4gIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgcGFkZGluZzogNXB4IDEwcHg7XG4gIGZvbnQtc2l6ZTogMTJweDtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgY29sb3I6IHdoaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51IHtcbiAgcGFkZGluZzogOHB4IDA7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBoZWlnaHQ6IDI1cHg7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMCAxMHB4O1xuICBjdXJzb3I6IHBvaW50ZXI7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICB3aWR0aDogNTBweDtcbiAgY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC41KTtcbiAgdHJhbnNpdGlvbjogY29sb3IgMzAwbXMsIGJhY2tncm91bmQtY29sb3IgMzAwbXM7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbTpob3ZlciB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyNTUsIDI1NSwgMjU1LCAwLjIpOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW06Zm9jdXMge1xuICAgIG91dGxpbmU6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbS5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW0tYWN0aXZlIHtcbiAgICBjb2xvcjogIzIyOThGQzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC13cmFwIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSkgdHJhbnNsYXRlWSgyMiUpO1xuICB0cmFuc2l0aW9uOiB2aXNpYmlsaXR5IDMwMG1zLCBvcGFjaXR5IDMwMG1zO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgaGVpZ2h0OiAxMjBweDtcbiAgd2lkdGg6IDUwcHg7XG4gIG92ZXJmbG93OiBoaWRkZW47IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtd3JhcC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLXdyYXAtc2hvdyB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMjFweDtcbiAgaGVpZ2h0OiA2MHB4O1xuICB3aWR0aDogNTBweDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC10ZXh0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiAwO1xuICB0b3A6IDA7XG4gIHdpZHRoOiA1MHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGNvbG9yOiAjZmZmO1xuICBmb250LXNpemU6IDEycHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDQ4cHg7XG4gIGxlZnQ6IDUwJTtcbiAgd2lkdGg6IDEycHg7XG4gIGhlaWdodDogMTJweDtcbiAgYm9yZGVyLXJhZGl1czogMTJweDtcbiAgbWFyZ2luLWxlZnQ6IC02cHg7XG4gIGJhY2tncm91bmQ6ICNmZmY7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUge1xuICAgIGJvdHRvbTogLTU0cHg7XG4gICAgYmFja2dyb3VuZDogI2ZmZjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC1oYW5kbGU6OmFmdGVyIHtcbiAgICBib3R0b206IDZweDtcbiAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMik7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUsIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLWhhbmRsZTo6YWZ0ZXIge1xuICAgIGNvbnRlbnQ6ICcnO1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICBsZWZ0OiA1MCU7XG4gICAgd2lkdGg6IDNweDtcbiAgICBtYXJnaW4tbGVmdDogLTFweDtcbiAgICBoZWlnaHQ6IDYwcHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICB3aWR0aDogMTAwdmg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtcGxheS1iaWc6YWZ0ZXIge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKSByb3RhdGUoMjcwZGVnKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nIHtcbiAgZmxleC1kaXJlY3Rpb246IHJvdzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nLXRleHQge1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgyNzBkZWcpOyB9XG4iXX0= */";
- styleInject(css_248z$1);
- // todo: 待定
- var hotkey = ((player, control) => {
- const {
- events: {
- proxy
- }
- } = player;
- const keys = {};
- function addHotkey(key, event) {
- if (keys[key]) {
- keys[key].push(event);
- } else {
- keys[key] = [event];
- }
- } //
- addHotkey(HOT_KEY.esc, () => {
- if (player.fullscreen) {
- player.fullscreen = false;
- }
- }); //
- addHotkey(HOT_KEY.arrowUp, () => {
- player.volume += 0.05;
- }); //
- addHotkey(HOT_KEY.arrowDown, () => {
- player.volume -= 0.05;
- });
- proxy(window, 'keydown', event => {
- if (control.isFocus) {
- const tag = document.activeElement.tagName.toUpperCase();
- const editable = document.activeElement.getAttribute('contenteditable');
- if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') {
- const events = keys[event.keyCode];
- if (events) {
- event.preventDefault();
- events.forEach(fn => fn());
- }
- }
- }
- });
- });
- class Control {
- constructor(player) {
- this.player = player;
- template(player, this);
- observer$1(player, this);
- property(player, this);
- events(player, this);
- if (player._opt.hotKey) {
- hotkey(player, this);
- }
- this.player.debug.log('Control', 'init');
- }
- destroy() {
- if (this.$poster) {
- this.player.$container.removeChild(this.$poster);
- }
- if (this.$loading) {
- this.player.$container.removeChild(this.$loading);
- }
- if (this.$controls) {
- this.player.$container.removeChild(this.$controls);
- }
- if (this.$playBig) {
- this.player.$container.removeChild(this.$playBig);
- }
- this.player.debug.log('control', 'destroy');
- }
- autoSize() {
- const player = this.player;
- player.$container.style.padding = '0 0';
- const playerWidth = player.width;
- const playerHeight = player.height;
- const playerRatio = playerWidth / playerHeight;
- const canvasWidth = player.video.$videoElement.width;
- const canvasHeight = player.video.$videoElement.height;
- const canvasRatio = canvasWidth / canvasHeight;
- if (playerRatio > canvasRatio) {
- const padding = (playerWidth - playerHeight * canvasRatio) / 2;
- player.$container.style.padding = `0 ${padding}px`;
- } else {
- const padding = (playerHeight - playerWidth / canvasRatio) / 2;
- player.$container.style.padding = `${padding}px 0`;
- }
- }
- }
- var css_248z = ".jessibuca-container{position:relative;width:100%;height:100%;overflow:hidden}.jessibuca-container.jessibuca-fullscreen-web{position:fixed;z-index:9999;left:0;top:0;right:0;bottom:0;width:100vw!important;height:100vh!important;background:#000}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEscUJBQ0UsaUJBQWtCLENBQ2xCLFVBQVcsQ0FDWCxXQUFZLENBQ1osZUFBa0IsQ0FDbEIsOENBQ0UsY0FBZSxDQUNmLFlBQWEsQ0FDYixNQUFPLENBQ1AsS0FBTSxDQUNOLE9BQVEsQ0FDUixRQUFTLENBQ1QscUJBQXVCLENBQ3ZCLHNCQUF3QixDQUN4QixlQUFrQiIsImZpbGUiOiJzdHlsZS5zY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLmplc3NpYnVjYS1jb250YWluZXIge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIG92ZXJmbG93OiBoaWRkZW47IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIHtcbiAgICBwb3NpdGlvbjogZml4ZWQ7XG4gICAgei1pbmRleDogOTk5OTtcbiAgICBsZWZ0OiAwO1xuICAgIHRvcDogMDtcbiAgICByaWdodDogMDtcbiAgICBib3R0b206IDA7XG4gICAgd2lkdGg6IDEwMHZ3ICFpbXBvcnRhbnQ7XG4gICAgaGVpZ2h0OiAxMDB2aCAhaW1wb3J0YW50O1xuICAgIGJhY2tncm91bmQ6ICMwMDA7IH1cbiJdfQ== */";
- styleInject(css_248z);
- var observer = (player => {
- const {
- _opt,
- debug,
- events: {
- proxy
- }
- } = player;
- if (_opt.supportDblclickFullscreen) {
- proxy(player.$container, 'dblclick', () => {
- player.fullscreen = !player.fullscreen;
- });
- } //
- proxy(document, 'visibilitychange', () => {
- if (_opt.hiddenAutoPause) {
- debug.log('visibilitychange', document.visibilityState, player._isPlayingBeforePageHidden);
- if ("visible" === document.visibilityState) {
- if (player._isPlayingBeforePageHidden) {
- player.play();
- }
- } else {
- player._isPlayingBeforePageHidden = player.playing; // hidden
- if (player.playing) {
- player.pause();
- }
- }
- }
- });
- proxy(window, 'fullscreenchange', () => {
- //
- if (player.keepScreenOn !== null && "visible" === document.visibilityState) {
- player.enableWakeLock();
- }
- });
- });
- class MP4$1 {
- static init() {
- MP4$1.types = {
- avc1: [],
- avcC: [],
- hvc1: [],
- hvcC: [],
- btrt: [],
- dinf: [],
- dref: [],
- esds: [],
- ftyp: [],
- hdlr: [],
- mdat: [],
- mdhd: [],
- mdia: [],
- mfhd: [],
- minf: [],
- moof: [],
- moov: [],
- mp4a: [],
- mvex: [],
- mvhd: [],
- sdtp: [],
- stbl: [],
- stco: [],
- stsc: [],
- stsd: [],
- stsz: [],
- stts: [],
- tfdt: [],
- tfhd: [],
- traf: [],
- trak: [],
- trun: [],
- trex: [],
- tkhd: [],
- vmhd: [],
- smhd: []
- };
- for (let name in MP4$1.types) {
- if (MP4$1.types.hasOwnProperty(name)) {
- MP4$1.types[name] = [name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)];
- }
- }
- let constants = MP4$1.constants = {};
- constants.FTYP = new Uint8Array([0x69, 0x73, 0x6F, 0x6D, // major_brand: isom
- 0x0, 0x0, 0x0, 0x1, // minor_version: 0x01
- 0x69, 0x73, 0x6F, 0x6D, // isom
- 0x61, 0x76, 0x63, 0x31 // avc1
- ]);
- constants.STSD_PREFIX = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x01 // entry_count
- ]);
- constants.STTS = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00 // entry_count
- ]);
- constants.STSC = constants.STCO = constants.STTS;
- constants.STSZ = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00, // sample_size
- 0x00, 0x00, 0x00, 0x00 // sample_count
- ]);
- constants.HDLR_VIDEO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
- 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x69, 0x64, 0x65, 0x6F, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler
- ]);
- constants.HDLR_AUDIO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun'
- 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x6F, 0x75, 0x6E, 0x64, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler
- ]);
- constants.DREF = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x01, // entry_count
- 0x00, 0x00, 0x00, 0x0C, // entry_size
- 0x75, 0x72, 0x6C, 0x20, // type 'url '
- 0x00, 0x00, 0x00, 0x01 // version(0) + flags
- ]); // Sound media header
- constants.SMHD = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2)
- ]); // video media header
- constants.VMHD = new Uint8Array([0x00, 0x00, 0x00, 0x01, // version(0) + flags
- 0x00, 0x00, // graphicsmode: 2 bytes
- 0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes
- 0x00, 0x00]);
- } // Generate a box
- static box(type) {
- let size = 8;
- let result = null;
- let datas = Array.prototype.slice.call(arguments, 1);
- let arrayCount = datas.length;
- for (let i = 0; i < arrayCount; i++) {
- size += datas[i].byteLength;
- }
- result = new Uint8Array(size);
- result[0] = size >>> 24 & 0xFF; // size
- result[1] = size >>> 16 & 0xFF;
- result[2] = size >>> 8 & 0xFF;
- result[3] = size & 0xFF;
- result.set(type, 4); // type
- let offset = 8;
- for (let i = 0; i < arrayCount; i++) {
- // data body
- result.set(datas[i], offset);
- offset += datas[i].byteLength;
- }
- return result;
- } // emit ftyp & moov
- static generateInitSegment(meta) {
- let ftyp = MP4$1.box(MP4$1.types.ftyp, MP4$1.constants.FTYP);
- let moov = MP4$1.moov(meta);
- let result = new Uint8Array(ftyp.byteLength + moov.byteLength);
- result.set(ftyp, 0);
- result.set(moov, ftyp.byteLength);
- return result;
- } // Movie metadata box
- static moov(meta) {
- let mvhd = MP4$1.mvhd(meta.timescale, meta.duration);
- let trak = MP4$1.trak(meta);
- let mvex = MP4$1.mvex(meta);
- return MP4$1.box(MP4$1.types.moov, mvhd, trak, mvex);
- } // Movie header box
- static mvhd(timescale, duration) {
- return MP4$1.box(MP4$1.types.mvhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00, // creation_time
- 0x00, 0x00, 0x00, 0x00, // modification_time
- timescale >>> 24 & 0xFF, // timescale: 4 bytes
- timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes
- duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0
- 0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes)
- 0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
- 0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes----
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes----
- 0xFF, 0xFF, 0xFF, 0xFF // next_track_ID
- ]));
- } // Track box
- static trak(meta) {
- return MP4$1.box(MP4$1.types.trak, MP4$1.tkhd(meta), MP4$1.mdia(meta));
- } // Track header box
- static tkhd(meta) {
- let trackId = meta.id,
- duration = meta.duration;
- let width = meta.presentWidth,
- height = meta.presentHeight;
- return MP4$1.box(MP4$1.types.tkhd, new Uint8Array([0x00, 0x00, 0x00, 0x07, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00, // creation_time
- 0x00, 0x00, 0x00, 0x00, // modification_time
- trackId >>> 24 & 0xFF, // track_ID: 4 bytes
- trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes
- duration >>> 24 & 0xFF, // duration: 4 bytes
- duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes)
- 0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes)
- 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
- width >>> 8 & 0xFF, // width and height
- width & 0xFF, 0x00, 0x00, height >>> 8 & 0xFF, height & 0xFF, 0x00, 0x00]));
- }
- static mdia(meta) {
- return MP4$1.box(MP4$1.types.mdia, MP4$1.mdhd(meta), MP4$1.hdlr(meta), MP4$1.minf(meta));
- } // Media header box
- static mdhd(meta) {
- let timescale = meta.timescale;
- let duration = meta.duration;
- return MP4$1.box(MP4$1.types.mdhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- 0x00, 0x00, 0x00, 0x00, // creation_time
- 0x00, 0x00, 0x00, 0x00, // modification_time
- timescale >>> 24 & 0xFF, // timescale: 4 bytes
- timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes
- duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x55, 0xC4, // language: und (undetermined)
- 0x00, 0x00 // pre_defined = 0
- ]));
- } // Media handler reference box
- static hdlr(meta) {
- let data = null;
- if (meta.type === 'audio') {
- data = MP4$1.constants.HDLR_AUDIO;
- } else {
- data = MP4$1.constants.HDLR_VIDEO;
- }
- return MP4$1.box(MP4$1.types.hdlr, data);
- } // Media infomation box
- static minf(meta) {
- let xmhd = null;
- if (meta.type === 'audio') {
- xmhd = MP4$1.box(MP4$1.types.smhd, MP4$1.constants.SMHD);
- } else {
- xmhd = MP4$1.box(MP4$1.types.vmhd, MP4$1.constants.VMHD);
- }
- return MP4$1.box(MP4$1.types.minf, xmhd, MP4$1.dinf(), MP4$1.stbl(meta));
- } // Data infomation box
- static dinf() {
- let result = MP4$1.box(MP4$1.types.dinf, MP4$1.box(MP4$1.types.dref, MP4$1.constants.DREF));
- return result;
- } // Sample table box
- static stbl(meta) {
- let result = MP4$1.box(MP4$1.types.stbl, // type: stbl
- MP4$1.stsd(meta), // Sample Description Table
- MP4$1.box(MP4$1.types.stts, MP4$1.constants.STTS), // Time-To-Sample
- MP4$1.box(MP4$1.types.stsc, MP4$1.constants.STSC), // Sample-To-Chunk
- MP4$1.box(MP4$1.types.stsz, MP4$1.constants.STSZ), // Sample size
- MP4$1.box(MP4$1.types.stco, MP4$1.constants.STCO) // Chunk offset
- );
- return result;
- } // Sample description box
- static stsd(meta) {
- if (meta.type === 'audio') {
- // else: aac -> mp4a
- return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.mp4a(meta));
- } else {
- if (meta.videoType === 'avc') {
- //
- return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.avc1(meta));
- } else {
- //
- return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.hvc1(meta));
- }
- }
- }
- static mp4a(meta) {
- let channelCount = meta.channelCount;
- let sampleRate = meta.audioSampleRate;
- let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4)
- 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2)
- 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
- 0x00, 0x00, 0x00, 0x00, 0x00, channelCount, // channelCount(2)
- 0x00, 0x10, // sampleSize(2)
- 0x00, 0x00, 0x00, 0x00, // reserved(4)
- sampleRate >>> 8 & 0xFF, // Audio sample rate
- sampleRate & 0xFF, 0x00, 0x00]);
- return MP4$1.box(MP4$1.types.mp4a, data, MP4$1.esds(meta));
- }
- static esds(meta) {
- let config = meta.config || [];
- let configSize = config.length;
- let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version 0 + flags
- 0x03, // descriptor_type
- 0x17 + configSize, // length3
- 0x00, 0x01, // es_id
- 0x00, // stream_priority
- 0x04, // descriptor_type
- 0x0F + configSize, // length
- 0x40, // codec: mpeg4_audio
- 0x15, // stream_type: Audio
- 0x00, 0x00, 0x00, // buffer_size
- 0x00, 0x00, 0x00, 0x00, // maxBitrate
- 0x00, 0x00, 0x00, 0x00, // avgBitrate
- 0x05 // descriptor_type
- ].concat([configSize]).concat(config).concat([0x06, 0x01, 0x02 // GASpecificConfig
- ]));
- return MP4$1.box(MP4$1.types.esds, data);
- } // avc
- static avc1(meta) {
- let avcc = meta.avcc;
- const width = meta.codecWidth;
- const height = meta.codecHeight;
- let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]);
- return MP4$1.box(MP4$1.types.avc1, data, MP4$1.box(MP4$1.types.avcC, avcc));
- } // hvc
- static hvc1(meta) {
- let avcc = meta.avcc;
- const width = meta.codecWidth;
- const height = meta.codecHeight;
- let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]);
- return MP4$1.box(MP4$1.types.hvc1, data, MP4$1.box(MP4$1.types.hvcC, avcc));
- } // Movie Extends box
- static mvex(meta) {
- return MP4$1.box(MP4$1.types.mvex, MP4$1.trex(meta));
- } // Track Extends box
- static trex(meta) {
- let trackId = meta.id;
- let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
- trackId >>> 24 & 0xFF, // track_ID
- trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x01, // default_sample_description_index
- 0x00, 0x00, 0x00, 0x00, // default_sample_duration
- 0x00, 0x00, 0x00, 0x00, // default_sample_size
- 0x00, 0x01, 0x00, 0x01 // default_sample_flags
- ]);
- return MP4$1.box(MP4$1.types.trex, data);
- } // Movie fragment box
- static moof(track, baseMediaDecodeTime) {
- return MP4$1.box(MP4$1.types.moof, MP4$1.mfhd(track.sequenceNumber), MP4$1.traf(track, baseMediaDecodeTime));
- } //
- static mfhd(sequenceNumber) {
- let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, sequenceNumber >>> 24 & 0xFF, // sequence_number: int32
- sequenceNumber >>> 16 & 0xFF, sequenceNumber >>> 8 & 0xFF, sequenceNumber & 0xFF]);
- return MP4$1.box(MP4$1.types.mfhd, data);
- } // Track fragment box
- static traf(track, baseMediaDecodeTime) {
- let trackId = track.id; // Track fragment header box
- let tfhd = MP4$1.box(MP4$1.types.tfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags
- trackId >>> 24 & 0xFF, // track_ID
- trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF])); // Track Fragment Decode Time
- let tfdt = MP4$1.box(MP4$1.types.tfdt, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags
- baseMediaDecodeTime >>> 24 & 0xFF, // baseMediaDecodeTime: int32
- baseMediaDecodeTime >>> 16 & 0xFF, baseMediaDecodeTime >>> 8 & 0xFF, baseMediaDecodeTime & 0xFF]));
- let sdtp = MP4$1.sdtp(track);
- let trun = MP4$1.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8);
- return MP4$1.box(MP4$1.types.traf, tfhd, tfdt, trun, sdtp);
- } // Sample Dependency Type box
- static sdtp(track) {
- let data = new Uint8Array(4 + 1);
- let flags = track.flags;
- data[4] = flags.isLeading << 6 | flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy;
- return MP4$1.box(MP4$1.types.sdtp, data);
- } // trun
- static trun(track, offset) {
- let dataSize = 12 + 16;
- let data = new Uint8Array(dataSize);
- offset += 8 + dataSize;
- data.set([0x00, 0x00, 0x0F, 0x01, // version(0) & flags
- 0x00, 0x00, 0x00, 0x01, // sample_count
- offset >>> 24 & 0xFF, // data_offset
- offset >>> 16 & 0xFF, offset >>> 8 & 0xFF, offset & 0xFF], 0);
- let duration = track.duration;
- let size = track.size;
- let flags = track.flags;
- let cts = track.cts;
- data.set([duration >>> 24 & 0xFF, // sample_duration
- duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, size >>> 24 & 0xFF, // sample_size
- size >>> 16 & 0xFF, size >>> 8 & 0xFF, size & 0xFF, flags.isLeading << 2 | flags.dependsOn, // sample_flags
- flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.isNonSync, 0x00, 0x00, // sample_degradation_priority
- cts >>> 24 & 0xFF, // sample_composition_time_offset
- cts >>> 16 & 0xFF, cts >>> 8 & 0xFF, cts & 0xFF], 12);
- return MP4$1.box(MP4$1.types.trun, data);
- } // mdat
- static mdat(data) {
- return MP4$1.box(MP4$1.types.mdat, data);
- }
- }
- MP4$1.init();
- /*
- * Copyright (C) 2016 Bilibili. All Rights Reserved.
- *
- * @author zheng qian <xqq@xqq.im>
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // Exponential-Golomb buffer decoder
- class ExpGolomb {
- constructor(uint8array) {
- this.TAG = 'ExpGolomb';
- this._buffer = uint8array;
- this._buffer_index = 0;
- this._total_bytes = uint8array.byteLength;
- this._total_bits = uint8array.byteLength * 8;
- this._current_word = 0;
- this._current_word_bits_left = 0;
- }
- destroy() {
- this._buffer = null;
- }
- _fillCurrentWord() {
- let buffer_bytes_left = this._total_bytes - this._buffer_index;
- let bytes_read = Math.min(4, buffer_bytes_left);
- let word = new Uint8Array(4);
- word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read));
- this._current_word = new DataView(word.buffer).getUint32(0, false);
- this._buffer_index += bytes_read;
- this._current_word_bits_left = bytes_read * 8;
- }
- readBits(bits) {
- if (bits <= this._current_word_bits_left) {
- let result = this._current_word >>> 32 - bits;
- this._current_word <<= bits;
- this._current_word_bits_left -= bits;
- return result;
- }
- let result = this._current_word_bits_left ? this._current_word : 0;
- result = result >>> 32 - this._current_word_bits_left;
- let bits_need_left = bits - this._current_word_bits_left;
- this._fillCurrentWord();
- let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left);
- let result2 = this._current_word >>> 32 - bits_read_next;
- this._current_word <<= bits_read_next;
- this._current_word_bits_left -= bits_read_next;
- result = result << bits_read_next | result2;
- return result;
- }
- readBool() {
- return this.readBits(1) === 1;
- }
- readByte() {
- return this.readBits(8);
- }
- _skipLeadingZero() {
- let zero_count;
- for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) {
- if (0 !== (this._current_word & 0x80000000 >>> zero_count)) {
- this._current_word <<= zero_count;
- this._current_word_bits_left -= zero_count;
- return zero_count;
- }
- }
- this._fillCurrentWord();
- return zero_count + this._skipLeadingZero();
- }
- readUEG() {
- // unsigned exponential golomb
- let leading_zeros = this._skipLeadingZero();
- return this.readBits(leading_zeros + 1) - 1;
- }
- readSEG() {
- // signed exponential golomb
- let value = this.readUEG();
- if (value & 0x01) {
- return value + 1 >>> 1;
- } else {
- return -1 * (value >>> 1);
- }
- }
- }
- /*
- * Copyright (C) 2016 Bilibili. All Rights Reserved.
- *
- * @author zheng qian <xqq@xqq.im>
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class SPSParser$1 {
- static _ebsp2rbsp(uint8array) {
- let src = uint8array;
- let src_length = src.byteLength;
- let dst = new Uint8Array(src_length);
- let dst_idx = 0;
- for (let i = 0; i < src_length; i++) {
- if (i >= 2) {
- // Unescape: Skip 0x03 after 00 00
- if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {
- continue;
- }
- }
- dst[dst_idx] = src[i];
- dst_idx++;
- }
- return new Uint8Array(dst.buffer, 0, dst_idx);
- } // 解析 SPS
- // https://zhuanlan.zhihu.com/p/27896239
- static parseSPS(uint8array) {
- let rbsp = SPSParser$1._ebsp2rbsp(uint8array);
- let gb = new ExpGolomb(rbsp);
- gb.readByte(); // 标识当前H.264码流的profile。
- // 我们知道,H.264中定义了三种常用的档次profile: 基准档次:baseline profile;主要档次:main profile; 扩展档次:extended profile;
- let profile_idc = gb.readByte(); // profile_idc
- gb.readByte(); // constraint_set_flags[5] + reserved_zero[3]
- // 标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。
- let level_idc = gb.readByte(); // level_idc
- // 表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。
- gb.readUEG(); // seq_parameter_set_id
- let profile_string = SPSParser$1.getProfileString(profile_idc);
- let level_string = SPSParser$1.getLevelString(level_idc);
- let chroma_format_idc = 1;
- let chroma_format = 420;
- let chroma_format_table = [0, 420, 422, 444];
- let bit_depth = 8; //
- if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 || profile_idc === 244 || profile_idc === 44 || profile_idc === 83 || profile_idc === 86 || profile_idc === 118 || profile_idc === 128 || profile_idc === 138 || profile_idc === 144) {
- //
- chroma_format_idc = gb.readUEG();
- if (chroma_format_idc === 3) {
- gb.readBits(1); // separate_colour_plane_flag
- }
- if (chroma_format_idc <= 3) {
- chroma_format = chroma_format_table[chroma_format_idc];
- }
- bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8
- gb.readUEG(); // bit_depth_chroma_minus8
- gb.readBits(1); // qpprime_y_zero_transform_bypass_flag
- if (gb.readBool()) {
- // seq_scaling_matrix_present_flag
- let scaling_list_count = chroma_format_idc !== 3 ? 8 : 12;
- for (let i = 0; i < scaling_list_count; i++) {
- if (gb.readBool()) {
- // seq_scaling_list_present_flag
- if (i < 6) {
- SPSParser$1._skipScalingList(gb, 16);
- } else {
- SPSParser$1._skipScalingList(gb, 64);
- }
- }
- }
- }
- } // 用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 +
- gb.readUEG(); // log2_max_frame_num_minus4
- // 表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。
- let pic_order_cnt_type = gb.readUEG();
- if (pic_order_cnt_type === 0) {
- gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4
- } else if (pic_order_cnt_type === 1) {
- gb.readBits(1); // delta_pic_order_always_zero_flag
- gb.readSEG(); // offset_for_non_ref_pic
- gb.readSEG(); // offset_for_top_to_bottom_field
- let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG();
- for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
- gb.readSEG(); // offset_for_ref_frame
- }
- } // 用于表示参考帧的最大数目。
- let ref_frames = gb.readUEG(); // max_num_ref_frames
- // 标识位,说明frame_num中是否允许不连续的值。
- gb.readBits(1); // gaps_in_frame_num_value_allowed_flag
- // 用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:
- let pic_width_in_mbs_minus1 = gb.readUEG(); // 使用PicHeightInMapUnits来度量视频中一帧图像的高度。
- // PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:
- let pic_height_in_map_units_minus1 = gb.readUEG(); // 标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;
- // 该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,
- // 为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。
- let frame_mbs_only_flag = gb.readBits(1);
- if (frame_mbs_only_flag === 0) {
- // 标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。
- gb.readBits(1); // mb_adaptive_frame_field_flag
- } // 标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。
- gb.readBits(1); // direct_8x8_inference_flag
- let frame_crop_left_offset = 0;
- let frame_crop_right_offset = 0;
- let frame_crop_top_offset = 0;
- let frame_crop_bottom_offset = 0;
- let frame_cropping_flag = gb.readBool();
- if (frame_cropping_flag) {
- frame_crop_left_offset = gb.readUEG();
- frame_crop_right_offset = gb.readUEG();
- frame_crop_top_offset = gb.readUEG();
- frame_crop_bottom_offset = gb.readUEG();
- }
- let sar_width = 1,
- sar_height = 1;
- let fps = 0,
- fps_fixed = true,
- fps_num = 0,
- fps_den = 0; // 标识位,说明SPS中是否存在VUI信息。
- let vui_parameters_present_flag = gb.readBool();
- if (vui_parameters_present_flag) {
- if (gb.readBool()) {
- // aspect_ratio_info_present_flag
- let aspect_ratio_idc = gb.readByte();
- let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2];
- let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1];
- if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) {
- sar_width = sar_w_table[aspect_ratio_idc - 1];
- sar_height = sar_h_table[aspect_ratio_idc - 1];
- } else if (aspect_ratio_idc === 255) {
- sar_width = gb.readByte() << 8 | gb.readByte();
- sar_height = gb.readByte() << 8 | gb.readByte();
- }
- }
- if (gb.readBool()) {
- // overscan_info_present_flag
- gb.readBool(); // overscan_appropriate_flag
- }
- if (gb.readBool()) {
- // video_signal_type_present_flag
- gb.readBits(4); // video_format & video_full_range_flag
- if (gb.readBool()) {
- // colour_description_present_flag
- gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients
- }
- }
- if (gb.readBool()) {
- // chroma_loc_info_present_flag
- gb.readUEG(); // chroma_sample_loc_type_top_field
- gb.readUEG(); // chroma_sample_loc_type_bottom_field
- }
- if (gb.readBool()) {
- // timing_info_present_flag
- let num_units_in_tick = gb.readBits(32);
- let time_scale = gb.readBits(32);
- fps_fixed = gb.readBool(); // fixed_frame_rate_flag
- fps_num = time_scale;
- fps_den = num_units_in_tick * 2;
- fps = fps_num / fps_den;
- }
- }
- let sarScale = 1;
- if (sar_width !== 1 || sar_height !== 1) {
- sarScale = sar_width / sar_height;
- }
- let crop_unit_x = 0,
- crop_unit_y = 0;
- if (chroma_format_idc === 0) {
- crop_unit_x = 1;
- crop_unit_y = 2 - frame_mbs_only_flag;
- } else {
- let sub_wc = chroma_format_idc === 3 ? 1 : 2;
- let sub_hc = chroma_format_idc === 1 ? 2 : 1;
- crop_unit_x = sub_wc;
- crop_unit_y = sub_hc * (2 - frame_mbs_only_flag);
- }
- let codec_width = (pic_width_in_mbs_minus1 + 1) * 16;
- let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16);
- codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
- codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
- let present_width = Math.ceil(codec_width * sarScale);
- gb.destroy();
- gb = null; // 解析出来的SPS 内容。
- return {
- profile_string: profile_string,
- // baseline, high, high10, ...
- level_string: level_string,
- // 3, 3.1, 4, 4.1, 5, 5.1, ...
- bit_depth: bit_depth,
- // 8bit, 10bit, ...
- ref_frames: ref_frames,
- chroma_format: chroma_format,
- // 4:2:0, 4:2:2, ...
- chroma_format_string: SPSParser$1.getChromaFormatString(chroma_format),
- frame_rate: {
- fixed: fps_fixed,
- fps: fps,
- fps_den: fps_den,
- fps_num: fps_num
- },
- sar_ratio: {
- width: sar_width,
- height: sar_height
- },
- codec_size: {
- width: codec_width,
- height: codec_height
- },
- present_size: {
- width: present_width,
- height: codec_height
- }
- };
- }
- static _skipScalingList(gb, count) {
- let last_scale = 8,
- next_scale = 8;
- let delta_scale = 0;
- for (let i = 0; i < count; i++) {
- if (next_scale !== 0) {
- delta_scale = gb.readSEG();
- next_scale = (last_scale + delta_scale + 256) % 256;
- }
- last_scale = next_scale === 0 ? last_scale : next_scale;
- }
- } // profile_idc = 66 → baseline profile;
- // profile_idc = 77 → main profile;
- // profile_idc = 88 → extended profile;
- // 在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High
- // 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra
- static getProfileString(profile_idc) {
- switch (profile_idc) {
- case 66:
- return 'Baseline';
- case 77:
- return 'Main';
- case 88:
- return 'Extended';
- case 100:
- return 'High';
- case 110:
- return 'High10';
- case 122:
- return 'High422';
- case 244:
- return 'High444';
- default:
- return 'Unknown';
- }
- }
- static getLevelString(level_idc) {
- return (level_idc / 10).toFixed(1);
- }
- static getChromaFormatString(chroma) {
- switch (chroma) {
- case 420:
- return '4:2:0';
- case 422:
- return '4:2:2';
- case 444:
- return '4:4:4';
- default:
- return 'Unknown';
- }
- }
- }
- function parseAVCDecoderConfigurationRecord(arrayBuffer) {
- const meta = {};
- const v = new DataView(arrayBuffer.buffer);
- let version = v.getUint8(0); // configurationVersion
- let avcProfile = v.getUint8(1); // avcProfileIndication
- v.getUint8(2); // profile_compatibil
- v.getUint8(3); // AVCLevelIndication
- if (version !== 1 || avcProfile === 0) {
- // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord');
- return;
- }
- const _naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
- if (_naluLengthSize !== 3 && _naluLengthSize !== 4) {
- // holy shit!!!
- // this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Strange NaluLengthSizeMinusOne: ${_naluLengthSize - 1}`);
- return;
- }
- let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
- if (spsCount === 0) {
- // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS');
- return;
- }
- let offset = 6;
- for (let i = 0; i < spsCount; i++) {
- let len = v.getUint16(offset, false); // sequenceParameterSetLength
- offset += 2;
- if (len === 0) {
- continue;
- } // Notice: Nalu without startcode header (00 00 00 01)
- let sps = new Uint8Array(arrayBuffer.buffer, offset, len);
- offset += len; // flv.js作者选择了自己来解析这个数据结构,也是迫不得已,因为JS环境下没有ffmpeg,解析这个结构主要是为了提取 sps和pps。虽然理论上sps允许有多个,但其实一般就一个。
- // packetTtype 为 1 表示 NALU,NALU= network abstract layer unit,这是H.264的概念,网络抽象层数据单元,其实简单理解就是一帧视频数据。
- // pps的信息没什么用,所以作者只实现了sps的分析器,说明作者下了很大功夫去学习264的标准,其中的Golomb解码还是挺复杂的,能解对不容易,我在PC和手机平台都是用ffmpeg去解析的。
- // SPS里面包括了视频分辨率,帧率,profile level等视频重要信息。
- let config = SPSParser$1.parseSPS(sps);
- if (i !== 0) {
- // ignore other sps's config
- continue;
- }
- meta.codecWidth = config.codec_size.width;
- meta.codecHeight = config.codec_size.height;
- meta.presentWidth = config.present_size.width;
- meta.presentHeight = config.present_size.height;
- meta.profile = config.profile_string;
- meta.level = config.level_string;
- meta.bitDepth = config.bit_depth;
- meta.chromaFormat = config.chroma_format;
- meta.sarRatio = config.sar_ratio;
- meta.frameRate = config.frame_rate;
- if (config.frame_rate.fixed === false || config.frame_rate.fps_num === 0 || config.frame_rate.fps_den === 0) {
- meta.frameRate = {};
- }
- let fps_den = meta.frameRate.fps_den;
- let fps_num = meta.frameRate.fps_num;
- meta.refSampleDuration = meta.timescale * (fps_den / fps_num);
- let codecArray = sps.subarray(1, 4);
- let codecString = 'avc1.';
- for (let j = 0; j < 3; j++) {
- let h = codecArray[j].toString(16);
- if (h.length < 2) {
- h = '0' + h;
- }
- codecString += h;
- } // codec
- meta.codec = codecString;
- }
- let ppsCount = v.getUint8(offset); // numOfPictureParameterSets
- if (ppsCount === 0) {
- // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS');
- return;
- }
- offset++;
- for (let i = 0; i < ppsCount; i++) {
- let len = v.getUint16(offset, false); // pictureParameterSetLength
- offset += 2;
- if (len === 0) {
- continue;
- }
- new Uint8Array(arrayBuffer.buffer, offset, len); // pps is useless for extracting video information
- offset += len;
- }
- meta.videoType = 'avc'; // meta.avcc = arrayBuffer;
- return meta;
- }
- class SPSParser {
- static _ebsp2rbsp(uint8array) {
- let src = uint8array;
- let src_length = src.byteLength;
- let dst = new Uint8Array(src_length);
- let dst_idx = 0;
- for (let i = 0; i < src_length; i++) {
- if (i >= 2) {
- // Unescape: Skip 0x03 after 00 00
- if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {
- continue;
- }
- }
- dst[dst_idx] = src[i];
- dst_idx++;
- }
- return new Uint8Array(dst.buffer, 0, dst_idx);
- }
- static parseSPS(uint8array) {
- let rbsp = SPSParser._ebsp2rbsp(uint8array);
- new ExpGolomb(rbsp);
- let profile_string = '';
- let level_string = '';
- let bit_depth = '';
- let ref_frames = '';
- let chroma_format = '';
- let fps_fixed = '';
- let fps = '';
- let fps_den = '';
- let fps_num = '';
- let sar_width = '';
- let sar_height = '';
- let codec_width = '';
- let codec_height = '';
- let present_width = '';
- return {
- profile_string: profile_string,
- // baseline, high, high10, ...
- level_string: level_string,
- // 3, 3.1, 4, 4.1, 5, 5.1, ...
- bit_depth: bit_depth,
- // 8bit, 10bit, ...
- ref_frames: ref_frames,
- chroma_format: chroma_format,
- // 4:2:0, 4:2:2, ...
- chroma_format_string: '',
- frame_rate: {
- fixed: fps_fixed,
- fps: fps,
- fps_den: fps_den,
- fps_num: fps_num
- },
- sar_ratio: {
- width: sar_width,
- height: sar_height
- },
- codec_size: {
- width: codec_width,
- height: codec_height
- },
- present_size: {
- width: present_width,
- height: codec_height
- }
- };
- }
- static _skipScalingList(gb, count) {
- let last_scale = 8,
- next_scale = 8;
- let delta_scale = 0;
- for (let i = 0; i < count; i++) {
- if (next_scale !== 0) {
- delta_scale = gb.readSEG();
- next_scale = (last_scale + delta_scale + 256) % 256;
- }
- last_scale = next_scale === 0 ? last_scale : next_scale;
- }
- }
- static getLevelString(level_idc) {
- return (level_idc / 10).toFixed(1);
- }
- }
- function parseHEVCDecoderConfigurationRecord(arrayBuffer) {
- const meta = {};
- meta.videoType = 'hevc';
- let offset = 28 - 5; //
- const vpsTag = arrayBuffer[offset];
- if (vpsTag !== H265_NAL_TYPE.vps) {
- return meta;
- }
- offset += 2;
- offset += 1;
- const vpsLength = arrayBuffer[offset + 1] | arrayBuffer[offset] << 8;
- offset += 2;
- const vpsData = arrayBuffer.slice(offset, offset + vpsLength);
- // console.log(Uint8Array.from(vpsData));
- offset += vpsLength;
- const spsTag = arrayBuffer[offset];
- if (spsTag !== H265_NAL_TYPE.sps) {
- return meta;
- }
- offset += 2;
- offset += 1;
- const spsLength = arrayBuffer[offset + 1] | arrayBuffer[offset] << 8;
- offset += 2;
- const spsData = arrayBuffer.slice(offset, offset + spsLength);
- // console.log(Uint8Array.from(spsData));
- offset += spsLength;
- const ppsTag = arrayBuffer[offset];
- if (ppsTag !== H265_NAL_TYPE.pps) {
- return meta;
- }
- offset += 2;
- offset += 1;
- const ppsLength = arrayBuffer[offset + 1] | arrayBuffer[offset] << 8;
- offset += 2;
- const ppsData = arrayBuffer.slice(offset, offset + ppsLength);
- // console.log(Uint8Array.from(ppsData));
- let sps = Uint8Array.from(spsData);
- let config = SPSParser.parseSPS(sps);
- meta.codecWidth = config.codec_size.width;
- meta.codecHeight = config.codec_size.height;
- meta.presentWidth = config.present_size.width;
- meta.presentHeight = config.present_size.height;
- meta.profile = config.profile_string;
- meta.level = config.level_string;
- meta.bitDepth = config.bit_depth;
- meta.chromaFormat = config.chroma_format;
- meta.sarRatio = config.sar_ratio;
- return meta;
- }
- class MseDecoder extends Emitter {
- constructor(player) {
- super();
- this.player = player;
- this.isAvc = true;
- this.mediaSource = new window.MediaSource();
- this.sourceBuffer = null;
- this.hasInit = false;
- this.isInitInfo = false;
- this.cacheTrack = {};
- this.timeInit = false;
- this.sequenceNumber = 0;
- this.mediaSourceOpen = false;
- this.bufferList = [];
- this.dropping = false;
- this.player.video.$videoElement.src = window.URL.createObjectURL(this.mediaSource);
- const {
- debug,
- events: {
- proxy
- }
- } = player;
- proxy(this.mediaSource, 'sourceopen', () => {
- this.mediaSourceOpen = true;
- this.player.emit(EVENTS.mseSourceOpen);
- });
- proxy(this.mediaSource, 'sourceclose', () => {
- this.player.emit(EVENTS.mseSourceClose);
- });
- player.debug.log('MediaSource', 'init');
- }
- destroy() {
- this.stop();
- this.bufferList = [];
- this.mediaSource = null;
- this.mediaSourceOpen = false;
- this.sourceBuffer = null;
- this.hasInit = false;
- this.isInitInfo = false;
- this.sequenceNumber = 0;
- this.cacheTrack = null;
- this.timeInit = false;
- this.off();
- this.player.debug.log('MediaSource', 'destroy');
- }
- get state() {
- return this.mediaSource.readyState;
- }
- get isStateOpen() {
- return this.state === MEDIA_SOURCE_STATE.open;
- }
- get isStateClosed() {
- return this.state === MEDIA_SOURCE_STATE.closed;
- }
- get isStateEnded() {
- return this.state === MEDIA_SOURCE_STATE.ended;
- }
- get duration() {
- return this.mediaSource.duration;
- }
- set duration(duration) {
- this.mediaSource.duration = duration;
- }
- decodeVideo(payload, ts, isIframe) {
- const player = this.player;
- if (!this.hasInit) {
- if (isIframe && payload[1] === 0) {
- const videoCodec = payload[0] & 0x0F;
- player.video.updateVideoInfo({
- encTypeCode: videoCodec
- }); // 如果解码出来的是
- if (videoCodec === VIDEO_ENC_CODE.h265) {
- this.emit(EVENTS_ERROR.mediaSourceH265NotSupport);
- return;
- }
- if (!player._times.decodeStart) {
- player._times.decodeStart = now();
- }
- this._decodeConfigurationRecord(payload, ts, isIframe, videoCodec);
- this.hasInit = true;
- }
- } else {
- this._decodeVideo(payload, ts, isIframe);
- }
- }
- _doDecode() {
- const bufferItem = this.bufferList.shift();
- if (bufferItem) {
- this._decodeVideo(bufferItem.payload, bufferItem.ts, bufferItem.isIframe);
- }
- }
- _decodeConfigurationRecord(payload, ts, isIframe, videoCodec) {
- let data = payload.slice(5);
- let config = {};
- if (videoCodec === VIDEO_ENC_CODE.h264) {
- config = parseAVCDecoderConfigurationRecord(data);
- } else if (videoCodec === VIDEO_ENC_CODE.h265) {
- config = parseHEVCDecoderConfigurationRecord(data);
- }
- const metaData = {
- id: 1,
- // video tag data
- type: 'video',
- timescale: 1000,
- duration: 0,
- avcc: data,
- codecWidth: config.codecWidth,
- codecHeight: config.codecHeight,
- videoType: config.videoType
- }; // ftyp
- const metaBox = MP4$1.generateInitSegment(metaData);
- this.isAvc = true;
- this.appendBuffer(metaBox.buffer);
- this.sequenceNumber = 0;
- this.cacheTrack = null;
- this.timeInit = false;
- } //
- _decodeVideo(payload, ts, isIframe) {
- const player = this.player;
- let arrayBuffer = payload.slice(5);
- let bytes = arrayBuffer.byteLength;
- let cts = 0;
- let dts = ts; // player.debug.log('MediaSource', '_decodeVideo', ts);
- const $video = player.video.$videoElement;
- const videoBufferDelay = player._opt.videoBufferDelay;
- if ($video.buffered.length > 1) {
- this.removeBuffer($video.buffered.start(0), $video.buffered.end(0));
- this.timeInit = false;
- }
- if (this.dropping && dts - this.cacheTrack.dts > videoBufferDelay) {
- this.dropping = false;
- this.cacheTrack = {};
- } else if (this.cacheTrack && dts > this.cacheTrack.dts) {
- // 需要额外加8个size
- let mdatBytes = 8 + this.cacheTrack.size;
- let mdatbox = new Uint8Array(mdatBytes);
- mdatbox[0] = mdatBytes >>> 24 & 255;
- mdatbox[1] = mdatBytes >>> 16 & 255;
- mdatbox[2] = mdatBytes >>> 8 & 255;
- mdatbox[3] = mdatBytes & 255;
- mdatbox.set(MP4$1.types.mdat, 4);
- mdatbox.set(this.cacheTrack.data, 8);
- this.cacheTrack.duration = dts - this.cacheTrack.dts; // moof
- let moofbox = MP4$1.moof(this.cacheTrack, this.cacheTrack.dts);
- let result = new Uint8Array(moofbox.byteLength + mdatbox.byteLength);
- result.set(moofbox, 0);
- result.set(mdatbox, moofbox.byteLength); // appendBuffer
- this.appendBuffer(result.buffer);
- player.handleRender();
- player.updateStats({
- fps: true,
- ts: ts,
- buf: player.demux.delay
- });
- if (!player._times.videoStart) {
- player._times.videoStart = now();
- player.handlePlayToRenderTimes();
- }
- } else {
- player.debug.log('MediaSource', 'timeInit set false , cacheTrack = {}');
- this.timeInit = false;
- this.cacheTrack = {};
- }
- this.cacheTrack.id = 1;
- this.cacheTrack.sequenceNumber = ++this.sequenceNumber;
- this.cacheTrack.size = bytes;
- this.cacheTrack.dts = dts;
- this.cacheTrack.cts = cts;
- this.cacheTrack.isKeyframe = isIframe;
- this.cacheTrack.data = arrayBuffer; //
- this.cacheTrack.flags = {
- isLeading: 0,
- dependsOn: isIframe ? 2 : 1,
- isDependedOn: isIframe ? 1 : 0,
- hasRedundancy: 0,
- isNonSync: isIframe ? 0 : 1
- }; //
- if (!this.timeInit && $video.buffered.length === 1) {
- player.debug.log('MediaSource', 'timeInit set true');
- this.timeInit = true;
- $video.currentTime = $video.buffered.end(0);
- }
- if (!this.isInitInfo && $video.videoWidth > 0 && $video.videoHeight > 0) {
- player.debug.log('MediaSource', `updateVideoInfo: ${$video.videoWidth},${$video.videoHeight}`);
- player.video.updateVideoInfo({
- width: $video.videoWidth,
- height: $video.videoHeight
- });
- player.video.initCanvasViewSize();
- this.isInitInfo = true;
- }
- }
- appendBuffer(buffer) {
- const {
- debug,
- events: {
- proxy
- }
- } = this.player;
- if (this.sourceBuffer === null) {
- this.sourceBuffer = this.mediaSource.addSourceBuffer(MP4_CODECS.avc);
- proxy(this.sourceBuffer, 'error', error => {
- this.player.emit(EVENTS.mseSourceBufferError, error); // this.dropSourceBuffer(false)
- });
- }
- if (this.sourceBuffer.updating === false && this.isStateOpen) {
- if (this.sourceBuffer.appendBuffer) {
- this.sourceBuffer.appendBuffer(buffer);
- } else {
- debug.log('MediaSource', 'this.sourceBuffer.appendBuffer function is undefined');
- }
- return;
- }
- if (this.isStateClosed) {
- this.player.emit(EVENTS.mseSourceBufferError, 'mediaSource is not attached to video or mediaSource is closed');
- } else if (this.isStateEnded) {
- this.player.emit(EVENTS.mseSourceBufferError, 'mediaSource is closed');
- } else {
- if (this.sourceBuffer.updating === true) {
- this.player.emit(EVENTS.mseSourceBufferBusy); // this.dropSourceBuffer(false);
- }
- }
- }
- stop() {
- if (this.isStateOpen) {
- if (this.sourceBuffer) {
- this.sourceBuffer.abort();
- }
- }
- this.endOfStream();
- }
- dropSourceBuffer(flag) {
- const video = this.player.video;
- const $video = video.$videoElement;
- this.dropping = flag;
- if ($video.buffered.length > 0) {
- if ($video.buffered.end(0) - $video.currentTime > 1) {
- $video.currentTime = $video.buffered.end(0);
- }
- }
- }
- removeBuffer(start, end) {
- if (this.isStateOpen && this.sourceBuffer.updating === false) {
- try {
- this.sourceBuffer.remove(start, end);
- } catch (e) {
- // console.error(e);
- }
- }
- }
- endOfStream() {
- if (this.isStateOpen) {
- this.mediaSource.endOfStream();
- }
- }
- }
- // tks: https://github.com/richtr/NoSleep.js
- const WEBM = "data:video/webm;base64,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";
- const MP4 = "data:video/mp4;base64,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"; // Detect iOS browsers < version 10
- const oldIOS = () => typeof navigator !== "undefined" && parseFloat(("" + (/CPU.*OS ([0-9_]{3,4})[0-9_]{0,1}|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ""])[1]).replace("undefined", "3_2").replace("_", ".").replace("_", "")) < 10 && !window.MSStream; // Detect native Wake Lock API support
- const nativeWakeLock = () => "wakeLock" in navigator;
- class NoSleep {
- constructor(player) {
- this.player = player;
- this.enabled = false;
- if (nativeWakeLock()) {
- this._wakeLock = null;
- const handleVisibilityChange = () => {
- if (this._wakeLock !== null && document.visibilityState === "visible") {
- this.enable();
- }
- };
- document.addEventListener("visibilitychange", handleVisibilityChange);
- document.addEventListener("fullscreenchange", handleVisibilityChange);
- } else if (oldIOS()) {
- this.noSleepTimer = null;
- } else {
- // Set up no sleep video element
- this.noSleepVideo = document.createElement("video");
- this.noSleepVideo.setAttribute("title", "No Sleep");
- this.noSleepVideo.setAttribute("playsinline", "");
- this._addSourceToVideo(this.noSleepVideo, "webm", WEBM);
- this._addSourceToVideo(this.noSleepVideo, "mp4", MP4);
- this.noSleepVideo.addEventListener("loadedmetadata", () => {
- if (this.noSleepVideo.duration <= 1) {
- // webm source
- this.noSleepVideo.setAttribute("loop", "");
- } else {
- // mp4 source
- this.noSleepVideo.addEventListener("timeupdate", () => {
- if (this.noSleepVideo.currentTime > 0.5) {
- this.noSleepVideo.currentTime = Math.random();
- }
- });
- }
- });
- }
- }
- _addSourceToVideo(element, type, dataURI) {
- var source = document.createElement("source");
- source.src = dataURI;
- source.type = `video/${type}`;
- element.appendChild(source);
- }
- get isEnabled() {
- return this.enabled;
- }
- enable() {
- const debug = this.player.debug;
- if (nativeWakeLock()) {
- return navigator.wakeLock.request("screen").then(wakeLock => {
- this._wakeLock = wakeLock;
- this.enabled = true;
- debug.log('wakeLock', 'Wake Lock active.');
- this._wakeLock.addEventListener("release", () => {
- // ToDo: Potentially emit an event for the page to observe since
- // Wake Lock releases happen when page visibility changes.
- // (https://web.dev/wakelock/#wake-lock-lifecycle)
- debug.log('wakeLock', 'Wake Lock released.');
- });
- }).catch(err => {
- this.enabled = false;
- debug.error('wakeLock', `${err.name}, ${err.message}`);
- throw err;
- });
- } else if (oldIOS()) {
- this.disable();
- this.noSleepTimer = window.setInterval(() => {
- if (!document.hidden) {
- window.location.href = window.location.href.split("#")[0];
- window.setTimeout(window.stop, 0);
- }
- }, 15000);
- this.enabled = true;
- return Promise.resolve();
- } else {
- let playPromise = this.noSleepVideo.play();
- return playPromise.then(res => {
- this.enabled = true;
- return res;
- }).catch(err => {
- this.enabled = false;
- throw err;
- });
- }
- }
- disable() {
- const debug = this.player.debug;
- if (nativeWakeLock()) {
- if (this._wakeLock) {
- this._wakeLock.release();
- }
- this._wakeLock = null;
- } else if (oldIOS()) {
- if (this.noSleepTimer) {
- debug.warn('wakeLock', 'NoSleep now disabled for older iOS devices.');
- window.clearInterval(this.noSleepTimer);
- this.noSleepTimer = null;
- }
- } else {
- this.noSleepVideo.pause();
- }
- this.enabled = false;
- }
- }
- class Player extends Emitter {
- constructor(container, options) {
- super();
- this.$container = container;
- this._opt = Object.assign({}, DEFAULT_PLAYER_OPTIONS, options);
- this.debug = new Debug(this);
- if (this._opt.useWCS) {
- this._opt.useWCS = supportWCS();
- }
- if (this._opt.useMSE) {
- this._opt.useMSE = supportMSE();
- }
- if (this._opt.wcsUseVideoRender) {
- this._opt.wcsUseVideoRender = supportMediaStreamTrack();
- } // 如果使用mse则强制不允许 webcodecs
- if (this._opt.useMSE) {
- if (this._opt.useWCS) {
- this.debug.log('Player', 'useWCS set true->false');
- }
- if (!this._opt.forceNoOffscreen) {
- this.debug.log('Player', 'forceNoOffscreen set false->true');
- }
- this._opt.useWCS = false;
- this._opt.forceNoOffscreen = true;
- } else if (this._opt.useWCS) ;
- if (!this._opt.forceNoOffscreen) {
- if (!supportOffscreenV2()) {
- this._opt.forceNoOffscreen = true;
- this._opt.useOffscreen = false;
- } else {
- this._opt.useOffscreen = true;
- }
- }
- if (!this._opt.hasAudio) {
- this._opt.operateBtns.audio = false;
- }
- this._opt.hasControl = this._hasControl(); //
- this._loading = false;
- this._playing = false;
- this._hasLoaded = false; //
- this._checkHeartTimeout = null;
- this._checkLoadingTimeout = null; //
- this._startBpsTime = null;
- this._isPlayingBeforePageHidden = false;
- this._stats = {
- buf: 0,
- // 当前缓冲区时长,单位毫秒,
- fps: 0,
- // 当前视频帧率
- abps: 0,
- // 当前音频码率,单位bit
- vbps: 0,
- // 当前视频码率,单位bit
- ts: 0 // 当前视频帧pts,单位毫秒
- }; // 各个步骤的时间统计
- this._times = initPlayTimes(); //
- this._videoTimestamp = 0;
- this._audioTimestamp = 0;
- property$1(this);
- this.events = new Events(this);
- this.video = new Video(this);
- if (this._opt.hasAudio) {
- this.audio = new Audio(this);
- }
- this.recorder = new Recorder(this);
- if (!this._onlyMseOrWcsVideo()) {
- this.decoderWorker = new DecoderWorker(this);
- } else {
- this.loaded = true;
- }
- this.stream = null;
- this.demux = null;
- if (this._opt.useWCS) {
- this.webcodecsDecoder = new WebcodecsDecoder(this);
- }
- if (this._opt.useMSE) {
- this.mseDecoder = new MseDecoder(this);
- } //
- this.control = new Control(this);
- this.keepScreenOn = new NoSleep(this);
- events$1(this);
- observer(this);
- if (this._opt.useWCS) {
- this.debug.log('Player', 'use WCS');
- }
- if (this._opt.useMSE) {
- this.debug.log('Player', 'use MSE');
- }
- if (this._opt.useOffscreen) {
- this.debug.log('Player', 'use offscreen');
- }
- this.debug.log('Player options', this._opt);
- }
- destroy() {
- this._loading = false;
- this._playing = false;
- this._hasLoaded = false;
- this._times = initPlayTimes();
- if (this.decoderWorker) {
- this.decoderWorker.destroy();
- this.decoderWorker = null;
- }
- if (this.video) {
- this.video.destroy();
- this.video = null;
- }
- if (this.audio) {
- this.audio.destroy();
- this.audio = null;
- }
- if (this.stream) {
- this.stream.destroy();
- this.stream = null;
- }
- if (this.recorder) {
- this.recorder.destroy();
- this.recorder = null;
- }
- if (this.control) {
- this.control.destroy();
- this.control = null;
- }
- if (this.webcodecsDecoder) {
- this.webcodecsDecoder.destroy();
- this.webcodecsDecoder = null;
- }
- if (this.mseDecoder) {
- this.mseDecoder.destroy();
- this.mseDecoder = null;
- }
- if (this.demux) {
- this.demux.destroy();
- this.demux = null;
- }
- if (this.events) {
- this.events.destroy();
- this.events = null;
- }
- this.clearCheckHeartTimeout();
- this.clearCheckLoadingTimeout(); //
- this.releaseWakeLock();
- this.keepScreenOn = null; // reset stats
- this.resetStats();
- this._audioTimestamp = 0;
- this._videoTimestamp = 0; // 其他没法解耦的,通过 destroy 方式
- this.emit('destroy'); // 接触所有绑定事件
- this.off();
- this.debug.log('play', 'destroy end');
- }
- set fullscreen(value) {
- if (isMobile()) {
- this.emit(EVENTS.webFullscreen, value);
- setTimeout(() => {
- this.updateOption({
- rotate: value ? 270 : 0
- });
- this.resize();
- }, 10);
- } else {
- this.emit(EVENTS.fullscreen, value);
- }
- }
- get fullscreen() {
- return isFullScreen() || this.webFullscreen;
- }
- set webFullscreen(value) {
- this.emit(EVENTS.webFullscreen, value);
- }
- get webFullscreen() {
- return this.$container.classList.contains('jessibuca-fullscreen-web');
- }
- set loaded(value) {
- this._hasLoaded = value;
- }
- get loaded() {
- return this._hasLoaded;
- } //
- set playing(value) {
- if (value) {
- // 将loading 设置为 false
- this.loading = false;
- }
- if (this.playing !== value) {
- this._playing = value;
- this.emit(EVENTS.playing, value);
- this.emit(EVENTS.volumechange, this.volume);
- if (value) {
- this.emit(EVENTS.play);
- } else {
- this.emit(EVENTS.pause);
- }
- }
- }
- get playing() {
- return this._playing;
- }
- get volume() {
- return this.audio && this.audio.volume || 0;
- }
- set volume(value) {
- this.audio && this.audio.setVolume(value);
- }
- set loading(value) {
- if (this.loading !== value) {
- this._loading = value;
- this.emit(EVENTS.loading, this._loading);
- }
- }
- get loading() {
- return this._loading;
- }
- set recording(value) {
- if (value) {
- if (this.playing) {
- this.recorder && this.recorder.startRecord();
- }
- } else {
- this.recorder && this.recorder.stopRecordAndSave();
- }
- }
- get recording() {
- return this.recorder ? this.recorder.recording : false;
- }
- set audioTimestamp(value) {
- if (value === null) {
- return;
- }
- this._audioTimestamp = value;
- } //
- get audioTimestamp() {
- return this._audioTimestamp;
- } //
- set videoTimestamp(value) {
- if (value === null) {
- return;
- }
- this._videoTimestamp = value; // just for wasm
- if (!this._opt.useWCS && !this._opt.useMSE) {
- if (this.audioTimestamp && this.videoTimestamp) {
- this.audio && this.audio.emit(EVENTS.videoSyncAudio, {
- audioTimestamp: this.audioTimestamp,
- videoTimestamp: this.videoTimestamp,
- diff: this.audioTimestamp - this.videoTimestamp
- });
- }
- }
- } //
- get videoTimestamp() {
- return this._videoTimestamp;
- }
- /**
- *
- * @param options
- */
- updateOption(options) {
- this._opt = Object.assign({}, this._opt, options);
- }
- /**
- *
- * @returns {Promise<unknown>}
- */
- init() {
- return new Promise((resolve, reject) => {
- if (!this.stream) {
- this.stream = new Stream(this);
- }
- if (!this.demux) {
- this.demux = new Demux(this);
- }
- if (this._opt.useWCS) {
- if (!this.webcodecsDecoder) {
- this.webcodecsDecoder = new WebcodecsDecoder(this);
- }
- }
- if (this._opt.useMSE) {
- if (!this.mseDecoder) {
- this.mseDecoder = new MseDecoder(this);
- }
- }
- if (!this.decoderWorker && !this._onlyMseOrWcsVideo()) {
- this.decoderWorker = new DecoderWorker(this);
- this.once(EVENTS.decoderWorkerInit, () => {
- resolve();
- });
- } else {
- resolve();
- }
- });
- }
- /**
- *
- * @param url
- * @returns {Promise<unknown>}
- */
- play(url) {
- return new Promise((resolve, reject) => {
- if (!url && !this._opt.url) {
- return reject();
- }
- this.loading = true;
- this.playing = false;
- this._times.playInitStart = now();
- if (!url) {
- url = this._opt.url;
- }
- this._opt.url = url;
- this.clearCheckHeartTimeout();
- this.init().then(() => {
- this._times.playStart = now(); //
- if (this._opt.isNotMute) {
- this.mute(false);
- }
- if (this.webcodecsDecoder) {
- this.webcodecsDecoder.once(EVENTS_ERROR.webcodecsH265NotSupport, () => {
- this.emit(EVENTS_ERROR.webcodecsH265NotSupport);
- if (!this._opt.autoWasm) {
- this.emit(EVENTS.error, EVENTS_ERROR.webcodecsH265NotSupport);
- }
- });
- }
- if (this.mseDecoder) {
- this.mseDecoder.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => {
- this.emit(EVENTS_ERROR.mediaSourceH265NotSupport);
- if (!this._opt.autoWasm) {
- this.emit(EVENTS.error, EVENTS_ERROR.mediaSourceH265NotSupport);
- }
- });
- }
- this.enableWakeLock();
- this.stream.fetchStream(url); //
- this.checkLoadingTimeout(); // fetch error
- this.stream.once(EVENTS_ERROR.fetchError, error => {
- reject(error);
- }); // ws
- this.stream.once(EVENTS_ERROR.websocketError, error => {
- reject(error);
- }); // success
- this.stream.once(EVENTS.streamSuccess, () => {
- resolve();
- this._times.streamResponse = now(); //
- this.video.play();
- });
- }).catch(e => {
- reject(e);
- });
- });
- }
- /**
- *
- */
- close() {
- return new Promise((resolve, reject) => {
- this._close().then(() => {
- this.video && this.video.clearView();
- resolve();
- });
- });
- }
- _close() {
- return new Promise((resolve, reject) => {
- //
- if (this.stream) {
- this.stream.destroy();
- this.stream = null;
- }
- if (this.demux) {
- this.demux.destroy();
- this.demux = null;
- } //
- if (this.decoderWorker) {
- this.decoderWorker.destroy();
- this.decoderWorker = null;
- }
- if (this.webcodecsDecoder) {
- this.webcodecsDecoder.destroy();
- this.webcodecsDecoder = null;
- }
- if (this.mseDecoder) {
- this.mseDecoder.destroy();
- this.mseDecoder = null;
- }
- this.clearCheckHeartTimeout();
- this.clearCheckLoadingTimeout();
- this.playing = false;
- this.loading = false;
- this.recording = false;
- if (this.audio) {
- this.audio.resetInit();
- this.audio.pause();
- }
- if (this.video) {
- this.video.resetInit();
- this.video.pause();
- } // release lock
- this.releaseWakeLock(); // reset stats
- this.resetStats(); //
- this._audioTimestamp = 0;
- this._videoTimestamp = 0; //
- this._times = initPlayTimes(); //
- setTimeout(() => {
- resolve();
- }, 0);
- });
- }
- /**
- *
- * @param flag {boolean} 是否清除画面
- * @returns {Promise<unknown>}
- */
- pause(flag) {
- if (flag) {
- return this.close();
- } else {
- return this._close();
- }
- }
- /**
- *
- * @param flag
- */
- mute(flag) {
- this.audio && this.audio.mute(flag);
- }
- /**
- *
- */
- resize() {
- this.video.resize();
- }
- /**
- *
- * @param fileName
- * @param fileType
- */
- startRecord(fileName, fileType) {
- if (this.recording) {
- return;
- }
- this.recorder.setFileName(fileName, fileType);
- this.recording = true;
- }
- /**
- *
- */
- stopRecordAndSave() {
- if (this.recording) {
- this.recording = false;
- }
- }
- _hasControl() {
- let result = false;
- let hasBtnShow = false;
- Object.keys(this._opt.operateBtns).forEach(key => {
- if (this._opt.operateBtns[key]) {
- hasBtnShow = true;
- }
- });
- if (this._opt.showBandwidth || this._opt.text || hasBtnShow) {
- result = true;
- }
- return result;
- }
- _onlyMseOrWcsVideo() {
- return this._opt.hasAudio === false && (this._opt.useMSE || this._opt.useWCS && !this._opt.useOffscreen);
- }
- checkHeart() {
- this.clearCheckHeartTimeout();
- this.checkHeartTimeout();
- } // 心跳检查,如果渲染间隔暂停了多少时间之后,就会抛出异常
- checkHeartTimeout() {
- this._checkHeartTimeout = setTimeout(() => {
- this.pause(false).then(() => {
- this.emit(EVENTS.timeout, EVENTS.delayTimeout);
- this.emit(EVENTS.delayTimeout);
- });
- }, this._opt.heartTimeout * 1000);
- } //
- clearCheckHeartTimeout() {
- if (this._checkHeartTimeout) {
- clearTimeout(this._checkHeartTimeout);
- this._checkHeartTimeout = null;
- }
- } // loading 等待时间
- checkLoadingTimeout() {
- this._checkLoadingTimeout = setTimeout(() => {
- this.pause(false).then(() => {
- this.emit(EVENTS.timeout, EVENTS.loadingTimeout);
- this.emit(EVENTS.loadingTimeout);
- });
- }, this._opt.loadingTimeout * 1000);
- }
- clearCheckLoadingTimeout() {
- if (this._checkLoadingTimeout) {
- clearTimeout(this._checkLoadingTimeout);
- this._checkLoadingTimeout = null;
- }
- }
- handleRender() {
- if (this.loading) {
- this.emit(EVENTS.start);
- this.loading = false;
- this.clearCheckLoadingTimeout();
- }
- if (!this.playing) {
- this.playing = true;
- }
- this.checkHeart();
- } //
- updateStats(options) {
- options = options || {};
- if (!this._startBpsTime) {
- this._startBpsTime = now();
- }
- if (isNotEmpty(options.ts)) {
- this._stats.ts = options.ts;
- }
- if (isNotEmpty(options.buf)) {
- this._stats.buf = options.buf;
- }
- if (options.fps) {
- this._stats.fps += 1;
- }
- if (options.abps) {
- this._stats.abps += options.abps;
- }
- if (options.vbps) {
- this._stats.vbps += options.vbps;
- }
- const _nowTime = now();
- const timestamp = _nowTime - this._startBpsTime;
- if (timestamp < 1 * 1000) {
- return;
- }
- this.emit(EVENTS.stats, this._stats);
- this.emit(EVENTS.performance, fpsStatus(this._stats.fps));
- this._stats.fps = 0;
- this._stats.abps = 0;
- this._stats.vbps = 0;
- this._startBpsTime = _nowTime;
- }
- resetStats() {
- this._startBpsTime = null;
- this._stats = {
- buf: 0,
- //ms
- fps: 0,
- abps: 0,
- vbps: 0,
- ts: 0
- };
- }
- enableWakeLock() {
- if (this._opt.keepScreenOn) {
- this.keepScreenOn.enable();
- }
- }
- releaseWakeLock() {
- if (this._opt.keepScreenOn) {
- this.keepScreenOn.disable();
- }
- }
- handlePlayToRenderTimes() {
- const _times = this._times;
- _times.playTimestamp = _times.playStart - _times.playInitStart;
- _times.streamTimestamp = _times.streamStart - _times.playStart;
- _times.streamResponseTimestamp = _times.streamResponse - _times.streamStart;
- _times.demuxTimestamp = _times.demuxStart - _times.streamResponse;
- _times.decodeTimestamp = _times.decodeStart - _times.demuxStart;
- _times.videoTimestamp = _times.videoStart - _times.decodeStart;
- _times.allTimestamp = _times.videoStart - _times.playInitStart;
- this.emit(EVENTS.playToRenderTimes, _times);
- }
- }
- class Jessibuca extends Emitter {
- constructor(options) {
- super();
- let _opt = options;
- let $container = options.container;
- if (typeof options.container === 'string') {
- $container = document.querySelector(options.container);
- }
- if (!$container) {
- throw new Error('Jessibuca need container option');
- }
- $container.classList.add('jessibuca-container');
- delete _opt.container; // s -> ms
- if (isNotEmpty(_opt.videoBuffer)) {
- _opt.videoBuffer = Number(_opt.videoBuffer) * 1000;
- } // setting
- if (isNotEmpty(_opt.timeout)) {
- if (isEmpty(_opt.loadingTimeout)) {
- _opt.loadingTimeout = _opt.timeout;
- }
- if (isEmpty(_opt.heartTimeout)) {
- _opt.heartTimeout = _opt.timeout;
- }
- }
- this._opt = _opt;
- this.$container = $container;
- this._loadingTimeoutReplayTimes = 0;
- this._heartTimeoutReplayTimes = 0;
- this.events = new Events(this);
- this._initPlayer($container, _opt);
- }
- /**
- *
- */
- destroy() {
- if (this.events) {
- this.events.destroy();
- this.events = null;
- }
- if (this.player) {
- this.player.destroy();
- this.player = null;
- }
- this.$container = null;
- this._opt = null;
- this._loadingTimeoutReplayTimes = 0;
- this._heartTimeoutReplayTimes = 0;
- this.off();
- }
- _initPlayer($container, options) {
- this.player = new Player($container, options);
- this._bindEvents();
- }
- _resetPlayer() {
- let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
- this.player.destroy();
- this.player = null;
- const _options = Object.assign(this._opt, options);
- this._initPlayer(this.$container, _options);
- }
- _bindEvents() {
- // 对外的事件
- Object.keys(JESSIBUCA_EVENTS).forEach(key => {
- this.player.on(JESSIBUCA_EVENTS[key], value => {
- this.emit(key, value);
- });
- });
- }
- /**
- * 是否开启控制台调试打印
- * @param value {Boolean}
- */
- setDebug(value) {
- this.player.updateOption({
- isDebug: !!value
- });
- }
- /**
- *
- */
- mute() {
- this.player.mute(true);
- }
- /**
- *
- */
- cancelMute() {
- this.player.mute(false);
- }
- /**
- *
- * @param value {number}
- */
- setVolume(value) {
- this.player.volume = value;
- }
- /**
- *
- */
- audioResume() {
- this.player.audio && this.player.audio.audioEnabled(true);
- }
- /**
- * 设置超时时长, 单位秒 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件
- * @param value {number}
- */
- setTimeout(time) {
- time = Number(time);
- this.player.updateOption({
- timeout: time,
- loadingTimeout: time,
- heartTimeout: time
- });
- }
- /**
- *
- * @param type {number}: 0,1,2
- */
- setScaleMode(type) {
- type = Number(type);
- let options = {
- isFullResize: false,
- isResize: false
- };
- switch (type) {
- case SCALE_MODE_TYPE.full:
- options.isFullResize = false;
- options.isResize = false;
- break;
- case SCALE_MODE_TYPE.auto:
- options.isFullResize = false;
- options.isResize = true;
- break;
- case SCALE_MODE_TYPE.fullAuto:
- options.isFullResize = true;
- options.isResize = true;
- break;
- }
- this.player.updateOption(options);
- this.resize();
- }
- /**
- *
- * @returns {Promise<commander.ParseOptionsResult.unknown>}
- */
- pause() {
- return this.player.pause();
- }
- /**
- *
- */
- close() {
- // clear url
- this._opt.url = '';
- return this.player.close();
- }
- /**
- *
- */
- clearView() {
- this.player.video.clearView();
- }
- /**
- *
- * @param url {string}
- * @returns {Promise<unknown>}
- */
- play(url) {
- return new Promise((resolve, reject) => {
- if (!url && !this._opt.url) {
- this.emit(EVENTS.error, EVENTS_ERROR.playError);
- reject();
- return;
- }
- if (url) {
- // url 相等的时候。
- if (this._opt.url) {
- // 存在相同的 url
- if (url === this._opt.url) {
- // 正在播放
- if (this.player.playing) {
- resolve();
- } else {
- // pause -> play
- this.clearView();
- this.player.play(this._opt.url).then(() => {
- resolve();
- }).catch(() => {
- this.player.pause().then(() => {
- reject();
- });
- });
- }
- } else {
- // url 发生改变了
- this.player.pause().then(() => {
- // 清除 画面
- this.clearView();
- return this._play(url);
- }).catch(() => {
- reject();
- });
- }
- } else {
- return this._play(url);
- }
- } else {
- // url 不存在的时候
- // 就是从 play -> pause -> play
- this.player.play(this._opt.url).then(() => {
- resolve();
- }).catch(() => {
- this.player.pause().then(() => {
- reject();
- });
- });
- }
- });
- }
- /**
- *
- * @param url {string}
- * @returns {Promise<unknown>}
- * @private
- */
- _play(url) {
- return new Promise((resolve, reject) => {
- this._opt.url = url; // 新的url
- const isHttp = url.indexOf("http") === 0; //
- const protocol = isHttp ? PLAYER_PLAY_PROTOCOL.fetch : PLAYER_PLAY_PROTOCOL.websocket; //
- const demuxType = isHttp || url.indexOf(".flv") !== -1 || this._opt.isFlv ? DEMUX_TYPE.flv : DEMUX_TYPE.m7s;
- this.player.updateOption({
- protocol,
- demuxType
- });
- this.player.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => {
- this.close().then(() => {
- if (this.player._opt.autoWasm) {
- this.player.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play');
- this._resetPlayer({
- useMSE: false
- });
- this.play(url).then(() => {
- // resolve();
- this.player.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play success');
- }).catch(() => {
- // reject();
- this.player.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play error');
- });
- }
- });
- });
- this.player.once(EVENTS_ERROR.webcodecsH265NotSupport, () => {
- this.close().then(() => {
- if (this.player._opt.autoWasm) {
- this.player.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play');
- this._resetPlayer({
- useWCS: false
- });
- this.play(url).then(() => {
- // resolve();
- this.player.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play success');
- }).catch(() => {
- // reject();
- this.player.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play error');
- });
- }
- });
- }); // 解码报错。
- this.player.once(EVENTS_ERROR.wasmDecodeError, () => {
- if (this.player._opt.wasmDecodeErrorReplay) {
- this.close().then(() => {
- this.player.debug.log('Jessibuca', 'wasm decode error and reset player and play');
- this._resetPlayer({
- useWCS: false
- });
- this.play(url).then(() => {
- // resolve();
- this.player.debug.log('Jessibuca', 'wasm decode error and reset player and play success');
- }).catch(() => {
- // reject();
- this.player.debug.log('Jessibuca', 'wasm decode error and reset player and play error');
- });
- });
- }
- }); // 监听 delay timeout
- this.player.once(EVENTS.delayTimeout, () => {
- if (this.player._opt.heartTimeoutReplay && this._heartTimeoutReplayTimes < this.player._opt.heartTimeoutReplayTimes) {
- this._heartTimeoutReplayTimes += 1;
- this.play(url).then(() => {
- // resolve();
- this._heartTimeoutReplayTimes = 0;
- }).catch(() => {// reject();
- });
- }
- }); // 监听 loading timeout
- this.player.once(EVENTS.loadingTimeout, () => {
- if (this.player._opt.loadingTimeoutReplay && this._loadingTimeoutReplayTimes < this.player._opt.loadingTimeoutReplayTimes) {
- this._loadingTimeoutReplayTimes += 1;
- this.play(url).then(() => {
- // resolve();
- this._loadingTimeoutReplayTimes = 0;
- }).catch(() => {// reject();
- });
- }
- });
- if (this.hasLoaded()) {
- this.player.play(url).then(() => {
- resolve();
- }).catch(() => {
- this.player.pause().then(() => {
- reject();
- });
- });
- } else {
- this.player.once(EVENTS.decoderWorkerInit, () => {
- this.player.play(url).then(() => {
- resolve();
- }).catch(() => {
- this.player.pause().then(() => {
- reject();
- });
- });
- });
- }
- });
- }
- /**
- *
- */
- resize() {
- this.player.resize();
- }
- /**
- *
- * @param time {number} s
- */
- setBufferTime(time) {
- time = Number(time); // s -> ms
- this.player.updateOption({
- videoBuffer: time * 1000
- }); // update worker config
- this.player.decoderWorker && this.player.decoderWorker.updateWorkConfig({
- key: 'videoBuffer',
- value: time * 1000
- });
- }
- /**
- *
- * @param deg {number}
- */
- setRotate(deg) {
- deg = parseInt(deg, 10);
- const list = [0, 90, 270];
- if (this._opt.rotate === deg || list.indexOf(deg) === -1) {
- return;
- }
- this.player.updateOption({
- rotate: deg
- });
- this.resize();
- }
- /**
- *
- * @returns {boolean}
- */
- hasLoaded() {
- return this.player.loaded;
- }
- /**
- *
- */
- setKeepScreenOn() {
- this.player.updateOption({
- keepScreenOn: true
- });
- }
- /**
- *
- * @param flag {Boolean}
- */
- setFullscreen(flag) {
- const fullscreen = !!flag;
- if (this.player.fullscreen !== fullscreen) {
- this.player.fullscreen = fullscreen;
- }
- }
- /**
- *
- * @param filename {string}
- * @param format {string}
- * @param quality {number}
- * @param type {string} download,base64,blob
- */
- screenshot(filename, format, quality, type) {
- return this.player.video.screenshot(filename, format, quality, type);
- }
- /**
- *
- * @param fileName {string}
- * @param fileType {string}
- * @returns {Promise<unknown>}
- */
- startRecord(fileName, fileType) {
- return new Promise((resolve, reject) => {
- if (this.player.playing) {
- this.player.startRecord(fileName, fileType);
- resolve();
- } else {
- reject();
- }
- });
- }
- stopRecordAndSave() {
- if (this.player.recording) {
- this.player.stopRecordAndSave();
- }
- }
- /**
- *
- * @returns {Boolean}
- */
- isPlaying() {
- return this.player ? this.player.playing : false;
- }
- /**
- * 是否静音状态
- * @returns {Boolean}
- */
- isMute() {
- return this.player.audio ? this.player.audio.isMute : true;
- }
- /**
- * 是否在录制视频
- * @returns {*}
- */
- isRecording() {
- return this.player.recorder.recording;
- }
- }
- _defineProperty(Jessibuca, "ERROR", EVENTS_ERROR);
- _defineProperty(Jessibuca, "TIMEOUT", {
- loadingTimeout: EVENTS.loadingTimeout,
- delayTimeout: EVENTS.delayTimeout
- });
- window.Jessibuca = Jessibuca;
- return Jessibuca;
- }));
- //# sourceMappingURL=jessibuca.js.map
|