// vim: et:ts=4:sw=4:sts=4:ft=javascript 'use strict'; Ox.VideoTimelinePlayer = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ cuts: [], duration: 0, find: '', getFrameURL: null, getImageURL: null, height: 0, 'in': 0, matches: [], out: 0, paused: false, position: 0, subtitles: [], timelineURL: '', videoRatio: 1, width: 0 }) .options(options || {}) .addClass('OxVideoTimelinePlayer'); self.fps = 25; self.frame = self.options.position * self.fps; self.frames = self.options.duration * self.fps; self.tileWidth = 1500; self.tileHeight = 64; self.margin = 8; self.contentWidth = self.options.width - 2 * self.margin - Ox.UI.SCROLLBAR_SIZE; self.contentHeight = self.options.height - 32; self.tiles = Math.ceil(self.frames / self.tileWidth); self.videoWidth = Math.round(self.tileHeight * self.options.videoRatio); self.videoLines = getVideoLines(); self.lines = getLines(); self.$topBar = Ox.Bar({size: 16}); self.$content = Ox.Element() .addClass('OxVideoTimelinePlayer') .css({ overflow: 'hidden', overflowY: 'scroll' }) .bind({ mousedown: mousedown, mouseleave: mouseleave, mousemove: mousemove }) .bindEvent({ mousedown: function() { this.gainFocus(); }, key_down: function() { self.options.position += self.contentWidth / self.fps; setPosition(); }, key_enter: function() { scrollToPosition(); }, key_left: function() { self.options.position -= self.videoWidth / self.fps; setPosition(); }, key_right: function() { self.options.position += self.videoWidth / self.fps; setPosition(); }, key_shift_left: function() { self.options.position -= 1 / self.fps; setPosition(); }, key_shift_right: function() { self.options.position += 1 / self.fps; setPosition(); }, key_space: togglePaused, key_up: function() { self.options.position -= self.contentWidth / self.fps; setPosition(); } }); self.$bottomBar = Ox.Bar({size: 16}); self.$playButton = Ox.Button({ style: 'symbol', title: 'play', type: 'image' }) .css({ float: 'left' }) .bindEvent({ click: togglePaused }) .appendTo(self.$bottomBar); self.$muteButton = Ox.Button({ style: 'symbol', title: 'mute', type: 'image' }) .css({ float: 'left' }) .bindEvent({ }) .appendTo(self.$bottomBar); self.$smallTimeline = $('
') .css({ float: 'left', width: self.options.width - 80 + 'px', height: '16px', borderRadius: '8px', background: 'rgb(0, 0, 0)' }) .appendTo(self.$bottomBar); self.$smallTimelineImage = $('') .attr({ src: self.options.timelineURL }) .css({ width: self.options.width - 96 + 'px', height: '16px', margin: '0 8px 0 8px' }) .appendTo(self.$smallTimeline); self.$position = $('
') .css({ float: 'left', width: '40px', height: '12px', padding: '2px 4px 2px 4px', fontSize: '9px' }) .html(Ox.formatDuration(self.options.position)) .appendTo(self.$bottomBar); self.$panel = Ox.SplitPanel({ elements: [ { element: self.$topBar, size: 16 }, { element: self.$content }, { element: self.$bottomBar, size: 16 } ], orientation: 'vertical' }); that.setElement(self.$panel); self.$lines = []; self.$timelines = []; self.$interfaces = []; self.$timeline = renderTimeline(); //setSubtitles(); Ox.loop(self.lines, function(i) { addLine(i); }); self.$frameBox = $('
') .css({ position: 'absolute', right: 0, top: self.margin / 2 - 1 + 'px', width: self.videoWidth + 'px', height: self.tileHeight + 'px', borderTop: '1px solid rgba(255, 255, 255, 0.5)', borderBottom: '1px solid rgba(255, 255, 255, 0.5)', background: 'rgb(0, 0, 0)' }) .appendTo(self.$timelines[self.videoLines[1]][0]); self.$frame = $('') .attr({ src: self.options.getFrameURL(self.options.position) }) .css({ width: self.videoWidth + 'px', height: self.tileHeight + 'px', }) .appendTo(self.$frameBox) $('
') .addClass('OxFrameInterface') .css({ position: 'absolute', left: 0, top: 0, width: self.videoWidth + 'px', height: self.tileHeight + 'px', }) .appendTo(self.$frameBox); self.$videoBox = $('
') .css({ position: 'absolute', right: 0, top: self.margin / 2 - 1 + 'px', width: self.videoWidth + 'px', height: self.tileHeight + 'px', borderTop: '1px solid rgba(255, 255, 255, 0.5)', borderBottom: '1px solid rgba(255, 255, 255, 0.5)', background: 'rgb(0, 0, 0)', zIndex: 5 }) .appendTo(self.$timelines[self.videoLines[0]][0]); self.$video = Ox.VideoPlayer({ duration: self.options.duration, height: self.tileHeight, paused: self.options.paused, position: self.options.position, scaleToFill: true, video: self.options.videoURL, width: self.videoWidth }) .bindEvent({ ended: function() { togglePaused(true); }, playing: function(data) { self.options.position = data.position; setPosition(true); } }) .appendTo(self.$videoBox); $('
') .addClass('OxFrameInterface OxVideoInterface') .css({ position: 'absolute', left: 0, top: 0, width: self.videoWidth + 'px', height: self.tileHeight + 'px', }) .appendTo(self.$videoBox); self.$tooltip = Ox.Tooltip({ animate: false }) .css({ textAlign: 'center' }); setTimeout(function() { scrollToPosition(); }); function addLine(i) { self.$lines[i] = $('
') .css({ position: 'absolute', left: self.margin + 'px', top: self.margin / 2 + i * (self.tileHeight + self.margin) + 'px', width: self.contentWidth + 'px', height: self.tileHeight + self.margin + 'px', overflowX: 'hidden' }) .appendTo(self.$content); self.$timelines[i] = [ self.$timeline.clone() .css({ width: self.frame + self.videoWidth + 'px', marginLeft: -i * self.contentWidth + 'px', }), self.$timeline.clone() .css({ marginLeft: -i * self.contentWidth + self.videoWidth - 1 + 'px', }) ]; self.$lines[i] .append(self.$timelines[i][1]) .append(self.$timelines[i][0]); } function getLines() { return Math.ceil((self.frames - 1 + self.videoWidth) / self.contentWidth) } function getPosition(e) { return ( e.offsetX ? e.offsetX : e.clientX - $(e.target).offset().left ) / self.fps; } function getSubtitle(position) { var subtitle = ''; Ox.forEach(self.options.subtitles, function(v) { if (v['in'] <= position && v.out > position) { subtitle = v; return false; } }); return subtitle; } function getVideoLine() { self.videoLine = Math.floor(getVideoFrame() / self.contentWidth); } function getVideoLines() { var videoFrame = getVideoFrame(), videoLeft = videoFrame % self.contentWidth, lines = []; lines[0] = Math.floor(videoFrame / self.contentWidth); lines[1] = lines[0] + ( videoLeft + self.videoWidth > self.contentWidth ? 1 : 0 ) if (videoLeft + Math.floor(self.videoWidth / 2) > self.contentWidth) { lines.reverse(); } return lines; } function getVideoFrame() { return Math.floor(self.options.position * self.fps); } function mousedown(e) { var $target = $(e.target), isTimeline = $target.is('.OxTimelineInterface'), isVideo = $target.is('.OxFrameInterface'); if (isTimeline) { self.options.position = getPosition(e); setPosition(); if (!self.triggered) { that.triggerEvent('position', { position: self.options.position }); self.triggered = true; setTimeout(function() { self.triggered = false; }, 250); } } else if (isVideo) { togglePaused(); } } function mouseleave() { self.$tooltip.hide(); } function mousemove(e) { var $target = $(e.target), isTimeline = $target.is('.OxTimelineInterface'), isVideo = $target.is('.OxFrameInterface'), position, subtitle; if (isTimeline || isVideo) { position = isTimeline ? getPosition(e) : self.options.position; subtitle = getSubtitle(position); self.$tooltip.options({ title: ( subtitle ? '' + Ox.highlight(subtitle.text, self.options.find, 'OxHighlight').replace(/\n/g, '
') + '

