From 4c3fe90f749dc723c7c02efd2504c6588843d56a Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Thu, 19 Jul 2018 15:13:11 +0200 Subject: [PATCH 1/6] mediaelement a11y plugin --- demo/a11y.html | 40 ++++ dist/a11y/a11y-i18n.js | 21 ++ dist/a11y/a11y.css | 35 +++ dist/a11y/a11y.js | 274 +++++++++++++++++++++++ dist/a11y/a11y.min.css | 1 + dist/a11y/a11y.min.js | 12 ++ dist/a11y/audio-description-icon.svg | 1 + dist/a11y/video-description-icon.svg | 1 + docs/a11y.md | 26 +++ src/a11y/a11y-i18n.js | 21 ++ src/a11y/a11y.css | 35 +++ src/a11y/a11y.js | 311 +++++++++++++++++++++++++++ src/a11y/audio-description-icon.svg | 1 + src/a11y/video-description-icon.svg | 1 + 14 files changed, 780 insertions(+) create mode 100644 demo/a11y.html create mode 100644 dist/a11y/a11y-i18n.js create mode 100644 dist/a11y/a11y.css create mode 100644 dist/a11y/a11y.js create mode 100644 dist/a11y/a11y.min.css create mode 100644 dist/a11y/a11y.min.js create mode 100644 dist/a11y/audio-description-icon.svg create mode 100644 dist/a11y/video-description-icon.svg create mode 100644 docs/a11y.md create mode 100644 src/a11y/a11y-i18n.js create mode 100644 src/a11y/a11y.css create mode 100644 src/a11y/a11y.js create mode 100644 src/a11y/audio-description-icon.svg create mode 100644 src/a11y/video-description-icon.svg diff --git a/demo/a11y.html b/demo/a11y.html new file mode 100644 index 00000000..a41bc404 --- /dev/null +++ b/demo/a11y.html @@ -0,0 +1,40 @@ + + + + + + MediaElement.js 4.2 - Accessibility Plugin + + + + + + + + + +
+

Accessibility Plugin

+

Back to Main

+ +

Video Player (w/o Voice-Over)

