├── .gitignore ├── LICENSE-MIT ├── README.markdown └── videosub.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | VideoSub by Thomas Sturm 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # VideoSub: HTML5 Video with SRT Subtitles # 2 | 3 | Version: 0.9.9, Last updated: 8/26/2012 4 | 5 | Video subtitles are an accessibility problem. In many countries websites are required by law to offer reasonable accessibility features, and one would hope that video subtitles are considered "reasonable" in 2012. 6 | 7 | While there is a W3C and WHATWG spec in the works, none of the major browsers fully supports the new track elements and the brand new WebVTT subtitle standard. And older browsers may support video tags, but have no built in way to show subtitles over HTM5 video at all. 8 | 9 | VideoSub is an easy solution for this problem and will get out of the way if the track element references a subtitle file in the new WebVTT standard. 10 | 11 | More information and examples can be found here: [http://www.storiesinflight.com/js_videosub/index.html](http://www.storiesinflight.com/js_videosub/index.html) 12 | 13 | 14 | ## Features ## 15 | 16 | * Standards compliant - follows the draft specification for Media Text Associations. Just add tags to your videos 17 | * No coding required - just include VideoSub on a page with video tags 18 | * No outside dependencies to other frameworks, contains a minified subset of the Ender framework 19 | * Handles multiple videos on a single page 20 | * Works with common .SRT subtitle files (WebVTT support for older browsers is planned as soon as there are standards compliant tools to create WebVTT files) 21 | * It will stay out of the way if the track tag references a subtitle file in the new WebVTT standard 22 | * Supports seeking in the video 23 | * Dispatches a 'subtitlechanged' event upon swapping out an old subtitle line with a new one 24 | 25 | 26 | ## Code ## 27 | 28 | ### JavaScript ### 29 | 30 | 31 | 32 | ### HTML ### 33 | 34 | 40 | 41 | That's all you need! There are NO outside dependencies. 42 | 43 | 44 | ## Release History ## 45 | 46 | 0.9.9 - (8/26/2012) Initial GitHub Release 47 | 48 | 49 | ## License ## 50 | Licensed under the MIT license. The contained Ender framework is licensed under MIT - copyright 2012 Dustin Diaz & Jacob Thornton [http://ender.no.de/](http://ender.no.de/) 51 | -------------------------------------------------------------------------------- /videosub.js: -------------------------------------------------------------------------------- 1 | // ============================================================================== 2 | // VideoSub v0.9.9 3 | // by Thomas Sturm, June 2010 - August 2012 4 | // http://www.storiesinflight.com 5 | // License MIT 6 | // 7 | // Ender is licensed under MIT - copyright 2012 Dustin Diaz & Jacob Thornton 8 | // http://ender.no.de/ 9 | // 10 | // Standards compliant video subtitles for HTML5 video tags. 11 | // Just add this library to your webpage, it will scan your page for HTML5 12 | // video tags and if they contain a subtitle, it will load and parse 13 | // the subtitle file (only if it is in SRT standard) and display the subtitles over 14 | // the playing video. The library can handle multiple video files in one page. 15 | // Currently, VideoSub will kick in for all browsers even if they have native track 16 | // support, since none are expected to support .SRT files at all. 17 | // ============================================================================== 18 | 19 | /*! 20 | * ======================================================= 21 | * Ender: open module JavaScript framework 22 | * copyright Dustin Diaz & Jacob Thornton 2011 (@ded @fat) 23 | * https://ender.no.de 24 | * License MIT 25 | * Module's individual licenses still apply 26 | * Build: ender build jeesh reqwest 27 | * ======================================================= 28 | */ 29 | 30 | 31 | // check for video tags and show subtitle track if the browser doesn't know how 32 | (function (w) { 33 | // integrated Ender library 34 | (function(a){function k(a,b){return new j(a,b)}function j(a,b){var c,d;this.selector=a,typeof a=="undefined"?(c=[],this.selector=""):typeof a=="string"||a.nodeName||a.length&&"item"in a||a==window?c=k._select(a,b):c=isFinite(a.length)?a:[a],this.length=c.length;for(d=this.length;d--;)this[d]=c[d]}function i(a,b){for(var c in b)c!="noConflict"&&c!="_VERSION"&&(a[c]=b[c]);return a}function h(a,c){return b["$"+a]=c}function g(a){var c=b["$"+a]||window[a];if(!c)throw new Error("Ender Error: Requested module '"+a+"' has not been defined.");return c}a.global=a;var b={},c=a.$,d=a.ender,e=a.require,f=a.provide;a.provide=h,a.require=g,j.prototype.forEach=function(a,b){var c,d;for(c=0,d=this.length;c=0?a.options[a.selectedIndex]:null);else for(var i=0;a.length&&i0){b=b.split(" ");for(i=b.length;i--;)N(a,b[i],c);return a}e=k&&b.replace(g,""),e&&z[e]&&(e=z[e].type);if(!b||k){if(h=k&&b.replace(f,""))h=h.split(".");j(a,e,c,h)}else if(typeof b=="function")j(a,null,b);else for(d in b)b.hasOwnProperty(d)&&N(a,d,b[d]);return a},O=function(a,b,c,d,e){var f,g,h,i,j=c,k=c&&typeof c=="string";if(b&&!c&&typeof b=="object")for(f in b)b.hasOwnProperty(f)&&O.apply(this,[a,f,b[f]]);else{i=arguments.length>3?s.call(arguments,3):[],g=(k?c:b).split(" "),k&&(c=M(b,j=d,e||E))&&(i=s.call(i,1)),this===x&&(c=J(N,a,b,c,j));for(h=g.length;h--;)L(a,g[h],c,j,i)}return a},P=function(){return O.apply(x,arguments)},Q=q?function(a,b,d){var e=o.createEvent(a?"HTMLEvents":"UIEvents");e[a?"initEvent":"initUIEvent"](b,!0,!0,c,1),d.dispatchEvent(e)}:function(a,b,c){c=B(c,a),a?c.fireEvent("on"+b,o.createEventObject()):c["_on"+b]++},R=function(a,b,c){var d,e,h,i,j,k=b.split(" ");for(d=k.length;d--;){b=k[d].replace(g,"");if(i=k[d].replace(f,""))i=i.split(".");if(!i&&!c&&a[r])Q(y[b],b,a);else{j=D.get(a,b),c=[!1].concat(c);for(e=0,h=j.length;e0?W(g,d):d)},null,d)},this,d),g.length=f,G(h,function(a){g[--f]=a},null,!d);return g}function P(a){a=="transform"&&(a=z.transform)||/^transform-?[Oo]rigin$/.test(a)&&(a=z.transform+"Origin")||a=="float"&&(a=z.cssFloat);return a?I(a):null}function O(a,b,c){for(var d=0,e=a.length;d","",1],i=["","
",3],j=["",1],k=["_","",0,1],l={thead:h,tbody:h,tfoot:h,colgroup:h,caption:h,tr:["","
",2],th:i,td:i,col:["","
",2],fieldset:["
","
",1],legend:["
","
",2],option:j,optgroup:j,script:k,style:k,link:k,param:k,base:k},m=/^(checked|selected|disabled)$/,n=/msie/i.test(navigator.userAgent),o,p,q,r={},s=0,t=/^-?[\d\.]+$/,u=/^data-(.+)$/,v="px",w="setAttribute",x="getAttribute",y="getElementsByTagName",z=function(){var a=b.createElement("p");a.innerHTML='x
';return{hrefExtended:a[y]("a")[0][x]("href")!="#x",autoTbody:a[y]("tbody").length!==0,computedStyle:b.defaultView&&b.defaultView.getComputedStyle,cssFloat:a[y]("table")[0].style.styleFloat?"styleFloat":"cssFloat",transform:function(){var b=["webkitTransform","MozTransform","OTransform","msTransform","Transform"],c;for(c=0;c]+)/.exec(a),e=b.createElement("div"),f=[],g=c?l[c[1].toLowerCase()]:null,h=g?g[2]+1:1,i=g&&g[3],j=d,k=z.autoTbody&&g&&g[0]==""&&!/~+]/,q=/^\s+|\s*([,\s\+\~>]|$)\s*/g,r=/[\s\>\+\~]/,s=/(?![\s\w\-\/\?\&\=\:\.\(\)\!,@#%<>\{\}\$\*\^'"]*\]|[\s\w\+\-]*\))/,t=/([.*+?\^=!:${}()|\[\]\/\\])/g,u=/^(\*|[a-z0-9]+)?(?:([\.\#]+[\w\-\.#]+)?)/,v=/\[([\w\-]+)(?:([\|\^\$\*\~]?\=)['"]?([ \w\-\/\?\&\=\:\.\(\)\!,@#%<>\{\}\$\*\^]+)["']?)?\]/,w=/:([\w\-]+)(\(['"]?([^()]+)['"]?\))?/,x=new RegExp(l.source+"|"+n.source+"|"+m.source),y=new RegExp("("+r.source+")"+s.source,"g"),z=new RegExp(r.source+s.source),A=new RegExp(u.source+"("+v.source+")?"+"("+w.source+")?"),B={" ":function(a){return a&&a!==b&&a.parentNode},">":function(a,b){return a&&a.parentNode==b.parentNode&&a.parentNode},"~":function(a){return a&&a.previousSibling},"+":function(a,b,c,d){if(!a)return!1;return(c=L(a))&&(d=L(b))&&c==d&&c}};C.prototype={g:function(a){return this.c[a]||undefined},s:function(a,b,c){b=c?new RegExp(b):b;return this.c[a]=b}};var D=new C,E=new C,F=new C,G=new C,$="compareDocumentPosition"in b?function(a,b){return(b.compareDocumentPosition(a)&16)==16}:"contains"in b?function(a,c){c=c[h]===9||c==window?b:c;return c!==a&&c.contains(a)}:function(a,b){while(a=a.parentNode)if(a===b)return 1;return 0},_=function(){var b=a.createElement("p");return(b.innerHTML='x')&&b.firstChild.getAttribute("href")!="#x"?function(a,b){return b==="class"?a.className:b==="href"||b==="src"?a.getAttribute(b,2):a.getAttribute(b)}:function(a,b){return a.getAttribute(b)}}(),ba=!!a[c],bb=a.querySelector&&a[e],bc=function(a,b){var c=[],d,f;try{if(b[h]===9||!p.test(a))return K(b[e](a));I(d=a.split(","),Z(b,function(a,b){f=a[e](b),f.length==1?c[c.length]=f.item(0):f.length&&(c=c.concat(K(f)))}));return d.length>1&&c.length>1?U(c):c}catch(g){}return bd(a,b)},bd=function(a,b){var c=[],e,f,g,i,j,k;a=a.replace(q,"$1");if(f=a.match(o)){j=H(f[2]),e=b[d](f[1]||"*");for(g=0,i=e.length;g1&&c.length>1?U(c):c},be=function(a){typeof a[f]!="undefined"&&(i=a[f]?bb?bc:bd:bd)};be({useNativeQSA:!0}),Y.configure=be,Y.uniq=U,Y.is=R,Y.pseudos={};return Y},this),provide("qwery",a.exports),function(a){var b=function(){var a;try{a=require("qwery")}catch(b){a=require("qwery-mobile")}finally{return a}}();a.pseudos=b.pseudos,a._select=function(c,d){return(a._select=function(){var c;if(typeof a.create=="function")return function(c,d){return/^\s* '); 43 | return videosub_tcsecs(tcpair[0]); 44 | } 45 | 46 | function videosub_timecode_max(tc) { 47 | tcpair = tc.split(' --> '); 48 | return videosub_tcsecs(tcpair[1]); 49 | } 50 | 51 | function videosub_tcsecs(tc) { 52 | tc1 = tc.split(','); 53 | tc2 = tc1[0].split(':'); 54 | secs = Math.floor(tc2[0]*60*60) + Math.floor(tc2[1]*60) + Math.floor(tc2[2]); 55 | return secs; 56 | } 57 | 58 | function videosub_main() { 59 | // detect media element track support in browser via the existence of the addtrack method 60 | var myVideo = document.getElementsByTagName('video')[0]; 61 | var tracksupport = typeof myVideo.addTextTrack == "function" ? true : false; // check for track element method, if it doesn't exist, the browser generally doesn't support track elements 62 | 63 | // first find all video tags 64 | $VIDEOSUB('video').each(function(el) { 65 | // find track tag (this should be extended to allow multiple tracks and trackgroups) and get URL of subtitle file 66 | var subtitlesrc = ''; 67 | if (el.hasChildNodes()) { 68 | // first we check if the object is not empty, if the object has child nodes 69 | var children = el.childNodes; 70 | for (var i = 0; i < children.length; i++) { 71 | if (children[i].nodeName.toLowerCase() == 'track') { 72 | subtitlesrc = $VIDEOSUB(children[i]).attr('src'); 73 | } 74 | 75 | }; 76 | }; 77 | if (subtitlesrc.indexOf('.srt') != -1) { // we have a track tag and it's a .srt file 78 | var videowidth = $VIDEOSUB(el).attr('width'); // set subtitle div as wide as video 79 | var fontsize = 12; 80 | if (videowidth > 400) { 81 | fontsize = fontsize + Math.ceil((videowidth - 400) / 100); 82 | } 83 | var videocontainer = w.document.createElement("div"); 84 | $VIDEOSUB(videocontainer).css({ 85 | 'position': "relative" 86 | }); 87 | // wrap the existing video into the new container 88 | videocontainer.appendChild(el.cloneNode(true)); 89 | el.parentNode.replaceChild(videocontainer, el); 90 | el = videocontainer.firstChild; 91 | var subcontainer = w.document.createElement("div"); 92 | $VIDEOSUB(subcontainer).css({ 93 | 'position': 'absolute', 94 | 'bottom': '34px', 95 | 'width': (videowidth-50)+'px', 96 | 'padding': '0 25px 0 25px', 97 | 'textAlign': 'center', 98 | 'backgroundColor': 'transparent', 99 | 'color': '#ffffff', 100 | 'fontFamily': 'Helvetica, Arial, sans-serif', 101 | 'fontSize': fontsize+'px', 102 | 'fontWeight': 'bold', 103 | 'textShadow': '-1px 0px black, 0px 1px black, 1px 0px black, 0px -1px black' 104 | }); 105 | $VIDEOSUB(subcontainer).addClass('videosubbar'); 106 | $VIDEOSUB(subcontainer).appendTo(videocontainer); 107 | 108 | // called on AJAX load onComplete (to work around element reference issues) 109 | el.update = function(req) { 110 | el.subtitles = new Array(); 111 | records = req.split('\n\r'); 112 | for (var r=0;r el.subtitles.length-1) { 135 | el.subcount = el.subtitles.length-1; 136 | break; 137 | } 138 | } 139 | }; 140 | $VIDEOSUB(el).addListener('play', update_position); 141 | $VIDEOSUB(el).addListener('ended', update_position); 142 | $VIDEOSUB(el).addListener('seeked', update_position); 143 | 144 | // add event handler to be called while video is playing 145 | $VIDEOSUB(el).addListener('timeupdate', function(an_event){ 146 | var subtitle = ''; 147 | // check if the next subtitle is in the current time range 148 | if (this.currentTime.toFixed(1) > videosub_timecode_min(el.subtitles[el.subcount][1]) && this.currentTime.toFixed(1) < videosub_timecode_max(el.subtitles[el.subcount][1])) { 149 | // a subtitle element countains metadata on the first 150 | // two lines (index and timing information); we skip 151 | // those two lines and display the rest 152 | var full = el.subtitles[el.subcount]; 153 | var text = full.slice(2, full.length); 154 | subtitle = text.join('
'); 155 | } 156 | // is there a next timecode? 157 | if (this.currentTime.toFixed(1) > videosub_timecode_max(el.subtitles[el.subcount][1]) && el.subcount < (el.subtitles.length-1)) { 158 | el.subcount++; 159 | } 160 | // update subtitle div 161 | if(this.nextSibling.innerHTML != subtitle){ 162 | this.nextSibling.innerHTML = subtitle; 163 | 164 | //create and dispatch a subtitlechanged event 165 | if(window.CustomEvent){//only dispatch the event if the browser supports it 166 | var event = new CustomEvent("subtitlechanged",{ 167 | detail:{ 168 | target:this.nextSibling,//target div where the subtitle appears 169 | video:this,//video div 170 | content:subtitle,//content of the subtitle (subtitle text) 171 | atTime:this.currentTime,//timecode of the video at the moment of change 172 | }, 173 | bubbles:true, 174 | cancelable:true 175 | }); 176 | 177 | this.dispatchEvent(event); 178 | } 179 | } 180 | }); 181 | 182 | } 183 | }); 184 | } 185 | })(window); 186 | --------------------------------------------------------------------------------