' : '' ) + Ox.formatDuration(position, 3) }) .show(e.clientX, e.clientY); } else { self.$tooltip.hide(); } } function renderTimeline() { var $timeline = $('
') .css({ position: 'absolute', width: self.frames + 'px', height: self.tileHeight + self.margin + 'px', overflow: 'hidden' }); Ox.loop(self.tiles, function(i) { $('') .attr({ src: self.options.getImageURL(i) }) .css({ position: 'absolute', left: i * self.tileWidth + 'px', top: self.margin / 2 + 'px' }) .appendTo($timeline); }); $('
') .addClass('OxTimelineInterface') .css({ position: 'absolute', left: 0, top: self.margin / 2 + 'px', width: self.frames + 'px', height: self.tileHeight + 'px', //background: 'rgba(255, 0, 0, 0.5)' }) .appendTo($timeline); return $timeline; } function scrollToPosition() { var scrollTop = self.$content.scrollTop(), videoTop = [ self.margin + Ox.min(self.videoLines) * (self.tileHeight + self.margin), self.margin + Ox.max(self.videoLines) * (self.tileHeight + self.margin) ], offset = self.contentHeight - self.tileHeight - self.margin; if (videoTop[0] < scrollTop + self.margin) { self.$content.scrollTop(videoTop[0] - self.margin); } else if (videoTop[1] > scrollTop + offset) { self.$content.scrollTop(videoTop[1] - offset); } } function setPosition(fromVideo) { var max, min, videoLines, isPlaying = !self.options.paused; self.options.position = Ox.limit(self.options.position, 0, self.options.duration); self.frame = Math.floor(self.options.position * self.fps); videoLines = getVideoLines(); min = Ox.min(Ox.flatten([self.videoLines, videoLines])); max = Ox.max(Ox.flatten([self.videoLines, videoLines])); Ox.loop(min, max + 1, function(i) { self.$timelines[i][0].css({ width: self.frame + self.videoWidth + 'px' }); }); if (videoLines[1] != self.videoLines[1]) { self.videoLines[1] = videoLines[1]; self.$frameBox.detach().appendTo(self.$timelines[videoLines[1]][0]); } if (videoLines[0] != self.videoLines[0]) { self.videoLines[0] = videoLines[0]; isPlaying && self.$video.togglePaused(); self.$videoBox.detach().appendTo(self.$timelines[videoLines[0]][0]); isPlaying && self.$video.togglePaused(); } if (videoLines[0] != videoLines[1]) { self.$frame.attr({ src: self.options.getFrameURL( self.paused ? self.options.position : Math.floor(self.options.position) ) }); } self.videoLines = videoLines; if (!fromVideo) { self.$video.options({position: self.options.position}); self.$frame.attr({ src: self.options.getFrameURL(self.options.position) }); scrollToPosition(); } self.$position.html(Ox.formatDuration(self.options.position)) } function setSubtitles() { self.$timeline.find('.OxSubtitle').remove(); self.$subtitles = []; self.options.subtitles.forEach(function(subtitle, i) { var found = self.options.find && subtitle.text.toLowerCase().indexOf(self.options.find.toLowerCase()) > -1; self.$subtitles[i] = $('
') .addClass('OxSubtitle' + (found ? ' OxHighlight' : '')) .css({ position: 'absolute', left: (subtitle['in'] * self.fps) + 'px', width: (((subtitle.out - subtitle['in']) * self.fps) - 2) + 'px' }) .html(Ox.highlight(subtitle.text, self.options.find, 'OxHighlight')) .appendTo(self.$timeline); }); } function setWidth() { self.contentWidth = self.options.width - 2 * self.margin - Ox.UI.SCROLLBAR_SIZE; self.lines = getLines(); Ox.loop(self.lines, function(i) { if (self.$lines[i]) { self.$lines[i].css({ width: self.contentWidth + 'px' }); self.$timelines[i][0].css({ marginLeft: -i * self.contentWidth + 'px' }); self.$timelines[i][1].css({ marginLeft: -i * self.contentWidth + self.videoWidth + 'px' }); } else { addLine(i); } }); while (self.$lines.length > self.lines) { self.$lines[self.$lines.length - 1].remove(); self.$lines.pop(); self.$timelines.pop(); } } function togglePaused(fromVideo) { self.options.paused = !self.options.paused; !fromVideo && self.$video.options({ paused: self.options.paused }); self.$playButton.options({ title: !self.options.paused ? 'pause' : 'play' }); } self.setOption = function(key, value) { if (key == 'width') { setWidth(); } }; return that; };