From 66c626ecd95e1708b2b9072ff9f0ff4500978149 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 20 Nov 2015 22:45:12 -0800 Subject: [PATCH] Initial commit. --- app.yaml | 23 + static/clicks.css | 316 +++++++++ static/clicks.html | 17 + static/clicks.js | 1011 ++++++++++++++++++++++++++++ static/icon.png | Bin 0 -> 23019 bytes static/images/buffering.svg | 27 + static/images/exitfullscreen.svg | 9 + static/images/faster.svg | 20 + static/images/fullscreen.svg | 9 + static/images/jumpback-10m.svg | 8 + static/images/jumpback-10s.svg | 8 + static/images/jumpback-1f.svg | 8 + static/images/jumpback-1m.svg | 8 + static/images/jumpback-1s.svg | 8 + static/images/jumpforward-10m.svg | 8 + static/images/jumpforward-10s.svg | 8 + static/images/jumpforward-1f.svg | 8 + static/images/jumpforward-1m.svg | 8 + static/images/jumpforward-1s.svg | 8 + static/images/mute.svg | 11 + static/images/mutetoggle.svg | 15 + static/images/pause.svg | 12 + static/images/play.svg | 10 + static/images/playpause.svg | 16 + static/images/rate-025x.svg | 9 + static/images/rate-05x.svg | 9 + static/images/rate-125x.svg | 9 + static/images/rate-15x.svg | 9 + static/images/rate-1x.svg | 9 + static/images/rate-2x.svg | 9 + static/images/slower.svg | 35 + static/images/togglefullscreen.svg | 12 + static/images/unmute.svg | 14 + static/images/zoom-in.svg | 13 + static/images/zoom-out.svg | 12 + 35 files changed, 1716 insertions(+) create mode 100644 app.yaml create mode 100644 static/clicks.css create mode 100644 static/clicks.html create mode 100644 static/clicks.js create mode 100644 static/icon.png create mode 100644 static/images/buffering.svg create mode 100644 static/images/exitfullscreen.svg create mode 100644 static/images/faster.svg create mode 100644 static/images/fullscreen.svg create mode 100644 static/images/jumpback-10m.svg create mode 100644 static/images/jumpback-10s.svg create mode 100644 static/images/jumpback-1f.svg create mode 100644 static/images/jumpback-1m.svg create mode 100644 static/images/jumpback-1s.svg create mode 100644 static/images/jumpforward-10m.svg create mode 100644 static/images/jumpforward-10s.svg create mode 100644 static/images/jumpforward-1f.svg create mode 100644 static/images/jumpforward-1m.svg create mode 100644 static/images/jumpforward-1s.svg create mode 100644 static/images/mute.svg create mode 100644 static/images/mutetoggle.svg create mode 100644 static/images/pause.svg create mode 100644 static/images/play.svg create mode 100644 static/images/playpause.svg create mode 100644 static/images/rate-025x.svg create mode 100644 static/images/rate-05x.svg create mode 100644 static/images/rate-125x.svg create mode 100644 static/images/rate-15x.svg create mode 100644 static/images/rate-1x.svg create mode 100644 static/images/rate-2x.svg create mode 100644 static/images/slower.svg create mode 100644 static/images/togglefullscreen.svg create mode 100644 static/images/unmute.svg create mode 100644 static/images/zoom-in.svg create mode 100644 static/images/zoom-out.svg diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..34510cd --- /dev/null +++ b/app.yaml @@ -0,0 +1,23 @@ +runtime: python27 +version: 1 +api_version: 1 +application: fctv-player-1127 +threadsafe: true + +handlers: +- url: / + static_files: static/clicks.html + upload: static/clicks.html + secure: always + http_headers: + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload + +- url: /static + static_dir: static + secure: always + http_headers: + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload diff --git a/static/clicks.css b/static/clicks.css new file mode 100644 index 0000000..7e1e7bd --- /dev/null +++ b/static/clicks.css @@ -0,0 +1,316 @@ +.clicks-fullscreen-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: black; +} + +.clicks-add-video { + position: absolute; + z-index: 3; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + font-family: "proxima-nova"; + display: flex; + align-items: center; + justify-content: center; + background-color: black; + + visibility: hidden; +} + +.clicks-add-video-active { + visibility: visible; +} + +.clicks-add-video-dialog { + border: 1px solid red; + border-radius: 10px; + padding: 10px; + color: white; +} + +.clicks-add-video-dialog:before { + content: "YouTube ID or URL: "; +} + +.clicks-add-video-input { + width: 30em; + height: 1.5em; + padding: 3px; + border: 1px dashed red; + font-family: "source-code-pro"; + outline: none; +} + +@keyframes clicks-loading { + 0% { + color: rgba(255,0,0,0.8); + } + + 50% { + color: rgba(255,0,0,0.4); + } + + 100% { + color: rgba(255,0,0,0.8); + } +} + +.clicks-loading { + position: absolute; + z-index: 2; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + font-family: "proxima-nova"; + font-size: 100px; + display: flex; + align-items: center; + justify-content: center; + background-color: black; + color: rgba(255,0,0,0.8); + + animation: clicks-loading 3s infinite ease-in-out; + + transition: visibility 0.3s, opacity 0.3s linear; + visibility: visible; + opacity: 0.8; +} + +.clicks-loading:before { + content: "Loading..."; + +} + +.clicks-loading-complete { + visibility: hidden; + opacity: 0.0; +} + +.clicks-controls { + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + z-index: 1; + left: 0; + top: 0; + bottom: 0; + max-width: 50%; + background: rgba(50,50,50,0.7); + border-width: 1px; + border-color: rgba(255,0,0,0.5); + border-style: none solid none none; + box-shadow: 5px 0 5px rgba(255,0,0,0.1); + color: white; + cursor: default; + display: flex; + flex-direction: column; + + transition: visibility 0.15s, opacity 0.15s linear; + visibility: hidden; + opacity: 0.0; +} + +.clicks-controls-active { + visibility: visible; + opacity: 1.0; +} + +.clicks-title { + font-weight: bold; +} + +.clicks-current-time, .clicks-total-time { + font-family: "source-code-pro"; + font-size: small; +} + +.clicks-current-time:before { + content: "Current: "; +} + +.clicks-total-time:before { + content: " Total: "; + white-space: pre; +} + +.clicks-buffering { + position: absolute; + right: 0; + bottom: 0; + width: 75px; + height: 75px; + + transition: opacity 0.15s linear; + opacity: 0.0; +} + +.clicks-buffering-active { + opacity: 0.8; +} + +.clicks-control-info-area, .clicks-control-section-select-area { + position: relative; + padding: 10px; + border-bottom: 1px solid rgba(255,0,0,0.5); +} + +.clicks-control-section-area { + flex-grow: 1; + position: relative; +} + +.clicks-control-section-select { + width: 100px; + height: 40px; + display: inline-flex; + justify-content: center; + align-items: center; + box-shadow: 5px 0 5px rgba(255,0,0,0.1); + overflow: hidden; + border-radius: 10px; + padding: 5px; + margin: 5px; + font-family: "source-code-pro"; + text-transform: uppercase; + cursor: pointer; + + transition: border 0.15s linear, background-color 0.15s linear; + border: 1px solid rgba(255,0,0,0.7); + background-color: rgba(255,255,255,0.1); +} + +.clicks-control-section-select-active { + border: 1px solid rgba(255,255,0,0.7); + background-color: rgba(255,255,0,0.3); +} + +.clicks-control-section { + display: none; + padding: 10px; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.clicks-control-section-active { + display: block; +} + +.clicks-button { + position: relative; + width: 75px; + height: 75px; + display: inline-block; + box-shadow: 5px 0 5px rgba(255,0,0,0.1); + background-color: rgba(0,0,0,0.8); + overflow: hidden; + border-radius: 10px; + padding: 5px; + text-align: center; + margin: 5px; + cursor: pointer; + + transition: border 0.15s linear; + border: 1px solid rgba(255,0,0,0.7); +} + +.clicks-button img { + width: 50px; + height: 50px; +} + +.clicks-shortcut { + position: absolute; + text-align: center; + bottom: 0; + left: 0; + right: 0; + padding-bottom: 3px; + font-family: "source-code-pro"; + text-transform: uppercase; + + transition: background-color 0.15s linear; + background-color: rgba(255,255,255,0.2); +} + +.clicks-button-active { + border: 1px solid rgba(255,255,0,0.7); +} + +.clicks-button-active .clicks-shortcut { + background-color: rgba(255,255,0,0.3); +} + +.clicks-controls-marker { + position: relative; + min-width: 300px; + height: 40px; + display: inline-block; + border: 1px solid rgba(255,0,0,0.7); + box-shadow: 5px 0 5px rgba(255,0,0,0.1); + background-color: rgba(0,0,0,0.8); + overflow: hidden; + border-radius: 10px; + padding: 8px; + text-align: left; + margin: 5px; + cursor: pointer; +} + +.clicks-controls-marker-time { + font-family: "source-code-pro"; +} + +.clicks-player-container { + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + top: 0; + left: 0; + right: 0; + bottom: 0; +# TODO(flamingcow): How do we center this? +# display: flex; +# align-items: center; +# justify-content: center; + overflow: scroll; + background-color: black; +} + +.clicks-player-crop { +} + +.clicks-player-scale { + position: relative; + transform-origin: top left; + transition: transform 0.3s ease-in-out; +} + +.clicks-player-overlay { + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; +} diff --git a/static/clicks.html b/static/clicks.html new file mode 100644 index 0000000..8ff86aa --- /dev/null +++ b/static/clicks.html @@ -0,0 +1,17 @@ + + + FlamingCowTV Player + + + + + + +
+ + + + + diff --git a/static/clicks.js b/static/clicks.js new file mode 100644 index 0000000..8f8252a --- /dev/null +++ b/static/clicks.js @@ -0,0 +1,1011 @@ +var Clicks = function(youTubeAPIKey, container, takeDocumentHashOwnership, trackingID) { + this.youTubeAPIKey_ = youTubeAPIKey; + this.container_ = container; + this.players_ = []; + this.activePlayer_ = null; + this.zoomLevel_ = 1.0; + this.delayedConfig_ = {}; + + this.buildUI_(); + + this.eventTarget_ = document.createDocumentFragment(); + this.addEventListener = + this.eventTarget_.addEventListener.bind(this.eventTarget_); + this.removeEventListener = + this.eventTarget_.removeEventListener.bind(this.eventTarget_); + this.dispatchEvent = + this.eventTarget_.dispatchEvent.bind(this.eventTarget_); + + if (trackingID) { + this.loadAnalytics(trackingID); + } + + window.addEventListener('resize', this.onWindowResize_.bind(this)); + + this.addEventListener('configchange', this.updateControls_.bind(this)); + if (takeDocumentHashOwnership) { + this.takeDocumentHashOwnership(); + } + + window.addEventListener('keypress', this.onKeyPress_.bind(this)); + window.setInterval(this.fireConfigChange.bind(this), 300); +}; + + +Clicks.youTubeIframeAPIReady = false; +Clicks.onYouTubeIframeAPIReady = []; +Clicks.keyStrings = { + ' ': '', + '\x1b': '', +}; +Clicks.zoomLevels = [ + 1.0, + 1.5, + 2.0, + 2.5, + 3.0, +]; + + +Clicks.prototype.trackEvent_ = function(var_args) { + if (this.analyticsObj_) { + this.analyticsObj_.apply(this, arguments); + } else if (this.analyticsObjName_) { + window[this.analyticsObjName_].q.push(arguments); + } +}; + + +Clicks.prototype.loadAnalytics = function(trackingID) { + this.analyticsObjName_ = 'ClicksAnalytics' + Math.round(Math.random() * 10000000).toString(); + window['GoogleAnalyticsObject'] = this.analyticsObjName_; + + var completeCallback = (function() { + this.analyticsObj_ = window[this.analyticsObjName_]; + delete window[this.analyticsObjName_]; + }).bind(this); + + window[this.analyticsObjName_] = { + 'l': 1 * new Date(), + 'q': [], + }; + + var script = document.createElement('script'); + script.src = 'https://www.google-analytics.com/analytics.js'; + script.async = true; + script.onload = completeCallback; + document.body.appendChild(script); + + this.trackEvent_('create', trackingID, { + 'storage': 'none', + 'clientId': localStorage['clicks_tracking_client_id'] + }); + this.trackEvent_((function(analytics) { + localStorage['clicks_tracking_client_id'] = + analytics.get('clientId'); + }).bind(this)); +}; + + +Clicks.prototype.takeDocumentHashOwnership = function() { + if (document.location.hash.length > 1) { + this.parseConfigString(document.location.hash.substring(1)); + } + + this.addEventListener('configchange', function(e) { + if (e.detail.length == 0) { + return; + } + document.location.hash = '#' + e.detail; + }); +}; + + +Clicks.prototype.createElementAndAppend_ = function(className, parentNode) { + var element = document.createElement('div'); + element.className = className; + parentNode.appendChild(element); + return element; +}; + + +Clicks.prototype.onAddVideoValueChanged_ = function(e) { + var value = e.target.textContent; + + if (value.length == 11 && value.indexOf(':') == -1 && value.indexOf('.') == -1) { + // Plausible YouTube video ID + this.addVideo(value); + return; + } + + var parse = document.createElement('a'); + parse.href = value; + + if (parse.hostname == 'youtu.be' || parse.hostname == 'www.youtu.be' && parse.pathname.length == 12) { + this.addVideo(parse.pathname.substring(1)); + return; + } + + var re = new RegExp('[?&]v=([^&]{11})(&|$)'); + var match = re.exec(parse.search); + if (match) { + this.addVideo(match[1]); + return; + } +} + + +Clicks.prototype.buildUI_ = function() { + this.container_.tabIndex = -1; + + this.addVideo_ = this.createElementAndAppend_( + 'clicks-add-video clicks-add-video-active', this.container_); + var addVideoDialog = this.createElementAndAppend_( + 'clicks-add-video-dialog', this.addVideo_); + this.addVideoValue_ = this.createElementAndAppend_( + 'clicks-add-video-input', addVideoDialog); + this.addVideoValue_.contentEditable = true; + this.addVideoValue_.addEventListener('keypress', function(e) { e.stopPropagation(); }); + this.addVideoValue_.addEventListener('input', this.onAddVideoValueChanged_.bind(this)); + this.addVideoValue_.focus(); + + this.loading_ = this.createElementAndAppend_( + 'clicks-loading', this.container_); + + this.controls_ = this.createElementAndAppend_( + 'clicks-controls', this.container_); + + var infoArea = this.createElementAndAppend_( + 'clicks-control-info-area', this.controls_); + this.title_ = this.createElementAndAppend_( + 'clicks-title', infoArea); + this.channel_ = this.createElementAndAppend_( + 'clicks-channel', infoArea); + this.currentTime_ = this.createElementAndAppend_( + 'clicks-current-time', infoArea); + this.totalTime_ = this.createElementAndAppend_( + 'clicks-total-time', infoArea); + + this.buffering_ = document.createElement('img'); + this.buffering_.src = '/static/images/buffering.svg'; + this.buffering_.className = 'clicks-buffering'; + infoArea.appendChild(this.buffering_); + + var controls = [ + { + 'title': 'Transport', + 'buttons': [ + [ + { + 'img': 'playpause', + 'key': ' ', + }, + { + 'img': 'play', + 'key': 'a', + }, + { + 'img': 'pause', + 'key': 's', + }, + ], + [ + { + 'img': 'jumpback-1f', + 'key': 't', + }, + { + 'img': 'jumpback-1s', + 'key': 'r', + }, + { + 'img': 'jumpback-10s', + 'key': 'e', + }, + { + 'img': 'jumpback-1m', + 'key': 'w', + }, + { + 'img': 'jumpback-10m', + 'key': 'q', + }, + ], + [ + { + 'img': 'jumpforward-1f', + 'key': 'y', + }, + { + 'img': 'jumpforward-1s', + 'key': 'u', + }, + { + 'img': 'jumpforward-10s', + 'key': 'i', + }, + { + 'img': 'jumpforward-1m', + 'key': 'o', + }, + { + 'img': 'jumpforward-10m', + 'key': 'p', + }, + ], + ], + }, + { + 'title': 'Rate', + 'buttons': [ + [ + { + 'img': 'slower', + 'key': '[', + }, + { + 'img': 'faster', + 'key': ']', + }, + ], + [ + { + 'img': 'rate-025x', + 'key': '3', + }, + { + 'img': 'rate-05x', + 'key': '4', + }, + { + 'img': 'rate-1x', + 'key': '5', + }, + { + 'img': 'rate-125x', + 'key': '6', + }, + { + 'img': 'rate-15x', + 'key': '7', + }, + { + 'img': 'rate-2x', + 'key': '8', + }, + ], + ], + }, + { + 'title': 'Zoom', + 'buttons': [ + [ + { + 'img': 'zoom-out', + 'key': '-', + }, + { + 'img': 'zoom-in', + 'key': '+', + }, + ], + ], + }, + { + 'title': 'Markers', + 'buttons': [], + }, + { + 'title': 'Player', + 'buttons': [ + [ + { + 'img': 'togglefullscreen', + 'key': 'd', + }, + { + 'img': 'fullscreen', + 'key': 'f', + }, + { + 'img': 'exitfullscreen', + 'key': '\x1b', + }, + ], + [ + { + 'img': 'mutetoggle', + 'key': 'm', + }, + { + 'img': 'mute', + 'key': 'b', + }, + { + 'img': 'unmute', + 'key': 'n', + }, + ], + ], + }, + ]; + + + var selectArea = this.createElementAndAppend_( + 'clicks-control-section-select-area', this.controls_); + + this.sectionSelectors_ = {}; + + for (var i = 0; i < controls.length; i++) { + var section = controls[i]; + + var sectionSelect = this.createElementAndAppend_( + 'clicks-control-section-select', selectArea); + sectionSelect.textContent = section.title; + sectionSelect.addEventListener( + 'click', this.activateControlSection_.bind(this, section.title)); + this.sectionSelectors_[section.title] = sectionSelect; + } + + var sectionArea = this.createElementAndAppend_( + 'clicks-control-section-area', this.controls_); + + this.sections_ = {}; + this.buttons_ = {}; + + for (var i = 0; i < controls.length; i++) { + var section = controls[i]; + + var sectionNode = this.createElementAndAppend_( + 'clicks-control-section', sectionArea); + this.sections_[section.title] = sectionNode; + + for (var j = 0; j < section.buttons.length; j++) { + var buttons = section.buttons[j]; + var row = this.createElementAndAppend_( + 'clicks-control-section-row', sectionNode); + + for (var k = 0; k < buttons.length; k++) { + var button = buttons[k]; + var buttonNode = this.buildButton_(button.img, button.key); + row.appendChild(buttonNode); + this.buttons_[button.img] = buttonNode; + } + } + } + + var playerContainer = this.createElementAndAppend_( + 'clicks-player-container', this.container_); + this.playerCrop_ = this.createElementAndAppend_( + 'clicks-player-crop', playerContainer); + this.playerScale_ = this.createElementAndAppend_( + 'clicks-player-scale', this.playerCrop_); + var playerOverlay = this.createElementAndAppend_( + 'clicks-player-overlay', this.playerScale_); + + this.activateControlSection_(controls[0].title); + + playerOverlay.addEventListener('click', this.showHideControls_.bind(this)); + this.container_.addEventListener('click', this.showHideControls_.bind(this)); +}; + + +Clicks.prototype.activateControlSection_ = function(title, e) { + if (e) { + e.stopPropagation(); + } + + for (var key in this.sections_) { + if (key == title) { + this.sectionSelectors_[key].className = + 'clicks-control-section-select clicks-control-section-select-active'; + this.sections_[key].className = + 'clicks-control-section clicks-control-section-active'; + } else { + this.sectionSelectors_[key].className = 'clicks-control-section-select'; + this.sections_[key].className = 'clicks-control-section'; + }; + } +}; + + +Clicks.prototype.buildButton_ = function(image, key) { + var button = document.createElement('div'); + button.className = 'clicks-button'; + + var img = document.createElement('img'); + img.src = '/static/images/' + image + '.svg'; + button.appendChild(img); + + var shortcut = document.createElement('div'); + shortcut.className = 'clicks-shortcut'; + shortcut.textContent = Clicks.keyStrings[key] || key; + button.appendChild(shortcut); + + button.addEventListener('click', function(e) { + this.onKeyPress_({ + 'charCode': key.charCodeAt(0), + }); + e.stopPropagation(); + }.bind(this)); + + return button; +}; + + +Clicks.prototype.isFullScreen_ = function() { + return window.innerHeight == screen.height; +}; + + +Clicks.prototype.parseConfigString = function(str) { + var params = str.split(','); + for (var i = 0; i < params.length; i++) { + var keyValue = params[i].split('=', 2); + switch (keyValue[0]) { + case 'ytid': + this.addVideo(keyValue[1]); + break; + case 'rate': + case 'zoom': + case 'muted': + case 'time': + this.delayedConfig_[keyValue[0]] = keyValue[1]; + break; + } + } +}; + + +Clicks.prototype.getConfigString = function() { + if (!this.activePlayer_) { + return ''; + } + + var config = { + 'ytid': this.activePlayer_.id, + 'rate': this.activePlayer_.getRate().realRate, + 'zoom': this.zoomLevel_, + 'muted': this.activePlayer_.player.isMuted() ? 1 : 0, + 'time': this.activePlayer_.player.getCurrentTime(), + }; + var params = []; + for (var key in config) { + params.push(key + '=' + config[key]); + } + return params.join(','); +}; + + +Clicks.prototype.durationToString_ = function(num) { + function zeroPad(x) { + var xstr = x.toString(); + return xstr.length == 1 ? '0' + xstr : xstr; + } + + return ( + zeroPad(Math.floor(num / 3600)) + 'h ' + + zeroPad(Math.floor(num % 3600 / 60)) + 'm ' + + zeroPad(Math.floor(num % 60)) + '.' + + zeroPad(Math.floor(num * 100 % 100)) + 's' + ); +}; + + +Clicks.prototype.setButtonActive_ = function(name, active) { + if (active) { + this.buttons_[name].className = 'clicks-button clicks-button-active'; + } else { + this.buttons_[name].className = 'clicks-button'; + } +}; + + +Clicks.prototype.fireConfigChange = function() { + var e = new CustomEvent('configchange', { + 'detail': this.getConfigString(), + }); + this.dispatchEvent(e); +}; + + +Clicks.prototype.updateControls_ = function(e) { + if (this.isFullScreen_()) { + this.setButtonActive_('fullscreen', true); + this.setButtonActive_('exitfullscreen', false); + } else { + this.setButtonActive_('fullscreen', false); + this.setButtonActive_('exitfullscreen', true); + } + if (!this.activePlayer_) { + return; + } + + this.currentTime_.textContent = this.durationToString_( + this.activePlayer_.player.getCurrentTime()); + this.totalTime_.textContent = this.durationToString_( + this.activePlayer_.player.getDuration()); + + if (this.activePlayer_.player.getPlayerState() == YT.PlayerState.BUFFERING) { + this.buffering_.className = 'clicks-buffering clicks-buffering-active'; + } else { + this.buffering_.className = 'clicks-buffering'; + } + + if (this.activePlayer_.player.getPlayerState() == YT.PlayerState.PLAYING) { + this.setButtonActive_('play', true); + this.setButtonActive_('pause', false); + } else { + this.setButtonActive_('play', false); + this.setButtonActive_('pause', true); + } + + if (this.activePlayer_.player.isMuted()) { + this.setButtonActive_('mute', true); + this.setButtonActive_('unmute', false); + } else { + this.setButtonActive_('mute', false); + this.setButtonActive_('unmute', true); + } + + var activeRate = this.activePlayer_.getRate().realRate; + this.setButtonActive_('rate-025x', activeRate == 0.25); + this.setButtonActive_('rate-05x', activeRate == 0.5); + this.setButtonActive_('rate-1x', activeRate == 1.0); + this.setButtonActive_('rate-125x', activeRate == 1.25); + this.setButtonActive_('rate-15x', activeRate == 1.5); + this.setButtonActive_('rate-2x', activeRate == 2.0); +}; + + +Clicks.prototype.addVideo = function(id) { + console.log('Adding YouTube video ID:', id); + var playerNode = document.createElement('div'); + playerNode.style.visibility = 'hidden'; + this.playerScale_.appendChild(playerNode); + new ClicksVideo(this.youTubeAPIKey_, id, playerNode, this.onVideoAdded_.bind(this)); + + this.addVideo_.className = 'clicks-add-video'; + this.container_.focus(); + this.addVideoValue_.textContent = ''; + + this.trackEvent_('send', 'event', 'Video', 'Add', id); +}; + + +Clicks.prototype.onVideoAdded_ = function(player) { + this.players_.push(player); + this.activePlayer_ = player; + for (var key in this.delayedConfig_) { + var value = this.delayedConfig_[key]; + switch (key) { + case 'rate': + this.activePlayer_.setRate(parseFloat(value)); + break; + case 'zoom': + this.zoomLevel_ = parseFloat(value); + break; + case 'muted': + if (parseInt(value)) { + this.activePlayer_.player.mute(); + } else { + this.activePlayer_.player.unMute(); + } + break; + case 'time': + this.activePlayer_.player.seekTo(parseFloat(value), true); + break; + } + } + this.resizePlayer_(player); + this.activePlayer_.player.unMute(); + + document.title = player.metadata.title; + this.title_.textContent = player.metadata.title; + this.channel_.textContent = player.metadata.channelTitle; + + for (var i = 0; i < player.metadata.markers.length; i++) { + var marker = player.metadata.markers[i]; + + var markerNode = document.createElement('div'); + markerNode.className = 'clicks-controls-marker'; + + var markerName = document.createElement('div'); + markerName.className = 'clicks-controls-marker-name'; + markerName.textContent = marker[1]; + markerNode.appendChild(markerName); + + var markerTime = document.createElement('div'); + markerTime.className = 'clicks-controls-marker-time'; + markerTime.textContent = this.durationToString_(marker[0]); + markerNode.appendChild(markerTime); + + markerNode.addEventListener('click', function(time, e) { + this.activePlayer_.player.seekTo(time, true); + e.stopPropagation(); + }.bind(this, marker[0])); + + this.sections_['Markers'].appendChild(markerNode); + } + + player.playerNode.style.visibility = 'visible'; + this.loading_.className = 'clicks-loading clicks-loading-complete'; + player.player.playVideo(); + + this.fireConfigChange(); +}; + + +Clicks.prototype.resizePlayer_ = function(player) { + var zoom = Math.min( + this.container_.clientWidth / player.videoRes[0], + this.container_.clientHeight / player.videoRes[1]); + zoom = Math.min(zoom * this.zoomLevel_, 1.0); + this.playerScale_.style.transform = [ + 'scale(' + zoom + ',' + zoom + ')', + ].join(' '); + this.playerScale_.style.width = player.videoRes[0]; + this.playerScale_.style.height = player.videoRes[1]; + this.playerCrop_.style.width = Math.ceil(player.videoRes[0] * zoom); + this.playerCrop_.style.height = Math.ceil(player.videoRes[1] * zoom); +}; + + +Clicks.prototype.onWindowResize_ = function(e) { + for (var i = 0; i < this.players_.length; i++) { + this.resizePlayer_(this.players_[i]); + } +}; + + +Clicks.prototype.onKeyPress_ = function(e) { + switch (String.fromCharCode(e.charCode).toLowerCase()) { + case ' ': + if (this.activePlayer_.player.getPlayerState() == YT.PlayerState.PLAYING) { + this.activePlayer_.player.pauseVideo(); + } else { + this.activePlayer_.player.playVideo(); + } + break; + case 'a': + this.activePlayer_.player.playVideo(); + break; + + case 's': + this.activePlayer_.player.pauseVideo(); + break; + + case '[': + var i = this.activePlayer_.getRateIndex(); + if (i > 0) { + this.activePlayer_.setRate(this.activePlayer_.rates[i - 1].realRate); + } + break; + case ']': + var i = this.activePlayer_.getRateIndex(); + if (i < this.activePlayer_.rates.length - 1) { + this.activePlayer_.setRate(this.activePlayer_.rates[i + 1].realRate); + } + break; + + case '3': + this.activePlayer_.setRate(0.25); + break; + case '4': + this.activePlayer_.setRate(0.5); + break; + case '5': + this.activePlayer_.setRate(1.0); + break; + case '6': + this.activePlayer_.setRate(1.25); + break; + case '7': + this.activePlayer_.setRate(1.5); + break; + case '8': + this.activePlayer_.setRate(2); + break; + + case 't': + this.activePlayer_.seekRelative(0 - this.activePlayer_.frameSkip); + break; + case 'r': + this.activePlayer_.seekRelative(-1); + break; + case 'e': + this.activePlayer_.seekRelative(-10); + break; + case 'w': + this.activePlayer_.seekRelative(-60); + break; + case 'q': + this.activePlayer_.seekRelative(-600); + break; + + case 'y': + this.activePlayer_.seekRelative(this.activePlayer_.frameSkip); + break; + case 'u': + this.activePlayer_.seekRelative(1); + break; + case 'i': + this.activePlayer_.seekRelative(10); + break; + case 'o': + this.activePlayer_.seekRelative(60); + break; + case 'p': + this.activePlayer_.seekRelative(600); + break; + + case 'd': + if (this.isFullScreen_()) { + this.exitFullScreen_(); + } else { + this.fullScreen_(); + } + case 'f': + this.fullScreen_(); + break; + case '\x1b': + this.exitFullScreen_(); + break; + + case 'm': + if (this.activePlayer_.player.isMuted()) { + this.activePlayer_.player.unMute(); + } else { + this.activePlayer_.player.mute(); + } + break; + case 'b': + this.activePlayer_.player.mute(); + break; + case 'n': + this.activePlayer_.player.unMute(); + break; + + case '-': + case '_': + var i = Clicks.zoomLevels.indexOf(this.zoomLevel_); + this.zoomLevel_ = Clicks.zoomLevels[i - 1] || this.zoomLevel_; + this.resizePlayer_(this.activePlayer_); + break; + + case '+': + case '=': + var i = Clicks.zoomLevels.indexOf(this.zoomLevel_); + this.zoomLevel_ = Clicks.zoomLevels[i + 1] || this.zoomLevel_; + this.resizePlayer_(this.activePlayer_); + break; + } + this.fireConfigChange(); +}; + + +Clicks.prototype.showHideControls_ = function(e) { + if (this.controls_.className == 'clicks-controls clicks-controls-active') { + this.controls_.className = 'clicks-controls'; + } else { + this.controls_.className = 'clicks-controls clicks-controls-active'; + } + e.stopPropagation(); +}; + + +Clicks.prototype.fullScreen_ = function() { + if (this.container_.requestFullscreen) { + this.container_.requestFullscreen(); + } else if (this.container_.webkitRequestFullscreen) { + this.container_.webkitRequestFullscreen(); + } else if (this.container_.mozRequestFullScreen) { + this.container_.mozRequestFullScreen(); + } +}; + + +Clicks.prototype.exitFullScreen_ = function() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } +}; + + + +var ClicksVideo = function(youTubeAPIKey, id, playerNode, onReady) { + this.youTubeAPIKey_ = youTubeAPIKey; + this.id = id; + this.playerNode = playerNode; + this.onReady_ = onReady; + this.loading_ = true; + + this.fetchVideoInfo_(id, this.onMetadataResponse_.bind(this)); + + if (Clicks.youTubeIframeAPIReady) { + this.onAPIReady_(); + } else { + Clicks.onYouTubeIframeAPIReady.push(this.onAPIReady_.bind(this)); + } +}; + + +ClicksVideo.prototype.onMetadataResponse_ = function(response) { + this.metadata = this.parseVideoDescription_(response); + this.checkComplete_(); +}; + + +ClicksVideo.prototype.onPlayerReady_ = function(e) { + this.playerRates = e.target.getAvailablePlaybackRates(); + this.checkComplete_(); +}; + + +ClicksVideo.prototype.onPlayerStateChange_ = function(e) { + if (e.data == YT.PlayerState.PLAYING && this.loading_) { + this.player.pauseVideo(); + this.player.seekTo(0, true); + this.setRate(1.0); + this.loading_ = false; + this.onReady_(this); + } +}; + + +ClicksVideo.prototype.onAPIReady_ = function() { + var tempNode = document.createElement('div'); + this.playerNode.appendChild(tempNode); + this.player = new YT.Player(tempNode, { + height: '1080', + width: '1920', + videoId: this.id, + playerVars: { + 'controls': 0, + 'enablejsapi': 1, + 'disablekb': 1, + 'showinfo': 0, + }, + events: { + 'onReady': this.onPlayerReady_.bind(this), + 'onStateChange': this.onPlayerStateChange_.bind(this), + }, + }); +}; + + +ClicksVideo.prototype.checkComplete_ = function() { + if (!this.metadata || !this.playerRates) { + return; + } + var baseRate = parseFloat(this.metadata.tags.realfps) / parseFloat(this.metadata.tags.ytfps) + this.rates = []; + for (var i = 0; i < this.playerRates.length; i++) { + this.rates.push({ + 'playerRate': this.playerRates[i], + 'realRate': this.playerRates[i] * baseRate, + 'fps': this.metadata.tags['ytfps'] * this.playerRates[i], + }); + } + this.frameSkip = 1.0 / parseFloat(this.metadata.tags.ytfps); + var yRes = parseInt(this.metadata.tags.res); + this.videoRes = [yRes * 16 / 9, yRes]; + this.player.setSize(this.videoRes[0], this.videoRes[1]); + this.player.setPlaybackQuality('highres'); + this.player.setVolume(100); + this.player.mute(); + this.player.playVideo(); +}; + + +ClicksVideo.prototype.seekRelative = function(offset) { + this.player.seekTo(this.player.getCurrentTime() + offset, true); +}; + + +ClicksVideo.prototype.getRateIndex = function() { + var playerRate = this.player.getPlaybackRate(); + for (var i = 0; i < this.rates.length; i++) { + if (this.rates[i].playerRate == playerRate) { + return i; + } + } + return null; +}; + + +ClicksVideo.prototype.getRate = function() { + return this.rates[this.getRateIndex()]; +}; + + +ClicksVideo.prototype.setRate = function(realRate) { + for (var i = 0; i < this.rates.length; i++) { + if (this.rates[i].realRate == realRate) { + this.player.setPlaybackRate(this.rates[i].playerRate); + return; + } + } +}; + + +ClicksVideo.prototype.buildQueryString_ = function(args) { + var ret = []; + for (var key in args) { + ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(args[key])); + } + return ret.join('&'); +}; + + +ClicksVideo.prototype.fetchVideoInfo_ = function(id, callback) { + var queryString = this.buildQueryString_({ + 'key': this.youTubeAPIKey_, + 'part': 'snippet', + 'id': id, + }); + + var sendRequest = function() { + var xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.open('GET', 'https://www.googleapis.com/youtube/v3/videos?' + queryString); + xhr.addEventListener('load', function(e) { + if (!e.target.response.items || e.target.response.items.length != 1) { + console.log('Invalid response:', e.target); + setTimeout(sendRequest, 1000); + return; + } + callback(e.target.response.items[0]); + }); + xhr.addEventListener('error', function(e) { + setTimeout(sendRequest, 1000); + }); + xhr.send(); + }; + + sendRequest(); +}; + + +ClicksVideo.prototype.parseVideoDescription_ = function(video) { + var markerRe = new RegExp('^fctv:marker=(\\d+):(.*)$'); + var tagRe = new RegExp('^fctv:(.*?)=(.*)$'); + var ret = { + 'title': video.snippet.title, + 'channelTitle': video.snippet.channelTitle, + 'tags': { + // Best guesses + 'ytfps': 30.0, + 'realfps': 30.0, + 'res': '1080', + }, + 'markers': [], + }; + for (var i = 0; i < video.snippet.tags.length; i++) { + var match = markerRe.exec(video.snippet.tags[i]); + if (match) { + ret.markers.push([parseFloat(match[1]), match[2]]); + continue; + } + + match = tagRe.exec(video.snippet.tags[i]); + if (match) { + ret.tags[match[1]] = match[2]; + } + } + return ret; +}; + + + +var onYouTubeIframeAPIReady = function() { + Clicks.youTubeIframeAPIReady = true; + for (var i = 0; i < Clicks.onYouTubeIframeAPIReady.length; i++) { + Clicks.onYouTubeIframeAPIReady[i](); + } + Clicks.onYouTubeIframeAPIReady.length = 0; +}; diff --git a/static/icon.png b/static/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d899d911522bb6c9f6b323fba8615c3b7a1d4f GIT binary patch literal 23019 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 + + + + + + diff --git a/static/images/exitfullscreen.svg b/static/images/exitfullscreen.svg new file mode 100644 index 0000000..292d009 --- /dev/null +++ b/static/images/exitfullscreen.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/static/images/faster.svg b/static/images/faster.svg new file mode 100644 index 0000000..06bb5d2 --- /dev/null +++ b/static/images/faster.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/static/images/fullscreen.svg b/static/images/fullscreen.svg new file mode 100644 index 0000000..5aab885 --- /dev/null +++ b/static/images/fullscreen.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/static/images/jumpback-10m.svg b/static/images/jumpback-10m.svg new file mode 100644 index 0000000..27df7bb --- /dev/null +++ b/static/images/jumpback-10m.svg @@ -0,0 +1,8 @@ + + + + +10m + + diff --git a/static/images/jumpback-10s.svg b/static/images/jumpback-10s.svg new file mode 100644 index 0000000..4adca49 --- /dev/null +++ b/static/images/jumpback-10s.svg @@ -0,0 +1,8 @@ + + + + +10s + + diff --git a/static/images/jumpback-1f.svg b/static/images/jumpback-1f.svg new file mode 100644 index 0000000..e343c33 --- /dev/null +++ b/static/images/jumpback-1f.svg @@ -0,0 +1,8 @@ + + + + +1f + + diff --git a/static/images/jumpback-1m.svg b/static/images/jumpback-1m.svg new file mode 100644 index 0000000..b769d00 --- /dev/null +++ b/static/images/jumpback-1m.svg @@ -0,0 +1,8 @@ + + + + +1m + + diff --git a/static/images/jumpback-1s.svg b/static/images/jumpback-1s.svg new file mode 100644 index 0000000..8af4ba2 --- /dev/null +++ b/static/images/jumpback-1s.svg @@ -0,0 +1,8 @@ + + + + +1s + + diff --git a/static/images/jumpforward-10m.svg b/static/images/jumpforward-10m.svg new file mode 100644 index 0000000..79e3625 --- /dev/null +++ b/static/images/jumpforward-10m.svg @@ -0,0 +1,8 @@ + + + + +10m + + diff --git a/static/images/jumpforward-10s.svg b/static/images/jumpforward-10s.svg new file mode 100644 index 0000000..8262211 --- /dev/null +++ b/static/images/jumpforward-10s.svg @@ -0,0 +1,8 @@ + + + + +10s + + diff --git a/static/images/jumpforward-1f.svg b/static/images/jumpforward-1f.svg new file mode 100644 index 0000000..70175a2 --- /dev/null +++ b/static/images/jumpforward-1f.svg @@ -0,0 +1,8 @@ + + + + +1f + + diff --git a/static/images/jumpforward-1m.svg b/static/images/jumpforward-1m.svg new file mode 100644 index 0000000..cea6948 --- /dev/null +++ b/static/images/jumpforward-1m.svg @@ -0,0 +1,8 @@ + + + + +1m + + diff --git a/static/images/jumpforward-1s.svg b/static/images/jumpforward-1s.svg new file mode 100644 index 0000000..8484338 --- /dev/null +++ b/static/images/jumpforward-1s.svg @@ -0,0 +1,8 @@ + + + + +1s + + diff --git a/static/images/mute.svg b/static/images/mute.svg new file mode 100644 index 0000000..848c54c --- /dev/null +++ b/static/images/mute.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/static/images/mutetoggle.svg b/static/images/mutetoggle.svg new file mode 100644 index 0000000..272deb5 --- /dev/null +++ b/static/images/mutetoggle.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/static/images/pause.svg b/static/images/pause.svg new file mode 100644 index 0000000..ac73161 --- /dev/null +++ b/static/images/pause.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/static/images/play.svg b/static/images/play.svg new file mode 100644 index 0000000..c29c7ed --- /dev/null +++ b/static/images/play.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/static/images/playpause.svg b/static/images/playpause.svg new file mode 100644 index 0000000..888b0bc --- /dev/null +++ b/static/images/playpause.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/static/images/rate-025x.svg b/static/images/rate-025x.svg new file mode 100644 index 0000000..372a79d --- /dev/null +++ b/static/images/rate-025x.svg @@ -0,0 +1,9 @@ + + + + +0.25x + + diff --git a/static/images/rate-05x.svg b/static/images/rate-05x.svg new file mode 100644 index 0000000..e33431a --- /dev/null +++ b/static/images/rate-05x.svg @@ -0,0 +1,9 @@ + + + + +0.5x + + diff --git a/static/images/rate-125x.svg b/static/images/rate-125x.svg new file mode 100644 index 0000000..998b450 --- /dev/null +++ b/static/images/rate-125x.svg @@ -0,0 +1,9 @@ + + + + +1.25x + + diff --git a/static/images/rate-15x.svg b/static/images/rate-15x.svg new file mode 100644 index 0000000..7499feb --- /dev/null +++ b/static/images/rate-15x.svg @@ -0,0 +1,9 @@ + + + + +1.5x + + diff --git a/static/images/rate-1x.svg b/static/images/rate-1x.svg new file mode 100644 index 0000000..6518461 --- /dev/null +++ b/static/images/rate-1x.svg @@ -0,0 +1,9 @@ + + + + +1.0x + + diff --git a/static/images/rate-2x.svg b/static/images/rate-2x.svg new file mode 100644 index 0000000..5ea3b61 --- /dev/null +++ b/static/images/rate-2x.svg @@ -0,0 +1,9 @@ + + + + +2.0x + + diff --git a/static/images/slower.svg b/static/images/slower.svg new file mode 100644 index 0000000..ecdd33c --- /dev/null +++ b/static/images/slower.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/static/images/togglefullscreen.svg b/static/images/togglefullscreen.svg new file mode 100644 index 0000000..7a9f33f --- /dev/null +++ b/static/images/togglefullscreen.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/static/images/unmute.svg b/static/images/unmute.svg new file mode 100644 index 0000000..5c0010d --- /dev/null +++ b/static/images/unmute.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/static/images/zoom-in.svg b/static/images/zoom-in.svg new file mode 100644 index 0000000..aed89af --- /dev/null +++ b/static/images/zoom-in.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/static/images/zoom-out.svg b/static/images/zoom-out.svg new file mode 100644 index 0000000..c145c4d --- /dev/null +++ b/static/images/zoom-out.svg @@ -0,0 +1,12 @@ + + + + + + +