// vim: et:ts=4:sw=4:sts=4:ft=javascript /*@ Ox.VideoPlayer <f> Generic Video Player (options, self) -> <o> Video Player options <o> Options annotation <[o]> Array of annotation tracks name <s> Name of the annotation track data <[o]> Annotation data in <n> In point (sec) out <n> Out point (sec) text <s> Text controls <[[s]]|[[][]]> Controls, first top, then bottom, from left to right Can be 'fullscreen', 'scale', 'title', 'find', 'menu', 'play', 'playInToOut', 'mute', 'volume', 'size', 'timeline', 'space', 'position', 'resolution', 'settings'. The 'space' control is just empty space that separates left-aligned from right-aligned controls. duration <n|-1> Duration (sec) enableFind <b|false> If true, enable find enableFullscreen <b|false> If true, enable fullscreen enableKeyboard <b|false> If true, enable keyboard controls externalControls <b|false> If true, controls are outside the video find <s|''> Query string focus <s|'click'> focus on 'click', 'load' or 'mouseover' fps <n|25> Frames per second fullscreen <b|false> If true, video is in fullscreen height <n|144> Height in px (excluding external controls) in <n> In point (sec) keepIconVisible <b|false> If true, play icon stays visible after mouseleave keepLargeTimelineVisible <b|false> If true, large timeline stays visible after mouseleave keepLogoVisible <b|false> If true, logo stays visible after mouseleave logo <s|''> Logo image URL logoLink <s|''> Logo link URL logoTitle <s|''> Text for tooltip muted <b|false> If true, video is muted paused <b|false> If true, video is paused playInToOut <b|false> If true, video plays only from in to out position <n|0> Initial position (sec) poster <s|''> Poster URL posterFrame <n|-1> Position of poster frame (sec) preload <s|'auto'> 'auto', 'metadata' or 'none' out <n|-1> Out point (sec) resolution <n|0> resolution scaleToFill <b|false> If true, scale to fill (otherwise, scale to fit) showControlsOnLoad <b|false> If true, show controls on load showFind <b|false> If true, show find input showHours <b|false> If true, don't show hours for videos shorter than one hour showIcon <b|false> If true, show play icon showIconOnLoad <b|false> If true, show icon on load showLargeTimeline <b|false> If true, show large timeline showMilliseconds <n|0> Number of decimals to show showMarkers <b|false> If true, show in/out/poster markers showProgress <|false> If true, show buffering progress sizeIsLarge <b|false> If true, initial state of the size control is large subtitles <s|[o]|[]> URL or SRT or array of subtitles in <n> In point (sec) out <n> Out point (sec) text <s> Text timeline <s> Timeline image URL title <s|''> Video title type <s|'play'> 'play', 'in' or 'out' video <s|o|''> Video URL String or object ({resolution: url, resolution: url, ...}) volume <n|1> Volume (0-1) width <n|256> Width in px @*/ Ox.VideoPlayer = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ annotations: [], controlsBottom: [], controlsTop: [], duration: 0, enableFind: false, enableFullscreen: false, enableKeyboard: false, externalControls: false, find: '', focus: 'click', fps: 25, fullscreen: false, height: 144, 'in': 0, keepIconVisible: false, keepLargeTimelineVisible: false, keepLogoVisible: false, logo: '', logoLink: '', logoTitle: '', muted: false, paused: false, playInToOut: false, position: 0, poster: '', posterFrame: -1, preload: 'auto', out: 0, resolution: 0, scaleToFill: false, showControlsOnLoad: false, showFind: false, showHours: false, showIcon: false, showIconOnLoad: false, showLargeTimeline: false, showMarkers: false, showMilliseconds: 0, showProgress: false, subtitles: [], timeline: '', title: '', type: 'play', video: '', volume: 1, width: 256 }) .options(options || {}) .addClass('OxVideoPlayer'); Ox.UI.$window.bind({ resize: function() { self.options.fullscreen && setSizes(); } }); if (Ox.isString(self.options.video)) { self.video = self.options.video; } else { self.resolutions = Ox.sort(Object.keys(self.options.video)); if (!(self.options.resolution in self.options.video)) { self.options.resolution = self.resolutions[0]; } self.video = self.options.video[self.options.resolution]; } if (self.options.playInToOut) { self['in'] = self.options['in']; self.out = self.options.out; self.options.duration = self.out - self['in']; } else { self['in'] = 0; self.out = self.options.duration || 86399; } self.options.position = Ox.limit(self.options.position, self['in'], self.out); self.millisecondsPerFrame = 1000 / self.options.fps; self.secondsPerFrame = 1 / self.options.fps; self.barHeight = 16; self.width = self.options.fullscreen ? window.innerWidth : self.options.width; self.height = self.options.fullscreen ? window.innerHeight : self.options.height; self.videoWidth = self.options.width; self.videoHeight = self.options.height; /* ---------------------------------------------------------------------------- Keyboard ---------------------------------------------------------------------------- */ if (self.options.enableKeyboard) { that.bindEvent({ key_0: toggleMuted, key_1: toggleScale, key_equal: function() { changeVolumeBy(0.1); }, key_escape: hideControlMenus, key_f: focusFind, key_g: function() { goToNextResult(1); }, key_left: function() { setPosition(self.options.position - self.secondsPerFrame); }, key_minus: function() { changeVolumeBy(-0.1); }, key_p: function() { playInToOut(); }, key_right: function() { setPosition(self.options.position + self.secondsPerFrame); }, key_shift_f: function() { self.options.enableFullscreen && toggleFullscreen(); }, key_shift_g: function() { goToNextResult(-1); }, key_shift_left: function() { setPosition(self.options.position - 1); }, key_shift_right: function() { setPosition(self.options.position + 1); }, key_space: togglePaused }); if (self.options.focus == 'mouseenter') { that.bind({ mouseenter: function() { if (!self.inputHasFocus) { that.gainFocus(); } }, mouseleave: function() { that.loseFocus(); } }); } else { that.bind({ click: that.gainFocus }); } } /* ---------------------------------------------------------------------------- Mouse ---------------------------------------------------------------------------- */ if ( (!self.options.externalControls && (self.options.controlsTop.length || self.options.controlsBottom.length)) || self.options.showIcon ) { that.bind({ mouseenter: function() { showControls(); self.mouseHasLeft = false; Ox.print('MOUSE HAS ENTERED') }, mouseleave: function() { hideControls(); self.mouseHasLeft = true; Ox.print('MOUSE HAS LEFT') } }); } /* ---------------------------------------------------------------------------- Video ---------------------------------------------------------------------------- */ self.$videoContainer = $('<div>') .addClass('OxVideoContainer') .css({ top: self.options.externalControls && self.options.controlsTop.length ? '16px' : 0, }) .appendTo(that.$element) if (self.options.type == 'play') { self.$videoContainer.bind({ click: function(e) { if (!$(e.target).is('.OxLogo')) { togglePaused(); } } }); self.$video = $('<video>') .attr(Ox.extend({ preload: self.options.preload, src: self.video }, !self.options.paused ? { autoplay: 'autoplay' } : {}/*, self.options.poster ? { poster: self.options.poster } : {}*/)) .css({ position: 'absolute' }) .bind(Ox.extend({ ended: ended, loadedmetadata: loadedmetadata, seeked: seeked, seeking: seeking }, self.options.progress ? { progress: progress } : {})) .appendTo(self.$videoContainer); self.video = self.$video[0]; } else { self.$videoContainer.bind({ click: function(e) { if (!$(e.target).is('.OxLogo')) { goToPoint(); } } }); self.$video = $('<img>') .attr({ src: Ox.UI.PATH + 'png/transparent.png' }) .appendTo(self.$videoContainer); loadImage(); } /* ---------------------------------------------------------------------------- Poster ---------------------------------------------------------------------------- */ if (self.options.poster) { self.$poster = $('<img>') .addClass('OxPoster') .attr({ src: self.options.poster }) .appendTo(self.$videoContainer); self.posterIsVisible = true; } /* ---------------------------------------------------------------------------- Logo ---------------------------------------------------------------------------- */ if (self.options.logo) { self.$logo = $('<img>') .addClass('OxLogo') .attr({ src: self.options.logo }) .css({ cursor: self.options.logoLink ? 'pointer' : 'default' }) .appendTo(self.$videoContainer); if (self.options.logoTitle) { self.$logoTooltip = Ox.Tooltip({ title: self.options.logoTitle }); } } /* ---------------------------------------------------------------------------- Icons ---------------------------------------------------------------------------- */ self.$loadingIcon = $('<img>') .addClass('OxLoadingIcon OxVideo') .attr({ src: Ox.UI.getImagePath('symbolLoadingAnimated.svg') .replace('/classic/', '/modern/') }) .appendTo(self.$videoContainer); if (self.options.showIcon || self.options.showIconOnLoad) { self.$playIcon = $('<img>') .addClass('OxPlayIcon OxVideo') .attr({ src: Ox.UI.getImagePath( 'symbol' + (self.options.paused ? 'Play' : 'Pause') + '.svg' ).replace('/classic/', '/modern/') }) .appendTo(self.$videoContainer); if (self.options.showIcon) { self.$playIcon.addClass('OxInterface'); } if (self.options.showIconOnLoad) { self.iconIsVisible = true; } } /* ---------------------------------------------------------------------------- Markers ---------------------------------------------------------------------------- */ if (self.options.showMarkers) { self.$posterMarker = {}; ['left', 'center', 'right'].forEach(function(position) { var titleCase = Ox.toTitleCase(position); self.$posterMarker[position] = $('<div>') .addClass('OxPosterMarker OxPosterMarker' + titleCase) .appendTo(self.$videoContainer); }); self.$pointMarker = {}; ['in', 'out'].forEach(function(point) { self.$pointMarker[point] = {}; ['top', 'bottom'].forEach(function(edge) { var titleCase = Ox.toTitleCase(point) + Ox.toTitleCase(edge); self.$pointMarker[point][edge] = $('<img>') .addClass('OxPointMarker OxPointMarker' + titleCase) .attr({ src: Ox.UI.PATH + 'png/videoMarker' + titleCase + '.png' }) .appendTo(self.$videoContainer); }); }); } /* ---------------------------------------------------------------------------- Subtitles ---------------------------------------------------------------------------- */ if (self.options.subtitles.length || true) { // fixme self.$subtitle = $('<div>') .addClass('OxSubtitle') .appendTo(self.$videoContainer); } /* ---------------------------------------------------------------------------- Controls ---------------------------------------------------------------------------- */ ['top', 'bottom'].forEach(function(edge) { var titleCase = Ox.toTitleCase(edge); if (self.options['controls' + titleCase].length) { self['$controls' + titleCase] = Ox.Bar({ size: self.barHeight }) .addClass('OxControls' + (self.options.externalControls ? '' : ' OxOnScreen')) .css({ opacity: self.options.externalControls ? 1 : 0 }) .css(edge, 0) .appendTo(that.$element); self.options['controls' + titleCase].forEach(function(control) { if (control == 'find') { self.$findButton = Ox.Button({ style: 'symbol', title: 'find', tooltip: 'Find', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: toggleFind }) .appendTo(self['$controls' + titleCase]); } else if (control == 'fullscreen') { self.$fullscreenButton = Ox.Button({ style: 'symbol', title: [ {id: 'grow', title: 'grow', selected: !self.options.fullscreen}, {id: 'shrink', title: 'shrink', selected: self.options.fullscreen} ], tooltip: ['Enter Fullscreen', 'Exit Fullscreen'], type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { toggleFullscreen('button'); } }) .appendTo(self['$controls' + titleCase]); } else if (control == 'goto') { self.$setButton = Ox.Button({ style: 'symbol', title: 'goTo' + Ox.toTitleCase(self.options.type), tooltip: 'Go to ' + Ox.toTitleCase(self.options.type) + ' Point', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: goToPoint }) .appendTo(self['$controls' + titleCase]); } else if (control == 'mute') { self.$muteButton = Ox.Button({ style: 'symbol', title: [ {id: 'mute', title: 'mute', selected: !self.options.muted}, {id: 'unmute', title: 'unmute', selected: self.options.muted} ], tooltip: ['Mute', 'Unmute'], type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { toggleMuted('button'); } }) .appendTo(self['$controls' + titleCase]); } else if (control == 'play') { self.$playButton = Ox.Button({ style: 'symbol', // FIXME: this is retarded, fix Ox.Button title: [ {id: 'play', title: 'play', selected: self.options.paused}, {id: 'pause', title: 'pause', selected: !self.options.paused} ], tooltip: ['Play', 'Pause'], type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { togglePaused('button'); } }) .appendTo(self['$controls' + titleCase]); } else if (control == 'playInToOut') { self.$playInToOutButton = Ox.Button({ style: 'symbol', title: 'playInToOut', tooltip: 'Play In to Out', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: playInToOut }) .appendTo(self['$controls' + titleCase]); } else if (control == 'position') { self.positionWidth = 48 + !!self.options.showMilliseconds * 2 + self.options.showMilliseconds * 6; self.$position = Ox.Element({ tooltip: 'Position' }) .addClass('OxPosition') .css({ width: (self.positionWidth - 4) + 'px', }) .html(formatPosition()) .bind({ click: function() { if (self.options.type == 'play') { if (!self.options.paused) { self.playOnSubmit = true; togglePaused(); } else if (self.playOnLoad) { // if clicked during resolution switch, // don't play on load self.playOnLoad = false; self.playOnSubmit = true; } } self.$position.hide(); self.$positionInput .options({ value: formatPosition() }) .show() .focusInput(false); } }) .appendTo(self['$controls' + titleCase]); self.$positionInput = Ox.Input({ value: formatPosition(), width: self.positionWidth }) .addClass('OxPositionInput') .bindEvent({ focus: function() { self.inputHasFocus = true; }, blur: function() { self.inputHasFocus = false; submitPositionInput(); } }) .appendTo(self['$controls' + titleCase].$element); self.$positionInput.children('input').css({ width: (self.positionWidth - 6) + 'px', fontSize: '9px' }); } else if (control == 'resolution') { self.$resolutionButton = Ox.Element({ tooltip: 'Resolution' }) .addClass('OxResolutionButton') .html(self.options.resolution + 'p') .bind({ click: function() { self.$resolution.toggle(); } }) .appendTo(self['$controls' + titleCase]); self.$resolution = $('<div>') .addClass('OxResolution') .css({ height: (self.resolutions.length * 16) + 'px' }) .bind({ click: function(e) { var resolution = $(e.target).parent().data('resolution'); self.$resolution.hide(); if (resolution != self.options.resolution) { self.$resolution.children().each(function() { var $this = $(this); $this.children()[1].src = $this.data('resolution') == resolution ? Ox.UI.getImagePath('symbolCheck.svg') : Ox.UI.PATH + 'png/transparent.png' }); self.$resolutionButton.html(resolution + 'p'); self.options.resolution = resolution setResolution(); } } }) .appendTo(that.$element); self.resolutions.forEach(function(resolution, i) { var $item = $('<div>') .data({ resolution: resolution }) .bind({ mouseenter: function() { $(this).addClass('OxSelected'); }, mouseleave: function() { $(this).removeClass('OxSelected'); } }) .appendTo(self.$resolution); $('<div>') .html(resolution + 'p') .appendTo($item); $('<img>') .addClass('OxVideo') .attr({ src: resolution == self.options.resolution ? Ox.UI.getImagePath('symbolCheck.svg').replace('/classic/', '/modern/') : Ox.UI.PATH + 'png/transparent.png' }) .appendTo($item); }); } else if (control == 'scale') { self.$scaleButton = Ox.Button({ style: 'symbol', title: [ {id: 'fill', title: 'fill', selected: !self.options.scaleToFill}, {id: 'fit', title: 'fit', selected: self.options.scaleToFill} ], tooltip: ['Scale to Fill', 'Scale to Fit'], type: 'image' }) .addClass('OxVideo') .bindEvent('click', function() { toggleScale('button'); }) .appendTo(self['$controls' + titleCase]); } else if (control == 'set') { self.$setButton = Ox.Button({ style: 'symbol', title: 'set' + Ox.toTitleCase(self.options.type), tooltip: 'Set ' + Ox.toTitleCase(self.options.type) + ' Point', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: setPoint }) .appendTo(self['$controls' + titleCase]); } else if (control == 'size') { self.$sizeButton = Ox.Button({ style: 'symbol', title: [ {id: 'grow', title: 'grow', selected: !self.options.sizeIsLarge}, {id: 'shrink', title: 'shrink', selected: self.options.sizeIsLarge} ], tooltip: ['Larger', 'Smaller'], type: 'image' }) .addClass('OxVideo') .bindEvent('click', toggleSize) .appendTo(self['$controls' + titleCase]); } else if (control == 'space') { self['$space' + titleCase] = $('<div>') .html(' ') // fixme: ?? .appendTo(self['$controls' + titleCase].$element); } else if (control == 'timeline') { /* if (self.options.showProgress) { self.$progress = $('<img>') .attr({ src: getProgressImageURL() }) .css({ float: 'left', height: self.barHeight + 'px', }) .appendTo(self.$timelineImages.$element); } */ if (self.options.duration) { self.$timeline = getTimeline() } else { self.$timeline = Ox.Element() .html(' '); } self.$timeline.appendTo(self['$controls' + titleCase]); } else if (control == 'title') { self.$title = $('<div>') .addClass('OxTitle') .html(self.options.title) .appendTo(self['$controls' + titleCase].$element); } else if (control == 'volume') { self.$volumeButton = Ox.Button({ style: 'symbol', title: 'mute', tooltip: 'Volume', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: toggleVolume }) .appendTo(self['$controls' + titleCase]); } }); } }); /* ---------------------------------------------------------------------------- Find ---------------------------------------------------------------------------- */ if (self.options.enableFind) { self.$find = $('<div>') .addClass('OxControls OxFind') .css({ top: self.options.controlsTop.length ? '16px' : 0 }) .appendTo(that.$element); self.$results = Ox.Element({ tooltip: 'Results' }) .addClass('OxResults') .html('0') .appendTo(self.$find); self.$previousButton = Ox.Button({ disabled: true, style: 'symbol', title: 'arrowLeft', tooltip: 'Previous', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { goToNextResult(-1); } }) .appendTo(self.$find); self.$nextButton = Ox.Button({ disabled: true, style: 'symbol', title: 'arrowRight', tooltip: 'Next', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { goToNextResult(1); } }) .appendTo(self.$find); self.$findInput = Ox.Input({ changeOnKeypress: true, value: self.options.find }) .bindEvent({ blur: function() { self.inputHasFocus = false; }, focus: function() { self.inputHasFocus = true; }, change: function(data) { submitFindInput(data.value, false); }, submit: function(data) { self.inputHasFocus = false; submitFindInput(data.value, true); } }) .appendTo(self.$find); self.$findInput.children('input').css({ width: (self.positionWidth - 6) + 'px', }) self.$clearButton = Ox.Button({ disabled: !self.options.find, style: 'symbol', title: 'delete', tooltip: 'Clear', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { self.options.find = ''; self.results = []; self.$results.html('0'); self.$findInput.clearInput(); self.subtitle && setSubtitleText(); self.$timeline && self.$timeline.options({ find: self.options.find, results: self.results }); //setTimeout(self.$findInput.focusInput, 10); } }) .appendTo(self.$find); self.$hideFindButton = Ox.Button({ style: 'symbol', title: 'close', tooltip: 'Hide', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: toggleFind }) .appendTo(self.$find); } /* ---------------------------------------------------------------------------- Volume ---------------------------------------------------------------------------- */ if (self.options.enableVolume || true) { // fixme self.$volume = $('<div>') .addClass('OxControls OxVolume') .css({ bottom: self.options.controlsBottom.length ? '16px' : 0 }) .appendTo(that.$element); self.$hideVolumeButton = Ox.Button({ style: 'symbol', title: 'close', tooltip: 'Hide', type: 'image' }) .addClass('OxVideo') .bindEvent({ click: toggleVolume }) .appendTo(self.$volume); self.$muteButton = Ox.Button({ style: 'symbol', title: [ {id: 'mute', title: 'mute', selected: !self.options.muted}, {id: 'unmute', title: 'unmute', selected: self.options.muted} ], tooltip: ['Mute', 'Unmute'], type: 'image' }) .addClass('OxVideo') .bindEvent({ click: function() { toggleMuted(); } }) .appendTo(self.$volume); self.$volumeInput = Ox.Range({ max: 1, min: 0, step: 0.001, value: self.options.muted ? 0 : self.options.volume }) .bindEvent({ change: function(data) { setVolume(data.value); } }) .appendTo(self.$volume); self.$volumeValue = $('<div>') .addClass('OxVolumeValue') .html(self.options.muted ? 0 : Math.round(self.options.volume * 100)) .appendTo(self.$volume); } self.options.type != 'play' && self.options.showMarkers && setMarkers(); self.results = []; if (self.options.subtitles) { if (Ox.isArray(self.options.subtitles)) { loadedsubtitles(); } else { if (self.options.subtitles.indexOf('\n') > -1) { self.options.subtitles = Ox.parseSRT(self.options.subtitles); loadedsubtitles(); } else { Ox.get(self.options.subtitles, function(data) { self.options.subtitles = Ox.parseSRT(data); loadedsubtitles(); }); self.options.subtitles = []; } } } setSizes(); function clearInterfaceTimeout() { clearTimeout(self.interfaceTimeout); self.interfaceTimeout = 0; } function ended() { if (!self.options.paused) { togglePaused(); } if (self.options.poster) { self.$poster.animate({ opacity: 1 }, 250); self.posterIsVisible = true; } if (self.options.showIconOnLoad) { self.$playIcon.animate({ opacity: 1 }, 250); self.iconIsVisible = true; } } function find(query) { var results = []; if (query.length) { query = query.toLowerCase(); results = Ox.map(self.options.subtitles, function(subtitle) { return subtitle.text.toLowerCase().indexOf(query) > -1 ? { 'in': subtitle['in'], out: subtitle.out } : null; }); } return results; } function focusFind() { !self.interfaceIsVisible && showControls(); // need timeout so the "f" doesn't appear in the input field setTimeout(function() { if (self.$find.is(':hidden')) { toggleFind(); } else { self.$findInput.focusInput(); } }, 0); } function formatPosition(position) { position = Ox.isUndefined(position) ? self.options.position : position; return Ox.formatDuration(position, self.options.showMilliseconds); } function getCSS(element) { var css; if (element == 'controlsTop' || element == 'controlsBottom') { css = { width: self.width + 'px' }; } else if (element == 'find') { css = { width: Math.min(208, self.width) + 'px' }; } else if (element == 'loadingIcon') { css = { width: self.iconSize + 'px', height: self.iconSize + 'px' }; } else if (element == 'logo') { var logoHeight = Math.round(self.height / 10), logoMargin = Math.round(self.height / 20); css = { left: logoMargin + 'px', top: logoMargin + (self.controlsTopAreVisible ? 16 : 0) + 'px', height: logoHeight + 'px', }; } else if (element == 'player') { var height = self.options.fullscreen ? window.innerHeight : self.height; if (self.options.externalControls) { height += ( !!self.options.controlsTop.length + !!self.options.controlsBottom.length ) * self.barHeight; } css = Ox.extend({ width: self.width + 'px', height: height + 'px' }, self.options.fullscreen ? { left: 0, top: 0 } : {}, self.exitFullscreen ? { left: self.absoluteOffset.left, top: self.absoluteOffset.top } : {}); } else if (element == 'playIcon') { var playIconPadding = Math.round(self.iconSize * 1/8), playIconSize = self.iconSize - 2 * playIconPadding - 4; css = { width: playIconSize + 'px', height: playIconSize + 'px', padding: playIconPadding + 'px', borderRadius: Math.round(self.iconSize / 2) + 'px' }; } else if (element == 'progress') { css = { width: self.timelineImageWidth + 'px', marginLeft: -self.timelineImageWidth + 'px' }; } else if (element == 'subtitle') { css = { bottom: (parseInt(self.height / 16) + !!self.controlsBottomAreVisible * 16) + 'px', width: self.width + 'px', fontSize: parseInt(self.height / 20) + 'px', WebkitTextStroke: (self.height / 1000) + 'px rgb(0, 0, 0)' }; } else if (element == 'spaceBottom' || element == 'timeline') { css = { width: self.timelineWidth + 'px' }; } else if (element == 'spaceTop' || element == 'title') { css = { width: getTitleWidth() + 'px' }; } else if (element == 'videoContainer') { css = { width: self.width + 'px', height: self.height + 'px' }; } else if (element == 'volume') { css = { width: Math.min(184, self.width) }; } return css; } 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.timelineImageWidth * self.video.duration, 0, self.video.duration ); } else { /*Ox.print(e.offsetX, Ox.limit( (e.offsetX - self.barHeight / 2) / self.timelineImageWidth * self.video.duration, 0, self.video.duration ))*/ return Ox.limit( (e.offsetX - self.barHeight / 2) / self.timelineImageWidth * self.video.duration, 0, self.video.duration ); } } function getPosterMarkerCSS() { self.videoCSS = getVideoCSS(); var left = Math.floor((self.videoCSS.width - self.videoCSS.height) / 2), right = Math.ceil((self.videoCSS.width - self.videoCSS.height) / 2); return { center: { left: self.videoCSS.left + left + 'px', top: self.videoCSS.top + 'px', width: (self.videoCSS.height - 2) + 'px', height: (self.videoCSS.height - 2) + 'px' }, left: { left: self.videoCSS.left + 'px', top: self.videoCSS.top + 'px', width: left + 'px', height: self.videoCSS.height + 'px' }, right: { left: self.videoCSS.left + left + self.videoCSS.height + 'px', top: self.videoCSS.top + 'px', width: right + 'px', height: self.videoCSS.height + 'px' } }; } function getProgressImageURL() { Ox.print('---', self.timelineImageWidth) if (!self.timelineImageWidth) return; var width = self.timelineImageWidth, height = self.barHeight, canvas = $('<canvas>') .attr({ width: width, height: height })[0], context = canvas.getContext('2d'), imageData, data; context.fillStyle = 'rgba(255, 0, 0, 0.5)'; context.fillRect(0, 0, width, height); imageData = context.getImageData(0, 0, width, height), data = imageData.data; self.buffered.forEach(function(range) { var left = Math.round(range[0] * width / self.video.duration), right = Math.round(range[1] * width / self.video.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 getSubtitle() { var subtitle = ''; Ox.forEach(self.options.subtitles, function(v) { if ( v['in'] <= self.options.position && v.out >= self.options.position ) { subtitle = v.text; return false; } }); return subtitle; } function getTimeline() { var $timeline = Ox.SmallVideoTimeline({ _offset: getTimelineLeft(), duration: self.options.duration, find: self.options.find, 'in': self.options['in'], out: self.options.out, paused: self.options.paused, results: self.results, showMilliseconds: self.options.showMilliseconds, subtitles: self.options.subtitles, timeline: self.options.timeline, type: 'player', width: getTimelineWidth() }) .css({ float: 'left' }) .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))' }) .bindEvent({ position: function(data) { setPosition(data.position, 'timeline'); } }); //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.controlsBottom, 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.controlsBottom.reduce(function(prev, curr) { return prev + ( curr == 'timeline' || curr == 'space' ? 0 : curr == 'position' ? self.positionWidth : curr == 'resolution' ? 36 : 16 ); }, 0); } function getTitleWidth() { return (self.options.fullscreen ? window.innerWidth : self.options.width) - self.options.controlsTop.reduce(function(prev, curr) { return prev + ( curr == 'title' || curr == 'space' ? 0 : 16 ); }, 0); } function getVideoCSS() { var playerWidth = self.width, playerHeight = self.height, playerRatio = playerWidth / playerHeight, videoWidth = self.videoWidth, videoHeight = self.videoHeight, videoRatio = videoWidth / videoHeight, videoIsWider = videoRatio > playerRatio, width, height; if (self.options.scaleToFill) { width = videoIsWider ? playerHeight * videoRatio : playerWidth; height = videoIsWider ? playerHeight : playerWidth / videoRatio; } else { width = videoIsWider ? playerWidth : playerHeight * videoRatio; height = videoIsWider ? playerWidth / videoRatio : playerHeight; } width = Math.round(width); height = Math.round(height); return { left: parseInt((playerWidth - width) / 2), top: parseInt((playerHeight - height) / 2), width: width, height: height }; } function getVolumeImageURL() { var symbol; if (self.options.muted || self.options.volume == 0) { symbol = 'unmute'; } else if (self.options.volume < 1/3) { symbol = 'VolumeUp'; } else if (self.options.volume < 2/3) { symbol = 'VolumeDown'; } else { symbol = 'mute'; } return Ox.UI.getImagePath('symbol' + symbol + '.svg').replace('/classic/', '/modern/'); } function goToPoint() { that.triggerEvent('gotopoint'); } function hideControlMenus() { ['find', 'volume', 'resolution'].forEach(function(element) { var $element = self['$' + element]; $element && $element.is(':visible') && $element.animate({ opacity: 0 }, 250, function() { $element.hide().css({opacity: 1}); }); }); //self.options.fullscreen && hideControls(); } function hideControls() { Ox.print('hideControls'); clearTimeout(self.interfaceTimeout); self.interfaceTimeout = setTimeout(function() { if (!self.exitFullscreen && !self.inputHasFocus && !self.mouseIsInControls) { self.interfaceIsVisible = false; self.controlsTopAreVisible = false; self.controlsBottomAreVisible = false; self.$controlsTop && self.$controlsTop.animate({ opacity: 0 }, 250); self.$controlsBottom && self.$controlsBottom.animate({ opacity: 0 }, 250); hideControlMenus(); self.$logo && self.$logo.animate({ top: getCSS('logo').top, opacity: 0.25 }, 250, function() { self.options.logoLink && self.$logo.unbind('click'); self.options.logoTitle && self.$logo.unbind('mouseenter').unbind('mouseleave'); }); self.$subtitle && self.$subtitle.animate({ bottom: getCSS('subtitle').bottom, }, 250); } }, self.options.fullscreen ? 2500 : 1000); } function hideLoadingIcon() { self.$loadingIcon.hide().attr({ src: Ox.UI.getImagePath('symbolLoading.svg') .replace('/classic/', '/modern/') }); } function hideMarkers() { Ox.forEach(self.$posterMarker, function(marker) { marker.hide(); }); Ox.forEach(self.$pointMarker, function(markers) { Ox.forEach(markers, function(marker) { marker.hide(); }); }); } function isEqual(a, b) { return Math.abs(a - b) < 0.001; } function loadImage() { self.$video .one({ load: function() { Ox.print('IMAGE LOADED', self.options.video(self.options.position, self.options.width)) hideLoadingIcon(); } }) .attr({ src: self.options.video(self.options.position, self.options.width) }); } function loadedmetadata() { Ox.print('LOADEDMETADATA') var hadDuration = !!self.options.duration; self.loadedMetadata = true; self.videoWidth = self.video.videoWidth; self.videoHeight = self.video.videoHeight; self.videoCSS = getVideoCSS(); self.posterMarkerCSS = getPosterMarkerCSS(); self.$video.css(self.videoCSS); self.$poster && self.$poster.css(self.videoCSS); self.$posterMarker && Ox.forEach(self.$posterMarker, function(marker, position) { marker.css(self.posterMarkerCSS[position]); }); self.out = self.options.playInToOut && self.options.out < self.video.duration ? self.options.out : self.video.duration; self.options.duration = self.out - self['in']; self.video.currentTime = self.options.position; self.options.paused && self.options.showMarkers && setMarkers(); self.options.paused && self.playOnLoad && togglePaused('button'); self.$playButton && self.$playButton.options({ disabled: false }); hideLoadingIcon(); if (self.options.showIcon || self.options.showIconOnLoad) { //!self.options.keepIconVisible && self.$playIcon.addClass('OxInterface'); if (self.options.showIconOnLoad) { self.$playIcon.animate({ opacity: 1 }, 250); } } !hadDuration && self.$timeline && self.$timeline.replaceWith( self.$timeline = getTimeline() ); if (self.options.enableKeyboard && self.options.focus == 'load') { that.gainFocus(); } } function loadedsubtitles() { if (self.options.find) { submitFindInput(self.options.find); if (self.options.duration) { // duration was known or video has loaded before subtitles self.$timeline && self.$timeline.options({ results: self.results, subtitles: self.options.subtitles }); } } } function parsePositionInput(str) { var split = str.split(':').reverse(); while (split.length > 3) { split.pop(); } return split.reduce(function(prev, curr, i) { return prev + (parseFloat(curr) || 0) * Math.pow(60, i); }, 0); } function playing() { self.options.position = self.video.currentTime; if ( (self.options.playInToOut || self.playInToOut) && self.options.position >= self.options.out ) { togglePaused(); setPosition(self.options.out/*, 'video'*/); //ended(); self.playInToOut = false; } else { setPosition(self.options.position, 'video'); } that.triggerEvent('position', { position: self.options.position }); } function playInToOut() { if (self.options.out > self.options['in']) { Ox.print('inToOut', self.options['in']) self.playInToOut = true; setPosition(self.options['in']); self.options.paused && togglePaused(); } } function progress() { var buffered = self.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; } } self.$progress.attr({ src: getProgressImageURL() }); } function seeked() { Ox.print('XX seeked') clearTimeout(self.seekTimeout); self.seekTimeout = 0; Ox.print('XX hide') hideLoadingIcon(); self.$playIcon && self.$playIcon.show(); } function seeking() { Ox.print('XX seeking') if (!self.seekTimeout) { self.seekTimeout = setTimeout(function() { self.$playIcon && self.$playIcon.hide(); Ox.print('XX show') showLoadingIcon(); }, 250); } } function setMarkers() { //Ox.print('SET MARKERS', self.options.position, self.options['in'], self.options.out, self.$pointMarker); Ox.forEach(self.$posterMarker, function(marker) { isEqual(self.options.position, self.options.posterFrame) ? marker.show() : marker.hide(); }); Ox.forEach(self.$pointMarker, function(markers, point) { Ox.forEach(markers, function(marker) { //Ox.print(self.options.position, self.options[point], isEqual(self.options.position, self.options[point])) // fixme: there's a bug in jquery and/or webkit // on load, show() doesn't work isEqual(self.options.position, self.options[point]) ? marker.css({display: 'block'}) : marker.hide(); }); }); } function setPoint() { that.triggerEvent('setpoint'); } function setPosition(position, from) { self.options.position = Ox.limit(position, self['in'], self['out']); /* self.options.position = Math.round( position * self.options.fps ) / self.options.fps; */ self.options.paused && self.options.showMarkers && setMarkers(); self.$subtitle && setSubtitle(); self.$position && self.$position.html(formatPosition()); if (self.options.type == 'play') { if (self.loadedMetadata && from != 'video') { self.video.currentTime = self.options.position; } if (self.iconIsVisible) { self.$playIcon.animate({ opacity: 0 }, 250); self.iconIsVisible = false; } if (self.posterIsVisible) { self.$poster.animate({ opacity: 0 }, 250); self.posterIsVisible = false; } self.$timeline /*&& from != 'timeline'*/ && self.$timeline.options({ position: self.options.position }); } else { //showLoadingIcon(); loadImage(); } } function setResolution() { if (!self.options.paused) { self.playOnLoad = true; togglePaused('button'); } self.loadedMetadata = false; showLoadingIcon(); self.video.src = self.options.video[self.options.resolution]; self.$playButton && self.$playButton.options({ disabled: true }); } function setSize($element, css, animate, callback) { if ($element) { if (animate) { $element.animate(css, 250, function() { callback && callback(); }); } else { $element.css(css); callback && callback(); } } } function setSizes(callback) { var animate = !!callback; self.width = self.options.fullscreen ? window.innerWidth : self.options.width; self.height = self.options.fullscreen ? window.innerHeight : self.options.height; self.videoCSS = getVideoCSS(); self.iconSize = Math.max(Math.round(self.height / 10), 16); if (self.$timeline || self.$spaceBottom) { self.timelineWidth = getTimelineWidth(); if (self.$timeline) { self.timelineImageWidth = self.timelineWidth - self.barHeight; } } setSize(that, getCSS('player'), animate, callback); setSize(self.$videoContainer, getCSS('videoContainer'), animate); setSize(self.$video, self.videoCSS, animate); setSize(self.$poster, self.videoCSS, animate); setSize(self.$logo, getCSS('logo'), animate); setSize(self.$loadingIcon, getCSS('loadingIcon'), animate); setSize(self.$playIcon, getCSS('playIcon'), animate); setSize(self.$subtitle, getCSS('subtitle'), animate); setSize(self.$controlsTop, getCSS('controlsTop'), animate); setSize(self.$title, getCSS('title'), animate); setSize(self.$spaceTop, getCSS('spaceTop'), animate); setSize(self.$controlsBottom, getCSS('controlsBottom'), animate); setSize(self.$timeline, getCSS('timeline'), animate, function() { self.$timeline.options({ width: self.timelineWidth }); }); setSize(self.$spaceBottom, getCSS('spaceBottom'), animate); setSize(self.$find, getCSS('find'), animate, function() { self.$findInput.options({ width: Math.min(128, self.width - 80) }); }); setSize(self.$volume, getCSS('volume'), animate, function() { self.$volumeInput.options({ size: Math.min(128, self.width - 56) }); }); if (self.$posterMarker) { self.posterMarkerCSS = getPosterMarkerCSS(); Ox.forEach(self.$posterMarker, function(marker, position) { setSize(marker, self.posterMarkerCSS[position], animate); }); } } function setSubtitle() { var subtitle = getSubtitle(); if (subtitle != self.subtitle) { self.subtitle = subtitle; setSubtitleText(); } } function setSubtitleText() { Ox.print('setSubTx', self.subtitle, self.options.find) self.$subtitle.html( self.subtitle ? Ox.highlight(self.subtitle, self.options.find, 'OxHighlight') .replace(/\n/g, '<br/>') : ' <br/> ' // FIXME: weird bug, only in fullscreen, only in chrome ); Ox.print('?!?', self.$subtitle.css('bottom'), self.$subtitle.height()) } function changeVolumeBy(num) { showVolume(); self.options.volume = Ox.limit(self.options.volume + num, 0, 1); setVolume(self.options.volume); self.$volumeInput && self.$volumeInput.options({ value: self.options.volume }); } function setVolume(volume) { self.options.volume = volume; if (!!self.options.volume == self.options.muted) { self.options.muted = !self.options.muted; self.video.muted = self.options.muted; self.$muteButton.toggleTitle(); } self.video.volume = self.options.volume; self.$volumeButton.attr({ src: getVolumeImageURL() }); self.$volumeValue.html(self.options.muted ? 0 : Math.round(self.options.volume * 100)); } function showControls() { Ox.print('showControls'); clearTimeout(self.interfaceTimeout); if (!self.interfaceIsVisible) { self.interfaceIsVisible = true; if (self.$controlsTop) { self.controlsTopAreVisible = true; } if (self.$controlsBottom) { self.controlsBottomAreVisible = true; } self.$controlsTop && self.$controlsTop.animate({ opacity: 1 }, 250); self.$controlsBottom && self.$controlsBottom.animate({ opacity: 1 }, 250); self.$find && self.$find.is(':visible') && self.$find.animate({ opacity: 1 }, 250); self.$volume && self.$volume.is(':visible') && self.$volume.animate({ opacity: 1 }, 250); self.$resolution && self.$resolution.is(':visible') && self.$resolution.animate({ opacity: 1 }, 250); self.$logo && self.$logo.animate({ top: getCSS('logo').top, opacity: 0.5 }, 250, function() { self.options.logoLink && self.$logo .bind({ click: function() { document.location.href = self.options.logoLink; } }); self.options.logoTitle && self.$logo .bind({ mouseenter: function(e) { self.$logoTooltip.show(e); }, mouseleave: self.$logoTooltip.hide }); }); self.$subtitle && self.$subtitle.animate({ bottom: getCSS('subtitle').bottom, }, 250); } } function showLoadingIcon() { self.$loadingIcon.attr({ src: Ox.UI.getImagePath('symbolLoadingAnimated.svg') .replace('/classic/', '/modern/') }).show(); } function showVolume() { !self.interfaceIsVisible && showControls(); self.$volume && self.$volume.is(':hidden') && toggleVolume(); } function submitFindInput(value, hasPressedEnter) { Ox.print('submitFindInput', value, hasPressedEnter) self.options.find = value; self.results = find(self.options.find); Ox.print('results', self.results.length); if (self.$find) { self.$results.html(self.results.length); self.$previousButton.options({ disabled: self.results.length <= 1 }); self.$nextButton.options({ disabled: self.results.length <= 1 }); self.$clearButton.options({ disabled: !self.options.find }); } self.subtitle && setSubtitleText(); self.$timeline && self.$timeline.options({ find: self.options.find, results: self.results }); if (hasPressedEnter) { self.results.length ? goToNextResult(1) : self.$findInput.focusInput(); } } 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']; } setPosition(position + self.secondsPerFrame); } function submitPositionInput() { self.$positionInput.hide(); self.$position.html('').show(); Ox.print('###', parsePositionInput(self.$positionInput.options('value'))) setPosition(parsePositionInput(self.$positionInput.options('value'))); if (self.playOnSubmit) { togglePaused(); self.video.play(); self.playOnSubmit = false; } if (self.focus == 'mouseenter' && !self.mouseHasLeft) { that.gainFocus(); } self.mouseHasLeft && hideControls(); that.triggerEvent('position', { position: self.options.position }); } function toggleFind() { var show = self.$find.is(':hidden'); !show && self.$findInput.blurInput(); self.$find.toggle(); show && self.$findInput.focusInput(false); } function toggleFullscreen(from) { var parentOffset, playOnFullscreen; self.options.fullscreen = !self.options.fullscreen; if (!self.options.paused) { // video won't keep playing accross detach/append self.video.pause(); playOnFullscreen = true; } if (self.options.fullscreen) { self.$parent = that.parent(); parentOffset = self.$parent.offset(); self.absoluteOffset = that.offset(); self.relativeOffset = { left: self.absoluteOffset.left - parentOffset.left, top: self.absoluteOffset.top - parentOffset.top }; that.detach() .css({ left: self.absoluteOffset.left + 'px', top: self.absoluteOffset.top + 'px', zIndex: 1000 }) .appendTo(Ox.UI.$body); setSizes(function() { playOnFullscreen && self.video.play(); that.bind({ mousemove: function() { showControls(); hideControls(); } }); that.find('.OxControls').bind({ mouseenter: function() { self.mouseIsInControls = true; }, mouseleave: function() { self.mouseIsInControls = false; } }); showControls(); hideControls(); that.gainFocus(); }); } else { // flag makes the animation end on absolute position self.exitFullscreen = true; that.unbind('mousemove'); that.find('.OxControls') .trigger('mouseleave') .unbind('mouseenter') .unbind('mouseleave'); clearTimeout(self.interfaceTimeout); setSizes(function() { self.exitFullscreen = false; that.detach() .css({ left: self.relativeOffset.left + 'px', top: self.relativeOffset.top + 'px', zIndex: 1 }) .appendTo(self.$parent); playOnFullscreen && self.video.play(); self.options.enableKeyboard && that.gainFocus(); //showControls(); }); } if (self.$fullscreenButton && from != 'button') { self.$fullscreenButton.toggleTitle(); } } function toggleMuted(from) { showVolume(); self.options.muted = !self.options.muted; self.video.muted = self.options.muted; if (!self.options.muted && !self.options.volume) { self.options.volume = 1; self.video.volume = 1; } if (self.$muteButton && from != 'button') { self.$muteButton.toggleTitle(); } self.$volumeButton && self.$volumeButton.attr({ src: getVolumeImageURL() }); self.$volumeInput && self.$volumeInput.options({ value: self.options.muted ? 0 : self.options.volume }); self.$volumeValue && self.$volumeValue.html( self.options.muted ? 0 : Math.round(self.options.volume * 100) ); } function togglePaused(from) { self.options.paused = !self.options.paused; self.$timeline && self.$timeline.options({ paused: self.options.paused }); if (self.options.paused) { self.video.pause(); clearInterval(self.playInterval); if (self.options.showIcon) { togglePlayIcon(); self.options.showIcon && self.$playIcon.animate({ opacity: 1 }, 250); } } else { if (self.options.playInToOut && self.options.position > self.options.out - self.secondsPerFrame) { setPosition(self.options['in']); } self.video.play(); self.playInterval = setInterval(playing, self.millisecondsPerFrame); if (self.options.showIcon) { self.options.showIcon && self.$playIcon.animate({ opacity: 0 }, 250, togglePlayIcon); } self.options.showMarkers && hideMarkers(); } if (self.$playButton && from != 'button') { self.$playButton.toggleTitle(); } } function togglePlayIcon() { self.$playIcon.attr({ src: Ox.UI.getImagePath( 'symbol' + (self.options.paused ? 'Play' : 'Pause' )+ '.svg').replace('/classic/', '/modern/') }); } function toggleScale(from) { self.options.scaleToFill = !self.options.scaleToFill; self.videoCSS = getVideoCSS(); self.$video.animate(self.videoCSS, 250); self.$poster && self.$poster.animate(self.videoCSS, 250); if (self.$scaleButton && from != 'button') { self.$scaleButton.toggleTitle(); } if (self.$posterMarker) { self.posterMarkerCSS = getPosterMarkerCSS(); Ox.forEach(self.$posterMarker, function(marker, position) { marker.animate(self.posterMarkerCSS[position], 250); }); } } function toggleSize() { self.options.sizeIsLarge = !self.options.sizeIsLarge; that.triggerEvent('size', { size: self.options.sizeIsLarge ? 'large' : 'small' }); } function toggleVolume() { self.$volume.toggle(); } self.setOption = function(key, value) { if (key == 'fullscreen') { toggleFullscreen(); } else if (key == 'height' || key == 'width') { setSizes(); } else if (key == 'in' || key == 'out') { self.options.paused && setMarkers(); } else if (key == 'muted') { toggleMuted(); } else if (key == 'paused') { togglePaused(); } else if (key == 'position') { setPosition(value); } else if (key == 'posterFrame') { self.options.paused && setMarkers(); } else if (key == 'scaleToFill') { toggleScale(); } }; that.playInToOut = function() { playInToOut(); return that; }; that.togglePaused = function() { togglePaused(); return that; } that.toggleMuted = function() { toggleMuted(); return that; } return that; };