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

+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;
}