plyr.js 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090
  1. // ==========================================================================
  2. // Plyr
  3. // plyr.js v1.3.6
  4. // https://github.com/selz/plyr
  5. // License: The MIT License (MIT)
  6. // ==========================================================================
  7. // Credits: http://paypal.github.io/accessible-html5-video-player/
  8. // ==========================================================================
  9. (function (api) {
  10. 'use strict';
  11. /*global YT*/
  12. // Globals
  13. var fullscreen, config, callbacks = {
  14. youtube: []
  15. };
  16. // Default config
  17. var defaults = {
  18. enabled: true,
  19. debug: false,
  20. seekTime: 10,
  21. volume: 5,
  22. click: true,
  23. tooltips: true,
  24. displayDuration: true,
  25. iconPrefix: 'icon',
  26. selectors: {
  27. container: '.player',
  28. controls: '.player-controls',
  29. labels: '[data-player] .sr-only, label .sr-only',
  30. buttons: {
  31. seek: '[data-player="seek"]',
  32. play: '[data-player="play"]',
  33. pause: '[data-player="pause"]',
  34. restart: '[data-player="restart"]',
  35. rewind: '[data-player="rewind"]',
  36. forward: '[data-player="fast-forward"]',
  37. mute: '[data-player="mute"]',
  38. volume: '[data-player="volume"]',
  39. captions: '[data-player="captions"]',
  40. fullscreen: '[data-player="fullscreen"]'
  41. },
  42. progress: {
  43. container: '.player-progress',
  44. buffer: '.player-progress-buffer',
  45. played: '.player-progress-played'
  46. },
  47. captions: '.player-captions',
  48. currentTime: '.player-current-time',
  49. duration: '.player-duration'
  50. },
  51. classes: {
  52. videoWrapper: 'player-video-wrapper',
  53. embedWrapper: 'player-video-embed',
  54. type: 'player-{0}',
  55. stopped: 'stopped',
  56. playing: 'playing',
  57. muted: 'muted',
  58. loading: 'loading',
  59. tooltip: 'player-tooltip',
  60. hidden: 'sr-only',
  61. hover: 'player-hover',
  62. captions: {
  63. enabled: 'captions-enabled',
  64. active: 'captions-active'
  65. },
  66. fullscreen: {
  67. enabled: 'fullscreen-enabled',
  68. active: 'fullscreen-active',
  69. hideControls: 'fullscreen-hide-controls'
  70. }
  71. },
  72. captions: {
  73. defaultActive: false
  74. },
  75. fullscreen: {
  76. enabled: true,
  77. fallback: true,
  78. hideControls: true
  79. },
  80. storage: {
  81. enabled: true,
  82. key: 'plyr_volume'
  83. },
  84. controls: ['restart', 'rewind', 'play', 'fast-forward', 'current-time', 'duration', 'mute', 'volume', /*'captions',*/ 'fullscreen'],
  85. i18n: {
  86. restart: '重新播放',
  87. rewind: '后退{seektime}秒',
  88. play: '播放',
  89. pause: '暂停',
  90. forward: '快进{seektime}秒',
  91. played: '播放中',
  92. buffered: '缓冲中',
  93. currentTime: '当前时间',
  94. duration: '持续时间',
  95. volume: '音量',
  96. toggleMute: '静音',
  97. toggleCaptions: '字幕',
  98. toggleFullscreen: '全屏'
  99. }
  100. };
  101. // Build the default HTML
  102. function _buildControls() {
  103. // Open and add the progress and seek elements
  104. var html = [
  105. '<div class="player-controls">',
  106. '<div class="player-progress">',
  107. '<label for="seek{id}" class="sr-only">Seek</label>',
  108. '<input id="seek{id}" class="player-progress-seek" type="range" min="0" max="100" step="0.5" value="0" data-player="seek">',
  109. '<progress class="player-progress-played" max="100" value="0">',
  110. '<span>0</span>% ' + config.i18n.played,
  111. '</progress>',
  112. '<progress class="player-progress-buffer" max="100" value="0">',
  113. '<span>0</span>% ' + config.i18n.buffered,
  114. '</progress>',
  115. '</div>',
  116. '<span class="player-controls-left">'];
  117. // Restart button
  118. if (_inArray(config.controls, 'restart')) {
  119. html.push(
  120. '<button type="button" data-player="restart">',
  121. '<svg><use xlink:href="#' + config.iconPrefix + '-restart" /></svg>',
  122. '<span class="sr-only">' + config.i18n.restart + '</span>',
  123. '</button>'
  124. );
  125. }
  126. // Rewind button
  127. if (_inArray(config.controls, 'rewind')) {
  128. html.push(
  129. '<button type="button" data-player="rewind">',
  130. '<svg><use xlink:href="#' + config.iconPrefix + '-rewind" /></svg>',
  131. '<span class="sr-only">' + config.i18n.rewind + '</span>',
  132. '</button>'
  133. );
  134. }
  135. // Play/pause button
  136. if (_inArray(config.controls, 'play')) {
  137. html.push(
  138. '<button type="button" data-player="play">',
  139. '<svg><use xlink:href="#' + config.iconPrefix + '-play" /></svg>',
  140. '<span class="sr-only">' + config.i18n.play + '</span>',
  141. '</button>',
  142. '<button type="button" data-player="pause">',
  143. '<svg><use xlink:href="#' + config.iconPrefix + '-pause" /></svg>',
  144. '<span class="sr-only">' + config.i18n.pause + '</span>',
  145. '</button>'
  146. );
  147. }
  148. // Fast forward button
  149. if (_inArray(config.controls, 'fast-forward')) {
  150. html.push(
  151. '<button type="button" data-player="fast-forward">',
  152. '<svg><use xlink:href="#' + config.iconPrefix + '-fast-forward" /></svg>',
  153. '<span class="sr-only">' + config.i18n.forward + '</span>',
  154. '</button>'
  155. );
  156. }
  157. // Media current time display
  158. if (_inArray(config.controls, 'current-time')) {
  159. html.push(
  160. '<span class="player-time">',
  161. '<span class="sr-only">' + config.i18n.currentTime + '</span>',
  162. '<span class="player-current-time">00:00</span>',
  163. '</span>'
  164. );
  165. }
  166. // Media duration display
  167. if (_inArray(config.controls, 'duration')) {
  168. html.push(
  169. '<span class="player-time">',
  170. '<span class="sr-only">' + config.i18n.duration + '</span>',
  171. '<span class="player-duration">00:00</span>',
  172. '</span>'
  173. );
  174. }
  175. // Close left controls
  176. html.push(
  177. '</span>',
  178. '<span class="player-controls-right">'
  179. );
  180. // Toggle mute button
  181. if (_inArray(config.controls, 'mute')) {
  182. html.push(
  183. '<button type="button" data-player="mute">',
  184. '<svg class="icon-muted"><use xlink:href="#' + config.iconPrefix + '-muted" /></svg>',
  185. '<svg><use xlink:href="#' + config.iconPrefix + '-volume" /></svg>',
  186. '<span class="sr-only">' + config.i18n.toggleMute + '</span>',
  187. '</button>'
  188. );
  189. }
  190. // Volume range control
  191. if (_inArray(config.controls, 'volume')) {
  192. html.push(
  193. '<label for="volume{id}" class="sr-only">' + config.i18n.volume + '</label>',
  194. '<input id="volume{id}" class="player-volume" type="range" min="0" max="10" value="5" data-player="volume">'
  195. );
  196. }
  197. // Toggle captions button
  198. if (_inArray(config.controls, 'captions')) {
  199. html.push(
  200. '<button type="button" data-player="captions">',
  201. '<svg class="icon-captions-on"><use xlink:href="#' + config.iconPrefix + '-captions-on" /></svg>',
  202. '<svg><use xlink:href="#' + config.iconPrefix + '-captions-off" /></svg>',
  203. '<span class="sr-only">' + config.i18n.toggleCaptions + '</span>',
  204. '</button>'
  205. );
  206. }
  207. // Toggle fullscreen button
  208. if (_inArray(config.controls, 'fullscreen')) {
  209. html.push(
  210. '<button type="button" data-player="fullscreen">',
  211. '<svg class="icon-exit-fullscreen"><use xlink:href="#' + config.iconPrefix + '-exit-fullscreen" /></svg>',
  212. '<svg><use xlink:href="#' + config.iconPrefix + '-enter-fullscreen" /></svg>',
  213. '<span class="sr-only">' + config.i18n.toggleFullscreen + '</span>',
  214. '</button>'
  215. );
  216. }
  217. // Close everything
  218. html.push(
  219. '</span>',
  220. '</div>'
  221. );
  222. return html.join('');
  223. }
  224. // Debugging
  225. function _log(text, error) {
  226. if (config.debug && window.console) {
  227. console[(error ? 'error' : 'log')](text);
  228. }
  229. }
  230. // Credits: http://paypal.github.io/accessible-html5-video-player/
  231. // Unfortunately, due to mixed support, UA sniffing is required
  232. function _browserSniff() {
  233. var nAgt = navigator.userAgent,
  234. name = navigator.appName,
  235. fullVersion = '' + parseFloat(navigator.appVersion),
  236. majorVersion = parseInt(navigator.appVersion, 10),
  237. nameOffset,
  238. verOffset,
  239. ix;
  240. // MSIE 11
  241. if ((navigator.appVersion.indexOf('Windows NT') !== -1) && (navigator.appVersion.indexOf('rv:11') !== -1)) {
  242. name = 'IE';
  243. fullVersion = '11;';
  244. }
  245. // MSIE
  246. else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) {
  247. name = 'IE';
  248. fullVersion = nAgt.substring(verOffset + 5);
  249. }
  250. // Chrome
  251. else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) {
  252. name = 'Chrome';
  253. fullVersion = nAgt.substring(verOffset + 7);
  254. }
  255. // Safari
  256. else if ((verOffset = nAgt.indexOf('Safari')) !== -1) {
  257. name = 'Safari';
  258. fullVersion = nAgt.substring(verOffset + 7);
  259. if ((verOffset = nAgt.indexOf('Version')) !== -1) {
  260. fullVersion = nAgt.substring(verOffset + 8);
  261. }
  262. }
  263. // Firefox
  264. else if ((verOffset = nAgt.indexOf('Firefox')) !== -1) {
  265. name = 'Firefox';
  266. fullVersion = nAgt.substring(verOffset + 8);
  267. }
  268. // In most other browsers, 'name/version' is at the end of userAgent
  269. else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) {
  270. name = nAgt.substring(nameOffset, verOffset);
  271. fullVersion = nAgt.substring(verOffset + 1);
  272. if (name.toLowerCase() == name.toUpperCase()) {
  273. name = navigator.appName;
  274. }
  275. }
  276. // Trim the fullVersion string at semicolon/space if present
  277. if ((ix = fullVersion.indexOf(';')) !== -1) {
  278. fullVersion = fullVersion.substring(0, ix);
  279. }
  280. if ((ix = fullVersion.indexOf(' ')) !== -1) {
  281. fullVersion = fullVersion.substring(0, ix);
  282. }
  283. // Get major version
  284. majorVersion = parseInt('' + fullVersion, 10);
  285. if (isNaN(majorVersion)) {
  286. fullVersion = '' + parseFloat(navigator.appVersion);
  287. majorVersion = parseInt(navigator.appVersion, 10);
  288. }
  289. // Return data
  290. return {
  291. name: name,
  292. version: majorVersion,
  293. ios: /(iPad|iPhone|iPod)/g.test(navigator.platform)
  294. };
  295. }
  296. // Check for mime type support against a player instance
  297. // Credits: http://diveintohtml5.info/everything.html
  298. // Related: http://www.leanbackplayer.com/test/h5mt.html
  299. function _supportMime(player, mimeType) {
  300. var media = player.media;
  301. // Only check video types for video players
  302. if (player.type == 'video') {
  303. // Check type
  304. switch (mimeType) {
  305. case 'video/webm':
  306. return !!(media.canPlayType && media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''));
  307. case 'video/mp4':
  308. return !!(media.canPlayType && media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
  309. case 'video/ogg':
  310. return !!(media.canPlayType && media.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''));
  311. }
  312. }
  313. // Only check audio types for audio players
  314. else if (player.type == 'audio') {
  315. // Check type
  316. switch (mimeType) {
  317. case 'audio/mpeg':
  318. return !!(media.canPlayType && media.canPlayType('audio/mpeg;').replace(/no/, ''));
  319. case 'audio/ogg':
  320. return !!(media.canPlayType && media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, ''));
  321. case 'audio/wav':
  322. return !!(media.canPlayType && media.canPlayType('audio/wav; codecs="1"').replace(/no/, ''));
  323. }
  324. }
  325. // If we got this far, we're stuffed
  326. return false;
  327. }
  328. // Inject a script
  329. function _injectScript(source) {
  330. if (document.querySelectorAll('script[src="' + source + '"]').length) {
  331. return;
  332. }
  333. var tag = document.createElement('script');
  334. tag.src = source;
  335. var firstScriptTag = document.getElementsByTagName('script')[0];
  336. firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  337. }
  338. // Element exists in an array
  339. function _inArray(haystack, needle) {
  340. return Array.prototype.indexOf && (haystack.indexOf(needle) != -1);
  341. }
  342. // Replace all
  343. function _replaceAll(string, find, replace) {
  344. return string.replace(new RegExp(find.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'), replace);
  345. }
  346. // Wrap an element
  347. function _wrap(elements, wrapper) {
  348. // Convert `elements` to an array, if necessary.
  349. if (!elements.length) {
  350. elements = [elements];
  351. }
  352. // Loops backwards to prevent having to clone the wrapper on the
  353. // first element (see `child` below).
  354. for (var i = elements.length - 1; i >= 0; i--) {
  355. var child = (i > 0) ? wrapper.cloneNode(true) : wrapper;
  356. var element = elements[i];
  357. // Cache the current parent and sibling.
  358. var parent = element.parentNode;
  359. var sibling = element.nextSibling;
  360. // Wrap the element (is automatically removed from its current
  361. // parent).
  362. child.appendChild(element);
  363. // If the element had a sibling, insert the wrapper before
  364. // the sibling to maintain the HTML structure; otherwise, just
  365. // append it to the parent.
  366. if (sibling) {
  367. parent.insertBefore(child, sibling);
  368. } else {
  369. parent.appendChild(child);
  370. }
  371. }
  372. }
  373. // Unwrap an element
  374. // http://plainjs.com/javascript/manipulation/unwrap-a-dom-element-35/
  375. function _unwrap(wrapper) {
  376. // Get the element's parent node
  377. var parent = wrapper.parentNode;
  378. // Move all children out of the element
  379. while (wrapper.firstChild) {
  380. parent.insertBefore(wrapper.firstChild, wrapper);
  381. }
  382. // Remove the empty element
  383. parent.removeChild(wrapper);
  384. }
  385. // Remove an element
  386. function _remove(element) {
  387. element.parentNode.removeChild(element);
  388. }
  389. // Prepend child
  390. function _prependChild(parent, element) {
  391. parent.insertBefore(element, parent.firstChild);
  392. }
  393. // Set attributes
  394. function _setAttributes(element, attributes) {
  395. for (var key in attributes) {
  396. element.setAttribute(key, attributes[key]);
  397. }
  398. }
  399. // Toggle class on an element
  400. function _toggleClass(element, name, state) {
  401. if (element) {
  402. if (element.classList) {
  403. element.classList[state ? 'add' : 'remove'](name);
  404. } else {
  405. var className = (' ' + element.className + ' ').replace(/\s+/g, ' ').replace(' ' + name + ' ', '');
  406. element.className = className + (state ? ' ' + name : '');
  407. }
  408. }
  409. }
  410. // Toggle event
  411. function _toggleHandler(element, events, callback, toggle) {
  412. var eventList = events.split(' ');
  413. // If a nodelist is passed, call itself on each node
  414. if (element instanceof NodeList) {
  415. for (var x = 0; x < element.length; x++) {
  416. if (element[x] instanceof Node) {
  417. _toggleHandler(element[x], arguments[1], arguments[2], arguments[3]);
  418. }
  419. }
  420. return;
  421. }
  422. // If a single node is passed, bind the event listener
  423. for (var i = 0; i < eventList.length; i++) {
  424. element[toggle ? 'addEventListener' : 'removeEventListener'](eventList[i], callback, false);
  425. }
  426. }
  427. // Bind event
  428. function _on(element, events, callback) {
  429. if (element) {
  430. _toggleHandler(element, events, callback, true);
  431. }
  432. }
  433. // Unbind event
  434. function _off(element, events, callback) {
  435. if (element) {
  436. _toggleHandler(element, events, callback, false);
  437. }
  438. }
  439. // Trigger event
  440. function _triggerEvent(element, event) {
  441. // Create faux event
  442. var fauxEvent = document.createEvent('MouseEvents');
  443. // Set the event type
  444. fauxEvent.initEvent(event, true, true);
  445. // Dispatch the event
  446. element.dispatchEvent(fauxEvent);
  447. }
  448. // Toggle aria-pressed state on a toggle button
  449. function _toggleState(target, state) {
  450. // Get state
  451. state = (typeof state === 'boolean' ? state : !target.getAttribute('aria-pressed'));
  452. // Set the attribute on target
  453. target.setAttribute('aria-pressed', state);
  454. return state;
  455. }
  456. // Get percentage
  457. function _getPercentage(current, max) {
  458. if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) {
  459. return 0;
  460. }
  461. return ((current / max) * 100).toFixed(2);
  462. }
  463. // Deep extend/merge two Objects
  464. // http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
  465. // Removed call to arguments.callee (used explicit function name instead)
  466. function _extend(destination, source) {
  467. for (var property in source) {
  468. if (source[property] && source[property].constructor && source[property].constructor === Object) {
  469. destination[property] = destination[property] || {};
  470. _extend(destination[property], source[property]);
  471. } else {
  472. destination[property] = source[property];
  473. }
  474. }
  475. return destination;
  476. }
  477. // Fullscreen API
  478. function _fullscreen() {
  479. var fullscreen = {
  480. supportsFullScreen: false,
  481. isFullScreen: function () {
  482. return false;
  483. },
  484. requestFullScreen: function () {},
  485. cancelFullScreen: function () {},
  486. fullScreenEventName: '',
  487. element: null,
  488. prefix: ''
  489. },
  490. browserPrefixes = 'webkit moz o ms khtml'.split(' ');
  491. // Check for native support
  492. if (typeof document.cancelFullScreen !== 'undefined') {
  493. fullscreen.supportsFullScreen = true;
  494. } else {
  495. // Check for fullscreen support by vendor prefix
  496. for (var i = 0, il = browserPrefixes.length; i < il; i++) {
  497. fullscreen.prefix = browserPrefixes[i];
  498. if (typeof document[fullscreen.prefix + 'CancelFullScreen'] !== 'undefined') {
  499. fullscreen.supportsFullScreen = true;
  500. break;
  501. }
  502. // Special case for MS (when isn't it?)
  503. else if (typeof document.msExitFullscreen !== 'undefined' && document.msFullscreenEnabled) {
  504. fullscreen.prefix = 'ms';
  505. fullscreen.supportsFullScreen = true;
  506. break;
  507. }
  508. }
  509. }
  510. // Update methods to do something useful
  511. if (fullscreen.supportsFullScreen) {
  512. // Yet again Microsoft awesomeness,
  513. // Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
  514. fullscreen.fullScreenEventName = (fullscreen.prefix == 'ms' ? 'MSFullscreenChange' : fullscreen.prefix + 'fullscreenchange');
  515. fullscreen.isFullScreen = function (element) {
  516. if (typeof element === 'undefined') {
  517. element = document.body;
  518. }
  519. switch (this.prefix) {
  520. case '':
  521. return document.fullscreenElement == element;
  522. case 'moz':
  523. return document.mozFullScreenElement == element;
  524. default:
  525. return document[this.prefix + 'FullscreenElement'] == element;
  526. }
  527. };
  528. fullscreen.requestFullScreen = function (element) {
  529. if (typeof element === 'undefined') {
  530. element = document.body;
  531. }
  532. return (this.prefix === '') ? element.requestFullScreen() : element[this.prefix + (this.prefix == 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')]();
  533. };
  534. fullscreen.cancelFullScreen = function () {
  535. return (this.prefix === '') ? document.cancelFullScreen() : document[this.prefix + (this.prefix == 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')]();
  536. };
  537. fullscreen.element = function () {
  538. return (this.prefix === '') ? document.fullscreenElement : document[this.prefix + 'FullscreenElement'];
  539. };
  540. }
  541. return fullscreen;
  542. }
  543. // Local storage
  544. function _storage() {
  545. var storage = {
  546. supported: (function () {
  547. try {
  548. return 'localStorage' in window && window.localStorage !== null;
  549. } catch (e) {
  550. return false;
  551. }
  552. })()
  553. };
  554. return storage;
  555. }
  556. // Player instance
  557. function Plyr(container) {
  558. var player = this;
  559. player.container = container;
  560. // Captions functions
  561. // Seek the manual caption time and update UI
  562. function _seekManualCaptions(time) {
  563. // If it's not video, or we're using textTracks, bail.
  564. if (player.usingTextTracks || player.type !== 'video' || !player.supported.full) {
  565. return;
  566. }
  567. // Reset subcount
  568. player.subcount = 0;
  569. // Check time is a number, if not use currentTime
  570. // IE has a bug where currentTime doesn't go to 0
  571. // https://twitter.com/Sam_Potts/status/573715746506731521
  572. time = typeof time === 'number' ? time : player.media.currentTime;
  573. while (_timecodeMax(player.captions[player.subcount][0]) < time.toFixed(1)) {
  574. player.subcount++;
  575. if (player.subcount > player.captions.length - 1) {
  576. player.subcount = player.captions.length - 1;
  577. break;
  578. }
  579. }
  580. // Check if the next caption is in the current time range
  581. if (player.media.currentTime.toFixed(1) >= _timecodeMin(player.captions[player.subcount][0]) &&
  582. player.media.currentTime.toFixed(1) <= _timecodeMax(player.captions[player.subcount][0])) {
  583. player.currentCaption = player.captions[player.subcount][1];
  584. // Trim caption text
  585. var content = player.currentCaption.trim();
  586. // Render the caption (only if changed)
  587. if (player.captionsContainer.innerHTML != content) {
  588. // Empty caption
  589. // Otherwise NVDA reads it twice
  590. player.captionsContainer.innerHTML = '';
  591. // Set new caption text
  592. player.captionsContainer.innerHTML = content;
  593. }
  594. } else {
  595. player.captionsContainer.innerHTML = '';
  596. }
  597. }
  598. // Display captions container and button (for initialization)
  599. function _showCaptions() {
  600. // If there's no caption toggle, bail
  601. if (!player.buttons.captions) {
  602. return;
  603. }
  604. _toggleClass(player.container, config.classes.captions.enabled, true);
  605. if (config.captions.defaultActive) {
  606. _toggleClass(player.container, config.classes.captions.active, true);
  607. _toggleState(player.buttons.captions, true);
  608. }
  609. }
  610. // Utilities for caption time codes
  611. function _timecodeMin(tc) {
  612. var tcpair = [];
  613. tcpair = tc.split(' --> ');
  614. return _subTcSecs(tcpair[0]);
  615. }
  616. function _timecodeMax(tc) {
  617. var tcpair = [];
  618. tcpair = tc.split(' --> ');
  619. return _subTcSecs(tcpair[1]);
  620. }
  621. function _subTcSecs(tc) {
  622. if (tc === null || tc === undefined) {
  623. return 0;
  624. } else {
  625. var tc1 = [],
  626. tc2 = [],
  627. seconds;
  628. tc1 = tc.split(',');
  629. tc2 = tc1[0].split(':');
  630. seconds = Math.floor(tc2[0] * 60 * 60) + Math.floor(tc2[1] * 60) + Math.floor(tc2[2]);
  631. return seconds;
  632. }
  633. }
  634. // Find all elements
  635. function _getElements(selector) {
  636. return player.container.querySelectorAll(selector);
  637. }
  638. // Find a single element
  639. function _getElement(selector) {
  640. return _getElements(selector)[0];
  641. }
  642. // Determine if we're in an iframe
  643. function _inFrame() {
  644. try {
  645. return window.self !== window.top;
  646. } catch (e) {
  647. return true;
  648. }
  649. }
  650. // Insert controls
  651. function _injectControls() {
  652. // Make a copy of the html
  653. var html = config.html;
  654. // Insert custom video controls
  655. _log('Injecting custom controls.');
  656. // If no controls are specified, create default
  657. if (!html) {
  658. html = _buildControls();
  659. }
  660. // Replace seek time instances
  661. html = _replaceAll(html, '{seektime}', config.seekTime);
  662. // Replace all id references with random numbers
  663. html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000)));
  664. // Inject into the container
  665. player.container.insertAdjacentHTML('beforeend', html);
  666. // Setup tooltips
  667. if (config.tooltips) {
  668. var labels = _getElements(config.selectors.labels);
  669. for (var i = labels.length - 1; i >= 0; i--) {
  670. var label = labels[i];
  671. _toggleClass(label, config.classes.hidden, false);
  672. _toggleClass(label, config.classes.tooltip, true);
  673. }
  674. }
  675. }
  676. // Find the UI controls and store references
  677. function _findElements() {
  678. try {
  679. player.controls = _getElement(config.selectors.controls);
  680. // Buttons
  681. player.buttons = {};
  682. player.buttons.seek = _getElement(config.selectors.buttons.seek);
  683. player.buttons.play = _getElement(config.selectors.buttons.play);
  684. player.buttons.pause = _getElement(config.selectors.buttons.pause);
  685. player.buttons.restart = _getElement(config.selectors.buttons.restart);
  686. player.buttons.rewind = _getElement(config.selectors.buttons.rewind);
  687. player.buttons.forward = _getElement(config.selectors.buttons.forward);
  688. player.buttons.fullscreen = _getElement(config.selectors.buttons.fullscreen);
  689. // Inputs
  690. player.buttons.mute = _getElement(config.selectors.buttons.mute);
  691. player.buttons.captions = _getElement(config.selectors.buttons.captions);
  692. player.checkboxes = _getElements('[type="checkbox"]');
  693. // Progress
  694. player.progress = {};
  695. player.progress.container = _getElement(config.selectors.progress.container);
  696. // Progress - Buffering
  697. player.progress.buffer = {};
  698. player.progress.buffer.bar = _getElement(config.selectors.progress.buffer);
  699. player.progress.buffer.text = player.progress.buffer.bar && player.progress.buffer.bar.getElementsByTagName('span')[0];
  700. // Progress - Played
  701. player.progress.played = {};
  702. player.progress.played.bar = _getElement(config.selectors.progress.played);
  703. player.progress.played.text = player.progress.played.bar && player.progress.played.bar.getElementsByTagName('span')[0];
  704. // Volume
  705. player.volume = _getElement(config.selectors.buttons.volume);
  706. // Timing
  707. player.duration = _getElement(config.selectors.duration);
  708. player.currentTime = _getElement(config.selectors.currentTime);
  709. player.seekTime = _getElements(config.selectors.seekTime);
  710. return true;
  711. } catch (e) {
  712. _log('It looks like there\'s a problem with your controls html. Bailing.', true);
  713. // Restore native video controls
  714. player.media.setAttribute('controls', '');
  715. return false;
  716. }
  717. }
  718. // Setup aria attribute for play
  719. function _setupPlayAria() {
  720. // If there's no play button, bail
  721. if (!player.buttons.play) {
  722. return;
  723. }
  724. // Find the current text
  725. var label = player.buttons.play.innerText || config.i18n.play;
  726. // If there's a media title set, use that for the label
  727. if (typeof (config.title) !== 'undefined' && config.title.length) {
  728. label += ', ' + config.title;
  729. }
  730. player.buttons.play.setAttribute('aria-label', label);
  731. }
  732. // Setup media
  733. function _setupMedia() {
  734. // If there's no media, bail
  735. if (!player.media) {
  736. _log('No audio or video element found!', true);
  737. return false;
  738. }
  739. if (player.supported.full) {
  740. // Remove native video controls
  741. player.media.removeAttribute('controls');
  742. // Add type class
  743. _toggleClass(player.container, config.classes.type.replace('{0}', player.type), true);
  744. // If there's no autoplay attribute, assume the video is stopped and add state class
  745. _toggleClass(player.container, config.classes.stopped, (player.media.getAttribute('autoplay') === null));
  746. // Add iOS class
  747. if (player.browser.ios) {
  748. _toggleClass(player.container, 'ios', true);
  749. }
  750. // Inject the player wrapper
  751. if (player.type === 'video') {
  752. // Create the wrapper div
  753. var wrapper = document.createElement('div');
  754. wrapper.setAttribute('class', config.classes.videoWrapper);
  755. // Wrap the video in a container
  756. _wrap(player.media, wrapper);
  757. // Cache the container
  758. player.videoContainer = wrapper;
  759. }
  760. }
  761. // YouTube
  762. if (player.type == 'youtube') {
  763. _setupYouTube(player.media.getAttribute('data-video-id'));
  764. }
  765. // Autoplay
  766. if (player.media.getAttribute('autoplay') !== null) {
  767. _play();
  768. }
  769. }
  770. // Setup YouTube
  771. function _setupYouTube(id) {
  772. // Remove old containers
  773. var containers = _getElements('[id^="youtube"]');
  774. for (var i = containers.length - 1; i >= 0; i--) {
  775. _remove(containers[i]);
  776. }
  777. // Create the YouTube container
  778. var container = document.createElement('div');
  779. container.setAttribute('id', 'youtube-' + Math.floor(Math.random() * (10000)));
  780. player.media.appendChild(container);
  781. // Add embed class for responsive
  782. _toggleClass(player.media, config.classes.videoWrapper, true);
  783. _toggleClass(player.media, config.classes.embedWrapper, true);
  784. if (typeof YT === 'object') {
  785. _YTReady(id, container);
  786. } else {
  787. // Load the API
  788. _injectScript('https://www.youtube.com/iframe_api');
  789. // Add callback to queue
  790. callbacks.youtube.push(function () {
  791. _YTReady(id, container);
  792. });
  793. // Setup callback for the API
  794. window.onYouTubeIframeAPIReady = function () {
  795. for (var i = callbacks.youtube.length - 1; i >= 0; i--) {
  796. // Fire callback
  797. callbacks.youtube[i]();
  798. // Remove from queue
  799. callbacks.youtube.splice(i, 1);
  800. }
  801. };
  802. }
  803. }
  804. // Handle API ready
  805. function _YTReady(id, container) {
  806. _log('YouTube API Ready');
  807. // Setup timers object
  808. // We have to poll YouTube for updates
  809. if (!('timer' in player)) {
  810. player.timer = {};
  811. }
  812. // Setup instance
  813. // https://developers.google.com/youtube/iframe_api_reference
  814. player.embed = new YT.Player(container.id, {
  815. videoId: id,
  816. playerVars: {
  817. autoplay: 0,
  818. controls: (player.supported.full ? 0 : 1),
  819. rel: 0,
  820. showinfo: 0,
  821. iv_load_policy: 3,
  822. cc_load_policy: (config.captions.defaultActive ? 1 : 0),
  823. cc_lang_pref: 'en',
  824. wmode: 'transparent',
  825. modestbranding: 1,
  826. disablekb: 1
  827. },
  828. events: {
  829. 'onReady': function (event) {
  830. // Get the instance
  831. var instance = event.target;
  832. // Create a faux HTML5 API using the YouTube API
  833. player.media.play = function () {
  834. instance.playVideo();
  835. };
  836. player.media.pause = function () {
  837. instance.pauseVideo();
  838. };
  839. player.media.stop = function () {
  840. instance.stopVideo();
  841. };
  842. player.media.duration = instance.getDuration();
  843. player.media.paused = true;
  844. player.media.currentTime = instance.getCurrentTime();
  845. player.media.muted = instance.isMuted();
  846. // Trigger timeupdate
  847. _triggerEvent(player.media, 'timeupdate');
  848. // Reset timer
  849. window.clearInterval(player.timer.buffering);
  850. // Setup buffering
  851. player.timer.buffering = window.setInterval(function () {
  852. // Get loaded % from YouTube
  853. player.media.buffered = instance.getVideoLoadedFraction();
  854. // Trigger progress
  855. _triggerEvent(player.media, 'progress');
  856. // Bail if we're at 100%
  857. if (player.media.buffered === 1) {
  858. window.clearInterval(player.timer.buffering);
  859. }
  860. }, 200);
  861. if (player.supported.full) {
  862. // Only setup controls once
  863. if (!player.container.querySelectorAll(config.selectors.controls).length) {
  864. _setupInterface();
  865. }
  866. // Display duration if available
  867. if (config.displayDuration) {
  868. _displayDuration();
  869. }
  870. }
  871. },
  872. 'onStateChange': function (event) {
  873. // Get the instance
  874. var instance = event.target;
  875. // Reset timer
  876. window.clearInterval(player.timer.playing);
  877. // Handle events
  878. // -1 Unstarted
  879. // 0 Ended
  880. // 1 Playing
  881. // 2 Paused
  882. // 3 Buffering
  883. // 5 Video cued
  884. switch (event.data) {
  885. case 0:
  886. player.media.paused = true;
  887. _triggerEvent(player.media, 'ended');
  888. break;
  889. case 1:
  890. player.media.paused = false;
  891. _triggerEvent(player.media, 'play');
  892. // Poll to get playback progress
  893. player.timer.playing = window.setInterval(function () {
  894. // Set the current time
  895. player.media.currentTime = instance.getCurrentTime();
  896. // Trigger timeupdate
  897. _triggerEvent(player.media, 'timeupdate');
  898. }, 200);
  899. break;
  900. case 2:
  901. player.media.paused = true;
  902. _triggerEvent(player.media, 'pause');
  903. }
  904. }
  905. }
  906. });
  907. }
  908. // Setup captions
  909. function _setupCaptions() {
  910. if (player.type === 'video') {
  911. // Inject the container
  912. player.videoContainer.insertAdjacentHTML('afterbegin', '<div class="' + config.selectors.captions.replace('.', '') + '"><span></span></div>');
  913. // Cache selector
  914. player.captionsContainer = _getElement(config.selectors.captions).querySelector('span');
  915. // Determine if HTML5 textTracks is supported
  916. player.usingTextTracks = false;
  917. if (player.media.textTracks) {
  918. player.usingTextTracks = true;
  919. }
  920. // Get URL of caption file if exists
  921. var captionSrc = '',
  922. kind,
  923. children = player.media.childNodes;
  924. for (var i = 0; i < children.length; i++) {
  925. if (children[i].nodeName.toLowerCase() === 'track') {
  926. kind = children[i].kind;
  927. if (kind === 'captions' || kind === 'subtitles') {
  928. captionSrc = children[i].getAttribute('src');
  929. }
  930. }
  931. }
  932. // Record if caption file exists or not
  933. player.captionExists = true;
  934. if (captionSrc === '') {
  935. player.captionExists = false;
  936. _log('No caption track found.');
  937. } else {
  938. _log('Caption track found; URI: ' + captionSrc);
  939. }
  940. // If no caption file exists, hide container for caption text
  941. if (!player.captionExists) {
  942. _toggleClass(player.container, config.classes.captions.enabled);
  943. }
  944. // If caption file exists, process captions
  945. else {
  946. // Turn off native caption rendering to avoid double captions
  947. // This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below
  948. var tracks = player.media.textTracks;
  949. for (var x = 0; x < tracks.length; x++) {
  950. tracks[x].mode = 'hidden';
  951. }
  952. // Enable UI
  953. _showCaptions(player);
  954. // Disable unsupported browsers than report false positive
  955. if ((player.browser.name === 'IE' && player.browser.version >= 10) ||
  956. (player.browser.name === 'Firefox' && player.browser.version >= 31) ||
  957. (player.browser.name === 'Chrome' && player.browser.version >= 43) ||
  958. (player.browser.name === 'Safari' && player.browser.version >= 7)) {
  959. // Debugging
  960. _log('Detected unsupported browser for HTML5 captions. Using fallback.');
  961. // Set to false so skips to 'manual' captioning
  962. player.usingTextTracks = false;
  963. }
  964. // Rendering caption tracks
  965. // Native support required - http://caniuse.com/webvtt
  966. if (player.usingTextTracks) {
  967. _log('TextTracks supported.');
  968. for (var y = 0; y < tracks.length; y++) {
  969. var track = tracks[y];
  970. if (track.kind === 'captions' || track.kind === 'subtitles') {
  971. _on(track, 'cuechange', function () {
  972. // Clear container
  973. player.captionsContainer.innerHTML = '';
  974. // Display a cue, if there is one
  975. if (this.activeCues[0] && this.activeCues[0].hasOwnProperty('text')) {
  976. player.captionsContainer.appendChild(this.activeCues[0].getCueAsHTML().trim());
  977. }
  978. });
  979. }
  980. }
  981. }
  982. // Caption tracks not natively supported
  983. else {
  984. _log('TextTracks not supported so rendering captions manually.');
  985. // Render captions from array at appropriate time
  986. player.currentCaption = '';
  987. player.captions = [];
  988. if (captionSrc !== '') {
  989. // Create XMLHttpRequest Object
  990. var xhr = new XMLHttpRequest();
  991. xhr.onreadystatechange = function () {
  992. if (xhr.readyState === 4) {
  993. if (xhr.status === 200) {
  994. var records = [],
  995. record,
  996. req = xhr.responseText;
  997. records = req.split('\n\n');
  998. for (var r = 0; r < records.length; r++) {
  999. record = records[r];
  1000. player.captions[r] = [];
  1001. player.captions[r] = record.split('\n');
  1002. }
  1003. // Remove first element ('VTT')
  1004. player.captions.shift();
  1005. _log('Successfully loaded the caption file via AJAX.');
  1006. } else {
  1007. _log('There was a problem loading the caption file via AJAX.', true);
  1008. }
  1009. }
  1010. };
  1011. xhr.open('get', captionSrc, true);
  1012. xhr.send();
  1013. }
  1014. }
  1015. // If Safari 7+, removing track from DOM [see 'turn off native caption rendering' above]
  1016. if (player.browser.name === 'Safari' && player.browser.version >= 7) {
  1017. _log('Safari 7+ detected; removing track from DOM.');
  1018. // Find all <track> elements
  1019. tracks = player.media.getElementsByTagName('track');
  1020. // Loop through and remove one by one
  1021. for (var t = 0; t < tracks.length; t++) {
  1022. player.media.removeChild(tracks[t]);
  1023. }
  1024. }
  1025. }
  1026. }
  1027. }
  1028. // Setup fullscreen
  1029. function _setupFullscreen() {
  1030. if (player.type != 'audio' && config.fullscreen.enabled) {
  1031. // Check for native support
  1032. var nativeSupport = fullscreen.supportsFullScreen;
  1033. if (nativeSupport || (config.fullscreen.fallback && !_inFrame())) {
  1034. _log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled.');
  1035. // Add styling hook
  1036. _toggleClass(player.container, config.classes.fullscreen.enabled, true);
  1037. } else {
  1038. _log('Fullscreen not supported and fallback disabled.');
  1039. }
  1040. // Toggle state
  1041. _toggleState(player.buttons.fullscreen, false);
  1042. // Set control hide class hook
  1043. if (config.fullscreen.hideControls) {
  1044. _toggleClass(player.container, config.classes.fullscreen.hideControls, true);
  1045. }
  1046. }
  1047. }
  1048. // Play media
  1049. function _play() {
  1050. player.media.play();
  1051. }
  1052. // Pause media
  1053. function _pause() {
  1054. player.media.pause();
  1055. }
  1056. // Toggle playback
  1057. function _togglePlay(toggle) {
  1058. // Play
  1059. if (toggle === true) {
  1060. _play();
  1061. }
  1062. // Pause
  1063. else if (toggle === false) {
  1064. _pause();
  1065. }
  1066. // True toggle
  1067. else {
  1068. player.media[player.media.paused ? 'play' : 'pause']();
  1069. }
  1070. }
  1071. // Rewind
  1072. function _rewind(seekTime) {
  1073. // Use default if needed
  1074. if (typeof seekTime !== 'number') {
  1075. seekTime = config.seekTime;
  1076. }
  1077. _seek(player.media.currentTime - seekTime);
  1078. }
  1079. // Fast forward
  1080. function _forward(seekTime) {
  1081. // Use default if needed
  1082. if (typeof seekTime !== 'number') {
  1083. seekTime = config.seekTime;
  1084. }
  1085. _seek(player.media.currentTime + seekTime);
  1086. }
  1087. // Seek to time
  1088. // The input parameter can be an event or a number
  1089. function _seek(input) {
  1090. var targetTime = 0,
  1091. paused = player.media.paused;
  1092. // Explicit position
  1093. if (typeof input === 'number') {
  1094. targetTime = input;
  1095. }
  1096. // Event
  1097. else if (typeof input === 'object' && (input.type === 'input' || input.type === 'change')) {
  1098. // It's the seek slider
  1099. // Seek to the selected time
  1100. targetTime = ((input.target.value / input.target.max) * player.media.duration);
  1101. }
  1102. // Normalise targetTime
  1103. if (targetTime < 0) {
  1104. targetTime = 0;
  1105. } else if (targetTime > player.media.duration) {
  1106. targetTime = player.media.duration;
  1107. }
  1108. // Set the current time
  1109. // Try/catch incase the media isn't set and we're calling seek() from source() and IE moans
  1110. try {
  1111. player.media.currentTime = targetTime.toFixed(1);
  1112. } catch (e) {}
  1113. // YouTube
  1114. if (player.type == 'youtube') {
  1115. player.embed.seekTo(targetTime);
  1116. if (paused) {
  1117. _pause();
  1118. }
  1119. // Trigger timeupdate
  1120. _triggerEvent(player.media, 'timeupdate');
  1121. }
  1122. // Logging
  1123. _log('Seeking to ' + player.media.currentTime + ' seconds');
  1124. // Special handling for 'manual' captions
  1125. _seekManualCaptions(targetTime);
  1126. }
  1127. // Check playing state
  1128. function _checkPlaying() {
  1129. _toggleClass(player.container, config.classes.playing, !player.media.paused);
  1130. _toggleClass(player.container, config.classes.stopped, player.media.paused);
  1131. }
  1132. // Toggle fullscreen
  1133. function _toggleFullscreen(event) {
  1134. // Check for native support
  1135. var nativeSupport = fullscreen.supportsFullScreen;
  1136. // If it's a fullscreen change event, it's probably a native close
  1137. if (event && event.type === fullscreen.fullScreenEventName) {
  1138. player.isFullscreen = fullscreen.isFullScreen(player.container);
  1139. }
  1140. // If there's native support, use it
  1141. else if (nativeSupport) {
  1142. // Request fullscreen
  1143. if (!fullscreen.isFullScreen(player.container)) {
  1144. fullscreen.requestFullScreen(player.container);
  1145. }
  1146. // Bail from fullscreen
  1147. else {
  1148. fullscreen.cancelFullScreen();
  1149. }
  1150. // Check if we're actually full screen (it could fail)
  1151. player.isFullscreen = fullscreen.isFullScreen(player.container);
  1152. } else {
  1153. // Otherwise, it's a simple toggle
  1154. player.isFullscreen = !player.isFullscreen;
  1155. // Bind/unbind escape key
  1156. if (player.isFullscreen) {
  1157. _on(document, 'keyup', _handleEscapeFullscreen);
  1158. document.body.style.overflow = 'hidden';
  1159. } else {
  1160. _off(document, 'keyup', _handleEscapeFullscreen);
  1161. document.body.style.overflow = '';
  1162. }
  1163. }
  1164. // Set class hook
  1165. _toggleClass(player.container, config.classes.fullscreen.active, player.isFullscreen);
  1166. // Set button state
  1167. _toggleState(player.buttons.fullscreen, player.isFullscreen);
  1168. // Toggle controls visibility based on mouse movement and location
  1169. var hoverTimer, isMouseOver = false;
  1170. // Show the player controls
  1171. function _showControls() {
  1172. // Set shown class
  1173. _toggleClass(player.container, config.classes.hover, true);
  1174. // Clear timer every movement
  1175. window.clearTimeout(hoverTimer);
  1176. // If the mouse is not over the controls, set a timeout to hide them
  1177. if (!isMouseOver) {
  1178. hoverTimer = window.setTimeout(function () {
  1179. _toggleClass(player.container, config.classes.hover, false);
  1180. }, 2000);
  1181. }
  1182. }
  1183. // Check mouse is over the controls
  1184. function _setMouseOver(event) {
  1185. isMouseOver = (event.type === 'mouseenter');
  1186. }
  1187. if (config.fullscreen.hideControls) {
  1188. // Hide on entering full screen
  1189. _toggleClass(player.controls, config.classes.hover, false);
  1190. // Keep an eye on the mouse location in relation to controls
  1191. _toggleHandler(player.controls, 'mouseenter mouseleave', _setMouseOver, player.isFullscreen);
  1192. // Show the controls on mouse move
  1193. _toggleHandler(player.container, 'mousemove', _showControls, player.isFullscreen);
  1194. }
  1195. }
  1196. // Bail from faux-fullscreen
  1197. function _handleEscapeFullscreen(event) {
  1198. // If it's a keypress and not escape, bail
  1199. if ((event.which || event.charCode || event.keyCode) === 27 && player.isFullscreen) {
  1200. _toggleFullscreen();
  1201. }
  1202. }
  1203. // Set volume
  1204. function _setVolume(volume) {
  1205. // Use default if no value specified
  1206. if (typeof volume === 'undefined') {
  1207. if (config.storage.enabled && _storage().supported) {
  1208. volume = window.localStorage[config.storage.key] || config.volume;
  1209. } else {
  1210. volume = config.volume;
  1211. }
  1212. }
  1213. // Maximum is 10
  1214. if (volume > 10) {
  1215. volume = 10;
  1216. }
  1217. // Minimum is 0
  1218. if (volume < 0) {
  1219. volume = 0;
  1220. }
  1221. // Set the player volume
  1222. player.media.volume = parseFloat(volume / 10);
  1223. // YouTube
  1224. if (player.type == 'youtube') {
  1225. player.embed.setVolume(player.media.volume * 100);
  1226. // Trigger timeupdate
  1227. _triggerEvent(player.media, 'volumechange');
  1228. }
  1229. // Toggle muted state
  1230. if (player.media.muted && volume > 0) {
  1231. _toggleMute();
  1232. }
  1233. }
  1234. // Mute
  1235. function _toggleMute(muted) {
  1236. // If the method is called without parameter, toggle based on current value
  1237. if (typeof muted !== 'boolean') {
  1238. muted = !player.media.muted;
  1239. }
  1240. // Set button state
  1241. _toggleState(player.buttons.mute, muted);
  1242. // Set mute on the player
  1243. player.media.muted = muted;
  1244. // YouTube
  1245. if (player.type === 'youtube') {
  1246. player.embed[player.media.muted ? 'mute' : 'unMute']();
  1247. // Trigger timeupdate
  1248. _triggerEvent(player.media, 'volumechange');
  1249. }
  1250. }
  1251. // Update volume UI and storage
  1252. function _updateVolume() {
  1253. // Get the current volume
  1254. var volume = player.media.muted ? 0 : (player.media.volume * 10);
  1255. // Update the <input type="range"> if present
  1256. if (player.supported.full && player.volume) {
  1257. player.volume.value = volume;
  1258. }
  1259. // Store the volume in storage
  1260. if (config.storage.enabled && _storage().supported) {
  1261. window.localStorage.setItem(config.storage.key, volume);
  1262. }
  1263. // Toggle class if muted
  1264. _toggleClass(player.container, config.classes.muted, (volume === 0));
  1265. // Update checkbox for mute state
  1266. if (player.supported.full && player.buttons.mute) {
  1267. _toggleState(player.buttons.mute, (volume === 0));
  1268. }
  1269. }
  1270. // Toggle captions
  1271. function _toggleCaptions(show) {
  1272. // If there's no full support, or there's no caption toggle
  1273. if (!player.supported.full || !player.buttons.captions) {
  1274. return;
  1275. }
  1276. // If the method is called without parameter, toggle based on current value
  1277. if (typeof show !== 'boolean') {
  1278. show = (player.container.className.indexOf(config.classes.captions.active) === -1);
  1279. }
  1280. // Toggle state
  1281. _toggleState(player.buttons.captions, show);
  1282. // Add class hook
  1283. _toggleClass(player.container, config.classes.captions.active, show);
  1284. }
  1285. // Check if media is loading
  1286. function _checkLoading(event) {
  1287. var loading = (event.type === 'waiting');
  1288. // Clear timer
  1289. clearTimeout(player.loadingTimer);
  1290. // Timer to prevent flicker when seeking
  1291. player.loadingTimer = setTimeout(function () {
  1292. _toggleClass(player.container, config.classes.loading, loading);
  1293. }, (loading ? 250 : 0));
  1294. }
  1295. // Update <progress> elements
  1296. function _updateProgress(event) {
  1297. var progress = player.progress.played.bar,
  1298. text = player.progress.played.text,
  1299. value = 0;
  1300. if (event) {
  1301. switch (event.type) {
  1302. // Video playing
  1303. case 'timeupdate':
  1304. case 'seeking':
  1305. value = _getPercentage(player.media.currentTime, player.media.duration);
  1306. // Set seek range value only if it's a 'natural' time event
  1307. if (event.type == 'timeupdate' && player.buttons.seek) {
  1308. player.buttons.seek.value = value;
  1309. }
  1310. break;
  1311. // Events from seek range
  1312. case 'change':
  1313. case 'input':
  1314. value = event.target.value;
  1315. break;
  1316. // Check buffer status
  1317. case 'playing':
  1318. case 'progress':
  1319. progress = player.progress.buffer.bar;
  1320. text = player.progress.buffer.text;
  1321. value = (function () {
  1322. var buffered = player.media.buffered;
  1323. // HTML5
  1324. if (buffered && buffered.length) {
  1325. return _getPercentage(buffered.end(0), player.media.duration);
  1326. }
  1327. // YouTube returns between 0 and 1
  1328. else if (typeof buffered === 'number') {
  1329. return (buffered * 100);
  1330. }
  1331. return 0;
  1332. })();
  1333. }
  1334. }
  1335. // Set values
  1336. if (progress) {
  1337. progress.value = value;
  1338. }
  1339. if (text) {
  1340. text.innerHTML = value;
  1341. }
  1342. }
  1343. // Update the displayed time
  1344. function _updateTimeDisplay(time, element) {
  1345. // Bail if there's no duration display
  1346. if (!element) {
  1347. return;
  1348. }
  1349. player.secs = parseInt(time % 60);
  1350. player.mins = parseInt((time / 60) % 60);
  1351. player.hours = parseInt(((time / 60) / 60) % 60);
  1352. // Do we need to display hours?
  1353. var displayHours = (parseInt(((player.media.duration / 60) / 60) % 60) > 0);
  1354. // Ensure it's two digits. For example, 03 rather than 3.
  1355. player.secs = ('0' + player.secs).slice(-2);
  1356. player.mins = ('0' + player.mins).slice(-2);
  1357. // Render
  1358. element.innerHTML = (displayHours ? player.hours + ':' : '') + player.mins + ':' + player.secs;
  1359. }
  1360. // Show the duration on metadataloaded
  1361. function _displayDuration() {
  1362. var duration = player.media.duration || 0;
  1363. // If there's only one time display, display duration there
  1364. if (!player.duration && config.displayDuration && player.media.paused) {
  1365. _updateTimeDisplay(duration, player.currentTime);
  1366. }
  1367. // If there's a duration element, update content
  1368. if (player.duration) {
  1369. _updateTimeDisplay(duration, player.duration);
  1370. }
  1371. }
  1372. // Handle time change event
  1373. function _timeUpdate(event) {
  1374. // Duration
  1375. _updateTimeDisplay(player.media.currentTime, player.currentTime);
  1376. // Playing progress
  1377. _updateProgress(event);
  1378. }
  1379. // Remove <source> children and src attribute
  1380. function _removeSources() {
  1381. // Find child <source> elements
  1382. var sources = player.media.querySelectorAll('source');
  1383. // Remove each
  1384. for (var i = sources.length - 1; i >= 0; i--) {
  1385. _remove(sources[i]);
  1386. }
  1387. // Remove src attribute
  1388. player.media.removeAttribute('src');
  1389. }
  1390. // Inject a source
  1391. function _addSource(attributes) {
  1392. if (attributes.src) {
  1393. // Create a new <source>
  1394. var element = document.createElement('source');
  1395. // Set all passed attributes
  1396. _setAttributes(element, attributes);
  1397. // Inject the new source
  1398. _prependChild(player.media, element);
  1399. }
  1400. }
  1401. // Update source
  1402. // Sources are not checked for support so be careful
  1403. function _parseSource(sources) {
  1404. // YouTube
  1405. if (player.type === 'youtube' && typeof sources === 'string') {
  1406. // Destroy YouTube instance
  1407. player.embed.destroy();
  1408. // Re-setup YouTube
  1409. // We don't use loadVideoBy[x] here since it has issues
  1410. _setupYouTube(sources);
  1411. // Update times
  1412. _timeUpdate();
  1413. // Bail
  1414. return;
  1415. }
  1416. // Pause playback (webkit freaks out)
  1417. _pause();
  1418. // Restart
  1419. _seek();
  1420. // Remove current sources
  1421. _removeSources();
  1422. // If a single source is passed
  1423. // .source('path/to/video.mp4')
  1424. if (typeof sources === 'string') {
  1425. _addSource({
  1426. src: sources
  1427. });
  1428. }
  1429. // An array of source objects
  1430. // Check if a source exists, use that or set the 'src' attribute?
  1431. // .source([{ src: 'path/to/video.mp4', type: 'video/mp4' },{ src: 'path/to/video.webm', type: 'video/webm' }])
  1432. else if (sources.constructor === Array) {
  1433. for (var index in sources) {
  1434. _addSource(sources[index]);
  1435. }
  1436. }
  1437. if (player.supported.full) {
  1438. // Reset time display
  1439. _timeUpdate();
  1440. // Update the UI
  1441. _checkPlaying();
  1442. }
  1443. // Re-load sources
  1444. player.media.load();
  1445. // Play if autoplay attribute is present
  1446. if (player.media.getAttribute('autoplay') !== null) {
  1447. _play();
  1448. }
  1449. }
  1450. // Update poster
  1451. function _updatePoster(source) {
  1452. if (player.type === 'video') {
  1453. player.media.setAttribute('poster', source);
  1454. }
  1455. }
  1456. // Listen for events
  1457. function _listeners() {
  1458. // IE doesn't support input event, so we fallback to change
  1459. var inputEvent = (player.browser.name == 'IE' ? 'change' : 'input');
  1460. // Detect tab focus
  1461. function checkFocus() {
  1462. var focused = document.activeElement;
  1463. if (!focused || focused == document.body) {
  1464. focused = null;
  1465. } else if (document.querySelector) {
  1466. focused = document.querySelector(':focus');
  1467. }
  1468. for (var button in player.buttons) {
  1469. var element = player.buttons[button];
  1470. _toggleClass(element, 'tab-focus', (element === focused));
  1471. }
  1472. }
  1473. _on(window, 'keyup', function (event) {
  1474. var code = (event.keyCode ? event.keyCode : event.which);
  1475. if (code == 9) {
  1476. checkFocus();
  1477. }
  1478. });
  1479. for (var button in player.buttons) {
  1480. var element = player.buttons[button];
  1481. _on(element, 'blur', function () {
  1482. _toggleClass(element, 'tab-focus', false);
  1483. });
  1484. }
  1485. // Play
  1486. _on(player.buttons.play, 'click', function () {
  1487. _play();
  1488. setTimeout(function () {
  1489. player.buttons.pause.focus();
  1490. }, 100);
  1491. });
  1492. // Pause
  1493. _on(player.buttons.pause, 'click', function () {
  1494. _pause();
  1495. setTimeout(function () {
  1496. player.buttons.play.focus();
  1497. }, 100);
  1498. });
  1499. // Restart
  1500. _on(player.buttons.restart, 'click', _seek);
  1501. // Rewind
  1502. _on(player.buttons.rewind, 'click', _rewind);
  1503. // Fast forward
  1504. _on(player.buttons.forward, 'click', _forward);
  1505. // Seek
  1506. _on(player.buttons.seek, inputEvent, _seek);
  1507. // Set volume
  1508. _on(player.volume, inputEvent, function () {
  1509. _setVolume(this.value);
  1510. });
  1511. // Mute
  1512. _on(player.buttons.mute, 'click', _toggleMute);
  1513. // Fullscreen
  1514. _on(player.buttons.fullscreen, 'click', _toggleFullscreen);
  1515. // Handle user exiting fullscreen by escaping etc
  1516. if (fullscreen.supportsFullScreen) {
  1517. _on(document, fullscreen.fullScreenEventName, _toggleFullscreen);
  1518. }
  1519. // Time change on media
  1520. _on(player.media, 'timeupdate seeking', _timeUpdate);
  1521. // Update manual captions
  1522. _on(player.media, 'timeupdate', _seekManualCaptions);
  1523. // Display duration
  1524. _on(player.media, 'loadedmetadata', _displayDuration);
  1525. // Captions
  1526. _on(player.buttons.captions, 'click', _toggleCaptions);
  1527. // Handle the media finishing
  1528. _on(player.media, 'ended', function () {
  1529. // Clear
  1530. if (player.type === 'video') {
  1531. player.captionsContainer.innerHTML = '';
  1532. }
  1533. // Reset UI
  1534. _checkPlaying();
  1535. });
  1536. // Check for buffer progress
  1537. _on(player.media, 'progress playing', _updateProgress);
  1538. // Handle native mute
  1539. _on(player.media, 'volumechange', _updateVolume);
  1540. // Handle native play/pause
  1541. _on(player.media, 'play pause', _checkPlaying);
  1542. // Loading
  1543. _on(player.media, 'waiting canplay seeked', _checkLoading);
  1544. // Click video
  1545. if (player.type === 'video' && config.click) {
  1546. _on(player.videoContainer, 'click', function () {
  1547. if (player.media.paused) {
  1548. _triggerEvent(player.buttons.play, 'click');
  1549. } else if (player.media.ended) {
  1550. _seek();
  1551. _triggerEvent(player.buttons.play, 'click');
  1552. } else {
  1553. _triggerEvent(player.buttons.pause, 'click');
  1554. }
  1555. });
  1556. }
  1557. }
  1558. // Destroy an instance
  1559. // Event listeners are removed when elements are removed
  1560. // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
  1561. function _destroy() {
  1562. // Bail if the element is not initialized
  1563. if (!player.init) {
  1564. return null;
  1565. }
  1566. // Reset container classname
  1567. player.container.setAttribute('class', config.selectors.container.replace('.', ''));
  1568. // Remove init flag
  1569. player.init = false;
  1570. // Remove controls
  1571. _remove(_getElement(config.selectors.controls));
  1572. // YouTube
  1573. if (player.type === 'youtube') {
  1574. player.embed.destroy();
  1575. return;
  1576. }
  1577. // If video, we need to remove some more
  1578. if (player.type === 'video') {
  1579. // Remove captions
  1580. _remove(_getElement(config.selectors.captions));
  1581. // Remove video wrapper
  1582. _unwrap(player.videoContainer);
  1583. }
  1584. // Restore native video controls
  1585. player.media.setAttribute('controls', '');
  1586. // Clone the media element to remove listeners
  1587. // http://stackoverflow.com/questions/19469881/javascript-remove-all-event-listeners-of-specific-type
  1588. var clone = player.media.cloneNode(true);
  1589. player.media.parentNode.replaceChild(clone, player.media);
  1590. }
  1591. // Setup a player
  1592. function _init() {
  1593. // Bail if the element is initialized
  1594. if (player.init) {
  1595. return null;
  1596. }
  1597. // Setup the fullscreen api
  1598. fullscreen = _fullscreen();
  1599. // Sniff out the browser
  1600. player.browser = _browserSniff();
  1601. // Get the media element
  1602. player.media = player.container.querySelectorAll('audio, video, div')[0];
  1603. // Set media type
  1604. var tagName = player.media.tagName.toLowerCase();
  1605. if (tagName === 'div') {
  1606. player.type = player.media.getAttribute('data-type');
  1607. } else {
  1608. player.type = tagName;
  1609. }
  1610. // Check for full support
  1611. player.supported = api.supported(player.type);
  1612. // If no native support, bail
  1613. if (!player.supported.basic) {
  1614. return false;
  1615. }
  1616. // Debug info
  1617. _log(player.browser.name + ' ' + player.browser.version);
  1618. // Setup media
  1619. _setupMedia();
  1620. // Setup interface
  1621. if (player.type == 'video' || player.type == 'audio') {
  1622. // Bail if no support
  1623. if (!player.supported.full) {
  1624. // Successful setup
  1625. player.init = true;
  1626. // Don't inject controls if no full support
  1627. return;
  1628. }
  1629. // Setup UI
  1630. _setupInterface();
  1631. // Display duration if available
  1632. if (config.displayDuration) {
  1633. _displayDuration();
  1634. }
  1635. // Set up aria-label for Play button with the title option
  1636. _setupPlayAria();
  1637. }
  1638. // Successful setup
  1639. player.init = true;
  1640. }
  1641. function _setupInterface() {
  1642. // Inject custom controls
  1643. _injectControls();
  1644. // Find the elements
  1645. if (!_findElements()) {
  1646. return false;
  1647. }
  1648. // Captions
  1649. _setupCaptions();
  1650. // Set volume
  1651. _setVolume();
  1652. _updateVolume();
  1653. // Setup fullscreen
  1654. _setupFullscreen();
  1655. // Listeners
  1656. _listeners();
  1657. }
  1658. // Initialize instance
  1659. _init();
  1660. // If init failed, return an empty object
  1661. if (!player.init) {
  1662. return {};
  1663. }
  1664. return {
  1665. media: player.media,
  1666. play: _play,
  1667. pause: _pause,
  1668. restart: _seek,
  1669. rewind: _rewind,
  1670. forward: _forward,
  1671. seek: _seek,
  1672. source: _parseSource,
  1673. poster: _updatePoster,
  1674. setVolume: _setVolume,
  1675. togglePlay: _togglePlay,
  1676. toggleMute: _toggleMute,
  1677. toggleCaptions: _toggleCaptions,
  1678. toggleFullscreen: _toggleFullscreen,
  1679. isFullscreen: function () {
  1680. return player.isFullscreen || false;
  1681. },
  1682. support: function (mimeType) {
  1683. return _supportMime(player, mimeType);
  1684. },
  1685. destroy: _destroy,
  1686. restore: _init
  1687. };
  1688. }
  1689. // Check for support
  1690. api.supported = function (type) {
  1691. var browser = _browserSniff(),
  1692. oldIE = (browser.name === 'IE' && browser.version <= 9),
  1693. iPhone = /iPhone|iPod/i.test(navigator.userAgent),
  1694. audio = !!document.createElement('audio').canPlayType,
  1695. video = !!document.createElement('video').canPlayType,
  1696. basic, full;
  1697. switch (type) {
  1698. case 'video':
  1699. basic = video;
  1700. full = (basic && (!oldIE && !iPhone));
  1701. break;
  1702. case 'audio':
  1703. basic = audio;
  1704. full = (basic && !oldIE);
  1705. break;
  1706. case 'youtube':
  1707. basic = true;
  1708. full = (!oldIE && !iPhone);
  1709. break;
  1710. default:
  1711. basic = (audio && video);
  1712. full = (basic && !oldIE);
  1713. }
  1714. return {
  1715. basic: basic,
  1716. full: full
  1717. };
  1718. };
  1719. // Expose setup function
  1720. api.setup = function (options) {
  1721. // Extend the default options with user specified
  1722. config = _extend(defaults, options);
  1723. // Bail if disabled or no basic support
  1724. // You may want to disable certain UAs etc
  1725. if (!config.enabled || !api.supported().basic) {
  1726. return false;
  1727. }
  1728. // Get the players
  1729. var elements = document.querySelectorAll(config.selectors.container),
  1730. players = [];
  1731. // Create a player instance for each element
  1732. for (var i = elements.length - 1; i >= 0; i--) {
  1733. // Get the current element
  1734. var element = elements[i];
  1735. // Setup a player instance and add to the element
  1736. if (typeof element.plyr === 'undefined') {
  1737. // Create new instance
  1738. var instance = new Plyr(element);
  1739. // Set plyr to false if setup failed
  1740. element.plyr = (Object.keys(instance).length ? instance : false);
  1741. // Callback
  1742. if (typeof config.onSetup === 'function') {
  1743. config.onSetup.apply(element.plyr);
  1744. }
  1745. }
  1746. // Add to return array even if it's already setup
  1747. players.push(element.plyr);
  1748. }
  1749. return players;
  1750. };
  1751. }(this.plyr = this.plyr || {}));