From 13b887abfb8ea4d24c87d9454186373cc94ef01b Mon Sep 17 00:00:00 2001 From: rolux Date: Thu, 12 May 2011 12:39:48 +0200 Subject: [PATCH] add Ox.VideoPlayer, demos/video/, and prove Chrome doesn't get buffered time ranges right --- demos/video/index.html | 10 + demos/video/js/video.js | 17 ++ source/Ox.UI/js/Core/Ox.Element.js | 2 +- source/Ox.UI/js/Video/Ox.VideoElement.js | 20 +- source/Ox.UI/js/Video/Ox.VideoPlayer.js | 317 +++++++++++++++++++++++ 5 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 demos/video/index.html create mode 100644 demos/video/js/video.js create mode 100644 source/Ox.UI/js/Video/Ox.VideoPlayer.js diff --git a/demos/video/index.html b/demos/video/index.html new file mode 100644 index 00000000..a3190658 --- /dev/null +++ b/demos/video/index.html @@ -0,0 +1,10 @@ + + + + OxJS Video Demo + + + + + + \ No newline at end of file diff --git a/demos/video/js/video.js b/demos/video/js/video.js new file mode 100644 index 00000000..915c0c4b --- /dev/null +++ b/demos/video/js/video.js @@ -0,0 +1,17 @@ +Ox.load('UI', { + debug: true, + theme: 'modern' +}, function() { + var id = '0393109'; + var url = 'http://next.0xdb.org/' + id + '/96p.webm?' + Ox.random(1000000); + var timeline = 'http://next.0xdb.org/' + id + '/timeline.16.png'; + Ox.UI.$body.css({ + padding: '16px' + }); + Ox.VideoPlayer({ + height: 288, + timeline: timeline, + url: url, + width: 540 + }).appendTo(Ox.UI.$body); +}); diff --git a/source/Ox.UI/js/Core/Ox.Element.js b/source/Ox.UI/js/Core/Ox.Element.js index 79acc1e2..8298aa55 100644 --- a/source/Ox.UI/js/Core/Ox.Element.js +++ b/source/Ox.UI/js/Core/Ox.Element.js @@ -331,7 +331,7 @@ Ox.Element = function() { Ox.forEach(Ox.makeObject(arguments), function(data, event) { if ([ 'mousedown', 'mouserepeat', 'anyclick', 'singleclick', 'doubleclick', - 'dragstart', 'drag', 'dragpause', 'dragend', 'playing' + 'dragstart', 'drag', 'dragpause', 'dragend', 'playing', 'progress' ].indexOf(event) == -1) { Ox.print(that.id, self.options.id, 'trigger', event, data); } diff --git a/source/Ox.UI/js/Video/Ox.VideoElement.js b/source/Ox.UI/js/Video/Ox.VideoElement.js index 3432c778..20a68ace 100644 --- a/source/Ox.UI/js/Video/Ox.VideoElement.js +++ b/source/Ox.UI/js/Video/Ox.VideoElement.js @@ -18,9 +18,11 @@ Ox.VideoElement = function(options, self) { }) .options(options || {}) .attr({ + //height: self.options.height, poster: self.options.poster, preload: 'auto', - src: self.options.url + src: self.options.url, + //width: self.options.width }) .css({ height: self.options.height + 'px', @@ -28,8 +30,23 @@ Ox.VideoElement = function(options, self) { }) .bind({ ended: ended, + canplay: function() { + Ox.print('canplay') + }, + durationchange: function() { + Ox.print('durationchange') + }, loadedmetadata: function() { + Ox.print('loadedmetadata', self.video.duration) self.video.currentTime = self.options.position; + that.triggerEvent('loadedmetadata', { + video: self.video + }) + }, + progress: function() { + that.triggerEvent('progress', { + video: self.video + }); } }); @@ -112,6 +129,7 @@ Ox.VideoElement = function(options, self) { } that.position = function(pos) { + // fixme: why not use options?? if (arguments.length == 0) { return self.video.currentTime; } else { diff --git a/source/Ox.UI/js/Video/Ox.VideoPlayer.js b/source/Ox.UI/js/Video/Ox.VideoPlayer.js new file mode 100644 index 00000000..6812356f --- /dev/null +++ b/source/Ox.UI/js/Video/Ox.VideoPlayer.js @@ -0,0 +1,317 @@ +Ox.VideoPlayer = function(options, self) { + + self = self || {}; + var that = Ox.Element({}, self) + .defaults({ + height: 192, + position: 0, + timeline: '', + url: '', + width: 256 + }) + .options(options || {}) + .css({ + width: self.options.width + 'px', + height: self.options.height + 'px' + //background: 'red' + }) + .bind({ + mouseenter: showControls, + mouseleave: hideControls + }); + + self.buffered = []; + self.controlsTimeout; + + self.barHeight = 16; + self.outerBarWidth = self.options.width - 96; + self.innerBarWidth = self.outerBarWidth - self.barHeight; + self.markerSize = 12; + self.markerBorderSize = 2; + self.markerOffset = -self.innerBarWidth - self.markerSize / 2; + + self.$video = Ox.VideoElement({ + height: self.options.height, + paused: true, + url: self.options.url, + width: self.options.width + }) + .css({ + position: 'absolute', + }) + .bindEvent({ + loadedmetadata: loadedmetadata, + paused: function(data) { + // called when playback ends + self.$playButton.toggleTitle(); + self.$positionMarker.css({ + borderColor: 'rgb(192, 192, 192)' + }); + }, + playing: function(data) { + setPosition(data.position); + }, + progress: function(data) { + var buffered = data.video.buffered; + for (var i = 0; i < buffered.length; i++) { + self.buffered[i] = [buffered.start(i), buffered.end(i)]; + // fixme: firefox weirdness + if (self.buffered[i][0] > self.buffered[i][1]) { + self.buffered[i][0] = 0; + } + Ox.print(i, self.buffered[i][0], self.buffered[i][1]) + } + self.$buffered.attr({ + src: getBufferedImageURL() + }) + //self.$bar.html(Ox.formatDuration(data.video.buffered.end(data.video.buffered.length - 1))) + } + }) + .appendTo(that); + + self.$controls = Ox.Bar({ + size: self.barHeight + }) + .css({ + position: 'absolute', + width: self.options.width + 'px', + marginTop: self.options.height - self.barHeight + 'px', + }) + .appendTo(that); + + self.$buttons = Ox.Element() + .css({ + float: 'left', + width: '48px' + }) + .appendTo(self.$controls); + + self.$playButton = Ox.Button({ + style: 'symbol', + title: [ + {id: 'play', title: 'play'}, + {id: 'pause', title: 'pause'} + ], + tooltip: ['Play', 'Pause'], + type: 'image' + }) + .css({ + borderRadius: 0 + }) + .bindEvent('click', togglePlay) + .appendTo(self.$buttons); + + self.$muteButton = Ox.Button({ + style: 'symbol', + title: [ + {id: 'mute', title: 'mute'}, + {id: 'unmute', title: 'unmute'} + ], + tooltip: ['Mute', 'Unmute'], + type: 'image' + }) + .css({ + borderRadius: 0 + }) + .bindEvent('click', toggleMute) + .appendTo(self.$buttons); + + self.$menuButton = Ox.Button({ + id: 'select', + style: 'symbol', + title: 'select', + tooltip: ['Menu'], + type: 'image' + }) + .css({ + borderRadius: 0 + }) + .bindEvent('click', togglePlay) + .appendTo(self.$buttons); + + self.$outerBar = Ox.Element() + .css({ + float: 'left', + width: self.outerBarWidth + 'px', + height: self.barHeight + 'px', + background: 'rgb(0, 0, 0)', + borderRadius: self.barHeight / 2 + 'px' + }) + .appendTo(self.$controls); + + self.$innerBar = Ox.Element() + .css({ + float: 'left', + width: self.innerBarWidth + 'px', + height: self.barHeight + 'px', + marginLeft: self.barHeight / 2 + 'px' + }) + .appendTo(self.$outerBar); + + self.$timeline = $('') + .attr({ + src: self.options.timeline + }) + .css({ + float: 'left', + width: self.innerBarWidth + 'px', + height: self.barHeight + 'px' + }) + .appendTo(self.$innerBar.$element); + + self.$buffered = $('') + .attr({ + src: getBufferedImageURL() + }) + .css({ + float: 'left', + marginLeft: -self.innerBarWidth + 'px', + width: self.innerBarWidth + 'px', + height: self.barHeight + 'px', + }) + .appendTo(self.$innerBar.$element); + + self.$positionMarker = Ox.Element() + .css({ + float: 'left', + width: self.markerSize - self.markerBorderSize * 2 + 'px', + height: self.markerSize - self.markerBorderSize * 2 + 'px', + marginTop: (self.barHeight - self.markerSize) / 2 + 'px', + marginLeft: self.markerOffset + 'px', + border: self.markerBorderSize + 'px solid rgb(192, 192, 192)', + borderRadius: self.markerSize - self.markerBorderSize * 2 + 'px', + //background: 'rgba(0, 0, 0, 0.5)', + boxShadow: '0 0 ' + (self.barHeight - self.markerSize) / 2 + 'px black' + }) + .appendTo(self.$outerBar); + + self.$tooltip = Ox.Tooltip({ + animate: false + }); + + self.$position = Ox.Element() + .css({ + float: 'left', + width: '44px', + height: '12px', + padding: '2px', + fontSize: '9px', + textAlign: 'center' + }) + .html(Ox.formatDuration(self.options.position)) + .appendTo(self.$controls) + + function mousedownBar(e) { + setPosition(getPosition(e)); + self.$video.position(self.options.position); + } + function mouseleaveBar(e) { + self.$tooltip.hide(); + } + function mousemoveBar(e) { + self.$tooltip.options({ + title: Ox.formatDuration(getPosition(e)) + }).show(e.clientX, e.clientY); + } + function getPosition(e) { + // fixme: no offsetX in firefox??? + if ($.browser.mozilla) { + //Ox.print(e, e.layerX - 56) + return Ox.limit( + (e.layerX - 48 - self.barHeight / 2) / self.innerBarWidth * self.duration, + 0, self.duration + ); + } else { + return Ox.limit( + (e.offsetX - self.barHeight / 2) / self.innerBarWidth * self.duration, + 0, self.duration + ); + } + } + + function setPosition(position) { + self.options.position = position; + //Ox.print(-self.barWidth - 4 + self.barWidth * position / self.duration) + self.$positionMarker.css({ + marginLeft: self.innerBarWidth * position / self.duration + self.markerOffset + 'px', + }); + self.$position.html(Ox.formatDuration(position)); + } + + function loadedmetadata(data) { + //self.$position.html(Ox.formatDuration(data.video.duration)) + Ox.print('!!!!', data.video.width, data.video.height, data.video.videoWidth, data.video.videoHeight) + self.duration = data.video.duration; + Ox.print('DURATION', Ox.formatDuration(self.duration)); + that.gainFocus().bindEvent({ + key_space: function() { + self.$playButton.toggleTitle(); + togglePlay(); + } + }); + self.$outerBar.bind({ + mousedown: mousedownBar, + mousemove: mousemoveBar, + mouseleave: mouseleaveBar + }); + } + + function getBufferedImageURL() { + var width = self.innerBarWidth, + height = self.barHeight, + $canvas = $('') + .attr({ + width: width, + height: height, + }), + canvas = $canvas[0], + context = canvas.getContext('2d'); + //Ox.print(width, height) + context.fillStyle = 'rgba(255, 0, 0, 0.5)'; + context.fillRect(0, 0, width, height); + var imageData = context.getImageData(0, 0, width, height), + data = imageData.data; + + self.buffered.forEach(function(range) { + var left = Math.round(range[0] * width / self.duration), + right = Math.round(range[1] * width / self.duration); + Ox.loop(left, right, function(x) { + Ox.loop(height, function(y) { + index = x * 4 + y * 4 * width; + data[index + 3] = 0; + }); + }); + }); + context.putImageData(imageData, 0, 0); + return canvas.toDataURL(); + } + + function hideControls() { + self.controlsTimeout = setTimeout(function() { + self.$controls.animate({ + opacity: 0 + }, 250); + }, 1000); + } + + function showControls() { + clearTimeout(self.controlsTimeout); + self.$controls.animate({ + opacity: 1 + }, 250); + } + + function toggleMute() { + self.$video.toggleMute(); + } + + function togglePlay() { + self.$video.togglePlay(); + self.$positionMarker.css({ + borderColor: self.$video.paused() ? 'rgb(192, 192, 192)' : 'rgb(255, 255, 255)' + }); + } + + return that; + +}; \ No newline at end of file