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 0000000..a5d899d
Binary files /dev/null and b/static/icon.png differ
diff --git a/static/images/buffering.svg b/static/images/buffering.svg
new file mode 100644
index 0000000..93e25b1
--- /dev/null
+++ b/static/images/buffering.svg
@@ -0,0 +1,27 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+