Private
Public Access
Release 0.11.0 with guided calibration, source transitions and root repair
This commit is contained in:
1 parent
e55d37772a
commit
f74a658097
33 files changed
+855
-75
No files matched your search
+46
-7
@@ -1,18 +1,31 @@
|
||||
'use strict';
|
||||
var pending=[];
|
||||
var targetDirty=false,receiver='Hyperion';
|
||||
var profileDirty=false,profile='auto';
|
||||
var profileDirty=false,profile='auto',advancedProfile=false;
|
||||
var profileNames={auto:'Auto',g4:'LG G4 (measured)',average:'Average (uniform)',custom:'Custom model'};
|
||||
function showProfile(){document.getElementById('profile').textContent='Profile: '+profileNames[profile];document.getElementById('customEditor').style.display=profile==='custom'?'block':'none';document.getElementById('customProfile').disabled=profile!=='custom';}
|
||||
function call(method,done,data){
|
||||
function showProfile(){
|
||||
document.getElementById('profile').textContent='Profile: '+profileNames[profile];
|
||||
var visible=profile==='custom' && advancedProfile;
|
||||
document.getElementById('customEditor').style.display=visible?'block':'none';
|
||||
document.getElementById('customProfile').disabled=!visible;
|
||||
document.getElementById('editProfile').disabled=profile!=='custom';
|
||||
document.getElementById('editProfile').textContent=visible?'Hide profile JSON':'Edit profile JSON';
|
||||
}
|
||||
|
||||
function call(method,done,data){callUri('luna://org.webosbrew.histogramhyperion.service/'+method,done,data);}
|
||||
function callUri(uri,done,data){
|
||||
if(typeof PalmServiceBridge==='undefined'){done({returnValue:false,errorText:'This app requires an LG TV with the native service installed.'});return;}
|
||||
var bridge=new PalmServiceBridge(),timer,finished=false;
|
||||
function finish(value){if(finished)return;finished=true;clearTimeout(timer);var i=pending.indexOf(bridge);if(i>=0)pending.splice(i,1);done(value);}
|
||||
bridge.onservicecallback=function(raw){try{finish(JSON.parse(raw));}catch(e){finish({returnValue:false,errorText:'Invalid service response'});}};
|
||||
pending.push(bridge);timer=setTimeout(function(){bridge.cancel();finish({returnValue:false,errorText:'The service is not responding. Check the installation and service log.'});},10000);
|
||||
bridge.call('luna://org.webosbrew.histogramhyperion.service/'+method,JSON.stringify(data||{}));
|
||||
pending.push(bridge);timer=setTimeout(function(){bridge.cancel();finish({returnValue:false,errorText:'The service is not responding. Check the installation and service log.'});},uri.indexOf('hbchannel')>=0?30000:10000);
|
||||
bridge.call(uri,JSON.stringify(data||{}));
|
||||
}
|
||||
function display(result){
|
||||
if(typeof result.uid==='number'){
|
||||
document.getElementById('grantRoot').style.display=result.uid===0?'none':'inline-block';
|
||||
document.getElementById('rootHint').style.display=result.uid===0?'none':'block';
|
||||
}
|
||||
if(result.calibrationProfile && !profileDirty){
|
||||
profile=result.calibrationProfile;showProfile();
|
||||
document.getElementById('customProfile').value=JSON.stringify({model:result.customModel||result.tvModel||'TV-MODEL',points:result.customPoints&&result.customPoints.length?result.customPoints:[]},null,2);
|
||||
@@ -46,6 +59,8 @@ function display(result){
|
||||
else if(result.returnValue!==false)document.getElementById('status').textContent+=' · No current color output';
|
||||
if(result.blackBarDetection && result.running)document.getElementById('status').textContent+=result.letterboxDetected?' · Top/bottom black bars detected':' · Full-screen capture';
|
||||
if(result.spatialInterpolation)document.getElementById('status').textContent+=' · Smooth color transitions';
|
||||
if(result.holdingColors)document.getElementById('status').textContent+=' · Holding last valid colors (missing data)';
|
||||
if(result.sourceFade)document.getElementById('status').textContent+=' · Blending capture sources';
|
||||
if(result.retryScheduled)document.getElementById('status').textContent+=' · Retrying receiver or capture startup';
|
||||
if(result.retryPaused)document.getElementById('status').textContent+=' · Automatic retry paused until Start';
|
||||
var box=document.getElementById('regions');box.textContent='';
|
||||
@@ -57,7 +72,30 @@ document.getElementById('refresh').onclick=function(){call('status',display);};
|
||||
call('status',display);setInterval(function(){if(!pending.length)call('status',display);},3000);
|
||||
document.getElementById('start').focus();
|
||||
|
||||
document.getElementById('autostart').onclick=function(){var button=this;var enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;call('autostart',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:enabled});};
|
||||
var repairingRoot=false,rootWaiters=[];
|
||||
function repairRoot(done){
|
||||
rootWaiters.push(done);if(repairingRoot)return;repairingRoot=true;document.getElementById('grantRoot').disabled=true;
|
||||
document.getElementById('error').textContent='Granting root access to HyperDRM...';
|
||||
function finish(error){repairingRoot=false;document.getElementById('grantRoot').disabled=false;if(error)document.getElementById('error').textContent=error;var waiters=rootWaiters;rootWaiters=[];waiters.forEach(function(callback){callback(!error);});}
|
||||
callUri('luna://org.webosbrew.hbchannel.service/elevateService',function(r){
|
||||
if(r.returnValue===false){finish('Homebrew Channel could not grant root access: '+(r.errorText||'Check Homebrew Channel installation.'));return;}
|
||||
call('restartService',function(r){
|
||||
if(r.returnValue===false){finish(r.errorText||'Could not restart the HyperDRM service.');return;}
|
||||
var attempts=0;
|
||||
function check(){call('status',function(s){if(s.uid===0){display(s);finish();}else if(++attempts<12)setTimeout(check,1000);else finish('The service is still missing root access. Elevate HyperDRM in Homebrew Channel and reopen the app.');});}
|
||||
setTimeout(check,1500);
|
||||
});
|
||||
},{id:'org.webosbrew.histogramhyperion.service'});
|
||||
}
|
||||
document.getElementById('grantRoot').onclick=function(){repairRoot(function(ok){if(ok)call('status',display);});};
|
||||
document.getElementById('autostart').onclick=function(){
|
||||
var button=this,enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;
|
||||
function save(){call('autostart',function(r){
|
||||
if(r.rootRequired){repairRoot(function(ok){if(ok)save();else button.disabled=false;});return;}
|
||||
button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);
|
||||
},{enabled:enabled});}
|
||||
save();
|
||||
};
|
||||
|
||||
document.getElementById('handover').onclick=function(){var button=this;button.disabled=true;call('handover',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:button.getAttribute('aria-checked')!=='true'});};
|
||||
['host','port','piccapPort','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});});
|
||||
@@ -75,4 +113,5 @@ document.getElementById('testConnection').onclick=function(){if(targetDirty){doc
|
||||
document.getElementById('profile').onclick=function(){var keys=['auto','g4','average','custom'];profile=keys[(keys.indexOf(profile)+1)%keys.length];profileDirty=true;showProfile();};
|
||||
document.getElementById('customProfile').addEventListener('input',function(){profileDirty=true;});
|
||||
document.getElementById('saveProfile').onclick=function(){var data={profile:profile},button=this;if(profile==='custom'){try{var parsed=JSON.parse(document.getElementById('customProfile').value);data.model=parsed.model;data.points=parsed.points;}catch(e){document.getElementById('error').textContent='Custom profile must be valid JSON.';return;}}button.disabled=true;call('calibration',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}profileDirty=false;call('status',display);},data);};
|
||||
setupRemoteNavigation(['start','stop','refresh','autostart','handover','receiver','host','port','piccapPort','token','clearToken','saveTarget','testConnection','profile','customProfile','saveProfile'].map(function(id){return document.getElementById(id);}),document,window);
|
||||
document.getElementById('editProfile').onclick=function(){advancedProfile=!advancedProfile;showProfile();if(advancedProfile)document.getElementById('customProfileField').focus();};
|
||||
setupRemoteNavigation(['overviewTab','receiverTab','calibrationTab','grantRoot','start','stop','refresh','autostart','handover','calibrationShortcut','receiver','hostField','portField','piccapPortField','tokenField','clearToken','saveTarget','testConnection','calibrate','validateCalibration','profile','saveProfile','editProfile','customProfileField','cancelCalibration'].map(function(id){return document.getElementById(id);}),document,window);
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.10.0", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.11.0", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* Pure measurement processing; the video uses 32 clockwise edge patches. */
|
||||
function calibrationFit(baseline,patches,model){
|
||||
if(!model || baseline.length!==16 || patches.length!==32)throw new Error('Incomplete calibration measurements.');
|
||||
var candidates=[],edges=[0,0,0,0];
|
||||
for(var region=0;region<16;region++){
|
||||
var responses=patches.map(function(p){if(!p || p.length!==16)throw new Error('Missing video measurements.');return Math.max(0,p[region]-baseline[region]);});
|
||||
var peak=Math.max.apply(Math,responses),best=responses.indexOf(peak);
|
||||
if(peak<8)continue;
|
||||
// Reject regions responding equally around the screen (global histograms).
|
||||
var strong=responses.filter(function(v){return v>=peak*.65;}).length;
|
||||
if(strong>10)continue;
|
||||
var distant=responses.some(function(v,i){var d=Math.min(Math.abs(i-best),32-Math.abs(i-best));return v>=peak*.65 && d>6;});
|
||||
if(distant)continue;
|
||||
var sum=0,weighted=0;
|
||||
for(var offset=-3;offset<=3;offset++){
|
||||
var index=(best+offset+32)%32,weight=Math.max(0,responses[index]-peak*.35);
|
||||
weighted+=offset*weight;sum+=weight;
|
||||
}
|
||||
var position=((best+.5+weighted/sum)/8+4)%4;
|
||||
candidates.push({position:position,region:region,score:peak/strong});
|
||||
}
|
||||
// Nested LG regions can measure the same edge position. Keep the most
|
||||
// localized response rather than making duplicate interpolation anchors.
|
||||
candidates.sort(function(a,b){return b.score-a.score || a.region-b.region;});
|
||||
var points=[];
|
||||
candidates.forEach(function(p){if(points.some(function(q){var d=Math.abs(p.position-q.position);return Math.min(d,4-d)<.06;}))return;points.push({position:p.position,region:p.region});edges[Math.floor(p.position)]++;});
|
||||
points.sort(function(a,b){return a.position-b.position;});
|
||||
if(points.length<4 || edges.some(function(n){return !n;}))throw new Error('No reliable spatial response on every edge. Keep the video full-screen; this TV may not expose local histogram regions. Your saved profile is unchanged.');
|
||||
return {model:model,points:points};
|
||||
}
|
||||
function calibrationLuma(regions){return regions.map(function(c){return (c[0]+c[1]+c[2])/3;});}
|
||||
function calibrationAverage(samples){
|
||||
if(samples.length<3)throw new Error('Not enough fresh histogram samples. Keep the app visible and repeat calibration.');
|
||||
return samples[0].map(function(_,r){return samples.reduce(function(sum,s){return sum+s[r];},0)/samples.length;});
|
||||
}
|
||||
Binary file not shown.
+58
-21
@@ -1,21 +1,58 @@
|
||||
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title>
|
||||
<style>body{background:#101827;color:#f1f5ff;font:28px sans-serif;margin:70px;max-width:1400px}h1{font-size:52px;display:flex;align-items:center;gap:24px}h1 img{border-radius:20px}button{font:inherit;padding:20px 32px;margin:12px 12px 12px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:12px}button:focus{outline:5px solid #66ddff}#regions{display:flex;flex-wrap:wrap;gap:8px}.swatch{width:64px;height:64px;border:1px solid #abc}p{line-height:1.5}#error{color:#ffbc99}small{color:#c0cbe0}fieldset{border:2px solid #739be0;border-radius:12px;padding:16px}label{display:inline-flex;flex-direction:column;font-size:24px;margin:8px 20px 8px 0}input,textarea{font:inherit;padding:12px;background:#223654;color:white;border:2px solid #739be0;border-radius:8px}input:focus,textarea:focus{outline:5px solid #66ddff}#host,#token{width:340px}#port,#piccapPort{width:140px}#clearToken{width:28px;height:28px}</style>
|
||||
<h1><img src="logo.png" alt="" width="104" height="104">HyperDRM</h1><p>Ambient lighting for webOS, Hyperion and HyperHDR.</p><p id="status">Checking service …</p><p id="error" role="alert"></p>
|
||||
<div role="group" aria-label="Service controls"><button id="start">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button>
|
||||
<button id="autostart" role="switch" aria-checked="false">Autostart: Off</button>
|
||||
<button id="handover" role="switch" aria-checked="false" disabled>Auto PicCap: Off</button></div><p><small>Use the arrow buttons to select a control and OK to activate it.</small></p><p><small>Autostart starts the service when the TV boots. Stop ends the current session. Auto PicCap uses installed PicCap for non-secure video and HyperDRM for protected or unknown playback. Keep PicCap's own autostart off. Stable unprotected playback selects PicCap; five seconds of connection loss restores HyperDRM, with a 30-second retry cooldown.</small></p>
|
||||
<fieldset><legend>Receiver settings</legend><button id="receiver" type="button">Receiver: Hyperion</button>
|
||||
<label>IP address <input id="host" type="text" maxlength="45" value="127.0.0.1" spellcheck="false"></label>
|
||||
<label>JSON port <input id="port" type="number" min="1" max="65535" value="19444"></label>
|
||||
<label>PicCap FlatBuffer port <input id="piccapPort" type="number" min="1" max="65535" value="19400"></label>
|
||||
<label>API token (optional) <input id="token" type="password" maxlength="256" autocomplete="off"></label>
|
||||
<label><input id="clearToken" type="checkbox"> Remove saved token</label><button id="saveTarget" type="button">Save receiver</button><button id="testConnection" type="button">Test connection</button><p id="testResult" role="status">Connection test: Not tested</p>
|
||||
<p><small>Use the receiver's JSON TCP port, usually 19444. IPv4 and IPv6 addresses are supported. Leave the token blank to keep a saved token. Save applies the new target to a running session. Use Up/Down to move between fields; Left/Right edit text.</small></p>
|
||||
<p id="piccapTargetHint"><small>Auto PicCap uses the same receiver IP with its FlatBuffer port, usually 19400. Enable the FlatBuffer server on the receiver. When switching to PicCap, its saved address, port and Unix socket setting are updated; other PicCap preferences are preserved. PicCap supports IPv4 targets; remote IPv6 uses HyperDRM. The API token applies to JSON only.</small></p></fieldset>
|
||||
<fieldset><legend>TV calibration</legend><button id="profile" type="button">Profile: Auto</button><button id="saveProfile" type="button">Save profile</button>
|
||||
<p id="profileStatus">Detecting TV model...</p><div id="customEditor" style="display:none"><label>Custom model profile JSON <textarea id="customProfile" rows="5" cols="68" maxlength="2048" disabled spellcheck="false"></textarea></label></div>
|
||||
<p><small>Auto selects an imported profile matching this exact TV model, or the measured LG G4 profile. Other models use uniform average colors until calibrated. Select Custom and paste a measured model profile to import it. G4 can also be selected manually. Profiles change spatial mapping; adjust colors in Hyperion/HyperHDR.</small></p></fieldset>
|
||||
<p><small id="endpoint">Priority 149 · 10 updates per second</small></p>
|
||||
<p><small id="diagnostics"></small></p>
|
||||
<div id="regions" aria-label="Histogram colors"></div><p><small>Adjust saturation, brightness and smoothing in your receiver. The receiver layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. HyperHDR receives a generated color plane, which its image mapping converts to LED colors. The LED layout is checked every five seconds and updated automatically.</small></p>
|
||||
<script src="remote.js"></script><script src="app.js"></script></html>
|
||||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title>
|
||||
<style>
|
||||
body{background:#101827;color:#f1f5ff;font:24px sans-serif;margin:36px 54px;max-width:1800px}
|
||||
h1{font-size:40px;display:flex;align-items:center;gap:18px;margin:0 0 16px}h1 img{border-radius:16px}
|
||||
button{font:inherit;padding:14px 24px;margin:8px 12px 8px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:10px}
|
||||
button:disabled{opacity:.45}button:focus,.field:focus{outline:4px solid #66ddff;outline-offset:3px}
|
||||
nav{border-bottom:1px solid #456185;padding-bottom:10px;margin-bottom:18px}nav button[aria-pressed="true"]{background:#126b85;border-color:#66ddff}
|
||||
.primary{background:#126b85;border-color:#66ddff}p{line-height:1.4;margin:12px 0}small{color:#c0cbe0;font-size:21px}
|
||||
#error{color:#ffbc99}fieldset{border:1px solid #456185;border-radius:12px;padding:18px 24px;margin:16px 0}
|
||||
.fields{display:flex;flex-wrap:wrap;gap:18px;margin:18px 0}.field{border:2px solid #739be0;border-radius:10px;padding:12px;background:#1b2b43;width:360px;cursor:pointer}
|
||||
.field label{display:flex;flex-direction:column;gap:10px;font-size:22px}.field input,.field textarea{font:inherit;color:white;background:transparent;border:0;outline:0;width:100%;padding:0;box-sizing:border-box}
|
||||
.field:focus-within{border-color:#66ddff}.field input:read-write,.field textarea:read-write{background:#223654;outline:2px solid #66ddff}
|
||||
.narrow{width:225px}.wide{width:95%;max-width:1200px}#clearToken{width:24px;height:24px}
|
||||
#regions{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0}.swatch{width:50px;height:50px;border:1px solid #abc;border-radius:6px}
|
||||
#diagnostics{display:block;margin-top:14px;max-width:1500px}#cancelCalibration{display:none}
|
||||
#cancelCalibration:not(:disabled){display:block;position:fixed;right:24px;top:24px;z-index:101;font-size:20px;opacity:.65}
|
||||
#calibrationVideo{display:none;position:fixed;inset:0;width:100vw;height:100vh;object-fit:fill;background:black;z-index:100}
|
||||
.steps{display:flex;gap:24px;flex-wrap:wrap}.steps p{flex:1;min-width:260px;color:#c0cbe0}.steps strong{display:block;color:#fff;margin-bottom:6px}
|
||||
</style>
|
||||
<h1><img src="logo.png" alt="" width="76" height="76">HyperDRM</h1>
|
||||
<nav aria-label="App sections"><button id="overviewTab" aria-pressed="true">Overview</button><button id="receiverTab" aria-pressed="false">Receiver</button><button id="calibrationTab" aria-pressed="false">Calibration</button></nav>
|
||||
<p id="status" role="status">Checking service …</p><p id="error" role="alert"></p>
|
||||
<section id="overviewPanel" aria-label="Overview">
|
||||
<button id="grantRoot" class="primary" style="display:none">Enable Root Access</button><p id="rootHint" style="display:none"><small>Homebrew Channel grants root to the HyperDRM service. The service then restarts with its updated permissions.</small></p>
|
||||
<div role="group" aria-label="Service controls"><button id="start" class="primary">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button></div>
|
||||
<fieldset><legend>Automatic operation</legend><button id="autostart" role="switch" aria-checked="false">Autostart: Off</button><button id="handover" role="switch" aria-checked="false" disabled>Auto PicCap: Off</button>
|
||||
<p><small>Autostart retries until the receiver is available. Auto PicCap uses PicCap for unprotected playback and HyperDRM for protected playback. Keep PicCap's own autostart off.</small></p></fieldset>
|
||||
<button id="calibrationShortcut">Auto Calibration</button><small>Measure a spatial profile for your TV.</small>
|
||||
<p><small id="endpoint">Priority 149 · 10 updates per second</small></p><div id="regions" aria-label="Histogram colors"></div>
|
||||
<p><small>Adjust brightness, saturation and normal smoothing in Hyperion or HyperHDR.</small></p><small id="diagnostics"></small>
|
||||
</section>
|
||||
<section id="receiverPanel" aria-label="Receiver settings" style="display:none">
|
||||
<fieldset><legend>Receiver connection</legend><button id="receiver">Receiver: Hyperion</button>
|
||||
<p><small>Select a field with the arrow buttons. Press OK or click to open the TV keyboard.</small></p>
|
||||
<div class="fields">
|
||||
<div id="hostField" class="field" tabindex="0" role="button" aria-label="IP address, press OK to edit"><label>IP address<input id="host" tabindex="-1" readonly type="text" maxlength="45" value="127.0.0.1" spellcheck="false"></label></div>
|
||||
<div id="portField" class="field narrow" tabindex="0" role="button" aria-label="JSON port, press OK to edit"><label>JSON port<input id="port" tabindex="-1" readonly type="number" min="1" max="65535" value="19444"></label></div>
|
||||
<div id="piccapPortField" class="field narrow" tabindex="0" role="button" aria-label="FlatBuffer port, press OK to edit"><label>PicCap FlatBuffer port<input id="piccapPort" tabindex="-1" readonly type="number" min="1" max="65535" value="19400"></label></div>
|
||||
<div id="tokenField" class="field" tabindex="0" role="button" aria-label="API token, press OK to edit"><label>API token (optional)<input id="token" tabindex="-1" readonly type="password" maxlength="256" autocomplete="off"></label></div>
|
||||
</div>
|
||||
<label><input id="clearToken" type="checkbox"> Remove saved token</label>
|
||||
<div><button id="saveTarget" class="primary">Save receiver</button><button id="testConnection">Test connection</button></div><p id="testResult" role="status">Connection test: Not tested</p>
|
||||
<p><small>The JSON TCP port is usually 19444. Leave the token blank to keep the saved token. Saving updates the current session.</small></p>
|
||||
<p id="piccapTargetHint"><small>Auto PicCap uses the same IP and its FlatBuffer port, usually 19400. Enable the receiver's FlatBuffer server. Remote IPv6 uses HyperDRM.</small></p></fieldset>
|
||||
</section>
|
||||
<section id="calibrationPanel" aria-label="TV calibration" style="display:none">
|
||||
<p id="profileStatus">Detecting TV model...</p>
|
||||
<fieldset><legend>Auto Calibration</legend>
|
||||
<div class="steps"><p><strong>1. Measure</strong>Play the built-in edge test. Keep the picture full-screen without bars.</p><p><strong>2. Save</strong>Review the result and save the measured profile.</p><p><strong>3. Check</strong>Validate all four colored screen corners against the LEDs.</p></div>
|
||||
<button id="calibrate" class="primary">Start Auto Calibration</button><button id="validateCalibration">Validate profile</button><p id="wizardStatus" role="status">Measurement takes about 70 seconds. Your saved profile is preserved until you select Save profile.</p>
|
||||
</fieldset>
|
||||
<fieldset><legend>Spatial profile</legend><button id="profile">Profile: Auto</button><button id="saveProfile" class="primary">Save profile</button><button id="editProfile" disabled>Edit profile JSON</button>
|
||||
<p><small>Auto uses your measured model profile or the built-in LG G4 profile. Other uncalibrated TVs use average colors.</small></p>
|
||||
<div id="customEditor" style="display:none"><div id="customProfileField" class="field wide" tabindex="0" role="button" aria-label="Custom profile JSON, press OK to edit"><label>Custom model profile JSON<textarea id="customProfile" tabindex="-1" readonly rows="4" maxlength="2048" disabled spellcheck="false"></textarea></label></div></div>
|
||||
</fieldset></section>
|
||||
<button id="cancelCalibration" disabled>Cancel calibration</button><video id="calibrationVideo" src="calibration.mp4" muted preload="auto" playsinline></video>
|
||||
<script src="remote.js"></script><script src="calibration.js"></script><script src="navigation.js"></script><script src="app.js"></script><script src="wizard.js"></script></html>
|
||||
@@ -0,0 +1,21 @@
|
||||
function showPanel(name,focus){
|
||||
['overview','receiver','calibration'].forEach(function(key){document.getElementById(key+'Panel').style.display=key===name?'block':'none';document.getElementById(key+'Tab').setAttribute('aria-pressed',String(key===name));});
|
||||
if(focus!==false)document.getElementById({overview:'start',receiver:'receiver',calibration:'calibrate'}[name]).focus();
|
||||
}
|
||||
['overview','receiver','calibration'].forEach(function(name){document.getElementById(name+'Tab').onclick=function(){showPanel(name);};});
|
||||
document.getElementById('calibrationShortcut').onclick=function(){showPanel('calibration');};
|
||||
function setupFormEditors(ids,doc,win){
|
||||
var pairs=ids.map(function(id){return {input:doc.getElementById(id),field:doc.getElementById(id+'Field')};});
|
||||
pairs.forEach(function(pair){
|
||||
var input=pair.input,field=pair.field;input.readOnly=true;
|
||||
field.onclick=function(){if(input.disabled || !input.readOnly)return;input.readOnly=false;input.focus();};
|
||||
input.addEventListener('blur',function(){input.readOnly=true;});
|
||||
input.addEventListener('mousedown',function(event){if(input.readOnly)event.preventDefault();});
|
||||
});
|
||||
function finishEditing(){
|
||||
pairs.forEach(function(pair){if(!pair.input.readOnly){pair.input.readOnly=true;pair.input.blur();pair.field.focus();}});
|
||||
}
|
||||
doc.addEventListener('keyboardStateChange',function(event){var detail=event.detail||{};if(detail.visibility===false || detail.isShowing===false)finishEditing();});
|
||||
doc.addEventListener('keydown',function(event){if((event.keyCode===461 || event.key==='Escape') && pairs.some(function(pair){return !pair.input.readOnly;})){event.preventDefault();finishEditing();}});
|
||||
}
|
||||
setupFormEditors(['host','port','piccapPort','token','customProfile'],document,window);
|
||||
+3
-3
@@ -11,10 +11,10 @@ function setupRemoteNavigation(buttons, doc, win) {
|
||||
event.key==='ArrowRight'||event.key==='ArrowDown'||code===39||code===40?1:0;
|
||||
var enter=event.key==='Enter'||code===13;
|
||||
if(!direction&&!enter)return;
|
||||
var editing=doc.activeElement && (doc.activeElement.tagName==='INPUT' && doc.activeElement.type!=='checkbox' || doc.activeElement.tagName==='TEXTAREA');
|
||||
if(editing && (enter||code===37||code===39||event.key==='ArrowLeft'||event.key==='ArrowRight'))return;
|
||||
var editing=doc.activeElement && !doc.activeElement.readOnly && (doc.activeElement.tagName==='INPUT' && doc.activeElement.type!=='checkbox' || doc.activeElement.tagName==='TEXTAREA');
|
||||
if(editing)return;
|
||||
event.preventDefault();
|
||||
var enabled=buttons.filter(function(button){return !button.disabled;});
|
||||
var enabled=buttons.filter(function(button){return !button.disabled && (typeof button.getClientRects!=='function' || button.getClientRects().length) && (!win.remoteNavigationScope || win.remoteNavigationScope.indexOf(button.id)>=0);});
|
||||
if(!enabled.length)return;
|
||||
var focused=doc.activeElement,index=enabled.indexOf(focused);
|
||||
if(direction){
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
var wizard=null;
|
||||
var video=document.getElementById('calibrationVideo');
|
||||
function wizardFinish(error){
|
||||
if(!wizard)return;
|
||||
clearInterval(wizard.timer);wizard=null;window.remoteNavigationScope=null;video.pause();video.style.display='none';
|
||||
if(typeof video.removeAttribute==='function'){video.removeAttribute('src');video.load();}
|
||||
document.getElementById('cancelCalibration').disabled=true;
|
||||
document.getElementById('calibrate').disabled=false;document.getElementById('validateCalibration').disabled=false;
|
||||
call('calibrationSession',function(r){if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:false});
|
||||
call('calibrationResult',function(){},{message:error||document.getElementById('wizardStatus').textContent});
|
||||
if(error)document.getElementById('wizardStatus').textContent=error;
|
||||
document.getElementById('calibrate').focus();
|
||||
}
|
||||
function wizardSample(){
|
||||
if(!wizard || wizard.busy)return;
|
||||
if(Date.now()-wizard.started>100000){wizardFinish('Calibration timed out. Saved profile unchanged.');return;}
|
||||
if(wizard.validation)return;
|
||||
var t=video.currentTime;
|
||||
if(t>=68){
|
||||
try{
|
||||
var measured=calibrationFit(calibrationAverage(wizard.baseline),wizard.patches.map(calibrationAverage),wizard.model);
|
||||
profile='custom';profileDirty=true;showProfile();document.getElementById('customProfile').value=JSON.stringify(measured,null,2);
|
||||
document.getElementById('wizardStatus').textContent='Measured '+measured.points.length+' edge regions. Select Save profile, then Validate profile to check all four colored edges. The previous saved profile is still active.';
|
||||
wizard.busy=true;
|
||||
call('calibrationDraft',function(r){if(wizard)wizardFinish(r.returnValue===false?r.errorText:undefined);},{profile:'custom',model:measured.model,points:measured.points});
|
||||
}catch(e){wizardFinish(e.message);}
|
||||
return;
|
||||
}
|
||||
var step=t<4?-1:Math.floor((t-4)/2),local=t<4?t:(t-4)%2;
|
||||
if(step<0 && (t<.8 || t>3.2) || step>=0 && (local<.65 || local>1.75))return;
|
||||
wizard.busy=true;
|
||||
call('status',function(s){
|
||||
if(!wizard)return;
|
||||
wizard.busy=false;
|
||||
if(s.returnValue===false || !s.calibrating){wizardFinish(s.errorText||'Calibration session ended. Saved profile unchanged.');return;}
|
||||
// Ignore duplicates, held output and replies crossing a stimulus boundary.
|
||||
var current=video.currentTime,nowStep=current<4?-1:Math.floor((current-4)/2);
|
||||
if(nowStep!==step || s.sampleSequence===wizard.sequence || s.sampleAgeMs<0 || s.sampleAgeMs>350 || !s.colorValid || !Array.isArray(s.regions) || s.regions.length!==16)return;
|
||||
wizard.sequence=s.sampleSequence;
|
||||
(step<0?wizard.baseline:wizard.patches[step]).push(calibrationLuma(s.regions));
|
||||
document.getElementById('wizardStatus').textContent=step<0?'Measuring black reference...':'Measuring edge patch '+(step+1)+' of 32...';
|
||||
});
|
||||
}
|
||||
function wizardStart(validation){
|
||||
if(wizard)return;
|
||||
if(typeof showPanel==='function')showPanel('calibration',false);
|
||||
if(targetDirty || profileDirty){document.getElementById('wizardStatus').textContent='Save receiver and profile edits before starting.';return;}
|
||||
document.getElementById('calibrate').disabled=true;document.getElementById('validateCalibration').disabled=true;
|
||||
document.getElementById('cancelCalibration').disabled=false;
|
||||
window.remoteNavigationScope=['cancelCalibration'];
|
||||
wizard={validation:validation,started:Date.now(),busy:false,sequence:-1,model:'',baseline:[],patches:Array.from({length:32},function(){return [];})};
|
||||
document.getElementById('wizardStatus').textContent='Starting full-screen capture. Keep the app visible; do not change zoom or aspect ratio.';
|
||||
call('calibrationSession',function(r){
|
||||
if(!wizard)return;if(r.returnValue===false){wizardFinish(r.errorText);return;}
|
||||
// Start the video plane before waiting for histogram data. Without another
|
||||
// running video, waiting first would prevent calibration from ever starting.
|
||||
video.src='calibration.mp4';if(typeof video.load==='function')video.load();
|
||||
video.style.display='block';var warmup=video.play();
|
||||
if(warmup && warmup.catch)warmup.catch(function(){wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');});
|
||||
call('start',function(){waitCapture();});
|
||||
},{enabled:true});
|
||||
function waitCapture(){
|
||||
if(!wizard)return;
|
||||
call('status',function(s){
|
||||
if(!wizard)return;
|
||||
if(s.running && s.activeSource!=='PicCap' && s.tvModel && s.colorValid){
|
||||
wizard.model=s.tvModel;video.currentTime=validation?68:0;video.style.display='block';
|
||||
document.getElementById('cancelCalibration').focus();
|
||||
var playing=video.play();if(playing && playing.catch)playing.catch(function(){wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');});
|
||||
wizard.timer=setInterval(wizardSample,250);
|
||||
}else if(Date.now()-wizard.started<15000)setTimeout(waitCapture,500);
|
||||
else wizardFinish('No fresh full-screen histogram capture or TV model. Saved profile unchanged.');
|
||||
});
|
||||
}
|
||||
}
|
||||
document.getElementById('calibrate').onclick=function(){wizardStart(false);};
|
||||
document.getElementById('validateCalibration').onclick=function(){wizardStart(true);};
|
||||
document.getElementById('cancelCalibration').onclick=function(){wizardFinish('Calibration cancelled. Saved profile unchanged.');};
|
||||
video.addEventListener('ended',function(){if(wizard && wizard.validation)wizardFinish('Validation finished. Top-left red, top-right green, bottom-right blue, bottom-left yellow. Check matching LED positions.');else wizardSample();});
|
||||
video.addEventListener('error',function(){wizardFinish('Calibration video playback failed. Saved profile unchanged.');});
|
||||
document.addEventListener('keydown',function(e){if(wizard && (e.keyCode===461 || e.key==='Escape')){e.preventDefault();wizardFinish('Calibration cancelled. Saved profile unchanged.');}});
|
||||
window.addEventListener('pagehide',function(){wizardFinish();});
|
||||
// An explicit launch action also supports starting the TV's guided test remotely.
|
||||
function wizardLaunch(raw){try{var params=typeof raw==='string'?JSON.parse(raw):raw;if(params && params.action==='calibrate')wizardStart(false);else if(params && params.action==='repairRoot'){showPanel('overview');repairRoot(function(ok){if(ok)call('status',display);});}}catch(e){document.getElementById('wizardStatus').textContent='Invalid launch parameters.';}}
|
||||
if(typeof PalmSystem!=='undefined' && PalmSystem.launchParams)setTimeout(function(){wizardLaunch(PalmSystem.launchParams);},1000);
|
||||
window.addEventListener('webOSRelaunch',function(){if(typeof PalmSystem!=='undefined')wizardLaunch(PalmSystem.launchParams);});
|
||||
Reference in new issue
Block a user