diff --git a/index.html b/index.html index c56ea1e..ff72d25 100644 --- a/index.html +++ b/index.html @@ -34,15 +34,20 @@ - 10 Mbps + + 10 Mbps
-
+
+ Add Host
diff --git a/static/css/style.css b/static/css/style.css index da8f73c..0d48836 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,36 +1,3 @@ -@font-face { - font-family: 'Material Icons'; - font-style: normal; - font-weight: 400; - src: local('Material Icons'), - local('MaterialIcons-Regular'), - url('../fonts/MaterialIcons-Regular.woff') format('woff'), -} -.material-icons { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 24px; /* Preferred icon size */ - display: inline-block; - line-height: 1; - text-transform: none; - letter-spacing: normal; - word-wrap: normal; - white-space: nowrap; - direction: ltr; - - /* Support for all WebKit browsers. */ - -webkit-font-smoothing: antialiased; - /* Support for Safari and Chrome. */ - text-rendering: optimizeLegibility; - - /* Support for Firefox. */ - -moz-osx-font-smoothing: grayscale; - - /* Support for IE. */ - font-feature-settings: 'liga'; -} - .mdl-layout__header-row { color: #fff; } @@ -46,21 +13,14 @@ color:#fff; padding:25px; } -#addHostIcon { - width:64px; - height:64px; - margin:auto; - display:block; -} #backIcon { height: 32px; width: 32px; margin-left: -20px; padding-right: 20px; } -#addHostCell { - padding-top:3px; - padding-bottom:3px; +#backIcon:hover { + cursor: pointer; } .mdl-dialog { border: none; @@ -173,6 +133,12 @@ main { margin: 0; width: 100%; height: 100%; + -webkit-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34); + -moz-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34); + box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34); +} +.box-art:hover { + cursor: pointer; } .current-game { border: 3px solid green; @@ -184,3 +150,27 @@ main { color: #000; font-weight: bold; } +.host-cell { + display: inline-block; + width: 150px !important; + height: 150px !important; + background-color: #000; + -webkit-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34); + -moz-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34); + box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34); + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; +} +.host-cell:hover { + cursor: pointer; + color: #000 !important; +} +.host-cell:hover img { + -webkit-filter: invert(100%); + filter: invert(100%); +} +.host-cell img { + width: 80px;; + margin-bottom: 10px; +} diff --git a/static/js/index.js b/static/js/index.js index 0c9e117..ca06485 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,4 +1,7 @@ -var host = ""; +// CURRENT ISSUE: host is not being saved. or it may have not been saved, and my state is screwed up. +// if (given host not in hosts) hosts.append(given host); + +var _host = ""; var hosts = []; var pairingCert; var myUniqueid; @@ -13,6 +16,7 @@ function attachListeners() { $('#selectFramerate').on('change', saveFramerate); $('#bitrateSlider').on('input', updateBitrateField); // input occurs every notch you slide $('#bitrateSlider').on('change', saveBitrate); // change occurs once the mouse lets go. + $("#remoteAudioEnabledSwitch").on('click', saveRemoteAudio); $('#hostChosen').on('click', hostChosen); $('#addHostCell').on('click', addHost); $('#cancelAddHost').on('click', cancelAddHost); @@ -149,14 +153,19 @@ function pairTo(host, onSuccess, onFailure) { function hostChosen(sourceEvent) { if(sourceEvent && sourceEvent.srcElement) { - console.log('parsing host from grid element.'); - host = sourceEvent.srcElement.innerText; + if (sourceEvent.srcElement.innerText == "") { + console.log('user clicked image. we gotta hack to parse out the host.'); + host = sourceEvent.currentTarget.childNodes[1].textContent; + } else { + console.log('parsing host from grid element.'); + host = sourceEvent.srcElement.innerText; + } } api = new NvHTTP(host, myUniqueid); api.refreshServerInfo().then(function (ret) { if(!api.paired) { - pairTo(host, function(){ showApps(); }, function(){}); + pairTo(host, function(){ showApps(); saveHosts(); }, function(){}); } else { showApps(); } @@ -178,15 +187,15 @@ function cancelAddHost() { } function addHostToGrid(host) { - if(hosts.indexOf(host) < 0) { // we don't have this host in our list. add it, and save it. - var cell = document.createElement('div'); - cell.className += 'mdl-cell mdl-cell--3-col'; - cell.id = 'hostgrid-' + host; - cell.innerHTML = host; - $('#host-grid').append(cell); - cell.onclick = hostChosen; + var cell = document.createElement('div'); + cell.className += 'mdl-cell mdl-cell--3-col host-cell mdl-button mdl-js-button mdl-js-ripple-effect'; + cell.id = 'hostgrid-' + host; + cell.innerHTML = host; + $(cell).prepend($("", {src: "static/res/ic_desktop_windows_white_24px.svg"})); + $('#host-grid').append(cell); + cell.onclick = hostChosen; + if(hosts.indexOf(host) < 0) { hosts.push(host); - saveHosts(); } } @@ -196,6 +205,7 @@ function continueAddHost() { pairTo(inputHost, function() { addHostToGrid(inputHost); + saveHosts(); document.querySelector('#addHostDialog').close(); }, function() { @@ -218,6 +228,25 @@ function pairingPopupCanceled() { document.querySelector('#pairingDialog').close(); } +// puts the CSS style for current app on the app that's currently running +// and puts the CSS style for non-current app apps that aren't running +// this requires a hot-off-the-host `api`, and the appId we're going to stylize +// the function was made like this so that we can remove duplicated code, but +// not do N*N stylizations of the box art, or make the code not flow very well +function stylizeBoxArt(freshApi, appIdToStylize) { + if (freshApi.currentGame === appIdToStylize){ // stylize the currently running game + // destylize it, if it has the not-current-game style + if ($('#game-'+ appIdToStylize).hasClass("not-current-game")) $('#game-'+ appIdToStylize).removeClass("not-current-game"); + // add the current-game style + $('#game-'+ appIdToStylize).addClass("current-game"); + } else { + // destylize it, if it has the current-game style + if ($('#game-'+ appIdToStylize).hasClass("current-game")) $('#game-'+ appIdToStylize).removeClass("current-game"); + // add the not-current-game style + $('#game-'+ appIdToStylize).addClass('not-current-game'); + } +} + // show the app list function showApps() { if(!api || !api.paired) { // safety checking. shouldn't happen. @@ -231,21 +260,13 @@ function showApps() { api.getAppList().then(function (appList) { appList.forEach(function (app) { api.getBoxArt(app.id).then(function (resolvedPromise) { + // put the box art into the image holder var imageBlob = new Blob([resolvedPromise], {type: "image/png"}); $("#game-grid").append($("
", {html:$("", {src: URL.createObjectURL(imageBlob), id: 'game-'+app.id, name: app.title }), class: 'box-art mdl-cell mdl-cell--3-col'}).append($("", {html: app.title, class:"game-title"}))); $('#game-'+app.id).on('click', startGame); - if (api.currentGame === app.id){ // stylize the currently running game - // destylize it, if it has the not-current-game style - if ($('#game-'+ app.id).hasClass("not-current-game")) $('#game-'+ app.id).removeClass("not-current-game"); - // add the current-game style - $('#game-'+ app.id).addClass("current-game"); - } else { - // destylize it, if it has the current-game style - if ($('#game-'+ app.id).hasClass("current-game")) $('#game-'+ app.id).removeClass("current-game"); - // add the not-current-game style - $('#game-'+ app.id).addClass('not-current-game'); - } + // apply CSS stylization to indicate whether the app is active + stylizeBoxArt(api, app.id); }, function (failedPromise) { console.log('Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise) @@ -349,6 +370,8 @@ function startGame(sourceEvent) { }); } + remote_audio_enabled = $("#remoteAudioEnabledSwitch").parent().hasClass('is-checked') ? 1 : 0; + api.launchApp(appID, streamWidth + "x" + streamHeight + "x" + frameRate, 1, // Allow GFE to optimize game settings @@ -477,6 +500,14 @@ function saveBitrate() { storeData('bitrate', $('#bitrateSlider').val(), null); } +function saveRemoteAudio() { + console.log('saving remote audio state'); + // problem: when off, and the app is just starting, a tick to the switch doesn't always toggle it + // second problem: this callback is called immediately after clicking, so the HTML class `is-checked` isn't toggled yet + // to solve the second problem, we invert the boolean. This has worked in all cases I've tried, except for the first case + storeData('remoteAudio', !$("#remoteAudioEnabledSwitch").parent().hasClass('is-checked'), null); +} + function updateDefaultBitrate() { var res = $('#selectResolution').val(); var frameRate = $('#selectFramerate').val(); @@ -514,6 +545,16 @@ function onWindowLoad(){ chrome.storage.sync.get('resolution', function(previousValue) { $('#selectResolution').val(previousValue.resolution != null ? previousValue.resolution : '1280:720'); }); + chrome.storage.sync.get('remoteAudio', function(previousValue) { + if(previousValue.remoteAudio == null) { + document.querySelector('#remoteAudioEnabledSwitchContainer').MaterialSwitch.off(); + return; + } else if(previousValue.remoteAudio == false) { + document.querySelector('#remoteAudioEnabledSwitchContainer').MaterialSwitch.off(); + } else { + document.querySelector('#remoteAudioEnabledSwitchContainer').MaterialSwitch.on(); + } + }); // load stored framerate prefs chrome.storage.sync.get('frameRate', function(previousValue) { $('#selectFramerate').val(previousValue.frameRate != null ? previousValue.frameRate : '60'); @@ -522,12 +563,7 @@ function onWindowLoad(){ chrome.storage.sync.get('hosts', function(previousValue) { hosts = previousValue.hosts != null ? previousValue.hosts : []; for(var i = 0; i < hosts.length; i++) { // programmatically add each new host. - var cell = document.createElement('div'); - cell.className += 'mdl-cell mdl-cell--3-col'; - cell.id = 'hostgrid-' + hosts[i]; - cell.innerHTML = hosts[i]; - $('#host-grid').append(cell); - cell.onclick = hostChosen; + addHostToGrid(hosts[i]); } }); diff --git a/static/js/messages.js b/static/js/messages.js index cc223c8..8279a0d 100644 --- a/static/js/messages.js +++ b/static/js/messages.js @@ -22,10 +22,17 @@ function handleMessage(msg) { console.log(msg.data); if(msg.data === 'streamTerminated') { // if it's a recognized event, notify the appropriate function $('#loadingSpinner').css('display', 'none'); // This is a fallback for RTSP handshake failing, which immediately terminates the stream. - api.refreshServerInfo().then(function (ret) { + + api.refreshServerInfo().then(function (ret) { // refresh the serverinfo to acknowledge the currently running app + api.getAppList().then(function (appList) { + appList.forEach(function (app) { + stylizeBoxArt(api, app.id); // and reapply stylization to indicate what's currently running + }); + }); showApps(); chrome.app.window.current().restore(); }); + } else if(msg.data === 'Connection Established') { $('#loadingSpinner').css('display', 'none'); } else if(msg.data.indexOf('ProgressMsg: ') === 0) { diff --git a/static/js/utils.js b/static/js/utils.js index 27e430a..441ef5e 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -41,6 +41,12 @@ function NvHTTP(address, clientUid) { this._baseUrlHttps = 'https://' + address + ':47984'; this._baseUrlHttp = 'http://' + address + ':47989'; this._memCachedBoxArtArray = {}; + + this.serverUid = ''; + this.GfeVersion = ''; + this.supportedDisplayModes = {}; // key: y-resolution:x-resolution, value: array of supported framerates (only ever seen 30 or 60, here) + this.gputype = ''; + this.numofapps = 0; _self = this; }; @@ -78,7 +84,7 @@ NvHTTP.prototype = { _parseServerInfo: function(xmlStr) { $xml = _self._parseXML(xmlStr); $root = $xml.find('root'); - + if($root.attr("status_code") != 200) { return false; } @@ -86,7 +92,24 @@ NvHTTP.prototype = { _self.paired = $root.find("PairStatus").text().trim() == 1; _self.currentGame = parseInt($root.find("currentgame").text().trim(), 10); _self.serverMajorVersion = parseInt($root.find("appversion").text().trim().substring(0, 1), 10); - + _self.serverUid = $root.find('uniqueid').text().trim(); + _self.GfeVersion = $root.find('GfeVersion').text().trim(); + _self.gputype = $root.find('gputype').text().trim(); + _self.numofapps = $root.find('numofapps').text().trim(); + // now for the hard part: parsing the supported streaming + $root.find('DisplayMode').each(function(index, value) { // for each resolution:FPS object + var yres = parseInt($(value).find('Height').text()); + var xres = parseInt($(value).find('Width').text()); + var fps = parseInt($(value).find('RefreshRate').text()); + if(!_self.supportedDisplayModes[yres + ':' + xres]) { + _self.supportedDisplayModes[yres + ':' + xres] = []; + } + if(!_self.supportedDisplayModes[yres + ':' + xres].includes(fps)) { + _self.supportedDisplayModes[yres + ':' + xres].push(fps); + } + + }); + // GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer // has the semantics that its name would indicate. To contain the effects of this change as much // as possible, we'll force the current game to zero if the server isn't in a streaming session. diff --git a/static/res/ic_desktop_windows_white_24px.svg b/static/res/ic_desktop_windows_white_24px.svg new file mode 100644 index 0000000..685582f --- /dev/null +++ b/static/res/ic_desktop_windows_white_24px.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file