{"id":4295,"date":"2010-03-05T00:51:19","date_gmt":"2010-03-04T23:51:19","guid":{"rendered":"http:\/\/www.chipwreck.de\/blog\/?p=4295"},"modified":"2018-01-10T13:41:55","modified_gmt":"2018-01-10T12:41:55","slug":"html-5-video-mootools-part-2","status":"publish","type":"post","link":"https:\/\/www.chipwreck.de\/blog\/2010\/03\/05\/html-5-video-mootools-part-2\/","title":{"rendered":"HTML 5 video &#038; mootools, part 2: Timeline"},"content":{"rendered":"<p>After creating buttons and a volume slider in the <a href=\"\/blog\/2010\/03\/02\/html-5-video-mootools\/\">last post<\/a>, we&#8217;re now going to develop a time slider &mdash; a movable slider which shows the current time of the video and which can be dragged to move the video playback accordingly.<!--more--><\/p>\n<p>Needed are two methods: Update the slider when the video is played and update the video when the slider is moved.<\/p>\n<h5>From time to range<\/h5>\n<p>The position of the slider relative to its width (or range) should correspond the relative timing position of the video (relative to its duration).<\/p>\n<p>So we have:<\/p>\n<p class=\"math\">\nposition = time \/ duration * range\n<\/p>\n<h5>updatePosition: function(time)<\/h5>\n<p class=\"small\">Is called onTimeupdate at the video<\/p>\n<p>The first one is called on every <kbd>timeupdate<\/kbd>-event: It receives the current time from the video, converts it relative to the slider range and then moves the slider to the calculated position.<\/p>\n<h5>updateVideo: function(position)<\/h5>\n<p class=\"small\">Is called onChange at the slider<\/p>\n<p>The second is the other way around: On changing the slider position we calculate the video position the other way around and move the video playback to that position.<\/p>\n<h5>Fireworks and other problems<\/h5>\n<p>Unfortunately moving the slider (updating the video time) triggers the timeupdate event, which then moves the slider, which then &#8230; A nice fireworks of events &#8211; which we better try to avoid. In order to prevent one event from triggering the other, we do not call the set()-method from Slider if the video time has changed (which would then fire an onChange event) but we move the slider by updating the knob position.<\/p>\n<p>We also have to avoid updating the slider if the video is in the <kbd>seeking<\/kbd>-state, because then the knob would jump around while the video is still loading data to find the target position.<\/p>\n<h5>Extending the slider<\/h5>\n<p>I created a new class (CwVideotimeline) with the methods described above. It is an extension of the Slider-class and automatically attaches the methods to the video events.<\/p>\n<h5>Class definition<\/h5>\n<pre lang=\"javascript\">\r\nCwVideotimeline = new Class({\r\n\r\n\tImplements: [Events, Options],\r\n\tExtends: Slider,\r\n\t\r\n\toptions: {\r\n\t\tvideo: false,\r\n\t\tonChange: function(position) {\r\n\t\t\tthis.updateVideo(position);\r\n\t\t}\r\n\t},\r\n\t\r\n\tinitialize: function(element, knob, options)\r\n\t{\r\n\t\tthis.parent(element, knob, options);\r\n\t\tthis.setOptions(options);\r\n\t\t\r\n\t\t\/\/ on video timeupdate: call updatePosition\t\t\r\n\t\t$(this.options.video).addEvent('timeupdate', function(an_event) {\r\n\t\t\tthis.updatePosition(an_event.target.get('currentTime'));\r\n\t\t}.bind(this));\r\n\r\n\t\t\/\/ if we have finished seeking in the video: update the time\r\n\t\t$(this.options.video).addEvent('seeked', function(an_event) {\r\n\t\t\tthis.updatePosition(an_event.target.get('currentTime'));\r\n\t\t}.bind(this));\r\n\t},\r\n\t\r\n\tupdatePosition: function(time)\r\n\t{\r\n\t\tduration = $(this.options.video).get('duration');\r\n\t\tif (duration == 0 || this.range == 0 || $(this.options.video).get('seeking')) return; \/\/ we are seeking or video has no duration\r\n\r\n\t\t\/\/ we \"manually\" set the knob position in order to avoid triggering another event\r\n\t\tposition = this.toPosition( time \/ duration * this.range );\r\n\t\tthis.knob.setStyle(this.property, position);\t\t\r\n\t},\r\n\t\r\n\tupdateVideo: function(position)\r\n\t{\r\n\t\tduration = $(this.options.video).get('duration');\r\n\t\tif (duration == 0 || this.range == 0) return;\r\n\t\tvideotime = ( position \/ this.range * duration); \/\/ from position to time\r\n\t\t$(this.options.video).set('currentTime', videotime);\r\n\t}\t\r\n\r\n});\r\n<\/pre>\n<h5>Class usage example<\/h5>\n<pre lang=\"javascript\">\r\nvar timeSlider = new CwVideotimeline('container', 'knob', {\r\n    range: [0, 200],\r\n    video: 'myvid',\r\n    steps: 100,\r\n    initialStep: 0\r\n});\r\n<\/pre>\n<h5>Chrome problems<\/h5>\n<p>Of course one browser always makes trouble: This time it&#8217;s Chrome, which does not continue to send <kbd>timeupdate<\/kbd>-events after seeking and suspending the video download. It&#8217;s a known bug, so hopefully fixed in the next version. Details: (<a href=\"http:\/\/code.google.com\/p\/chromium\/issues\/detail?id=34390\" class=\"external\">Bugtracker for Chrome<\/a>)<\/p>\n<h5>Example<\/h5>\n<p>See it in action below &#8211; note that not only dragging but also clicking the timline works (this is because Fx.Slider already handles this) and the rewind-button automatically updates the slider:<\/p>\n<p>\t<script type=\"text\/javascript\" src=\"\/blog\/wp-content\/themes\/chipwreck\/js\/mootools-1.2.5-core-yc.js\"><\/script><br \/>\n\t<script type=\"text\/javascript\" src=\"\/blog\/wp-content\/themes\/chipwreck\/js\/mootools-1.2.4.4-more-all-yc.js\"><\/script><br \/>\n\t<script type=\"text\/javascript\" src=\"\/blog\/wp-content\/themes\/chipwreck\/js\/chipwreck.js\"><\/script><\/p>\n<p>\t<video id=\"myvid\" width=\"320\" height=\"240\" poster=\"\/videos\/daftpunk\/DaftPunk_Poster.jpg\"><source src=\"\/videos\/daftpunk\/DaftPunk_Concert_2.ogg\" type='video\/ogg; codecs=\"theora, vorbis\"'><source src=\"\/videos\/daftpunk\/DaftPunk_Concert_2.mp4\" type='video\/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"'><\/video><\/p>\n<p>\n\t\t<button onclick=\"$('myvid').set('currentTime', 0)\">&laquo; rewind<\/button><br \/>\n\t\t&nbsp;<br \/>\n\t\t<a class=\"download\" id=\"playbutton\" onclick=\"$('myvid').play()\">play<\/a><br \/>\n\t\t<a class=\"download\" id=\"pausebutton\" onclick=\"$('myvid').pause()\">pause<\/a><br \/>\n\t\t<kbd id=\"timemeter\" style=\"width: 3em; display: inline-block; text-align: right;\">0.0<\/kbd>s<br \/>\n\t\t&nbsp;&nbsp;<br \/>\n\t\t<button id=\"mutebutton\" onclick=\"$('myvid').set('muted', !$('myvid').get('muted'))\">mute<\/button>\n\t<\/p>\n<h5>CwVideotimeline:<\/h5>\n<p id=\"timeSliderBg\" style=\"width: 200px; border: 1px dotted #75838a; padding: 1px 0px;\"><span id=\"timeSlider\" class=\"download\" style=\"background-color: #75838a; margin: 0; width: 40px; padding: 3px; display: inline-block;\">&nbsp;<\/span><\/p>\n<p><script type=\"text\/javascript\">\nwindow.addEvent('domready', function() {<\/p>\n<p>\tvar readyState = function(el)\n\t{\n\t\tswitch (el.get('readyState')) {\n\t\t \tcase el.HAVE_NOTHING: return 'have nothing';\n\t \t\tcase el.HAVE_METADATA: return 'have meta';\n\t \t\tcase el.HAVE_CURRENT_DATA: return 'have current';\n\t \t\tcase el.HAVE_FUTURE_DATA: return 'have future data';\n\t \t\tcase el.HAVE_ENOUGH_DATA: return 'have enough data';\n\t \t\tdefault: return 'unknown state';\n\t\t}\n\t}\n\tvar networkState = function(el)\n\t{\n\t\tswitch (el.get('networkState')) {\t\t\t\n\t\t\tcase el.NETWORK_EMPTY: return 'empty';\n\t\t\tcase el.NETWORK_IDLE: return 'idle';\n\t\t\tcase el.NETWORK_LOADING: return 'loading';\n\t\t\tcase el.NETWORK_LOADED: return 'loaded';\n\t\t\tcase el.NETWORK_NO_SOURCE: return 'no source';\n\t\t\tdefault: return 'unknown state';\n\t\t}\n\t}<\/p>\n<p>\tvar media_events = {\n\t    loadstart: 2,\n\t    progress: 2,\n\t    suspend: 2,\n\t    abort: 2,\n\t    error: 2,\n\t    emptied: 2,\n\t    stalled: 2,\n\t    play: 2,\n\t    pause: 2,\n\t    loadedmetadata: 2,\n\t    loadeddata: 2,\n\t    waiting: 2,\n\t    playing: 2,\n\t    canplay: 2,\n\t    canplaythrough: 2,    \n\t\tseeking: 2,\n\t\tseeked: 2,\n\t    timeupdate: 2,\n\t    ended: 2,\n\t    ratechange: 2,\n\t    durationchange: 2,\n\t    volumechange: 2\n\t}\n\tElement.NativeEvents = $merge(Element.NativeEvents, media_events);<\/p>\n<p>\tvar media_properties = [\n\t\t'videoWidth',\n\t\t'videoHeight',\n\t\t'readyState',\n\t\t'autobuffer',\n\t\t'buffered', \/\/ no ff\n\t\t'error', \n\t\t'networkState',\n\t\t'currentTime',\n\t\t'startTime', \/\/ no ff\n\t\t'duration',\n\t\t'paused',\n\t\t'defaultPlaybackRate', \/\/ no ff\n\t\t'playbackRate', \/\/ no ff\n\t\t'played', \/\/ no ff\n\t\t'seeking',\n\t\t'seekable', \/\/ no ff\n\t\t'ended',\n\t\t'autoplay',\n\t\t'loop', \n\t\t'controls',\n\t\t'volume',\n\t\t'muted'\n\t];<\/p>\n<p>\tmedia_properties.each(function(prop){\n\t\tElement.Properties.set(prop, {\n\t\t\tset: function(value){\n\t\t\t\tthis[prop] = value;\n\t\t\t},\n\t\t\tget: function(){\n\t\t\t\treturn this[prop];\n\t\t\t}\n\t\t})\n\t});<\/p>\n<p>\tCwVideotimeline = new Class({<\/p>\n<p>\t\tImplements: [Events, Options],\n\t\tExtends: Slider,<\/p>\n<p>\t\toptions: {\n\t\t\tvideo: false,\n\t\t\tonChange: function(position) {\n\t\t\t\tthis.updateVideo(position);\n\t\t\t}\n\t\t},<\/p>\n<p>\t\tinitialize: function(element, knob, options)\n\t\t{\n\t\t\tthis.parent(element, knob, options);\n\t\t\tthis.setOptions(options);<\/p>\n<p>\t\t\t\/\/ on video timeupdate: call updatePosition\t\t\n\t\t\t$(this.options.video).addEvent('timeupdate', function(an_event) {\n\t\t\t\tthis.updatePosition(an_event.target.get('currentTime'));\n\t\t\t}.bind(this));<\/p>\n<p>\t\t\t\/\/ if we have finished seeking in the video: fire timeupdate\n\t\t\t$(this.options.video).addEvent('seeked', function(an_event) {\n\t\t\t\tthis.fireEvent('timeupdate', an_event.target.get('currentTime'));\n\t\t\t}.bind(this));<\/p>\n<p>\t\t},<\/p>\n<p>\t\tupdatePosition: function(time)\n\t\t{\n\t\t\tduration = $(this.options.video).get('duration');\n\t\t\tif (duration == 0 || this.range == 0 || $(this.options.video).get('seeking')) return; \/\/ we are seeking or video has no duration<\/p>\n<p>\t\t\t\/\/ we \"manually\" set the knob position in order to avoid triggering another event\n\t\t\tposition = this.toPosition( time \/ duration * this.range );\n\t\t\tthis.knob.setStyle(this.property, position);\t\t\n\t\t},<\/p>\n<p>\t\tupdateVideo: function(position)\n\t\t{\n\t\t\tduration = $(this.options.video).get('duration');\n\t\t\tif (duration == 0 || this.range == 0) return;\n\t\t\tvideotime = ( position \/ this.range * duration); \/\/ from position to time\n\t\t\t$(this.options.video).set('currentTime', videotime);\n\t\t}\t<\/p>\n<p>\t});<\/p>\n<p>\tvar timeSlider = new CwVideotimeline('timeSliderBg', 'timeSlider', {\n\t    range: [0, 200],\n\t    wheel: true,\n\t    video: 'myvid'\n\t});<\/p>\n<p>\t$('myvid').addEvent('timeupdate', function(an_event){\n\t\t$('timemeter').set('html', this.get('currentTime').toFixed(1));\n\t});<\/p>\n<p>});\n<\/script><\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>After creating buttons and a volume slider in the last post, we&#8217;re now going to develop a time slider &mdash; a movable slider which shows the current time of the video and which can be dragged to move the video playback accordingly.<\/p>\n","protected":false},"author":2,"featured_media":4313,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[14],"tags":[55,10,54,79],"class_list":["post-4295","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-javascript","tag-javascript-mootools","tag-mootools","tag-track","tag-webdesign"],"jetpack_featured_media_url":"https:\/\/www.chipwreck.de\/blog\/wp-content\/uploads\/2010\/03\/videotimeline.png","jetpack_shortlink":"https:\/\/wp.me\/paPEN-17h","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/posts\/4295","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/comments?post=4295"}],"version-history":[{"count":1,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/posts\/4295\/revisions"}],"predecessor-version":[{"id":8156,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/posts\/4295\/revisions\/8156"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/media\/4313"}],"wp:attachment":[{"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/media?parent=4295"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/categories?post=4295"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.chipwreck.de\/blog\/wp-json\/wp\/v2\/tags?post=4295"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}