Add optional automatic PicCap handover for non-secure playback

This commit is contained in:
matze19999 committed 2026-10-06 15:59:14 +02:00
1 parent e720045bb1
commit a475bd4a60
16 files changed
+285 -16

No files matched your search

+3
View File
@@ -25,3 +25,6 @@ service/led-interpolation.txt
test-autostart
test-retry
test-playback
test-handover
+4 -4
View File
@@ -1,15 +1,15 @@
{
"id": "org.webosbrew.histogramhyperion",
"version": "0.6.4",
"version": "0.7.0",
"type": "web",
"title": "HyperDRM",
"appDescription": "Ambient lighting for webOS and Hyperion",
"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.6.4_arm.ipk",
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.7.0_arm.ipk",
"ipkHash": {
"sha256": "53762efd7e8d915d570fed96dbd1844d18ca825c21a9bc53f3aa28fea0f823ae"
"sha256": "0668d8abab98d81d51368863c7bc920f85f6107ec3872ea1810a332c2ddc87f5"
},
"ipkSize": 1164794
"ipkSize": 1169660
}
Binary file not shown.
+12 -2
View File
@@ -21,11 +21,11 @@ DRM playback color output was confirmed on the test TV. Support depends on LG's
The 16 histogram samples are independent measurements; interpolation produces smoother transitions without adding independently captured detail. Sample positions are approximate G4 calibration values. Top/bottom bar detection is supported; side bars, arbitrary bar thickness and HDR color calibration remain limited.
Only the own service is modified. No Hue, LG library, driver or firmware binaries are included. For histogram initialization, the service temporarily creates LG's developer marker and uses a guardian process to remove its own marker afterward; an existing marker is preserved.
Only the own service is modified. Optional Auto PicCap can start and stop the installed PicCap service through its existing API. No Hue, LG library, driver or firmware binaries are included. For histogram initialization, the service temporarily creates LG's developer marker and uses a guardian process to remove its own marker afterward; an existing marker is preserved.
## Installation
The current package is [HyperDRM_0.6.4_arm.ipk](HyperDRM_0.6.4_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
The current package is [HyperDRM_0.7.0_arm.ipk](HyperDRM_0.7.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
Elevate the installed own service through Homebrew Channel:
@@ -85,3 +85,13 @@ The logo master is `app/logo.png`, with 80 px and 130 px installation icons.
See [the publishing review](docs/publishing-review.md) for the requirements audit and unresolved submission conditions. This repository is not a claim that webOSbrew has approved the app.
Remote navigation regression tests: `node tests/remote-navigation.test.js`.
## Optional automatic PicCap handover
Version 0.7.0 adds **Auto PicCap**, disabled by default. The switch is available when PicCap is installed. Keep PicCap's own autostart disabled and configure its output for your Hyperion instance. HyperDRM does not change PicCap's configuration or autostart preference.
When the session is started, the controller polls LG's active media resources. Two consecutive foreground media-decoder samples without a Secure Video Path resource allow PicCap to start. HyperDRM releases its histogram capture and clears priority 149 first, allowing PicCap's configured input priority to take over. Any `SVP_CPB` resource selects protected playback; missing, stale, malformed, idle or HDMI state selects the conservative HyperDRM fallback. PicCap must confirm that it has stopped before histogram capture resumes. Detection is specific to the tested G4 resource model, not a universal DRM-state API.
The switch is saved in the own service container and applies after reboot when Autostart is enabled. Stop pauses both managed capture paths until Start. Turning Auto PicCap off stops a PicCap session started by this controller and restores HyperDRM. Avoid starting another capture service independently while automatic handover is active.
The app displays the selected source and PicCap's Hyperion connection status. Histogram region swatches are hidden while PicCap supplies the colors. Playback classification and isolated controller tests are in `native/test_playback.c` and `native/test_handover.c`; link both with `-ldl`.
+10 -1
View File
@@ -9,9 +9,17 @@ function call(method,done,data){
bridge.call('luna://org.webosbrew.histogramhyperion.service/'+method,JSON.stringify(data||{}));
}
function display(result){
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';
if(typeof result.autostart==='boolean'){var toggle=document.getElementById('autostart');toggle.setAttribute('aria-checked',String(result.autostart));toggle.textContent='Autostart: '+(result.autostart?'On':'Off');}
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('regions').textContent='';return;
}
if(result.piccapHandover)document.getElementById('status').textContent+=' · HyperDRM · '+result.playbackProtection;
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';
@@ -29,4 +37,5 @@ 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});};
setupRemoteNavigation(['start','stop','refresh','autostart'].map(function(id){return document.getElementById(id);}),document,window);
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);
+1 -1
View File
@@ -1 +1 @@
{"id": "org.webosbrew.histogramhyperion", "version": "0.6.4", "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.7.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 and Hyperion", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
+2 -1
View File
@@ -2,7 +2,8 @@
<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}</style>
<h1><img src="logo.png" alt="" width="104" height="104">HyperDRM</h1><p>Ambient lighting for webOS and Hyperion.</p><p id="status">Checking service &hellip;</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></div><p><small>Use the arrow buttons to select a control and OK to activate it.</small></p><p><small>Starts the service when the TV boots. Stop ends only the current session.</small></p>
<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.</small></p>
<p><small>127.0.0.1:19444 &middot; Priority 149 &middot; 10 updates per second</small></p>
<div id="regions" aria-label="Histogram colors"></div><p><small>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.</small></p>
<script src="remote.js"></script><script src="app.js"></script></html>
+6
View File
@@ -49,3 +49,9 @@ With autostart enabled, the native service retries startup every five seconds wi
ARM retry-state tests on the G4 passed 10000 delayed-start attempts, five-second pacing, manual Stop, disabling autostart, the cleanup safety block, bounded non-autostart source recovery and disconnect retry. Live tests temporarily stopped and restored Hyperion without changing configuration; automatic retry continued beyond six attempts (observed counts 10 and 11). Manual Stop left retryPaused:true and retryScheduled:false after Hyperion was restored.
A second live disconnect test confirmed retryScheduled:true while Hyperion was unavailable, followed by running:true and colorValid:true automatically within the six-second observation window after Hyperion restarted, without a new HyperDRM Start command.
## Optional PicCap handover (0.7.0)
The default-off Auto PicCap switch stores its preference in the own service container. If enabled, it uses the installed PicCap service's existing start/stop/status methods; it does not rewrite PicCap settings or its autostart hook. Only foreground media decoder resources without `SVP_CPB` are eligible, after two samples. Any secure resource or unknown/stale state restores the histogram path. VT capture is released and HyperDRM priority cleared before starting PicCap; stopped status is required before histogram capture resumes. Manual Stop pauses both managed paths. Shutdown attempts to stop a managed PicCap session before unregistering; a forced kill or unresponsive external service cannot guarantee this cleanup.
On the G4, playback-classification and isolated controller tests passed: secure/clear/unknown states, mixed pipelines, malformed records, two-sample gating, exclusive transitions, manual Stop, disabling handover and timeout cancellation. The fifth control passed remote-navigation regression tests. Version 0.7.0 was installed and elevated; with Disney+ playing, enabling handover reported protected playback, maintained histogram color output and left PicCap stopped. The test preference was restored to off. Live transitions through unprotected playback remain to be verified with the user's test video.
+96
View File
@@ -0,0 +1,96 @@
#ifndef HYPERDRM_HANDOVER_H
#define HYPERDRM_HANDOVER_H
#include "playback.h"
#ifndef PICCAP_BINARY
#define PICCAP_BINARY "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
#endif
#define HANDOVER_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/handover.json"
static PlaybackJson playback_json;
static bool handover_enabled,piccap_owned,piccap_busy,media_busy,piccap_connected;
static PlaybackProtection protection;
static uint64_t protection_at,next_media_probe,next_piccap_probe,handover_cooldown,piccap_action_at;
typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource;
static CaptureSource capture_source;
static void *service_handle;
static bool (*call_once)(void*,const char*,const char*,Callback,void*,unsigned long*,LSError*);
static bool (*cancel_call)(void*,unsigned long,LSError*);
static unsigned long piccap_token,media_token;
static uint64_t piccap_deadline,media_deadline;
static unsigned clear_samples;
static bool piccap_request(int command);
static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;}
static bool handover_save(bool enabled){
const char *data=enabled?"{\"piccapHandover\":true}\n":"{\"piccapHandover\":false}\n";
int fd=open(HANDOVER_PREF ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
ssize_t n=write(fd,data,strlen(data));bool ok=n==(ssize_t)strlen(data) && fsync(fd)==0;
if(close(fd)!=0)ok=false;
if(ok)ok=rename(HANDOVER_PREF ".tmp",HANDOVER_PREF)==0;
if(!ok)unlink(HANDOVER_PREF ".tmp");
return ok;
}
static void handover_load(void){
int fd=open(HANDOVER_PREF,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return;
char text[128];ssize_t n=read(fd,text,sizeof(text)-1);close(fd);
if(n>0){text[n]=0;void *root=playback_json.parse(text);handover_enabled=playback_true(&playback_json,root,"piccapHandover");if(root)playback_json.put(root);}
}
static bool media_reply(void *h,void *m,void *ctx){
(void)h;(void)ctx;media_busy=false;media_token=0;
protection=playback_classify(&playback_json,payload(m));protection_at=milliseconds();
if(protection==PLAYBACK_CLEAR){if(clear_samples<2)clear_samples++;}else clear_samples=0;
return true;
}
/* Commands: 0 status, 1 start, 2 stop. Confirm stopped status before VT reuse. */
static bool piccap_reply(void *h,void *m,void *ctx){
(void)h;int command=(int)(intptr_t)ctx;piccap_busy=false;piccap_token=0;
void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue");
if(command==0){
void *running=playback_field(&playback_json,root,"isRunning");
if(ok && running && playback_json.type(running)==1){
bool active=playback_json.boolean(running);
piccap_connected=playback_true(&playback_json,root,"connected");
if(active){if(capture_source==SOURCE_PICCAP_STARTING)capture_source=SOURCE_PICCAP;}
else if(capture_source!=SOURCE_PICCAP_STARTING || milliseconds()-piccap_action_at>=2000){
if(capture_source!=SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;
if(!retries.paused)retries.due=milliseconds();
}
}else if(!piccap_installed()){piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;}
}else if(command==1 && !ok){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=milliseconds()+10000;}
if(root)playback_json.put(root);
next_piccap_probe=milliseconds()+500;
if(command==2)piccap_request(0);
return true;
}
static bool piccap_request(int command){
if(piccap_busy)return false;
const char *uri=command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
LSError e;error_init(&e);
piccap_busy=call_once(service_handle,uri,"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
piccap_deadline=milliseconds()+3000;
error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy;
}
/* Return true while PicCap owns capture or a stop confirmation is pending. */
static bool handover_tick(void){
uint64_t now=milliseconds();
if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;}
if(piccap_busy && now>=piccap_deadline){LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
if(handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && !media_busy && now>=next_media_probe){
LSError e;error_init(&e);
media_busy=call_once(service_handle,"luna://com.webos.media/getActivePipelines","{}",media_reply,NULL,&media_token,&e);
media_deadline=now+3000;
error_free(&e);next_media_probe=now+1000;
}
bool clear=handover_enabled && 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;
if(!piccap_request(1)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+10000;}
}
if(piccap_owned && !clear && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
if(piccap_owned && !piccap_busy && now>=next_piccap_probe){
if(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0);
next_piccap_probe=now+1000;
}
return piccap_owned;
}
#endif
+47
View File
@@ -0,0 +1,47 @@
#ifndef HYPERDRM_PLAYBACK_H
#define HYPERDRM_PLAYBACK_H
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <dlfcn.h>
typedef struct {
void *lib;void *(*parse)(const char*);int (*get)(const void*,const char*,void**);
int (*type)(const void*);size_t (*length)(const void*);void *(*at)(const void*,size_t);
const char *(*string)(const void*);int (*boolean)(const void*);int (*put)(void*);
} PlaybackJson;
static inline bool playback_json_init(PlaybackJson *j){
j->lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!j->lib)return false;
j->parse=dlsym(j->lib,"json_tokener_parse");j->get=dlsym(j->lib,"json_object_object_get_ex");
j->type=dlsym(j->lib,"json_object_get_type");j->length=dlsym(j->lib,"json_object_array_length");
j->at=dlsym(j->lib,"json_object_array_get_idx");j->string=dlsym(j->lib,"json_object_get_string");
j->boolean=dlsym(j->lib,"json_object_get_boolean");j->put=dlsym(j->lib,"json_object_put");
if(!j->parse||!j->get||!j->type||!j->length||!j->at||!j->string||!j->boolean||!j->put){dlclose(j->lib);j->lib=NULL;return false;}return true;
}
static inline void *playback_field(PlaybackJson *j,void *o,const char *key){void *v=NULL;return o && j->type(o)==4 && j->get(o,key,&v)?v:NULL;}
static inline bool playback_true(PlaybackJson *j,void *o,const char *key){void *v=playback_field(j,o,key);return v && j->type(v)==1 && j->boolean(v);}
static inline bool playback_is(PlaybackJson *j,void *o,const char *key,const char *value){void *v=playback_field(j,o,key);return v && j->type(v)==6 && strcmp(j->string(v),value)==0;}
typedef enum {PLAYBACK_UNKNOWN,PLAYBACK_CLEAR,PLAYBACK_PROTECTED} PlaybackProtection;
/* G4 resource-state classification, not black-frame or app-name guessing.
* Any active Secure Video Path wins. Only a foreground media decoder without
* a secure resource is eligible; HDMI, idle and malformed replies are unknown. */
static inline PlaybackProtection playback_classify(PlaybackJson *j,const char *text){
void *root=j->parse(text);if(!root)return PLAYBACK_UNKNOWN;
bool clear=false,invalid=j->type(root)!=5,secure=false;
size_t n=invalid?0:j->length(root);
for(size_t i=0;i<n;i++){
void *p=j->at(root,i);if(!p || j->type(p)!=4){invalid=true;continue;}
void *resources=playback_field(j,p,"resource");if(!resources)resources=playback_field(j,p,"resources");
if(!resources || j->type(resources)!=5){invalid=true;continue;}
bool decoder=false;
for(size_t k=0;k<j->length(resources);k++){
void *resource=j->at(resources,k);void *name=playback_field(j,resource,"resource");
if(!name || j->type(name)!=6){invalid=true;continue;}
const char *s=j->string(name);
if(strcmp(s,"SVP_CPB")==0)secure=true;
if(strcmp(s,"VDEC")==0)decoder=true;
}
if(decoder && playback_true(j,p,"is_foreground") && playback_is(j,p,"type","media"))clear=true;
}
j->put(root);return secure?PLAYBACK_PROTECTED:clear&&!invalid?PLAYBACK_CLEAR:PLAYBACK_UNKNOWN;
}
#endif
+29 -4
View File
@@ -118,6 +118,7 @@ static void stop_capture(void){
if(capture){vt_finish(capture);capture=NULL;}
}
static bool respond(void *h,void *m,const char *s){if(!h||!m)return true;LSError e;error_init(&e);bool ok=reply(h,m,s,&e);error_free(&e);return ok;}
#include "handover.h"
static bool start_failure(void *h,void *m,const char *message){
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
char json[512];snprintf(json,sizeof(json),"{\"returnValue\":false,\"errorText\":\"%s\",\"retryScheduled\":%s,\"retryBlocked\":%s}",message,retries.due?"true":"false",retries.blocked?"true":"false");
@@ -127,6 +128,7 @@ static bool start(void *h,void *m,void *ctx){
(void)ctx;const char *p=m?payload(m):NULL;
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; endpoint is localhost:19444\"}");
if(m)retry_reset(&retries);
if(piccap_owned)return respond(h,m,"{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}");
frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0;
output=socket(AF_INET,SOCK_STREAM,0);
@@ -155,17 +157,29 @@ static bool autostart(void *h,void *m,void *ctx){
retry_update(&retries,enabled,capture!=NULL,milliseconds());
return respond(h,m,enabled?"{\"returnValue\":true,\"autostart\":true}":"{\"returnValue\":true,\"autostart\":false}");
}
static bool handover(void *h,void *m,void *ctx){
(void)ctx;const char *p=payload(m);bool enabled;
if(strcmp(p,"{\"enabled\":true}")==0)enabled=true;
else if(strcmp(p,"{\"enabled\":false}")==0)enabled=false;
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
if(!playback_json.lib || geteuid()!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Root service and JSON-C are required\"}");
if(enabled && !piccap_installed())return respond(h,m,"{\"returnValue\":false,\"errorText\":\"PicCap is not installed\"}");
if(!handover_save(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save PicCap handover preference\"}");
handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;next_media_probe=0;
return respond(h,m,"{\"returnValue\":true}");
}
static bool status(void *h,void *m,void *ctx){
(void)ctx;char json[4096],safe[512];size_t n=0;
for(size_t i=0;last_error[i] && n+2<sizeof(safe);i++){char c=last_error[i];if(c=='"'||c=='\\')safe[n++]='\\';if((unsigned char)c>=32)safe[n++]=c;}safe[n]=0;
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.4\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",capture?"true":"false",color_valid?"true":"false",frames,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.7.0\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]);
used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false");
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]);
snprintf(json+used,sizeof(json)-used,"],\"preset\":%u,\"saturationGain\":2.0,\"brightnessGain\":1.33,\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,\"letterboxDetected\":%s}",capture_preset,interpolation_enabled?"true":"false",boot_enabled()?"true":"false",retries.due?"true":"false",retries.paused?"true":"false",retries.blocked?"true":"false",retries.attempts,capture&&letterbox_mode?"true":"false");return respond(h,m,json);
snprintf(json+used,sizeof(json)-used,"],\"preset\":%u,\"saturationGain\":2.0,\"brightnessGain\":1.33,\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}",capture_preset,interpolation_enabled?"true":"false",boot_enabled()?"true":"false",retries.due?"true":"false",retries.paused?"true":"false",retries.blocked?"true":"false",retries.attempts,piccap_installed()?"true":"false",handover_enabled?"true":"false",piccap_connected?"true":"false",piccap_owned?"PicCap":"HyperDRM",protection==PLAYBACK_PROTECTED?"protected":protection==PLAYBACK_CLEAR?"non-secure decoder":"unknown",capture&&letterbox_mode?"true":"false");return respond(h,m,json);
}
static int tick(void *unused){
(void)unused;
if(handover_tick())return 1;
if(!capture){
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
if(retry_take(&retries,milliseconds())){
@@ -231,6 +245,9 @@ int main(void){
void *vt=dlopen("libvtcapture.so.1",RTLD_NOW),*ls=dlopen("libluna-service2.so.3",RTLD_NOW),*glib=dlopen("libglib-2.0.so.0",RTLD_NOW);
if(!vt||!ls||!glib){fprintf(stderr,"Missing LG library: %s\n",dlerror());return 2;}
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
call_once=symbol(ls,"LSCallOneReply");
cancel_call=symbol(ls,"LSCallCancel");
if(playback_json_init(&playback_json))handover_load();
reply=symbol(ls,"LSMessageReply");payload=symbol(ls,"LSMessageGetPayload");error_init=symbol(ls,"LSErrorInit");error_free=symbol(ls,"LSErrorFree");
bool (*reg)(const char*,void**,LSError*)=symbol(ls,"LSRegister");
bool (*category)(void*,const char*,Method*,void*,void*,LSError*)=symbol(ls,"LSRegisterCategory");
@@ -241,9 +258,17 @@ int main(void){
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{NULL,NULL,0}};
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{NULL,NULL,0}};
if(!reg(SERVICE,&handle,&e)||!category(handle,"/",methods,NULL,NULL,&e)||!attach(handle,loop,&e)){fprintf(stderr,"Luna registration failed: %s\n",e.message?e.message:"unknown");error_free(&e);return 3;}
service_handle=handle;
if(!boot_enabled())retry_pause(&retries);
timer(100,tick,NULL);
while(!quitting){iterate(NULL,0);usleep(10000);}
stop_capture();unreg(handle,&e);error_free(&e);loop_unref(loop);return 0;
stop_capture();
if(piccap_owned){
handover_enabled=false;retry_pause(&retries);
uint64_t deadline=milliseconds()+4000;
while(piccap_owned && milliseconds()<deadline){handover_tick();iterate(NULL,0);usleep(10000);}
}
unreg(handle,&e);error_free(&e);loop_unref(loop);return 0;
}
+52
View File
@@ -0,0 +1,52 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include "retry.h"
typedef struct {int unused;} LSError;
typedef bool (*Callback)(void*,void*,void*);
static RetryState retries;
static uint64_t clock_ms=10000;
static unsigned stopped,called,canceled;
static char last_uri[128];
static uint64_t milliseconds(void){return clock_ms;}
static void stop_capture(void){stopped++;}
static const char *payload(void *m){return m;}
static void error_init(LSError *e){(void)e;}
static void error_free(LSError *e){(void)e;}
#define PICCAP_BINARY "/bin/sh"
#include "handover.h"
static bool mock_call(void *h,const char *uri,const char *data,Callback cb,void *ctx,unsigned long *token,LSError *e){
(void)h;(void)data;(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
}
static bool mock_cancel(void *h,unsigned long token,LSError *e){(void)h;(void)token;(void)e;canceled++;return true;}
static void answer(int cmd,bool running){
char json[128];snprintf(json,sizeof(json),"{\"returnValue\":true,\"isRunning\":%s,\"connected\":true}",running?"true":"false");
piccap_reply(NULL,json,(void*)(intptr_t)cmd);
}
int main(void){
(void)handover_save;(void)handover_load;
(void)retry_failure;(void)retry_take;(void)retry_update;
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
handover_enabled=true;protection=PLAYBACK_PROTECTED;protection_at=clock_ms;next_media_probe=UINT64_MAX;
assert(!handover_tick());assert(called==0 && stopped==0);
protection=PLAYBACK_CLEAR;clear_samples=1;assert(!handover_tick());assert(called==0);
clear_samples=2;assert(handover_tick());assert(stopped==1 && strstr(last_uri,"/start"));
answer(1,false);clock_ms+=600;assert(handover_tick());assert(strstr(last_uri,"/status"));answer(0,true);
assert(capture_source==SOURCE_PICCAP && piccap_connected);
protection=PLAYBACK_PROTECTED;assert(handover_tick());assert(strstr(last_uri,"/stop"));
answer(2,true);assert(strstr(last_uri,"/status"));assert(piccap_owned);answer(0,false);
assert(!piccap_owned && capture_source==SOURCE_HISTOGRAM && retries.due==clock_ms);
protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;assert(handover_tick());answer(1,true);
retry_pause(&retries);assert(handover_tick());assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
assert(!piccap_owned && retries.due==0);assert(!handover_tick());
retry_reset(&retries);assert(handover_tick());answer(1,true);handover_enabled=false;
assert(handover_tick());answer(2,false);answer(0,false);assert(!piccap_owned);
handover_enabled=true;protection_at=clock_ms;assert(handover_tick());
clock_ms+=3100;assert(handover_tick());assert(canceled==1 && capture_source==SOURCE_PICCAP_STOPPING);
assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
assert(!piccap_owned);assert(!handover_tick());
puts("PicCap handover tests passed");return 0;
}
+18
View File
@@ -0,0 +1,18 @@
#include <assert.h>
#include <stdio.h>
#include "playback.h"
int main(void){
PlaybackJson j={0};assert(playback_json_init(&j));
assert(playback_classify(&j,"[]")==PLAYBACK_UNKNOWN);
assert(playback_classify(&j,"not json")==PLAYBACK_UNKNOWN);
assert(playback_classify(&j,"{\"returnValue\":false}")==PLAYBACK_UNKNOWN);
assert(playback_classify(&j,"[{\"is_foreground\":true,\"type\":\"media\",\"resource\":[{\"resource\":\"VDEC\"}]}]")==PLAYBACK_CLEAR);
assert(playback_classify(&j,"[{\"is_foreground\":true,\"type\":\"media\",\"resources\":[{\"resource\":\"VDEC\"}]}]")==PLAYBACK_CLEAR);
assert(playback_classify(&j,"[{\"is_foreground\":true,\"type\":\"media\",\"resource\":[{\"resource\":\"VDEC\"},{\"resource\":\"SVP_CPB\"}]}]")==PLAYBACK_PROTECTED);
assert(playback_classify(&j,"[{\"is_foreground\":false,\"type\":\"media\",\"resource\":[{\"resource\":\"VDEC\"}]}]")==PLAYBACK_UNKNOWN);
assert(playback_classify(&j,"[{\"is_foreground\":true,\"type\":\"hdmi\",\"resource\":[{\"resource\":\"VDEC\"}]}]")==PLAYBACK_UNKNOWN);
assert(playback_classify(&j,"[{\"is_foreground\":true,\"type\":\"media\",\"resource\":[{\"resource\":\"VDEC\"}]},{}]")==PLAYBACK_UNKNOWN);
assert(playback_classify(&j,"[{}, {\"resource\":[{\"resource\":\"SVP_CPB\"}]}]")==PLAYBACK_PROTECTED);
assert(playback_classify(&j,"[{\"is_foreground\":true,\"type\":\"media\",\"resource\":[{\"resource\":\"VDEC\"}]},{\"is_foreground\":false,\"resource\":[{\"resource\":\"SVP_CPB\"}]}]")==PLAYBACK_PROTECTED);
puts("Playback classification tests passed");return 0;
}
+1 -1
View File
@@ -1 +1 @@
{"id": "org.webosbrew.histogramhyperion", "version": "0.6.4", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
{"id": "org.webosbrew.histogramhyperion", "version": "0.7.0", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
Binary file not shown.
+4 -2
View File
@@ -4,8 +4,8 @@ const path=require('path');
const vm=require('vm');
const handlers={},windowHandlers={};
const doc={activeElement:null,addEventListener(type,handler){handlers[type]=handler;}};
const buttons=['start','stop','refresh','autostart'].map(id=>({
id,disabled:false,clicks:0,handlers:{},
const buttons=['start','stop','refresh','autostart','handover'].map(id=>({
id,disabled:id==='handover',clicks:0,handlers:{},
addEventListener(type,handler){this.handlers[type]=handler;},
focus(){doc.activeElement=this;this.handlers.focus();},
click(){this.clicks++;}
@@ -25,5 +25,7 @@ doc.activeElement=null;key(39);assert.equal(doc.activeElement.id,'refresh');
assert(!key(461));assert(!key(65));
key(40);assert.equal(doc.activeElement.id,'autostart');key(38);assert.equal(doc.activeElement.id,'refresh');
key(13);windowHandlers.blur();key(13);assert.equal(buttons[2].clicks,4);
buttons[4].disabled=false;buttons[3].focus();key(39);assert.equal(doc.activeElement.id,'handover');
key(39);assert.equal(doc.activeElement.id,'start');key(37);assert.equal(doc.activeElement.id,'handover');
buttons.forEach(button=>button.disabled=true);assert(key(39));
console.log('PASS: arrow navigation, wraparound, disabled controls, OK once per press, pointer handoff, focus recovery, Back preserved');