You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

jquery.acornmediaplayer.js 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. /*
  2. * Acorn Media Player - jQuery plugin 1.6
  3. *
  4. * Copyright (C) 2012 Ionut Cristian Colceriu
  5. *
  6. * Dual licensed under the MIT and GPL licenses:
  7. * http://www.opensource.org/licenses/mit-license.php
  8. * http://www.gnu.org/licenses/gpl.html
  9. *
  10. * www.ghinda.net
  11. * contact@ghinda.net
  12. *
  13. */
  14. (function($) {
  15. $.fn.acornMediaPlayer = function(options) {
  16. /*
  17. * Define default plugin options
  18. */
  19. var defaults = {
  20. theme: 'access',
  21. nativeSliders: false,
  22. volumeSlider: 'horizontal',
  23. captionsOn: false
  24. };
  25. options = $.extend(defaults, options);
  26. /*
  27. * Function for generating a unique identifier using the current date and time
  28. * Used for generating an ID for the media elmenet when none is available
  29. */
  30. var uniqueID = function() {
  31. var currentDate = new Date();
  32. return currentDate.getTime();
  33. };
  34. /*
  35. * Detect support for localStorage
  36. */
  37. function supports_local_storage() {
  38. try {
  39. return 'localStorage' in window && window.localStorage !== null;
  40. } catch(e){
  41. return false;
  42. }
  43. }
  44. /*
  45. * Get the volume value from localStorage
  46. * If no value is present, define as maximum
  47. */
  48. var volume = (supports_local_storage) ? localStorage.getItem('acornvolume') : 1;
  49. if(!volume) {
  50. volume = 1;
  51. }
  52. /*
  53. * Main plugin function
  54. * It will be called on each element in the matched set
  55. */
  56. var acornPlayer = function() {
  57. // set the acorn object, will contain the needed DOM nodes and others
  58. var acorn = {
  59. $self: $(this)
  60. };
  61. var loadedMetadata; // Is the metadata loaded
  62. var seeking; // The user is seeking the media
  63. var wasPlaying; // Media was playing when the seeking started
  64. var fullscreenMode; // The media is in fullscreen mode
  65. var captionsActive; // Captions are active
  66. /* Define all the texts used
  67. * This makes it easier to maintain, make translations, etc.
  68. */
  69. var text = {
  70. play: 'Play',
  71. playTitle: 'Start the playback',
  72. pause: 'Pause',
  73. pauseTitle: 'Pause the playback',
  74. mute: 'Mute',
  75. unmute: 'Unmute',
  76. fullscreen: 'Fullscreen',
  77. fullscreenTitle: 'Toggle fullscreen mode',
  78. swap: 'Swap',
  79. swapTitle: 'Toggle video and presention swap',
  80. volumeTitle: 'Volume control',
  81. seekTitle: 'Video seek control',
  82. captions: 'Captions',
  83. captionsTitle: 'Show captions',
  84. captionsChoose: 'Choose caption',
  85. transcript: 'Transcript',
  86. transcriptTitle: 'Show transcript'
  87. };
  88. // main wrapper element
  89. var $wrapper = $('<div class="acorn-player" role="application"></div>').addClass(options.theme);
  90. /*
  91. * Define attribute tabindex on the main element to make it readchable by keyboard
  92. * Useful when "aria-describedby" is present
  93. *
  94. * It makes more sense for screen reader users to first reach the actual <video> or <audio> elment and read of description of it,
  95. * than directly reach the Media Player controls, without knowing what they control.
  96. */
  97. acorn.$self.attr('tabindex', '0');
  98. /*
  99. * Check if the main element has an ID attribute
  100. * If not present, generate one
  101. */
  102. acorn.id = acorn.$self.attr('id');
  103. if(!acorn.id) {
  104. acorn.id = 'acorn' + uniqueID();
  105. acorn.$self.attr('id', acorn.id);
  106. }
  107. /*
  108. * Markup for the fullscreen button
  109. * If the element is not <video> we leave if blank, as the button if useless on <audio> elements
  110. */
  111. var fullscreenBtnMarkup = (acorn.$self.is('video')) ? '<button class="acorn-fullscreen-button" title="' + text.fullscreenTitle + '" aria-controls="' + acorn.id + '" tabindex="3">' + text.fullscreen + '</button>' : '';
  112. /*
  113. * Markup for the swap button
  114. * If the element is not <video> we leave if blank, as the button if useless on <audio> elements
  115. */
  116. var swapBtnMarkup = (acorn.$self.is('video')) ? '<button class="acorn-swap-button" title="' + text.swapTitle + '" aria-controls="' + acorn.id + '" tabindex="4" >' + text.swap + '</button>' : '';
  117. /*
  118. * Complete markup
  119. */
  120. var template = '<div class="acorn-controls">' +
  121. '<button class="acorn-play-button" title="' + text.playTitle + '" aria-controls="' + acorn.id + '" tabindex="1">' + text.play + '</button>' +
  122. '<input type="range" class="acorn-seek-slider" title="' + text.seekTitle + '" value="0" min="0" max="150" step="0.1" aria-controls="' + acorn.id + '" tabindex="2" />' +
  123. '<span class="acorn-timer">00:00</span>' +
  124. '<div class="acorn-volume-box">' +
  125. '<button class="acorn-volume-button" title="' + text.mute + '" aria-controls="' + acorn.id + '" tabindex="5" >' + text.mute + '</button>' +
  126. '<input type="range" class="acorn-volume-slider" title="' + text.volumeTitle + '" value="1" min="0" max="1" step="0.05" aria-controls="' + acorn.id + '" tabindex="6" />' +
  127. '</div>' +
  128. swapBtnMarkup +
  129. fullscreenBtnMarkup +
  130. '<button class="acorn-caption-button" title="' + text.captionsTitle + '" aria-controls="' + acorn.id + '">' + text.captions + '</button>' +
  131. '<div class="acorn-caption-selector"></div>' +
  132. '<button class="acorn-transcript-button" title="' + text.transcriptTitle + '">' + text.transcript + '</button>' +
  133. '</div>';
  134. var captionMarkup = '<div class="acorn-caption"></div>';
  135. var transcriptMarkup = '<div class="acorn-transcript" role="region" aria-live="assertive"></div>';
  136. /*
  137. * Append the HTML markup
  138. */
  139. // append the wrapper
  140. acorn.$self.after($wrapper);
  141. // For iOS support, I have to clone the node, remove the original, and get a reference to the new one.
  142. // This is because iOS doesn't want to play videos that have just been `moved around`.
  143. // More details on the issue: http://bugs.jquery.com/ticket/8015
  144. $wrapper[0].appendChild( acorn.$self[0].cloneNode(true) );
  145. acorn.$self.trigger('pause');
  146. acorn.$self.remove();
  147. acorn.$self = $wrapper.find('video, audio');
  148. // append the controls and loading mask
  149. acorn.$self.after(template).after('<div class="loading-media"></div>');
  150. /*
  151. * Define the newly created DOM nodes
  152. */
  153. acorn.$container = acorn.$self.parent('.acorn-player');
  154. acorn.$controls = $('.acorn-controls', acorn.$container);
  155. acorn.$playBtn = $('.acorn-play-button', acorn.$container);
  156. acorn.$seek = $('.acorn-seek-slider', acorn.$container);
  157. acorn.$timer = $('.acorn-timer', acorn.$container);
  158. acorn.$volume = $('.acorn-volume-slider', acorn.$container);
  159. acorn.$volumeBtn = $('.acorn-volume-button', acorn.$container);
  160. acorn.$fullscreenBtn = $('.acorn-fullscreen-button', acorn.$container);
  161. acorn.$swapBtn = $('.acorn-swap-button', acorn.$container);
  162. /*
  163. * Append the markup for the Captions and Transcript
  164. * and define newly created DOM nodes for these
  165. */
  166. acorn.$controls.after(captionMarkup);
  167. acorn.$container.after(transcriptMarkup);
  168. acorn.$transcript = acorn.$container.next('.acorn-transcript');
  169. acorn.$transcriptBtn = $('.acorn-transcript-button', acorn.$container);
  170. acorn.$caption = $('.acorn-caption', acorn.$container);
  171. acorn.$captionBtn = $('.acorn-caption-button', acorn.$container);
  172. acorn.$captionSelector = $('.acorn-caption-selector', acorn.$container);
  173. /*
  174. * Use HTML5 "data-" attributes to set the original Width&Height for the <video>
  175. * These are used when returning from Fullscreen Mode
  176. */
  177. acorn.$self.attr('data-width', acorn.$self.width());
  178. acorn.$self.attr('data-height', acorn.$self.height());
  179. /*
  180. * Time formatting function
  181. * Takes the number of seconds as a parameter and return a readable format "minutes:seconds"
  182. * Used with the number of seconds returned by "currentTime"
  183. */
  184. var timeFormat = function(sec) {
  185. var m = Math.floor(sec/60)<10?"0" + Math.floor(sec/60):Math.floor(sec/60);
  186. var s = Math.floor(sec-(m*60))<10?"0" + Math.floor(sec-(m*60)):Math.floor(sec-(m*60));
  187. return m + ":" + s;
  188. };
  189. /*
  190. * PLAY/PAUSE Behaviour
  191. *
  192. * Function for the Play button
  193. * It triggers the native Play or Pause events
  194. */
  195. var playMedia = function() {
  196. if(!acorn.$self.prop('paused')) {
  197. acorn.$self.trigger('pause');
  198. } else {
  199. //acorn.$self.trigger('play');
  200. acorn.$self[0].play();
  201. }
  202. // We return false to stop the followup click event on tablets
  203. return false;
  204. };
  205. /*
  206. * Functions for native playback events (Play, Pause, Ended)
  207. * These are attached to the native media events.
  208. *
  209. * Even if the user is still using some form of native playback control (such as using the Context Menu)
  210. * it will not break the behviour of our player.
  211. */
  212. var startPlayback = function() {
  213. acorn.$playBtn.text(text.pause).attr('title', text.pauseTitle);
  214. acorn.$playBtn.addClass('acorn-paused-button');
  215. // if the metadata is not loaded yet, add the loading class
  216. if (!loadedMetadata) $wrapper.addClass('show-loading');
  217. };
  218. var stopPlayback = function() {
  219. acorn.$playBtn.text(text.play).attr('title', text.playTitle);
  220. acorn.$playBtn.removeClass('acorn-paused-button');
  221. };
  222. /*
  223. * SEEK SLIDER Behaviour
  224. *
  225. * Updates the Timer and Seek Slider values
  226. * Is called on each "timeupdate"
  227. */
  228. var seekUpdate = function() {
  229. var currenttime = acorn.$self.prop('currentTime');
  230. acorn.$timer.text(timeFormat(currenttime));
  231. // If the user is not manualy seeking
  232. if(!seeking) {
  233. // Check type of sliders (Range <input> or jQuery UI)
  234. if(options.nativeSliders) {
  235. acorn.$seek.attr('value', currenttime);
  236. } else {
  237. acorn.$seek.slider('value', currenttime);
  238. }
  239. }
  240. };
  241. /*
  242. * Time formatting function
  243. * Takes the number of seconds as a paramenter
  244. *
  245. * Used with "aria-valuetext" on the Seek Slider to provide a human readable time format to AT
  246. * Returns "X minutes Y seconds"
  247. */
  248. var ariaTimeFormat = function(sec) {
  249. var m = Math.floor(sec/60)<10?"" + Math.floor(sec/60):Math.floor(sec/60);
  250. var s = Math.floor(sec-(m*60))<10?"" + Math.floor(sec-(m*60)):Math.floor(sec-(m*60));
  251. var formatedTime;
  252. var mins = 'minutes';
  253. var secs = 'seconds';
  254. if(m == 1) {
  255. min = 'minute';
  256. }
  257. if(s == 1) {
  258. sec = 'second';
  259. }
  260. if(m === 0) {
  261. formatedTime = s + ' ' + secs;
  262. } else {
  263. formatedTime = m + ' ' + mins + ' ' + s + ' ' + secs;
  264. }
  265. return formatedTime;
  266. };
  267. /*
  268. * jQuery UI slider uses preventDefault when clicking any element
  269. * so it stops the Blur event from being fired.
  270. * This causes problems with the Caption Selector.
  271. * We trigger the Blur event manually.
  272. */
  273. var blurCaptionBtn = function() {
  274. acorn.$captionBtn.trigger('blur');
  275. };
  276. /*
  277. * Triggered when the user starts to seek manually
  278. * Pauses the media during seek and changes the "currentTime" to the slider's value
  279. */
  280. var startSeek = function(e, ui) {
  281. if(!acorn.$self.attr('paused')) {
  282. wasPlaying = true;
  283. }
  284. acorn.$self.trigger('pause');
  285. seeking = true;
  286. var seekLocation;
  287. if(options.nativeSliders) {
  288. seekLocation = acorn.$seek.val();
  289. } else {
  290. seekLocation = ui.value;
  291. }
  292. acorn.$self[0].currentTime = seekLocation;
  293. // manually blur the Caption Button
  294. blurCaptionBtn();
  295. };
  296. /*
  297. * Triggered when user stoped manual seek
  298. * If the media was playing when seek started, it triggeres the playback,
  299. * and updates ARIA attributes
  300. */
  301. var endSeek = function(e, ui) {
  302. if(wasPlaying) {
  303. acorn.$self.trigger('play');
  304. wasPlaying = false;
  305. }
  306. seeking = false;
  307. var sliderUI = $(ui.handle);
  308. sliderUI.attr("aria-valuenow", parseInt(ui.value, 10));
  309. sliderUI.attr("aria-valuetext", ariaTimeFormat(ui.value));
  310. };
  311. /*
  312. * Transforms element into ARIA Slider adding attributes and "tabindex"
  313. * Used on jQuery UI sliders
  314. *
  315. * Will not needed once the jQuery UI slider gets built-in ARIA
  316. */
  317. var initSliderAccess = function (elem, opts) {
  318. var accessDefaults = {
  319. 'role': 'slider',
  320. 'aria-valuenow': parseInt(opts.value, 10),
  321. 'aria-valuemin': parseInt(opts.min, 10),
  322. 'aria-valuemax': parseInt(opts.max, 10),
  323. 'aria-valuetext': opts.valuetext
  324. };
  325. elem.attr(accessDefaults);
  326. };
  327. /*
  328. * Init jQuery UI slider
  329. */
  330. var initSeek = function() {
  331. // get existing classes
  332. var seekClass = acorn.$seek.attr('class');
  333. // create the new markup
  334. var divSeek = '<div class="' + seekClass + '" title="' + text.seekTitle + '"></div>';
  335. acorn.$seek.after(divSeek).remove();
  336. // get the newly created DOM node
  337. acorn.$seek = $('.' + seekClass, acorn.$container);
  338. // create the buffer element
  339. var bufferBar = '<div class="ui-slider-range acorn-buffer"></div>';
  340. acorn.$seek.append(bufferBar);
  341. // get the buffer element DOM node
  342. acorn.$buffer = $('.acorn-buffer', acorn.$container);
  343. // set up the slider options for the jQuery UI slider
  344. var sliderOptions = {
  345. value: 0,
  346. step: 1,
  347. orientation: 'horizontal',
  348. range: 'min',
  349. min: 0,
  350. max: 100
  351. };
  352. // init the jQuery UI slider
  353. acorn.$seek.slider(sliderOptions);
  354. };
  355. /*
  356. * Seek slider update, after metadata is loaded
  357. * Attach events, add the "duration" attribute and generate the jQuery UI Seek Slider
  358. */
  359. var updateSeek = function() {
  360. // Get the duration of the media
  361. var duration = acorn.$self[0].duration;
  362. // Check for the nativeSliders option
  363. if(options.nativeSliders) {
  364. acorn.$seek.attr('max', duration);
  365. acorn.$seek.bind('change', startSeek);
  366. acorn.$seek.bind('mousedown', startSeek);
  367. acorn.$seek.bind('mouseup', endSeek);
  368. } else {
  369. // set up the slider options for the jQuery UI slider
  370. var sliderOptions = {
  371. value: 0,
  372. step: 1,
  373. orientation: 'horizontal',
  374. range: 'min',
  375. min: 0,
  376. max: duration,
  377. slide: startSeek,
  378. stop: endSeek
  379. };
  380. // init the jQuery UI slider
  381. acorn.$seek.slider('option', sliderOptions);
  382. // add valuetext value to the slider options for better ARIA values
  383. sliderOptions.valuetext = ariaTimeFormat(sliderOptions.value);
  384. // accessify the slider
  385. initSliderAccess(acorn.$seek.find('.ui-slider-handle'), sliderOptions);
  386. // manully blur the Caption Button when clicking the handle
  387. $('.ui-slider-handle', acorn.$seek).click(blurCaptionBtn);
  388. // set the tab index
  389. $('.ui-slider-handle', acorn.$seek).attr("tabindex", "2");
  390. // show buffering progress on progress
  391. acorn.$self.bind('progress', showBuffer);
  392. }
  393. $wrapper.removeClass('show-loading');
  394. // remove the loading element
  395. //acorn.$self.next('.loading-media').remove();
  396. };
  397. /*
  398. * Show buffering progress
  399. */
  400. var showBuffer = function(e) {
  401. var max = parseInt(acorn.$self.prop('duration'), 10);
  402. var tr = this.buffered;
  403. if(tr && tr.length) {
  404. var buffer = parseInt(this.buffered.end(0)-this.buffered.start(0), 10);
  405. var bufferWidth = (buffer*100)/max;
  406. acorn.$buffer.css('width', bufferWidth + '%');
  407. }
  408. };
  409. /*
  410. * VOLUME BUTTON and SLIDER Behaviour
  411. *
  412. * Change volume using the Volume Slider
  413. * Also update ARIA attributes and set the volume value as a localStorage item
  414. */
  415. var changeVolume = function(e, ui) {
  416. // get the slider value
  417. volume = ui.value;
  418. // set the value as a localStorage item
  419. localStorage.setItem('acornvolume', volume);
  420. // check if the volume was muted before
  421. if(acorn.$self.prop('muted')) {
  422. acorn.$self.prop('muted', false);
  423. acorn.$volumeBtn.removeClass('acorn-volume-mute');
  424. acorn.$volumeBtn.text(text.mute).attr('title', text.mute);
  425. }
  426. // set the new volume on the media
  427. acorn.$self.prop('volume', volume);
  428. // set the ARIA attributes
  429. acorn.$volume.$handle.attr("aria-valuenow", Math.round(volume*100));
  430. acorn.$volume.$handle.attr("aria-valuetext", Math.round(volume*100) + ' percent');
  431. // manually trigger the Blur event on the Caption Button
  432. blurCaptionBtn();
  433. };
  434. /*
  435. * Mute and Unmute volume
  436. * Also add classes and change label on the Volume Button
  437. */
  438. var muteVolume = function() {
  439. if(acorn.$self.prop('muted') === true) {
  440. acorn.$self.prop('muted', false);
  441. if(options.nativeSliders) {
  442. acorn.$volume.val(volume);
  443. } else {
  444. acorn.$volume.slider('value', volume);
  445. }
  446. acorn.$volumeBtn.removeClass('acorn-volume-mute');
  447. acorn.$volumeBtn.text(text.mute).attr('title', text.mute);
  448. } else {
  449. acorn.$self.prop('muted', true);
  450. if(options.nativeSliders) {
  451. acorn.$volume.val('0');
  452. } else {
  453. acorn.$volume.slider('value', '0');
  454. }
  455. acorn.$volumeBtn.addClass('acorn-volume-mute');
  456. acorn.$volumeBtn.text(text.unmute).attr('title', text.unmute);
  457. }
  458. };
  459. /*
  460. * Init the Volume Button and Slider
  461. *
  462. * Attach events, create the jQuery UI Slider for the Volume Slider and add ARIA support
  463. */
  464. var initVolume = function() {
  465. if(options.nativeSliders) {
  466. acorn.$volume.bind('change', function() {
  467. acorn.$self.prop('muted',false);
  468. volume = acorn.$volume.val();
  469. acorn.$self.prop('volume', volume);
  470. });
  471. } else {
  472. var volumeClass = acorn.$volume.attr('class');
  473. var divVolume = '<div class="' + volumeClass + '" title="' + text.volumeTitle + '"></div>';
  474. acorn.$volume.after(divVolume).remove();
  475. acorn.$volume = $('.' + volumeClass, acorn.$container);
  476. var volumeSliderOptions = {
  477. value: volume,
  478. orientation: options.volumeSlider,
  479. range: "min",
  480. max: 1,
  481. min: 0,
  482. step: 0.1,
  483. animate: false,
  484. slide: changeVolume
  485. };
  486. acorn.$volume.slider(volumeSliderOptions);
  487. acorn.$volume.$handle = acorn.$volume.find('.ui-slider-handle');
  488. // change and add values to volumeSliderOptions for better values in the ARIA attributes
  489. volumeSliderOptions.max = 100;
  490. volumeSliderOptions.value = volumeSliderOptions.value * 100;
  491. volumeSliderOptions.valuetext = volumeSliderOptions.value + ' percent';
  492. initSliderAccess(acorn.$volume.$handle, volumeSliderOptions);
  493. acorn.$volume.$handle.attr("tabindex", "6");
  494. // show the volume slider when it is tabbed into
  495. acorn.$volume.$handle.focus(function(){
  496. if (!acorn.$volume.parent().is(":hover")) {
  497. acorn.$volume.addClass("handle-focused");
  498. }
  499. });
  500. acorn.$volume.$handle.blur(function(){
  501. acorn.$volume.removeClass("handle-focused");
  502. });
  503. // manully blur the Caption Button when clicking the handle
  504. $('.ui-slider-handle', acorn.$volume).click(blurCaptionBtn);
  505. }
  506. acorn.$volumeBtn.click(muteVolume);
  507. };
  508. /*
  509. * FULLSCREEN Behviour
  510. *
  511. * Resize the video while in Fullscreen Mode
  512. * Attached to window.resize
  513. */
  514. var resizeFullscreenVideo = function() {
  515. acorn.$self.attr({
  516. 'width': $(window).width(),
  517. 'height': $(window).height()
  518. });
  519. };
  520. /*
  521. * Enter and exit Fullscreen Mode
  522. *
  523. * Resizes the Width & Height of the <video> element
  524. * and add classes to the controls and wrapper
  525. */
  526. var goFullscreen = function() {
  527. if(fullscreenMode) {
  528. if(acorn.$self[0].webkitSupportsFullscreen) {
  529. acorn.$self[0].webkitExitFullScreen();
  530. } else {
  531. $('body').css('overflow', 'auto');
  532. var w = acorn.$self.attr('data-width');
  533. var h = acorn.$self.attr('data-height');
  534. acorn.$self.removeClass('fullscreen-video').attr({
  535. 'width': w,
  536. 'height': h
  537. });
  538. $(window).unbind('resize');
  539. acorn.$controls.removeClass('fullscreen-controls');
  540. }
  541. fullscreenMode = false;
  542. } else {
  543. if(acorn.$self[0].webkitSupportsFullscreen) {
  544. acorn.$self[0].webkitEnterFullScreen();
  545. } else if (acorn.$self[0].mozRequestFullScreen) {
  546. acorn.$self[0].mozRequestFullScreen();
  547. acorn.$self.attr('controls', 'controls');
  548. document.addEventListener('mozfullscreenchange', function() {
  549. console.log('screenchange event found');
  550. if (!document.mozFullScreenElement) {
  551. acorn.$self.removeAttr('controls');
  552. //document.removeEventListener('mozfullscreenchange');
  553. }
  554. });
  555. } else {
  556. $('body').css('overflow', 'hidden');
  557. acorn.$self.addClass('fullscreen-video').attr({
  558. width: $(window).width(),
  559. height: $(window).height()
  560. });
  561. $(window).resize(resizeFullscreenVideo);
  562. acorn.$controls.addClass('fullscreen-controls');
  563. }
  564. fullscreenMode = true;
  565. }
  566. };
  567. /*
  568. * Swap the video and presentation areas
  569. *
  570. * Resizes and moves based on hard coded numbers
  571. * Uses css to move it
  572. */
  573. var goSwap = function() {
  574. acorn.$self.trigger('swap');
  575. }
  576. /*
  577. * CAPTIONS Behaviour
  578. *
  579. * Turning off the captions
  580. * When selecting "None" from the Caption Selector or when the caption fails to load
  581. */
  582. var captionBtnActiveClass = 'acorn-caption-active';
  583. var captionBtnLoadingClass = 'acorn-caption-loading';
  584. var transcriptBtnActiveClass = 'acorn-transcript-active';
  585. var captionRadioName = 'acornCaptions' + uniqueID();
  586. var captionOff = function() {
  587. for (var i = 0; i < acorn.$track.length; i++) {
  588. var track = acorn.$track[i];
  589. track.track.mode = "disabled";
  590. }
  591. acorn.$captionBtn.removeClass(captionBtnActiveClass);
  592. };
  593. /*
  594. * Initialize the Caption Selector
  595. * Used when multiple <track>s are present
  596. */
  597. var initCaptionSelector = function() {
  598. // calculate the position relative to the parent controls element
  599. var setUpCaptionSelector = function() {
  600. var pos = acorn.$captionBtn.offset();
  601. var top = pos.top - acorn.$captionSelector.outerHeight(true);
  602. var left = pos.left - ((acorn.$captionSelector.outerWidth(true) - acorn.$captionBtn.outerWidth(true))/2);
  603. var parentPos = acorn.$controls.offset();
  604. left = left - parentPos.left;
  605. top = top - parentPos.top;
  606. acorn.$captionSelector.css({
  607. 'top': top,
  608. 'left': left
  609. });
  610. };
  611. acorn.$fullscreenBtn.click(setUpCaptionSelector);
  612. $(window).resize(function() {
  613. setUpCaptionSelector();
  614. });
  615. setUpCaptionSelector();
  616. /*
  617. * Show and hide the caption selector based on focus rather than hover.
  618. * This benefits both touchscreen and AT users.
  619. */
  620. var hideSelector; // timeout for hiding the Caption Selector
  621. var showCaptionSelector = function() {
  622. if(hideSelector) {
  623. clearTimeout(hideSelector);
  624. }
  625. acorn.$captionSelector.show();
  626. };
  627. var hideCaptionSelector = function() {
  628. hideSelector = setTimeout(function() {
  629. acorn.$captionSelector.hide();
  630. }, 200);
  631. };
  632. /* Little TEMPORARY hack to focus the caption button on click
  633. This is because Webkit does not focus the button on click */
  634. acorn.$captionBtn.click(function() {
  635. $(this).focus();
  636. });
  637. acorn.$captionBtn.bind('focus', showCaptionSelector);
  638. acorn.$captionBtn.bind('blur', hideCaptionSelector);
  639. $('input[name=' + captionRadioName + ']', acorn.$container).bind('focus', showCaptionSelector);
  640. $('input[name=' + captionRadioName + ']', acorn.$container).bind('blur', hideCaptionSelector);
  641. /*
  642. * Make the Caption Selector focusable and attach events to it
  643. * If we wouldn't do this, when we'd use the scroll on the Caption Selector, it would dissapear
  644. */
  645. acorn.$captionSelector.attr('tabindex', '-1');
  646. acorn.$captionSelector.bind('focus', showCaptionSelector);
  647. acorn.$captionSelector.bind('blur', hideCaptionSelector);
  648. };
  649. /*
  650. * Current caption loader
  651. * Loads a SRT file and uses it as captions
  652. * Takes the url as a parameter
  653. */
  654. var loadCaption = function(url) {
  655. // Iterate through the available captions, and disable all but the selected one
  656. for (var i = 0; i < acorn.$track.length; i++) {
  657. var track = acorn.$track[i];
  658. if (track.getAttribute('src') == url) {
  659. track.track.mode = "showing";
  660. // TODO transcript markup?
  661. // show the Transcript Button
  662. //acorn.$transcriptBtn.show();
  663. /*
  664. * Generate the markup for the transcript
  665. * Markup based on Bruce Lawson's “Accessible HTML5 Video with JavaScripted captions”
  666. * http://dev.opera.com/articles/view/accessible-html5-video-with-javascripted-captions/
  667. */
  668. //var transcriptText = '';
  669. //$(captions).each(function() {
  670. // transcriptText += '<span data-begin="' + parseInt(this.start, 10) + '" data-end=' + parseInt(this.end, 10) + '>' + this.content.replace("'","") + '</span>';
  671. //});
  672. // append the generated markup
  673. //acorn.$transcript.html(transcriptText);
  674. } else {
  675. track.track.mode = "disabled";
  676. }
  677. }
  678. captionsActive = true;
  679. acorn.$captionBtn.addClass(captionBtnActiveClass);
  680. };
  681. /*
  682. * Show or hide the Transcript based on the presence of the active class
  683. */
  684. var showTranscript = function() {
  685. if($(this).hasClass(transcriptBtnActiveClass)) {
  686. acorn.$transcript.hide();
  687. } else {
  688. acorn.$transcript.show();
  689. }
  690. $(this).toggleClass(transcriptBtnActiveClass);
  691. };
  692. /*
  693. * Caption loading and initialization
  694. */
  695. var initCaption = function() {
  696. // Check if we have browser support for captions
  697. if (typeof(TextTrack) === "undefined") {
  698. return;
  699. }
  700. // get all <track> elements
  701. acorn.$track = $('track', acorn.$self);
  702. // if there is at least one <track> element, show the Caption Button
  703. if(acorn.$track.length) {
  704. acorn.$captionBtn.show();
  705. }
  706. // check if there is more than one <track> element
  707. // if there is more than one track element we'll create the Caption Selector
  708. if(acorn.$track.length>1) {
  709. // set a different "title" attribute
  710. acorn.$captionBtn.attr('title', text.captionsChoose);
  711. // markup for the Caption Selector
  712. var captionList = '<ul><li><label><input type="radio" name="' + captionRadioName + '" checked="true" />None</label></li>';
  713. acorn.$track.each(function() {
  714. var tracksrc = $(this).attr('src');
  715. captionList += '<li><label><input type="radio" name="' + captionRadioName + '" data-url="' + $(this).attr('src') + '" />' + $(this).attr('label') + '</label></li>';
  716. });
  717. captionList += '</ul>';
  718. // append the generated markup
  719. acorn.$captionSelector.html(captionList);
  720. // change selected caption
  721. var changeCaption = function() {
  722. // get the original <track> "src" attribute from the custom "data-url" attribute of the radio input
  723. var tracksrc = $(this).attr('data-url');
  724. if(tracksrc) {
  725. loadCaption(tracksrc);
  726. } else {
  727. // if there's not "data-url" attribute, turn off the caption
  728. captionOff();
  729. }
  730. };
  731. // attach event handler
  732. $('input[name=' + captionRadioName + ']', acorn.$container).change(changeCaption);
  733. // initialize Caption Selector
  734. initCaptionSelector();
  735. // load first caption if captionsOn is true
  736. var firstCaption = acorn.$track.first().attr('src');
  737. if(options.captionsOn) {
  738. loadCaption(firstCaption);
  739. $('input[name=' + captionRadioName + ']', acorn.$container).removeAttr('checked');
  740. $('input[name=' + captionRadioName + ']:eq(1)', acorn.$container).attr('checked', 'true');
  741. };
  742. } else if(acorn.$track.length) {
  743. // if there's only one <track> element
  744. // load the specific caption when activating the Caption Button
  745. var tracksrc = acorn.$track.attr('src');
  746. acorn.$captionBtn.bind('click', function() {
  747. if($(this).hasClass(captionBtnActiveClass)) {
  748. captionOff();
  749. } else {
  750. loadCaption(tracksrc);
  751. }
  752. });
  753. // load default caption if captionsOn is true
  754. if(options.captionsOn) loadCaption(tracksrc);
  755. }
  756. // attach event to Transcript Button
  757. acorn.$transcriptBtn.bind('click', showTranscript);
  758. };
  759. /*
  760. * Initialization self-invoking function
  761. * Runs other initialization functions, attaches events, removes native controls
  762. */
  763. var init = function() {
  764. // attach playback handlers
  765. acorn.$playBtn.bind( 'touchstart click', playMedia);
  766. acorn.$self.bind( 'touchstart click' , playMedia);
  767. acorn.$self.bind('play', startPlayback);
  768. acorn.$self.bind('pause', stopPlayback);
  769. acorn.$self.bind('ended', stopPlayback);
  770. // update the Seek Slider when timeupdate is triggered
  771. acorn.$self.bind('timeupdate', seekUpdate);
  772. // bind Fullscreen Button
  773. acorn.$fullscreenBtn.click(goFullscreen);
  774. // bind Swap Button
  775. acorn.$swapBtn.click(goSwap);
  776. // initialize volume controls
  777. initVolume();
  778. // add the loading class
  779. $wrapper.addClass('');
  780. if(!options.nativeSliders) initSeek();
  781. // once the metadata has loaded
  782. acorn.$self.bind('loadedmetadata', function() {
  783. /* I use an interval to make sure the video has the right readyState
  784. * to bypass a known webkit bug that causes loadedmetadata to be triggered
  785. * before the duration is available
  786. */
  787. var t = window.setInterval(function() {
  788. if (acorn.$self[0].readyState > 0) {
  789. loadedMetadata = true;
  790. updateSeek();
  791. clearInterval(t);
  792. }
  793. }, 500);
  794. initCaption();
  795. });
  796. // trigger update seek manualy for the first time, for iOS support
  797. updateSeek();
  798. // remove the native controls
  799. acorn.$self.removeAttr('controls');
  800. if(acorn.$self.is('audio')) {
  801. /*
  802. * If the media is <audio>, we're adding the 'audio-player' class to the element.
  803. * This is because Opera 10.62 does not allow the <audio> element to be targeted by CSS
  804. * and this can cause problems with themeing.
  805. */
  806. acorn.$container.addClass('audio-player');
  807. }
  808. }();
  809. };
  810. // iterate and reformat each matched element
  811. return this.each(acornPlayer);
  812. };
  813. })(jQuery);