diff --git a/.gitignore b/.gitignore index 78c6538..32092cc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ test-retry test-playback test-handover + +test-target +tests/receiver-proxy.log diff --git a/HyperDRM.manifest.json b/HyperDRM.manifest.json index b86b875..ecc9812 100644 --- a/HyperDRM.manifest.json +++ b/HyperDRM.manifest.json @@ -1,15 +1,15 @@ { "id": "org.webosbrew.histogramhyperion", - "version": "0.7.2", + "version": "0.8.0", "type": "web", "title": "HyperDRM", - "appDescription": "Ambient lighting for webOS and Hyperion", + "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM", "rootRequired": true, - "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.7.2_arm.ipk", + "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.8.0_arm.ipk", "ipkHash": { - "sha256": "cc73ed6b93caf86b32df52a87b0160a8cdec34b79802fcc71fc1f0f04d5fcf74" + "sha256": "7cbb9c3a6e9dc5fe0c421cff6082807231c06a697420767761735f405855ae4a" }, - "ipkSize": 1169234 + "ipkSize": 1173194 } diff --git a/HyperDRM_0.7.2_arm.ipk b/HyperDRM_0.8.0_arm.ipk similarity index 89% rename from HyperDRM_0.7.2_arm.ipk rename to HyperDRM_0.8.0_arm.ipk index 2758c25..c92b0d9 100644 Binary files a/HyperDRM_0.7.2_arm.ipk and b/HyperDRM_0.8.0_arm.ipk differ diff --git a/README.md b/README.md index cc4c8a0..567de77 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HyperDRM -Ambient Light for webOS and Hyperion. An own webOS app and native ARM service that reads LG color histograms and sends spatial LED colors to a local Hyperion instance. +Ambient Light for webOS, Hyperion and HyperHDR. An own webOS app and native ARM service that reads LG color histograms and sends spatial LED colors to a configurable Hyperion or HyperHDR receiver.  @@ -11,11 +11,11 @@ Ambient Light for webOS and Hyperion. An own webOS app and native ARM service th - Automatic detection of top/bottom black bars on the calibrated LG G4. - Original saturation and brightness (both gains 1.0). - Start, stop, status and persistent boot autostart switch in the TV app. -- Local Hyperion JSON connection on `127.0.0.1:19444`, priority 149, up to 10 updates per second. +- Configurable IPv4/IPv6 receiver and JSON TCP port, optional token authentication, priority 149, up to 10 updates per second. ## Requirements and limits -Tested on a rooted LG G4 with webOS 24, Homebrew Channel and Hyperion 2.0.16 installed on the TV. The native service requires root elevation and the TV's existing `libvtcapture`, Luna, GLib and JSON-C libraries. Other TV models or firmware versions have not been validated. +Tested on a rooted LG G4 with webOS 24, Homebrew Channel and Hyperion 2.0.16 and HyperHDR 22.0.0 tested on the TV. The native service requires root elevation and the TV's existing `libvtcapture`, Luna, GLib and JSON-C libraries. Other TV models or firmware versions have not been validated. DRM playback color output was confirmed on the test TV. Support depends on LG's capture implementation; this does not guarantee support on other devices or apps. HyperDRM transmits colors, not captured video frames. The video preview in Hyperion therefore does not show the TV picture. @@ -25,7 +25,7 @@ Only the own service is modified. Optional Auto PicCap can start and stop the in ## Installation -The current package is [HyperDRM_0.7.2_arm.ipk](HyperDRM_0.7.2_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. +The current package is [HyperDRM_0.8.0_arm.ipk](HyperDRM_0.8.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. Elevate the installed own service through Homebrew Channel: @@ -35,7 +35,7 @@ luna-send -n 1 luna://org.webosbrew.hbchannel.service/elevateService '{"id":"org Perform elevation before first opening the app or calling its service. If the service was already started without root, elevation updates its launch permissions but does not restart the existing process: terminate only the own `histogram-service` process (verify its executable path first), then reopen the app or call start again. Closing the UI alone does not terminate the native service. -Open **HyperDRM** on the TV and select **Start**. Use the remote arrow buttons to move between controls and OK to activate the selected control. Magic Remote pointer input is also supported. Hyperion's JSON server must be enabled on port 19444. The existing Hyperion LED device/output configuration remains responsible for controlling the hardware. +Open **HyperDRM** on the TV and select **Start**. Use the remote arrow buttons to move between controls and OK to activate the selected control. Magic Remote pointer input is also supported. The receiver's JSON TCP server must be enabled at the IP/port saved in Receiver settings (default 127.0.0.1:19444). The existing Hyperion LED device/output configuration remains responsible for controlling the hardware. The internal app/service IDs remain `org.webosbrew.histogramhyperion` and `org.webosbrew.histogramhyperion.service` so existing installations and the autostart hook remain compatible. Hyperion displays the source as **HyperDRM**. After changing the LED layout, restart the service. @@ -99,3 +99,15 @@ The app displays the selected source and PicCap's Hyperion connection status. Hi ## Capture interruption recovery LG may revoke histogram capture during playback transitions. From 0.7.1, HyperDRM releases capture while leaving its last color update to expire normally after 1.5 seconds, then retries initialization after 200 ms and up to two additional 400 ms intervals. Successful recovery clears this rapid retry budget. Persistent failure returns to the existing five-second retry interval; manual Stop, PicCap handover and shutdown still explicitly clear the own color source. Status exposes `captureRecoveries` and `lastRecoveryMs` for diagnosis. + +## Hyperion and HyperHDR receiver settings + +Choose **Receiver: Hyperion/HyperHDR**, enter an IPv4 or IPv6 address and the JSON TCP port, then select **Save receiver**. These receivers use the same color, clear and serverinfo commands. Use the JSON port (usually 19444), not the FlatBuffer port (usually 19400) or web interface port. Numeric IP addresses are supported; hostnames and scoped IPv6 addresses are not currently accepted. Layout and LED count come from the selected receiver, including remote receivers. + +Settings are saved in `target.json` inside the own service container and loaded after service restart/boot. Saving valid settings switches a running session to the new receiver and clears the own priority at the old receiver. Invalid settings leave the current target intact. Saving while manually stopped does not start a session. Uninstalling removes the saved settings with the container. + +For a receiver requiring authentication, enter its API token. The token file is mode 0600; status only reports whether a token is saved. Leaving the input blank retains it; **Remove saved token** deletes it when saving. A token login happens before requesting the layout or initializing LG capture. JSON TCP traffic is unencrypted, so use a trusted network. Receiver authorization rules are unchanged. + +Auto PicCap remains available for the default local receiver at 127.0.0.1/::1:19444. For custom targets, automatic PicCap capture is paused and HyperDRM supplies colors for all playback; the PicCap preference remains saved. PicCap has a separate FlatBuffer endpoint managed by its own app, which HyperDRM does not rewrite. + +`native/test_target.c` verifies validation, token retention/removal, private persistence and TCP connection/refusal. `tests/receiver-proxy.py` is a test-only relay with synthetic token login and fragmented replies. Local HyperHDR 22.0.0 and a remote relay using a different IP/port were tested on the G4; the relay forwarded real color updates to HyperHDR without changing its configuration. diff --git a/app/app.js b/app/app.js index f1d7852..649e235 100644 --- a/app/app.js +++ b/app/app.js @@ -1,5 +1,6 @@ 'use strict'; var pending=[]; +var targetDirty=false,receiver='Hyperion'; function call(method,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; @@ -9,6 +10,12 @@ function call(method,done,data){ bridge.call('luna://org.webosbrew.histogramhyperion.service/'+method,JSON.stringify(data||{})); } function display(result){ + if(result.host && !targetDirty){ + document.getElementById('host').value=result.host;document.getElementById('port').value=result.port;receiver=result.receiver; + document.getElementById('receiver').textContent='Receiver: '+receiver; + } + document.getElementById('token').placeholder=result.tokenSet?'Saved token (leave blank to keep)':'No token saved'; + if(result.endpoint)document.getElementById('endpoint').textContent=result.receiver+' · '+result.endpoint+' · Priority 149 · 10 updates per second'; var handover=document.getElementById('handover');handover.disabled=!result.piccapInstalled; handover.setAttribute('aria-checked',String(!!result.piccapHandover)); handover.textContent=result.piccapInstalled?'Auto PicCap: '+(result.piccapHandover?'On':'Off'):'Auto PicCap: Not installed'; @@ -16,15 +23,16 @@ function display(result){ document.getElementById('error').textContent=result.errorText||result.error||''; document.getElementById('status').textContent=result.returnValue===false?'Service unavailable':(result.running?'Running':'Stopped')+' · '+(result.frames||0)+' acknowledged color updates'; if(result.activeSource==='PicCap'){ - document.getElementById('status').textContent='PicCap · '+(result.running?'Running':'Switching capture source')+' · '+(result.piccapConnected?'Hyperion connected':'Waiting for Hyperion'); + document.getElementById('status').textContent='PicCap · '+(result.running?'Running':'Switching capture source')+' · '+(result.piccapConnected?'Receiver connected':'Waiting for receiver'); document.getElementById('regions').textContent='';return; } if(result.piccapHandover)document.getElementById('status').textContent+=' · HyperDRM · '+result.playbackProtection; + if(result.piccapHandover && !result.piccapTargetCompatible)document.getElementById('status').textContent+=' · Auto PicCap paused for custom target'; if(result.colorValid)document.getElementById('status').textContent+=result.spatialOutput?' · '+result.ledCount+' LEDs from 16 color regions':' · Output RGB: '+result.outputColor.join(', '); 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.retryScheduled)document.getElementById('status').textContent+=' · Retrying startup every 5 seconds'; + 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=''; (result.regions||[]).forEach(function(rgb,index){var e=document.createElement('div');e.className='swatch';e.style.backgroundColor='rgb('+rgb.join(',')+')';e.title='Histogram '+index;box.appendChild(e);}); @@ -38,4 +46,13 @@ 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});}; 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'});}; -setupRemoteNavigation(['start','stop','refresh','autostart','handover'].map(function(id){return document.getElementById(id);}),document,window); +['host','port','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});}); +document.getElementById('receiver').onclick=function(){receiver=receiver==='Hyperion'?'HyperHDR':'Hyperion';this.textContent='Receiver: '+receiver;targetDirty=true;}; +document.getElementById('saveTarget').onclick=function(){ + var port=Number(document.getElementById('port').value),button=this; + if(!Number.isInteger(port)||port<1||port>65535){document.getElementById('error').textContent='JSON port must be an integer from 1 to 65535.';return;} + var settings={host:document.getElementById('host').value.trim(),port:port,receiver:receiver,clearToken:document.getElementById('clearToken').checked}; + var token=document.getElementById('token').value.trim();if(token)settings.token=token; + button.disabled=true;call('settings',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}targetDirty=false;document.getElementById('token').value='';document.getElementById('clearToken').checked=false;call('status',display);},settings); +}; +setupRemoteNavigation(['start','stop','refresh','autostart','handover','receiver','host','port','token','clearToken','saveTarget'].map(function(id){return document.getElementById(id);}),document,window); diff --git a/app/appinfo.json b/app/appinfo.json index cbf8a19..14dc44b 100644 --- a/app/appinfo.json +++ b/app/appinfo.json @@ -1 +1 @@ -{"id": "org.webosbrew.histogramhyperion", "version": "0.7.2", "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 and Hyperion", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} +{"id": "org.webosbrew.histogramhyperion", "version": "0.8.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"} diff --git a/app/index.html b/app/index.html index 36d2543..a43b398 100644 --- a/app/index.html +++ b/app/index.html @@ -1,9 +1,16 @@
HyperDRMAmbient lighting for webOS and Hyperion.
Checking service …
+ +
HyperDRMAmbient lighting for webOS, Hyperion and HyperHDR.
Checking service …
Use the arrow buttons to select a control and OK to activate it.
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.
-127.0.0.1:19444 · Priority 149 · 10 updates per second
+ +Priority 149 · 10 updates per second
The Hyperion layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. Restart the service after changing the LED layout.
diff --git a/app/remote.js b/app/remote.js index 5a4c93f..1af554f 100644 --- a/app/remote.js +++ b/app/remote.js @@ -11,6 +11,8 @@ 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'; + if(editing && (enter||code===37||code===39||event.key==='ArrowLeft'||event.key==='ArrowRight'))return; event.preventDefault(); var enabled=buttons.filter(function(button){return !button.disabled;}); if(!enabled.length)return; diff --git a/docs/publishing-review.md b/docs/publishing-review.md index 793d507..dd166bc 100644 --- a/docs/publishing-review.md +++ b/docs/publishing-review.md @@ -63,3 +63,11 @@ A live 0.7.0 observation reproduced the reported blackout while Auto PicCap was Version 0.7.1 preserves the last update's existing 1.5-second expiry during a revocation recovery, instead of explicitly clearing it. Initialization retries begin after 200 ms with two additional 400 ms retry intervals before returning to normal five-second pacing. This does not extend stale colors indefinitely. Explicit capture stops and PicCap handovers retain their clear behavior. Status records recovery count and elapsed initialization time. ARM retry and handover regression tests passed on the G4, as did remote navigation tests. Version 0.7.1 was installed and elevated on the TV. A later actual LG revocation recovered in 762 ms, including one refused initialization attempt before success. A 55-second Hyperion observation contained 125 color samples with no all-black output. These measurements do not guarantee that prolonged LG capture unavailability or actual black video scenes cannot darken the LEDs. + +## Configurable Hyperion/HyperHDR receiver (0.8.0) + +The app can save receiver type, numeric IPv4/IPv6 address, JSON TCP port and an optional API token. Targets and tokens are validated before replacing the configuration or interrupting capture. The own container stores `target.json` atomically with mode 0600; status omits the token value. TCP connection attempts are bounded to one second. Authentication and remote layout validation happen before acquiring LG capture. The receiver's own authorization and LED-device settings are not changed. Auto PicCap remains limited to the default local target; custom targets use histogram capture rather than sending PicCap data to an independently configured receiver. + +Native target tests passed on the G4, including invalid values, token retention/removal, private persistence and TCP success/refusal. Handover and input-field remote-navigation regression tests passed. HyperDRM 0.8.0 was installed and tested against the user's HyperHDR 22.0.0 on localhost:19444: acknowledged spatial color updates for its 120-LED layout. A temporary test relay on another LAN host at port 19445 exercised synthetic token login and fragmented replies, forwarding actual layout/color/clear commands to HyperHDR. Omitting the token preserved it; an invalid target was rejected without changing the active target. A verified own-service process restart loaded the saved HyperHDR target. The test relay was stopped and the target restored to localhost:19444 with no saved token. Network tests used IPv4; IPv6 parsing is covered by unit tests, but an IPv6 receiver was not available for an end-to-end test. Cold boot remains untested. + +The API compatibility was checked against official HyperHDR `sources/api/HyperAPI.cpp` and `sources/base/HyperHdrInstance.cpp`: serverinfo includes the LED layout, color accepts RGB arrays, and token authorization precedes API commands. diff --git a/native/handover.h b/native/handover.h index 23030b4..9fb9304 100644 --- a/native/handover.h +++ b/native/handover.h @@ -80,7 +80,7 @@ static bool handover_tick(void){ media_deadline=now+3000; error_free(&e);next_media_probe=now+1000; } - bool clear=handover_enabled && piccap_installed() && !retries.paused && !retries.blocked && + bool clear=handover_enabled && target_piccap_local(&target) && piccap_installed() && !retries.paused && !retries.blocked && protection==PLAYBACK_CLEAR && clear_samples>=2 && now-protection_at<3000 && now>=handover_cooldown; if(capture_source==SOURCE_HISTOGRAM && clear){ stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING; diff --git a/native/service.c b/native/service.c index 343dd1e..b98f8c8 100644 --- a/native/service.c +++ b/native/service.c @@ -24,6 +24,7 @@ #include "layout.h" #include "autostart.h" #include "retry.h" +#include "target.h" #define SERVICE "org.webosbrew.histogramhyperion.service" #define PRIORITY 149 typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError; @@ -113,6 +114,18 @@ static bool read_layout(void){ } bool ok=complete && layout_from_json(data,&led_blends,&led_count);free(data);interpolation_enabled=ok;return ok; } +static bool target_login(void){ + if(!target.token[0])return true; + char request[384],answer[4096];size_t used=0; + snprintf(request,sizeof(request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token); + if(send_line(request)!=0)return false; + while(used+1