+
+ +
+
+ + + + + diff --git a/dist/a11y/a11y-i18n.js b/dist/a11y/a11y-i18n.js new file mode 100644 index 00000000..552aa9cb --- /dev/null +++ b/dist/a11y/a11y-i18n.js @@ -0,0 +1,21 @@ +'use strict'; + +if (mejs.i18n.de !== undefined) { + mejs.i18n.de['mejs.a11y-audio-description'] = 'Audio Deskription An/Aus'; + mejs.i18n.de['mejs.a11y-video-description'] = 'Video Deskription An/Aus'; +} + +if (mejs.i18n.fr !== undefined) { + mejs.i18n.fr['mejs.a11y-audio-description'] = 'Audio-description étendue On/Off'; + mejs.i18n.fr['mejs.a11y-video-description'] = 'Langue des signes On/Off'; +} + +if (mejs.i18n.nl !== undefined) { + mejs.i18n.nl['mejs.a11y-audio-description'] = 'Audiobeschrijving Aan/Uit'; + mejs.i18n.nl['mejs.a11y-video-description'] = 'Gebarentaal Aan/Uit'; +} + +if (mejs.i18n.tr !== undefined) { + mejs.i18n.tr['mejs.a11y-audio-description'] = 'Sesli betimleme değiştir'; + mejs.i18n.tr['mejs.a11y-video-description'] = 'Görüntülü betimleme değişti'; +} diff --git a/dist/a11y/a11y.css b/dist/a11y/a11y.css new file mode 100644 index 00000000..be7b3cc1 --- /dev/null +++ b/dist/a11y/a11y.css @@ -0,0 +1,35 @@ +.mejs-video-description-button > button, +.mejs__video-description-button > button, +.mejs-audio-description-button > button, +.mejs__audio-description-button > button { + background-repeat: no-repeat; + background-size: contain; + opacity: 0.7; +} + +.mejs-video-description-button.video-description-on > button, +.mejs__video-description-button.video-description-on > button, +.mejs-audio-description-button.audio-description-on > button, +.mejs__audio-description-button.audio-description-on > button { + opacity: 1; +} + +.mejs-video-description-button > button, +.mejs__video-description-button > button { + background-image: url('video-description-icon.svg'); +} + +.mejs-audio-description-button > button, +.mejs__audio-description-button > button { + background-image: url('audio-description-icon.svg'); +} + +.mejs-volume-button.hidden, +.mejs__volume-button.hidden { + display: none; +} + +.mejs-audio-description-player, +.mejs__audio-description-player { + display: none; +} diff --git a/dist/a11y/a11y.js b/dist/a11y/a11y.js new file mode 100644 index 00000000..25b8995a --- /dev/null +++ b/dist/a11y/a11y.js @@ -0,0 +1,274 @@ +/*! + * MediaElement.js + * http://www.mediaelementjs.com/ + * + * Wrapper that mimics native HTML5 MediaElement (audio and video) + * using a variety of technologies (pure JavaScript, Flash, iframe) + * + * Copyright 2010-2017, John Dyer (http://j.hn/) + * License: MIT + * + */(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i -1; + }); + }, + _createAudioDescription: function _createAudioDescription() { + var t = this; + + var audioDescriptionTitle = mejs.i18n.t('mejs.a11y-audio-description'); + var audioDescriptionButton = document.createElement('div'); + audioDescriptionButton.className = t.options.classPrefix + 'button ' + t.options.classPrefix + 'audio-description-button'; + audioDescriptionButton.innerHTML = ''; + + t.addControlElement(audioDescriptionButton, 'audio-description'); + + audioDescriptionButton.addEventListener('click', function () { + t.options.audioDescriptionToggled = !t.options.audioDescriptionToggled; + mejs.Utils.toggleClass(audioDescriptionButton, 'audio-description-on'); + + t._toggleAudioDescription(); + }); + }, + _createVideoDescription: function _createVideoDescription() { + var t = this; + var videoDescriptionTitle = mejs.i18n.t('mejs.a11y-video-description'); + var videoDescriptionButton = document.createElement('div'); + videoDescriptionButton.className = t.options.classPrefix + 'button ' + t.options.classPrefix + 'video-description-button'; + videoDescriptionButton.innerHTML = ''; + t.addControlElement(videoDescriptionButton, 'video-description'); + + videoDescriptionButton.addEventListener('click', function () { + t.options.videoDescriptionToggled = !t.options.videoDescriptionToggled; + mejs.Utils.toggleClass(videoDescriptionButton, 'video-description-on'); + + t._toggleVideoDescription(); + }); + }, + _loadSourceFromAttribute: function _loadSourceFromAttribute(attribute) { + var t = this; + if (!t.node.hasAttribute(attribute)) return null; + + var sources = null; + var json = void 0; + + try { + var data = t.node.getAttribute(attribute); + json = JSON.parse(data); + } catch (error) { + console.error('error loading ' + attribute + ': ' + error.message); + } finally { + sources = json; + } + + return sources ? this._evaluateBestMatchingSource(sources) : null; + }, + _loadBooleanFromAttribute: function _loadBooleanFromAttribute(attribute) { + var t = this; + if (!t.node.hasAttribute(attribute)) return false; + + var boolValue = t.node.getAttribute(attribute); + return boolValue === 'true' || boolValue === ''; + }, + _evaluateBestMatchingSource: function _evaluateBestMatchingSource(sources) { + var _this = this; + + var getMimeFromType = function getMimeFromType(type) { + return mejs.Utils.getMimeFromType(type); + }; + var canPlayType = function canPlayType(type) { + return _this.node.canPlayType(type); + }; + var matchesBrowser = function matchesBrowser(file) { + return canPlayType(getMimeFromType(file.type)); + }; + + var propablySource = sources.find(function (file) { + return matchesBrowser(file) === 'probably'; + }); + if (propablySource) return propablySource; + + var alternativeSource = sources.find(function (file) { + return matchesBrowser(file) === 'maybe'; + }); + if (alternativeSource) return alternativeSource; + + return null; + }, + _createAudioDescriptionPlayer: function _createAudioDescriptionPlayer() { + var t = this; + + var audioNode = document.createElement('audio'); + audioNode.setAttribute('preload', 'auto'); + audioNode.classList.add(t.options.classPrefix + 'audio-description-player'); + audioNode.setAttribute('src', t.options.audioDescriptionSource.src); + audioNode.setAttribute('type', t.options.audioDescriptionSource.type); + audioNode.load(); + document.body.appendChild(audioNode); + + t.audioDescription = new mejs.MediaElementPlayer(audioNode, { + features: ['volume'], + audioVolume: t.options.videoVolume, + startVolume: t.node.volume, + pauseOtherPlayers: false + }); + + t.audioDescription.node.addEventListener('canplay', function () { + return t.options.audioDescriptionCanPlay = true; + }); + t.node.addEventListener('play', function () { + return t.audioDescription.node.play().catch(function (e) { + return console.error(e); + }); + }); + t.node.addEventListener('playing', function () { + return t.audioDescription.node.play().catch(function (e) { + return console.error(e); + }); + }); + t.node.addEventListener('pause', function () { + return t.audioDescription.node.pause(); + }); + t.node.addEventListener('waiting', function () { + return t.audioDescription.node.pause(); + }); + t.node.addEventListener('ended', function () { + return t.audioDescription.node.pause(); + }); + t.node.addEventListener('timeupdate', function () { + var shouldSync = Math.abs(t.node.currentTime - t.audioDescription.node.currentTime) > 0.35; + var canPlay = t.options.audioDescriptionCanPlay; + if (shouldSync && canPlay) t.audioDescription.node.currentTime = t.node.currentTime; + }); + + if (t.options.isVoiceover) { + t.node.addEventListener('volumechange', function () { + return t.audioDescription.node.volume = t.node.volume; + }); + } else { + var volumeButtonClass = t.options.classPrefix + 'volume-button'; + var videoVolumeButton = t._getFirstChildNodeByClassName(t.controls, volumeButtonClass); + t.videoVolumeButton = videoVolumeButton; + + if (videoVolumeButton) { + var descriptiveVolumeButton = t._getFirstChildNodeByClassName(t.audioDescription.controls, volumeButtonClass); + videoVolumeButton.classList.add('hidden'); + t.controls.insertBefore(descriptiveVolumeButton, videoVolumeButton.nextSibling); + t.descriptiveVolumeButton = descriptiveVolumeButton; + } + } + }, + _toggleAudioDescription: function _toggleAudioDescription() { + var t = this; + + if (!t.audioDescription) t._createAudioDescriptionPlayer(); + + if (t.options.audioDescriptionToggled) { + t.audioDescription.node.volume = t.node.volume; + if (t.options.isPlaying && t.audioDescription) t.audioDescription.node.play().catch(function (e) { + return console.error(e); + }); + + if (!t.options.isVoiceover) { + t.node.muted = true; + t.audioDescription.node.muted = false; + } + + if (!t.options.isVoiceover && t.videoVolumeButton && t.descriptiveVolumeButton) { + mejs.Utils.addClass(t.videoVolumeButton, 'hidden'); + mejs.Utils.removeClass(t.descriptiveVolumeButton, 'hidden'); + } + } else { + t.node.volume = t.audioDescription.node.volume; + t.audioDescription.node.pause(); + + if (!t.options.isVoiceover) { + t.node.muted = false; + t.audioDescription.node.muted = true; + } + + if (!t.options.isVoiceover && t.videoVolumeButton && t.descriptiveVolumeButton) { + mejs.Utils.removeClass(t.videoVolumeButton, 'hidden'); + mejs.Utils.addClass(t.descriptiveVolumeButton, 'hidden'); + } + } + }, + _toggleVideoDescription: function _toggleVideoDescription() { + var t = this; + var currentTime = t.node.currentTime; + var wasPlaying = t.options.isPlaying; + var active = t.options.videoDescriptionToggled; + + t.node.pause(); + + t.node.src = active ? t.options.videoDescriptionSource.src : t.options.defaultSource.src; + t.node.type = active ? t.options.videoDescriptionSource.type : t.options.defaultSource.type; + t.node.load(); + + if (wasPlaying) { + t.node.play().then(function () { + return t.node.currentTime = currentTime; + }).catch(function (e) { + return console.error(e); + }); + } else { + t.node.setCurrentTime(currentTime); + } + } +}); + +},{}]},{},[1]); diff --git a/dist/a11y/a11y.min.css b/dist/a11y/a11y.min.css new file mode 100644 index 00000000..b05a9697 --- /dev/null +++ b/dist/a11y/a11y.min.css @@ -0,0 +1 @@ +.mejs-audio-description-button>button,.mejs-video-description-button>button,.mejs__audio-description-button>button,.mejs__video-description-button>button{background-repeat:no-repeat;background-size:contain;opacity:.7}.mejs-audio-description-button.audio-description-on>button,.mejs-video-description-button.video-description-on>button,.mejs__audio-description-button.audio-description-on>button,.mejs__video-description-button.video-description-on>button{opacity:1}.mejs-video-description-button>button,.mejs__video-description-button>button{background-image:url(video-description-icon.svg)}.mejs-audio-description-button>button,.mejs__audio-description-button>button{background-image:url(audio-description-icon.svg)}.mejs-audio-description-player,.mejs-volume-button.hidden,.mejs__audio-description-player,.mejs__volume-button.hidden{display:none} \ No newline at end of file diff --git a/dist/a11y/a11y.min.js b/dist/a11y/a11y.min.js new file mode 100644 index 00000000..e380513a --- /dev/null +++ b/dist/a11y/a11y.min.js @@ -0,0 +1,12 @@ +/*! + * MediaElement.js + * http://www.mediaelementjs.com/ + * + * Wrapper that mimics native HTML5 MediaElement (audio and video) + * using a variety of technologies (pure JavaScript, Flash, iframe) + * + * Copyright 2010-2017, John Dyer (http://j.hn/) + * License: MIT + * + */ +!function r(s,d,u){function a(o,e){if(!d[o]){if(!s[o]){var i="function"==typeof require&&require;if(!e&&i)return i(o,!0);if(c)return c(o,!0);var t=new Error("Cannot find module '"+o+"'");throw t.code="MODULE_NOT_FOUND",t}var n=d[o]={exports:{}};s[o][0].call(n.exports,function(e){return a(s[o][1][e]||e)},n,n.exports,r,s,d,u)}return d[o].exports}for(var c="function"==typeof require&&require,e=0;e',e.addControlElement(i,"audio-description"),i.addEventListener("click",function(){e.options.audioDescriptionToggled=!e.options.audioDescriptionToggled,mejs.Utils.toggleClass(i,"audio-description-on"),e._toggleAudioDescription()})},_createVideoDescription:function(){var e=this,o=mejs.i18n.t("mejs.a11y-video-description"),i=document.createElement("div");i.className=e.options.classPrefix+"button "+e.options.classPrefix+"video-description-button",i.innerHTML='',e.addControlElement(i,"video-description"),i.addEventListener("click",function(){e.options.videoDescriptionToggled=!e.options.videoDescriptionToggled,mejs.Utils.toggleClass(i,"video-description-on"),e._toggleVideoDescription()})},_loadSourceFromAttribute:function(o){if(!this.node.hasAttribute(o))return null;var e,i=void 0;try{var t=this.node.getAttribute(o);i=JSON.parse(t)}catch(e){console.error("error loading "+o+": "+e.message)}finally{e=i}return e?this._evaluateBestMatchingSource(e):null},_loadBooleanFromAttribute:function(e){if(!this.node.hasAttribute(e))return!1;var o=this.node.getAttribute(e);return"true"===o||""===o},_evaluateBestMatchingSource:function(e){var t=this,o=function(e){return i=e.type,o=mejs.Utils.getMimeFromType(i),t.node.canPlayType(o);var o,i},i=e.find(function(e){return"probably"===o(e)});if(i)return i;var n=e.find(function(e){return"maybe"===o(e)});return n||null},_createAudioDescriptionPlayer:function(){var i=this,e=document.createElement("audio");if(e.setAttribute("preload","auto"),e.classList.add(i.options.classPrefix+"audio-description-player"),e.setAttribute("src",i.options.audioDescriptionSource.src),e.setAttribute("type",i.options.audioDescriptionSource.type),e.load(),document.body.appendChild(e),i.audioDescription=new mejs.MediaElementPlayer(e,{features:["volume"],audioVolume:i.options.videoVolume,startVolume:i.node.volume,pauseOtherPlayers:!1}),i.audioDescription.node.addEventListener("canplay",function(){return i.options.audioDescriptionCanPlay=!0}),i.node.addEventListener("play",function(){return i.audioDescription.node.play().catch(function(e){return console.error(e)})}),i.node.addEventListener("playing",function(){return i.audioDescription.node.play().catch(function(e){return console.error(e)})}),i.node.addEventListener("pause",function(){return i.audioDescription.node.pause()}),i.node.addEventListener("waiting",function(){return i.audioDescription.node.pause()}),i.node.addEventListener("ended",function(){return i.audioDescription.node.pause()}),i.node.addEventListener("timeupdate",function(){var e=.35 diff --git a/dist/a11y/video-description-icon.svg b/dist/a11y/video-description-icon.svg new file mode 100644 index 00000000..8652a91b --- /dev/null +++ b/dist/a11y/video-description-icon.svg @@ -0,0 +1 @@ + diff --git a/docs/a11y.md b/docs/a11y.md new file mode 100644 index 00000000..cfbe6c47 --- /dev/null +++ b/docs/a11y.md @@ -0,0 +1,26 @@ +# Accessibility + +## Overview +This plugin enables special accessibility features for adding an audio description or sign language annotated movie file. + +## Keyword to use it +```javascript +features: [..., 'a11y'] +``` + +## API +Parameter | Type | Default | Description +------ | --------- | ------- | -------- +`data-video-description` | array | null | An array of video source objects like `{ src: "description.mp4", type: "video/mp4" }`. This plugin will evaluate the best matching type out of the array. +`data-audio-description` | array | null | An array of audio description source objects like `{ src: "description.mp3", type: "audio/mp3" }`. This plugin will evaluate the best matching type out of the array. +`data-audio-description-voiceover` | boolean | false | If set as data attribute only or with value `true` audio description will be started in voice-over mode. + +#### Audio-description node +The Audio description node is bound to the MediaElement.js object at `mejs.audioDescription.node`, like the original node is bound under `mejs.node`. + +## Icons +The sign language and audio description icon were made by [Font Awesome](https://fontawesome.com) and underlie the following [License](https://fontawesome.com/license). + +--- + +The development of this plugin was sponsored by [Aktion Mensch e.V.](https://www.aktion-mensch.de) diff --git a/src/a11y/a11y-i18n.js b/src/a11y/a11y-i18n.js new file mode 100644 index 00000000..552aa9cb --- /dev/null +++ b/src/a11y/a11y-i18n.js @@ -0,0 +1,21 @@ +'use strict'; + +if (mejs.i18n.de !== undefined) { + mejs.i18n.de['mejs.a11y-audio-description'] = 'Audio Deskription An/Aus'; + mejs.i18n.de['mejs.a11y-video-description'] = 'Video Deskription An/Aus'; +} + +if (mejs.i18n.fr !== undefined) { + mejs.i18n.fr['mejs.a11y-audio-description'] = 'Audio-description étendue On/Off'; + mejs.i18n.fr['mejs.a11y-video-description'] = 'Langue des signes On/Off'; +} + +if (mejs.i18n.nl !== undefined) { + mejs.i18n.nl['mejs.a11y-audio-description'] = 'Audiobeschrijving Aan/Uit'; + mejs.i18n.nl['mejs.a11y-video-description'] = 'Gebarentaal Aan/Uit'; +} + +if (mejs.i18n.tr !== undefined) { + mejs.i18n.tr['mejs.a11y-audio-description'] = 'Sesli betimleme değiştir'; + mejs.i18n.tr['mejs.a11y-video-description'] = 'Görüntülü betimleme değişti'; +} diff --git a/src/a11y/a11y.css b/src/a11y/a11y.css new file mode 100644 index 00000000..be7b3cc1 --- /dev/null +++ b/src/a11y/a11y.css @@ -0,0 +1,35 @@ +.mejs-video-description-button > button, +.mejs__video-description-button > button, +.mejs-audio-description-button > button, +.mejs__audio-description-button > button { + background-repeat: no-repeat; + background-size: contain; + opacity: 0.7; +} + +.mejs-video-description-button.video-description-on > button, +.mejs__video-description-button.video-description-on > button, +.mejs-audio-description-button.audio-description-on > button, +.mejs__audio-description-button.audio-description-on > button { + opacity: 1; +} + +.mejs-video-description-button > button, +.mejs__video-description-button > button { + background-image: url('video-description-icon.svg'); +} + +.mejs-audio-description-button > button, +.mejs__audio-description-button > button { + background-image: url('audio-description-icon.svg'); +} + +.mejs-volume-button.hidden, +.mejs__volume-button.hidden { + display: none; +} + +.mejs-audio-description-player, +.mejs__audio-description-player { + display: none; +} diff --git a/src/a11y/a11y.js b/src/a11y/a11y.js new file mode 100644 index 00000000..6317e6d4 --- /dev/null +++ b/src/a11y/a11y.js @@ -0,0 +1,311 @@ +'use strict'; + +mejs.i18n.en['mejs.a11y-audio-description'] = 'Toggle audio description'; +mejs.i18n.en['mejs.a11y-video-description'] = 'Toggle sign language'; + +Object.assign(mejs.MepDefaults, { + /** + * Video description is toggled + * @type {Boolean} + */ + videoDescriptionToggled: false, + + /** + * Audio description is toggled + * @type {Boolean} + */ + audioDescriptionToggled: false, + + /** + * Store for initial source file + * @type ?{src: String, type: String} + */ + defaultSource: null, + + /** + * Store for best matching audio description file + * @type {?String} + */ + audioDescriptionSource: null, + + /** + * Store for best matching video description file + * @type {?String} + */ + videoDescriptionSource: null, + + /** + * Player is currently playing + * @type {Boolean} + */ + isPlaying: false, + + /** + * Should audio description be voiceover + * @type {Boolean} + */ + isVoiceover: false, + + /** + * Audio description player has fired the canplay event + * @type {Boolean} + */ + audioDescriptionCanPlay: false, +}); + + +Object.assign(MediaElementPlayer.prototype, { + builda11y () { + const t = this; + + t.options.defaultSource = { + src: t.node.src, + type: t.node.type + }; + t.options.isVoiceover = t._loadBooleanFromAttribute('data-audio-description-voiceover'); + t.options.audioDescriptionSource = t._loadSourceFromAttribute('data-audio-description'); + t.options.videoDescriptionSource = t._loadSourceFromAttribute('data-video-description'); + + if (t.options.audioDescriptionSource) t._createAudioDescription(); + if (t.options.videoDescriptionSource) t._createVideoDescription(); + + t.node.addEventListener('play', () => t.options.isPlaying = true); + t.node.addEventListener('playing', () => t.options.isPlaying = true); + t.node.addEventListener('pause', () => t.options.isPlaying = false); + t.node.addEventListener('ended', () => t.options.isPlaying = false); + }, + + /** + * Get the first child node by class name + * @private + * @param {Node} parentNode + * @param {String} className + * @returns {Node} childNode + */ + _getFirstChildNodeByClassName(parentNode, className) { + return [...parentNode.childNodes].find(node => node.className.indexOf(className) > -1); + }, + + /** + * Create audio description button and bind events + * @private + * @returns {Undefined} + */ + _createAudioDescription() { + const t = this; + + const audioDescriptionTitle = mejs.i18n.t('mejs.a11y-audio-description'); + const audioDescriptionButton = document.createElement('div'); + audioDescriptionButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}audio-description-button`; + audioDescriptionButton.innerHTML = ``; + + t.addControlElement(audioDescriptionButton, 'audio-description'); + + audioDescriptionButton.addEventListener('click', () => { + t.options.audioDescriptionToggled = !t.options.audioDescriptionToggled; + mejs.Utils.toggleClass(audioDescriptionButton, 'audio-description-on'); + + t._toggleAudioDescription(); + }); + }, + + /** + * Create video description button and bind events + * @private + * @returns {Undefined} + */ + _createVideoDescription() { + const t = this; + const videoDescriptionTitle = mejs.i18n.t('mejs.a11y-video-description'); + const videoDescriptionButton = document.createElement('div'); + videoDescriptionButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}video-description-button`; + videoDescriptionButton.innerHTML = ``; + t.addControlElement(videoDescriptionButton, 'video-description'); + + videoDescriptionButton.addEventListener('click', () => { + t.options.videoDescriptionToggled = !t.options.videoDescriptionToggled; + mejs.Utils.toggleClass(videoDescriptionButton, 'video-description-on'); + + t._toggleVideoDescription(); + }); + }, + + /** + * Load the best matching source file from a data attribute + * @private + * @param {String} attribute - data attribute for a source object. + * @returns {?String} source - best matching source file or null + */ + _loadSourceFromAttribute(attribute) { + const t = this; + if (!t.node.hasAttribute(attribute)) return null; + + let sources = null; + let json; + + try { + const data = t.node.getAttribute(attribute); + json = JSON.parse(data); + } catch(error) { + console.error(`error loading ${attribute}: ${error.message}`); + } finally { + sources = json; + } + + return (sources) ? this._evaluateBestMatchingSource(sources) : null; + }, + + /** + * Evaluate if audio description should be toggled as voiceover + * @private + * @param {String} attribute - data attribute for voice over toggle + * @returns {?Boolean} + */ + _loadBooleanFromAttribute(attribute) { + const t = this; + if(!t.node.hasAttribute(attribute)) return false; + + const boolValue = t.node.getAttribute(attribute); + return boolValue === 'true' || boolValue === ''; + }, + + /** + * Evaluate the best matching source from an array of sources + * @private + * @param {Array.<{src: String, type: String}>} sources + * @returns ?{src: String, type: String} source + */ + _evaluateBestMatchingSource(sources) { + const getMimeFromType = type => mejs.Utils.getMimeFromType(type); + const canPlayType = type => this.node.canPlayType(type); + const matchesBrowser = file => canPlayType(getMimeFromType(file.type)); + + // checking most likely support + const propablySource = sources.find(file => matchesBrowser(file) === 'probably'); + if (propablySource) return propablySource; + + // checking might support + const alternativeSource = sources.find(file => matchesBrowser(file) === 'maybe'); + if (alternativeSource) return alternativeSource; + + return null; + }, + + /** + * Create a hidden audio dom node for audio description + * @private + * @returns {Undefined} + */ + _createAudioDescriptionPlayer() { + const t = this; + + const audioNode = document.createElement('audio'); + audioNode.setAttribute('preload', 'auto'); + audioNode.classList.add(`${t.options.classPrefix}audio-description-player`); + audioNode.setAttribute('src', t.options.audioDescriptionSource.src); + audioNode.setAttribute('type', t.options.audioDescriptionSource.type); + audioNode.load(); + document.body.appendChild(audioNode); + + t.audioDescription = new mejs.MediaElementPlayer(audioNode, { + features: ['volume'], + audioVolume: t.options.videoVolume, + startVolume: t.node.volume, + pauseOtherPlayers: false + }); + + t.audioDescription.node.addEventListener('canplay', () => t.options.audioDescriptionCanPlay = true); + t.node.addEventListener('play', () => t.audioDescription.node.play().catch(e => console.error(e))); + t.node.addEventListener('playing', () => t.audioDescription.node.play().catch(e => console.error(e))); + t.node.addEventListener('pause', () => t.audioDescription.node.pause()); + t.node.addEventListener('waiting', () => t.audioDescription.node.pause()); + t.node.addEventListener('ended', () => t.audioDescription.node.pause()); + t.node.addEventListener('timeupdate', () => { + const shouldSync = Math.abs(t.node.currentTime - t.audioDescription.node.currentTime) > 0.35; + const canPlay = t.options.audioDescriptionCanPlay; + if (shouldSync && canPlay) t.audioDescription.node.currentTime = t.node.currentTime; + }); + + // if audio description is voice over, map volume slider to both players + // otherwise move the audio players volume slider inside the movie player to simulate normal volume handling + if(t.options.isVoiceover) { + t.node.addEventListener('volumechange', () => t.audioDescription.node.volume = t.node.volume); + } else { + const volumeButtonClass = `${t.options.classPrefix}volume-button`; + const videoVolumeButton = t._getFirstChildNodeByClassName(t.controls, volumeButtonClass); + t.videoVolumeButton = videoVolumeButton; + + if(videoVolumeButton) { + const descriptiveVolumeButton = t._getFirstChildNodeByClassName(t.audioDescription.controls, volumeButtonClass); + videoVolumeButton.classList.add('hidden'); + t.controls.insertBefore(descriptiveVolumeButton, videoVolumeButton.nextSibling); + t.descriptiveVolumeButton = descriptiveVolumeButton; + } + } + }, + + /** + * Handle audio description toggling + * @private + * @returns {Undefined} + */ + _toggleAudioDescription() { + const t = this; + + if (!t.audioDescription) t._createAudioDescriptionPlayer(); + + if (t.options.audioDescriptionToggled) { + t.audioDescription.node.volume = t.node.volume; + if (t.options.isPlaying && t.audioDescription) t.audioDescription.node.play().catch(e => console.error(e)); + + if(!t.options.isVoiceover) { + t.node.muted = true; + t.audioDescription.node.muted = false; + } + + if(!t.options.isVoiceover && t.videoVolumeButton && t.descriptiveVolumeButton) { + mejs.Utils.addClass(t.videoVolumeButton, 'hidden'); + mejs.Utils.removeClass(t.descriptiveVolumeButton, 'hidden'); + } + } else { + t.node.volume = t.audioDescription.node.volume; + t.audioDescription.node.pause(); + + if(!t.options.isVoiceover) { + t.node.muted = false; + t.audioDescription.node.muted = true; + } + + if(!t.options.isVoiceover && t.videoVolumeButton && t.descriptiveVolumeButton) { + mejs.Utils.removeClass(t.videoVolumeButton, 'hidden'); + mejs.Utils.addClass(t.descriptiveVolumeButton, 'hidden'); + } + } + }, + + /** + * Handle video description toggling + * @private + * @returns {Undefined} + */ + _toggleVideoDescription() { + const t = this; + const currentTime = t.node.currentTime; + const wasPlaying = t.options.isPlaying; + const active = t.options.videoDescriptionToggled; + + t.node.pause(); + + t.node.src = active ? t.options.videoDescriptionSource.src : t.options.defaultSource.src; + t.node.type = active ? t.options.videoDescriptionSource.type : t.options.defaultSource.type; + t.node.load(); + + if (wasPlaying) { + t.node.play() + .then(() => t.node.currentTime = currentTime) + .catch(e => console.error(e)) + } else { + t.node.setCurrentTime(currentTime); + } + } +}); diff --git a/src/a11y/audio-description-icon.svg b/src/a11y/audio-description-icon.svg new file mode 100644 index 00000000..4900ccff --- /dev/null +++ b/src/a11y/audio-description-icon.svg @@ -0,0 +1 @@ + diff --git a/src/a11y/video-description-icon.svg b/src/a11y/video-description-icon.svg new file mode 100644 index 00000000..8652a91b --- /dev/null +++ b/src/a11y/video-description-icon.svg @@ -0,0 +1 @@ + From a7518f1c960e1dba884c1bf701ea3ac41f537f30 Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Thu, 19 Jul 2018 15:13:41 +0200 Subject: [PATCH 2/6] demo entry page optimization --- demo/index.html | 60 +++++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/demo/index.html b/demo/index.html index 9a7684ef..4f4bc16c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -38,26 +38,22 @@ flex: 1; color: #000; } - .columns{ - display: flex; - flex:1; - } .main{ flex: 1; order: 2; } - .sidebar-first{ - width: 33%; - order: 1; - } - .sidebar-second{ - width: 33%; - order: 3; - } ul { + box-sizing: border-box; + display: flex; + padding: 0 30px; + width: 100%; + flex-wrap: wrap; list-style: none; } li { + box-sizing: border-box; + flex: 0 0 auto; + width: 33%; padding: 5px; } @@ -70,31 +66,21 @@

- +
+ +
From 18be7196c323b1fd52e6f766cfce81b6b76c6b7b Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Thu, 19 Jul 2018 15:31:21 +0200 Subject: [PATCH 3/6] added a11y to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9f423cf5..50b6c116 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ See`src/` directory, and check how the files were written to ensure compatibilit ## Available plugins +* [A11y](docs/a11y.md) * [Ads](docs/ads.md) * [AirPlay](docs/airplay.md) * [Chromecast](docs/chromecast.md) From 19ac76e26c425f3435e97d139b992197a28207b5 Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Fri, 20 Jul 2018 10:12:46 +0200 Subject: [PATCH 4/6] Gruntfile adjustment --- Gruntfile.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 08c37b60..73ad5825 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -19,11 +19,11 @@ module.exports = function (grunt) { watch: { scripts: { files: ['src/**/*.js', 'test/core/*.js'], - tasks: ['eslint', 'browserify', 'concat', 'uglify', 'copy:translation'] + tasks: ['eslint', 'browserify', 'concat', 'uglify', 'copy'] }, stylesheet: { files: ['src/**/*.css', 'src/css/**/*.png', 'src/css/**/*.svg'], - tasks: ['postcss', 'copy:build'] + tasks: ['postcss', 'copy'] } }, @@ -40,6 +40,7 @@ module.exports = function (grunt) { browserify: { dist: { files: { + 'dist/a11y/a11y.js': 'src/a11y/a11y.js', 'dist/ads/ads.js': 'src/ads/ads.js', 'dist/ads-vast-vpaid/ads-vast-vpaid.js': 'src/ads-vast-vpaid/ads-vast-vpaid.js', 'dist/airplay/airplay.js': 'src/airplay/airplay.js', @@ -110,6 +111,7 @@ module.exports = function (grunt) { ] }, files: { + 'dist/a11y/a11y.css': 'src/a11y/a11y.css', 'dist/ads/ads.css': 'src/ads/ads.css', 'dist/airplay/airplay.css': 'src/airplay/airplay.css', 'dist/chromecast/chromecast.css': 'src/chromecast/chromecast.css', @@ -138,6 +140,7 @@ module.exports = function (grunt) { ] }, files: { + 'dist/a11y/a11y.min.css': 'dist/a11y/a11y.css', 'dist/ads/ads.min.css': 'dist/ads/ads.css', 'dist/airplay/airplay.min.css': 'dist/airplay/airplay.css', 'dist/chromecast/chromecast.min.css': 'dist/chromecast/chromecast.css', @@ -154,7 +157,6 @@ module.exports = function (grunt) { 'dist/vrview/vrview.min.css': 'dist/vrview/vrview.css' } }, - }, copy: { main: { From a7f1c2c7f29f8a0be5f5ab486cd64560788decc4 Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Fri, 27 Jul 2018 11:29:55 +0200 Subject: [PATCH 5/6] added German translation to a11y/README.md --- docs/a11y.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/a11y.md b/docs/a11y.md index cfbe6c47..e61ccdf7 100644 --- a/docs/a11y.md +++ b/docs/a11y.md @@ -23,4 +23,34 @@ The sign language and audio description icon were made by [Font Awesome](https:/ --- -The development of this plugin was sponsored by [Aktion Mensch e.V.](https://www.aktion-mensch.de) +This Plugin is sponsored by [Aktion Mensch e.V.](https://www.aktion-mensch.de) + +--- + +## German Translation + +### Übersicht +Dieses Plugin ermöglicht besondere Barrierefreiheit-Erweiterungen zum Hinzufügen von Audio-Deskription oder Videos mit Übersetzung in Gebärdensprache. + +### Keyword zum Einbinden +```javascript +features: [..., 'a11y'] +``` + +### API +Parameter | Type | Default | Beschreibung +------ | --------- | ------- | -------- +`data-video-description` | array | null | Ein Array von Gebärden-Sprachen-Video-Source Objekten, die wie folgt auszusehen haben: `{ src: "description.mp4", type: "video/mp4" }`. Das Plugin wählt die am besten passende Video-Source aus dem Array aus. +`data-audio-description` | array | null | Ein Array von Audio-Deskription-Source Objekten, die wie folgt auszusehen haben: `{ src: "description.mp3", type: "audio/mp3" }`. Das Plugin wählt die am besten passende Audio-Source aus dem Array aus. +`data-audio-description-voiceover` | boolean | false | Wenn der Parameter als Data-Attribut gesetzt oder mit dem Wert `true` belegt ist, wird die Audio-Deskription als Voice-Over gestartet. + +##### Audio-Deskription Node +Der Audio-Deskription Node wird im MediaElement.js Objekt unter `mejs.audioDescription.node` eingebunden, so wie der eigentliche Node unter `mejs.node` eingebunden ist. + +### Icons +Das Gebärdensprache- und Audio-Deskriptions-Icon stammen von [Font Awesome](https://fontawesome.com) und unterliegen der folgenden [Lizenz](https://fontawesome.com/license). + + +--- + +Dieses Plugin wurde gesponsert von [Aktion Mensch e.V.](https://www.aktion-mensch.de) From 4e8fa904aa8433629911d709d4b1af9f602b05f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Tho=CC=88ne?= Date: Tue, 21 Mar 2023 10:26:44 +0100 Subject: [PATCH 6/6] update Aktion Mensch demo assets urls --- demo/a11y.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/a11y.html b/demo/a11y.html index a41bc404..812c9111 100644 --- a/demo/a11y.html +++ b/demo/a11y.html @@ -20,12 +20,12 @@

Accessibility Plugin

Video Player (w/o Voice-Over)