diff --git a/demos/video/js/video.js b/demos/video/js/video.js index 5325365d..e1bbbbd8 100644 --- a/demos/video/js/video.js +++ b/demos/video/js/video.js @@ -27,14 +27,15 @@ Ox.load('UI', { {'in': 5780.235, out: 5782.435}, {'in': 5881.365, out: 5886.635} ], - timeline = 'http://next.0xdb.org/' + id + '/timeline.16.png', + timeline = 'png/timeline.16.png', url = 'http://next.0xdb.org/' + id + '/96p.webm', videoSize = getVideoSize(), $videos = [ Ox.VideoPlayer({ controls: ['play', 'mute', 'fullscreen', 'scale', 'timeline', 'position'], + enableFind: true, + enableFullscreen: true, enableKeyboard: true, - find: 'brick', focus: 'mouseenter', height: 192, 'in': 3128.725, @@ -47,7 +48,9 @@ Ox.load('UI', { showIconOnLoad: true, showProgress: false, subtitles: 'srt/' + id + '.srt', - timeline: timeline, + timeline: function(i) { + return 'png/timeline.16.' + i + '.png'; + }, title: 'Brick - Rian Johnson - 2005', video: url + '?' + + Ox.random(1000000), width: 360 diff --git a/demos/video/png/logo.png b/demos/video/png/logo.png new file mode 100644 index 00000000..ce3e7202 Binary files /dev/null and b/demos/video/png/logo.png differ diff --git a/source/Ox.UI/js/Video/Ox.SmallVideoTimeline.js b/source/Ox.UI/js/Video/Ox.SmallVideoTimeline.js index 52e005aa..6b09106a 100644 --- a/source/Ox.UI/js/Video/Ox.SmallVideoTimeline.js +++ b/source/Ox.UI/js/Video/Ox.SmallVideoTimeline.js @@ -3,6 +3,7 @@ Ox.SmallVideoTimeline = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ + _offset: 0, // hack for cases where all these position: absolute elements have to go into a float: left duration: 0, editing: false, getTimelineURL: null, @@ -226,13 +227,15 @@ Ox.SmallVideoTimeline = function(options, self) { self.$positionMarker.css({ left: self.interfaceLeft + Math.round( self.options.position * self.imageWidth / self.options.duration - ) - (self.options.type == 'editor' ? 4 : 0) + 'px', + ) - (self.options.type == 'editor' ? 4 : 0) + self.options._offset + 'px', }); } function setWidth() { self.imageWidth = self.options.width - (self.options.type == 'player' ? 16 : 8); + self.interfaceWidth = self.options.type == 'player' ? + self.options.width : self.imageWidth; that.css({ width: self.options.width + 'px' }); @@ -252,10 +255,24 @@ Ox.SmallVideoTimeline = function(options, self) { } self.setOption = function(key, value) { - if (key == 'paused') { + if (key == 'duration') { + self.$image.options({ + duration: value + }); + } else if (key == 'paused') { self.$positionMarkerRing.css({ borderColor: 'rgba(255, 255, 255, ' + (self.options.paused ? 0.5 : 1) + ')' }) + } else if (key == 'position') { + setPositionMarker(); + } else if (key == 'results') { + self.$image.options({ + results: value + }); + } else if (key == 'subtitles') { + self.$image.options({ + subtitles: value + }); } else if (key == 'width') { setWidth(); } diff --git a/source/Ox.UI/js/Video/Ox.SmallVideoTimelineImage.js b/source/Ox.UI/js/Video/Ox.SmallVideoTimelineImage.js index 746b76f5..06445a7d 100644 --- a/source/Ox.UI/js/Video/Ox.SmallVideoTimelineImage.js +++ b/source/Ox.UI/js/Video/Ox.SmallVideoTimelineImage.js @@ -14,7 +14,6 @@ Ox.SmallVideoTimelineImage = function(options, self) { type: 'player' }) .options(options || {}) - .addClass('OxSmallVideoTimeline') .css({ position: 'absolute', width: self.options.width + 'px' @@ -84,6 +83,8 @@ Ox.SmallVideoTimelineImage = function(options, self) { .appendTo(that.$element); function getImageURL(image, callback) { + Ox.print(image == 'results' || image == 'selection' ? + self.options.width : Math.ceil(self.options.duration)) var width = image == 'results' || image == 'selection' ? self.options.width : Math.ceil(self.options.duration), height = self.imageHeight, @@ -160,21 +161,33 @@ Ox.SmallVideoTimelineImage = function(options, self) { }); }); } else if (image == 'timeline') { - var counter = 0, - images = Math.ceil(self.options.duration / 3600), + var $image, counter, images, top = self.options.type == 'player' ? 0 : 1; - Ox.loop(images, function(i) { - var $image = $('') + if (Ox.isString(self.options.timeline)) { + $image = $('') .attr({ - src: self.options.getTimelineURL(i) + src: self.options.getTimelineURL(0) }) .load(function() { - context.drawImage($image[0], i * 3600, top); - if (++counter == images) { - callback(canvas.toDataURL()); - } + context.drawImage($image[0]) + callback(canvas.toDataURL()); }); - }) + } else { + counter = 0; + images = Math.ceil(self.options.duration / 3600); + Ox.loop(images, function(i) { + var $image = $('') + .attr({ + src: self.options.getTimelineURL(i) + }) + .load(function() { + context.drawImage($image[0], i * 3600, top); + if (++counter == images) { + callback(canvas.toDataURL()); + } + }); + }); + } } if (image != 'timeline') { context.putImageData(imageData, 0, 0); diff --git a/source/Ox.UI/js/Video/Ox.VideoPlayer.js b/source/Ox.UI/js/Video/Ox.VideoPlayer.js index 70e91a6c..3c985aa1 100644 --- a/source/Ox.UI/js/Video/Ox.VideoPlayer.js +++ b/source/Ox.UI/js/Video/Ox.VideoPlayer.js @@ -14,6 +14,8 @@ Ox.VideoPlayer Generic Video Player is just empty space that separates left-aligned from right-aligned controls duration Duration (sec) + enableFind If true, enable find + enableFullscreen If true, enable fullscreen enableKeyboard If true, enable keyboard controls externalControls If true, controls are outside the video find Query string @@ -66,7 +68,9 @@ Ox.VideoPlayer = function(options, self) { .defaults({ annotations: [], controls: [], - duration: 86399, + duration: 0, + enableFind: false, + enableFullscreen: false, enableKeyboard: false, externalControls: false, find: '', @@ -135,6 +139,13 @@ Ox.VideoPlayer = function(options, self) { key_1: function() { toggleScale(); }, + key_f: function() { + // need timeout so the "f" doesn't appear in the input field + setTimeout(self.$findInput.focusInput, 0); + }, + key_g: function() { + goToNextResult(1); + }, key_left: function() { setPosition(self.options.position - self.secondsPerFrame, true); }, @@ -145,7 +156,10 @@ Ox.VideoPlayer = function(options, self) { setPosition(self.options.position + self.secondsPerFrame, true); }, key_shift_f: function() { - toggleFullscreen(true); + self.options.enableFullscreen && toggleFullscreen(true); + }, + key_shift_g: function() { + goToNextResult(-1); }, key_space: function() { togglePaused(true); @@ -196,15 +210,24 @@ Ox.VideoPlayer = function(options, self) { } else { Ox.get(self.options.subtitles, function(data) { self.options.subtitles = Ox.parseSRT(data); + self.results = find(self.options.find); + Ox.print('--setting results--', self.$timeline) + if (self.options.duration) { + self.$timeline && self.$timeline.options({ + results: self.results, + subtitles: self.options.subtitles + }); + } else { + + } }); - //self.options.subtitles = []; + self.options.subtitles = []; } } - + self.results = find(self.options.find); self.buffered = []; self.controlsTimeout; - self.dragTimeout; self.$videoContainer = $('
') .css({ @@ -304,7 +327,7 @@ Ox.VideoPlayer = function(options, self) { } } - if (self.options.subtitles.length) { + if (self.options.subtitles.length || true) { // fixme self.$subtitle = $('
') //.addClass('OxSubtitle') .css({ @@ -339,6 +362,114 @@ Ox.VideoPlayer = function(options, self) { .appendTo(that.$element); } + if (self.options.enableFind) { + + self.$find = $('
') + .addClass('OxInterface') + .css({ + position: 'absolute', + right: '16px', + top: (self.options.title ? 32 : 16) + 'px', + width: '128px', + borderRadius: '8px', + opacity: 0 + }) + .css({ + backgroundImage: '-moz-linear-gradient(top, rgba(64, 64, 64, 0.5), rgba(0, 0, 0, 0.5))' + }) + .css({ + backgroundImage: '-webkit-linear-gradient(top, rgba(64, 64, 64, 0.5), rgba(0, 0, 0, 0.5))' + }) + .appendTo(that.$element); + + self.$previousButton = Ox.Button({ + style: 'symbol', + title: 'arrowLeft', + tooltip: 'Previous [Shift+G]', + type: 'image' + }) + .css({float: 'left', opacity: 0.25}) + .bindEvent({ + click: function() { + goToNextResult(-1); + } + }) + .appendTo(self.$find); + + self.$nextButton = Ox.Button({ + style: 'symbol', + title: 'arrowRight', + tooltip: 'Next [G]', + type: 'image' + }) + .css({float: 'left', opacity: 0.25}) + .bindEvent({ + click: function() { + goToNextResult(1); + } + }) + .appendTo(self.$find); + + self.$findInput = Ox.Input({ + placeholder: 'Find', + value: self.options.find, + width: 86 + }) + .css({ + float: 'left', + background: 'rgba(0, 0, 0, 0)', + MozBoxShadow: '0 0 0', + WebkitBoxShadow: '0 0 0' + }) + .bindEvent({ + focus: function() { + self.inputHasFocus = true; + }, + blur: function() { + self.inputHasFocus = false; + submitFindInput(); + } + }) + .appendTo(self.$find); + + self.$findInput.children('input').css({ + width: (self.positionWidth - 6) + 'px', + height: '16px', + padding: '0 3px 0 3px', + border: '0px', + borderRadius: '8px', + fontSize: '11px', + color: 'rgb(255, 255, 255)' + }) + .css({ + background: '-moz-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))' + }) + .css({ + background: '-webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))' + }); + + self.$clearButton = Ox.Button({ + style: 'symbol', + title: 'close', + tooltip: 'Clear', + type: 'image' + }) + .css({float: 'left'}) + .bindEvent({ + click: function() { + self.$findInput + .options({value: ''}) + .focusInput(); + self.results = []; + self.$timeline && self.$timeline.options({ + results: self.results + }); + } + }) + .appendTo(self.$find); + + } + if (self.options.controls.length) { self.$controls = Ox.Bar({ @@ -463,39 +594,9 @@ Ox.VideoPlayer = function(options, self) { .bindEvent('click', toggleSize) .appendTo(self.$controls); - }else if (control == 'timeline') { + } else if (control == 'timeline') { - self.$timeline = Ox.Element() - .addClass('timeline') - .css({ - float: 'left', - height: self.barHeight + 'px', - background: 'rgba(0, 0, 0, 0.75)', - borderRadius: self.barHeight / 2 + 'px' - }) - .appendTo(self.$controls); - - self.$timelineImages = Ox.Element() - .addClass('timelineimages') - .css({ - float: 'left', - height: self.barHeight + 'px', - marginLeft: self.barHeight / 2 + 'px' - }) - .appendTo(self.$timeline); - - self.$timelineImage = $('') - .addClass('timelineimage') - .attr({ - src: self.options.timeline - }) - .css({ - float: 'left', - height: self.barHeight + 'px' - }) - .appendTo(self.$timelineImages.$element); - - ///* + /* if (self.options.showProgress) { self.$progress = $('') .attr({ @@ -507,46 +608,18 @@ Ox.VideoPlayer = function(options, self) { }) .appendTo(self.$timelineImages.$element); } - //*/ + */ - self.$positionMarker = $('
') - .css({ - float: 'left', - width: '14px', - height: '14px', - border: '1px solid rgba(0, 0, 0, 0.5)', - borderRadius: '8px' - }) - .append( - self.$positionMarkerRing = $('
') - .css({ - width: '10px', - height: '10px', - border: '2px solid rgba(255, 255, 255, 0.5)', - borderRadius: '7px', - }) - .append( - $('
') - .css({ - width: '8px', - height: '8px', - border: '1px solid rgba(0, 0, 0, 0.5)', - borderRadius: '5px', - }) - ) - ) - .appendTo(self.$timeline.$element); - - self.$timelineInterface = Ox.Element() - .css({ - float: 'left', - height: self.barHeight + 'px', - }) - .appendTo(self.$controls); - - self.$tooltip = Ox.Tooltip({ - animate: false - }); + if (self.options.duration) { + self.$timeline = getTimeline() + } else { + self.$timeline = Ox.Element() + .css({ + float: 'left' + }) + .html(' '); + } + self.$timeline.appendTo(self.$controls); } else if (control == 'space') { @@ -659,6 +732,16 @@ Ox.VideoPlayer = function(options, self) { } } + function find(query) { + query = query.toLowerCase(); + return query.length ? Ox.map(self.options.subtitles, function(subtitle) { + return subtitle.text.toLowerCase().indexOf(query) > -1 ? { + 'in': subtitle['in'], + out: subtitle.out + } : null; + }) : []; + } + function formatPosition(position) { position = Ox.isUndefined(position) ? self.options.position : position; return Ox.formatDuration(position, self.options.showMilliseconds); @@ -712,13 +795,6 @@ Ox.VideoPlayer = function(options, self) { padding: playIconPadding + 'px', borderRadius: Math.round(self.iconSize / 2) + 'px' }; - } else if (element == 'positionMarker') { - var position = self.options.duration ? - (self.options.position - self['in']) / self.options.duration : 0; - css = { - marginLeft: position * self.timelineImageWidth - - self.timelineImageWidth - 8 + 'px', - }; } else if (element == 'poster' || element == 'video') { var playerWidth = self.width, playerHeight = self.height, @@ -759,15 +835,6 @@ Ox.VideoPlayer = function(options, self) { css = { width: self.timelineWidth + 'px' }; - } else if (element == 'timelineImage' || element == 'timelineImages') { - css = { - width: self.timelineImageWidth + 'px' - }; - } else if (element == 'timelineInterface') { - css = { - width: self.timelineWidth + 'px', - marginLeft: -self.timelineWidth + 'px' - }; } else if (element == 'videoContainer') { css = { width: self.width + 'px', @@ -841,6 +908,61 @@ Ox.VideoPlayer = function(options, self) { return subtitle; } + function getTimeline() { + var $timeline = Ox.SmallVideoTimeline({ + _offset: getTimelineLeft(), + duration: self.options.duration, + getTimelineURL: Ox.isString(self.options.timeline) ? + function() { return self.options.timeline; } : + self.options.timeline, + 'in': self.options['in'], + out: self.options.out, + paused: self.options.paused, + results: self.results, + showMilliseconds: self.options.showMilliseconds, + subtitles: self.options.subtitles, + type: 'player', + width: getTimelineWidth() + }) + .css({ + float: 'left' + }) + .bindEvent({ + position: function(data) { + setPosition(data.position, true); + } + }); + //Ox.print('??', $timeline.find('.OxInterface')) + $timeline.children().css({ + marginLeft: getTimelineLeft() + 'px' + }); + $timeline.find('.OxInterface').css({ + marginLeft: getTimelineLeft() + 'px' + }); + return $timeline; + } + + function getTimelineLeft() { + var left = 0; + Ox.forEach(self.options.controls, function(control) { + if (control == 'timeline') { + return false; + } + left += control == 'position' ? self.positionWidth : 16 + }); + return left; + } + + function getTimelineWidth() { + return (self.options.fullscreen ? window.innerWidth : self.options.width) - + self.options.controls.reduce(function(prev, curr) { + return prev + ( + curr == 'timeline' || curr == 'space' ? 0 : + curr == 'position' ? self.positionWidth : 16 + ); + }, 0); + } + function hideInterface() { Ox.print('hideInterface'); self.interfaceTimeout = setTimeout(function() { @@ -874,6 +996,8 @@ Ox.VideoPlayer = function(options, self) { function loadedmetadata() { + var hadDuration = !!self.options.duration; + self.loaded = true; self.out = self.options.playInToOut && self.options.out < self.video.duration ? @@ -893,46 +1017,19 @@ Ox.VideoPlayer = function(options, self) { } } + if (self.$timeline) { + if (!hadDuration) { + self.$timeline.replaceWith(self.$timeline = getTimeline()); + } + self.$timeline.options({ + duration: self.options.duration + }); + } + if (self.options.enableKeyboard && self.options.focus == 'load') { that.gainFocus(); } - self.$timeline && self.$timelineInterface - .bind({ - mousedown: mousedownTrack, - mouseleave: mouseleaveTrack, - mousemove: mousemoveTrack, - }) - .bindEvent({ - drag: dragTrack, - dragpause: dragpauseTrack, - dragend: dragpauseTrack - }); - } - function dragTrack(e) { - setPosition(getPosition(e), true); - if (self.dragTimeout) { - clearTimeout(self.dragTimeout); - self.dragTimeout = 0; - } - } - - function dragpauseTrack(e) { - self.video.currentTime = self.options.position; - } - - function mousedownTrack(e) { - setPosition(getPosition(e), true); - } - - function mouseleaveTrack(e) { - self.$tooltip.hide(); - } - - function mousemoveTrack(e) { - self.$tooltip.options({ - title: formatPosition(getPosition(e)) - }).show(e); } function parsePositionInput(str) { @@ -1019,7 +1116,9 @@ Ox.VideoPlayer = function(options, self) { self.posterIsVisible = false; } self.$subtitle && setSubtitle(); - self.$timeline && self.$positionMarker.css(getCSS('positionMarker')); + self.$timeline && self.$timeline.options({ + position: self.options.position + }); self.$position && self.$position.html(formatPosition()); } @@ -1031,12 +1130,13 @@ Ox.VideoPlayer = function(options, self) { self.iconLeft = parseInt((self.width - self.iconSize) / 2), self.iconTop = parseInt((self.height - self.iconSize) / 2); if (self.$timeline || self.$space) { - self.timelineWidth = self.width - self.options.controls.reduce(function(prev, curr) { - return prev + ( - curr == 'timeline' || curr == 'space' ? 0 : - curr == 'position' ? self.positionWidth : 16 - ); - }, 0); + self.timelineWidth = self.width - + self.options.controls.reduce(function(prev, curr) { + return prev + ( + curr == 'timeline' || curr == 'space' ? 0 : + curr == 'position' ? self.positionWidth : 16 + ); + }, 0); if (self.$timeline) { self.timelineImageWidth = self.timelineWidth - self.barHeight; } @@ -1052,12 +1152,11 @@ Ox.VideoPlayer = function(options, self) { self.$title && self.$title.animate(getCSS('title'), ms); self.$controls && self.$controls.animate(getCSS('controls'), ms); if (self.$timeline) { - self.$timeline.animate(getCSS('timeline'), ms); - self.$timelineImages.animate(getCSS('timelineImages'), ms); - self.$timelineImage.animate(getCSS('timelineImage'), ms); - self.$progress && self.$progress.animate(getCSS('progress'), ms); - self.$positionMarker.animate(getCSS('positionMarker'), ms); - self.$timelineInterface.animate(getCSS('timelineInterface'), ms); + self.$timeline.animate(getCSS('timeline'), ms, function() { + self.$timeline.options({ + width: self.timelineWidth + }) + }); } self.$space && self.$space.animate(getCSS('space'), ms); } @@ -1115,6 +1214,37 @@ Ox.VideoPlayer = function(options, self) { }).show(); } + function submitFindInput() { + self.options.find = self.$findInput.options('value'); + self.results = find(self.options.find); + self.$timeline && self.$timeline.options({ + results: self.results + }); + if (self.results.length) { + setPosition(self.results[0]['in'] + self.secondsPerFrame, true); + self.currentResult = 0; + } + } + + function goToNextResult(direction) { + var found = false, + position = 0; + direction == -1 && self.results.reverse(); + Ox.forEach(self.results, function(v) { + if (direction == 1 ? v['in'] > self.options.position : v['out'] < self.options.position) { + position = v['in']; + found = true; + return false; + } + }); + direction == -1 && self.results.reverse(); + if (!found) { + position = self.results[direction == 1 ? 0 : self.results.length - 1]['in']; + } + Ox.print('>>', self.results, position) + setPosition(position + self.secondsPerFrame, true); + } + function submitPositionInput() { self.$positionInput.hide(); self.$position.html('').show(); @@ -1211,8 +1341,8 @@ Ox.VideoPlayer = function(options, self) { function togglePaused(toggleButton) { self.options.paused = !self.options.paused; - self.$timeline && self.$positionMarkerRing.css({ - borderColor: 'rgba(255, 255, 255, ' + (self.options.paused ? 0.5 : 1) + ')' + self.$timeline && self.$timeline.options({ + paused: self.options.paused }); if (self.options.paused) { self.video.pause(); diff --git a/source/Ox.js b/source/Ox.js index f2d5f3cf..184502de 100644 --- a/source/Ox.js +++ b/source/Ox.js @@ -3864,11 +3864,12 @@ Ox.highlight Highlight matches in a string > Ox.highlight('foobar', 'foo', 'match') 'foobar' @*/ +// fixme: with regexp, special chars would have to be escaped Ox.highlight = function(txt, str, classname) { - return txt.replace( + return str.length ? txt.replace( new RegExp('(' + str + ')', 'ig'), '$1' - ); + ) : txt; }; /*@