diff options
Diffstat (limited to 'ccast/ccast.c')
-rw-r--r-- | ccast/ccast.c | 1319 |
1 files changed, 1319 insertions, 0 deletions
diff --git a/ccast/ccast.c b/ccast/ccast.c new file mode 100644 index 0000000..5c1ed7b --- /dev/null +++ b/ccast/ccast.c @@ -0,0 +1,1319 @@ + +/* + * Argyll Color Correction System + * ChromCast support. + * + * Author: Graeme W. Gill + * Date: 10/9/2014 + * + * Copyright 2014 Graeme W. Gill + * All rights reserved. + * + * This material is licenced under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 :- + * see the License2.txt file for licencing details. + * + */ + +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <signal.h> +#include <sys/types.h> +#include <time.h> +#include "copyright.h" +#include "aconfig.h" +#ifndef SALONEINSTLIB +#include "numlib.h" +#else +#include "numsup.h" +#endif +#include "yajl.h" +#include "conv.h" +#include "base64.h" +#include "ccpacket.h" +#include "ccmes.h" +#include "ccast.h" + +#undef DEBUG +#undef CHECK_JSON + +#ifdef DEBUG +# define dbgo stdout +# define DBG(xxx) fprintf xxx ; +void cc_dump_bytes(FILE *fp, char *pfx, unsigned char *buf, int len); +#else +# define DBG(xxx) ; +#endif /* DEBUG */ + +#define START_TRIES 6 +#define LOAD_TRIES 4 + +/* - - - - - - - - - - - - - - - - - - - - - - - - */ + +#ifdef CHECK_JSON + +/* Check if JSON is invalid */ +/* Exits if invalid */ +static void check_json(char *mesbuf) { + yajl_val node; + char errbuf[1024]; + + if ((node = yajl_tree_parse(mesbuf, errbuf, sizeof(errbuf))) == NULL) { + fprintf(dbgo,"yajl_tree_parse of send message failed with '%s'\n",errbuf); + fprintf(dbgo,"JSON = '%s'\n",mesbuf); + exit(1); + } + yajl_tree_free(node); +} +#endif + +/* ============================================================ */ +/* Receive thread */ + +/* Read messages. If they are ones we deal with, send a reply */ +/* If they are anonomous (sessionId == 0), then ignore them */ +/* (Could save last known anonomous message if they prove useful) */ +/* and if they are numbered, keep then in a sorted list. */ + +static int cc_rec_thread(void *context) { + ccast *p = (ccast *)context; + ccmessv *sv = p->messv; + ccmessv_err merr; + ccmes mes, smes; + int errc = 0; + char errbuf[1024]; + yajl_val tyn, idn; + int rv = 0; + + DBG((dbgo,"ccthread starting\n")) + + ccmes_init(&mes); + ccmes_init(&smes); + + /* Preset PONG message */ + smes.source_id = "sender-0"; + smes.destination_id = "receiver-0"; + smes.namespace = "urn:x-cast:com.google.cast.tp.heartbeat"; + smes.binary = 0; + smes.data = (ORD8 *)"{ \"type\": \"PONG\" }"; + + for(;!p->stop;) { + + if ((merr = sv->receive(sv, &mes)) != ccmessv_OK) { + if (merr == ccmessv_timeout) { + DBG((dbgo,"ccthread: got receive timeout (OK)\n")) + msec_sleep(100); + } else { + DBG((dbgo,"ccthread: messv->receive failed with '%s'\n",ccmessv_emes(merr))) + msec_sleep(100); +#ifdef NEVER +//This won't work - we need to re-join the session etc., + if (p->messv->pk->reconnect(p->messv->pk)) { + DBG((dbgo,"ccthread: reconnect after error failed\n")) + rv = 1; + break; + } +#endif + if (errc++ > 20) { /* Too many failures */ + DBG((dbgo,"ccthread: too many errors - giving up\n")) + /* Hmm. The connection seems to have gone down ? */ + rv = 1; + break; + } + } + continue; + } + + errc = 0; + + /* Got status query */ + if (mes.mtype != NULL && strcmp(mes.mtype, "CLOSE") == 0) { + /* Hmm. That indicates an error */ + + DBG((dbgo,"ccthread: got CLOSE message - giving up\n")) + rv = 1; + break; + + } else if (mes.mtype != NULL && strcmp(mes.mtype, "PING") == 0) { + if ((merr = sv->send(sv, &smes)) != ccmessv_OK) { + DBG((dbgo,"ccthread: send PONG failed with '%s'\n",ccmessv_emes(merr))) + } + + /* Got reply - add to linked list */ + } else { + int found; +#ifdef DEBUG + if (p->w_rq) { + DBG((dbgo,"ccthread: waiting for ns '%s' and mes->ns '%s'\n", + p->w_rqns, mes.namespace)) + DBG((dbgo,"ccthread: waiting for id %d and mes->id %d\n", + p->w_rqid, mes.rqid)) + } else { + DBG((dbgo,"ccthread: has no client waiting\n")) + } +#endif + + /* Is it the one the client is waiting for ? */ + found = (p->w_rq != 0 + && (p->w_rqns == NULL || strcmp(p->w_rqns, mes.namespace) == 0) + && (p->w_rqid == 0 || p->w_rqid == mes.rqid)); + + if (found || mes.rqid != 0) { + ccmes *nmes; + if ((nmes = (ccmes *)calloc(1, sizeof(ccmes))) == NULL) { + DBG((dbgo,"ccthread: calloc failed\n")) + } else { + ccmes_transfer(nmes, &mes); + + DBG((dbgo,"ccthread: adding message type '%s' id %d to list (found %d)\n",nmes->mtype,nmes->rqid,found)) + amutex_lock(p->rlock); /* We're modifying p->rmes list */ + nmes->next = p->rmes; /* Put at start of list */ + p->rmes = nmes; + + /* Client is waiting for this */ + if (found) { + DBG((dbgo,"ccthread: client was waiting for this message\n")) + acond_signal(p->rcond); + } + amutex_unlock(p->rlock); /* We've finished modifying p->rmes list */ + } + } + } + + /* Got anonomous status message */ + ccmes_empty(&mes); + } + DBG((dbgo, "ccthread: about to exit - stop = %d\n",p->stop)) + + /* We're bailing out or stopping */ + p->stopped = 1; + + /* Release client if it was waiting */ + amutex_lock(p->rlock); + if (p->w_rq != 0) { + DBG((dbgo,"ccthread: client was waiting for message - abort it\n")) + acond_signal(p->rcond); + } + amutex_unlock(p->rlock); + + DBG((dbgo,"ccthread returning %d\n",rv)) + + return rv; +} + +/* Wait for a specific message rqid on a specific channel. */ +/* Use rqid = 0 to get first message rather than specific one. */ +/* Use namespace = NULL to ignore channel */ +/* Return 1 on an error */ +/* Return 2 on a timeout */ +static int get_a_reply_id(ccast *p, char *namespace, int rqid, ccmes *rmes, int to) { + ccmes *nlist = NULL; + ccmes *mes, *xmes, *fmes = NULL; + int rv = 0; + + DBG((dbgo," get_a_reply_id getting namespace '%s' id %d\n", + namespace == NULL ? "(none)" : namespace, rqid)) + + amutex_lock(p->rlock); /* We're modifying p->rmes list */ + + if (p->stop || p->stopped) { + amutex_unlock(p->rlock); /* Allow thread to modify p->rmes list */ + DBG((dbgo," get_a_reply_id: thread is stopping or stopped\n")) + return 1; + } + + /* Setup request to thread */ + p->w_rq = 1; + p->w_rqns = namespace; + p->w_rqid = rqid; + + /* Until we've got our message, we time out, or the thread is being stopped */ + for (;!p->stop && !p->stopped;) { + + /* Check if the message has already been received */ + for (mes = p->rmes; mes != NULL; mes = xmes) { + int ins = (namespace == NULL || strcmp(namespace, mes->namespace) == 0); + xmes = mes->next; + if (ins && rqid != 0 && mes->rqid < rqid) { + ccmes_del(mes); /* Too old - throw away */ + } else if (ins && (rqid == 0 || mes->rqid == rqid)) { + fmes = mes; /* The one we want */ + } else { + mes->next = nlist; /* Keep in list */ + nlist = mes; + } + } + p->rmes = nlist; + + if (fmes != NULL) + break; /* Got it */ + + +#ifndef NEVER + /* We need to wait until it turns up */ + /* Allow thread to modify p->rmes list and signal us */ + if (acond_timedwait(p->rcond, p->rlock, to) != 0) { + DBG((dbgo," get_a_reply_id timed out after %f secs\n",to/1000.0)) + rv = 2; + break; + } +#else + acond_wait(p->rcond, p->rlock); +#endif + DBG((dbgo," get_a_reply_id got released\n")) + } + p->w_rq = 0; /* We're not looking for anything now */ + amutex_unlock(p->rlock); /* Allow thread to modify p->rmes list */ + + if (p->stop || p->stopped) { + DBG((dbgo," get_a_reply_id failed because thread is stopped or stopping\n")) + ccmes_init(rmes); + return 1; + } + + if (rv != 0) { + DBG((dbgo," get_a_reply_id returning error %d\n",rv)) + } else { + ccmes_transfer(rmes, fmes); + DBG((dbgo," get_a_reply_id returning type '%s' id %d\n",rmes->mtype,rmes->rqid)) + } + + return rv; +} + +/* ============================================================ */ + +void ccast_delete_from_cleanup_list(ccast *p); + +/* Cleanup any created objects */ +static void cleanup_ccast(ccast *p) { + + DBG((dbgo," cleanup_ccast() called\n")) + + p->stop = 1; /* Tell the thread to exit */ + + /* Wait for thread (could use semaphore) */ + /* and then delete it */ + if (p->rmesth != NULL) { + + while (!p->stopped) { + msec_sleep(10); + } + p->rmesth->del(p->rmesth); + p->rmesth = NULL; + } + + if (p->sessionId != NULL) { + free(p->sessionId); + p->sessionId = NULL; + } + + if (p->transportId != NULL) { + free(p->transportId); + p->transportId = NULL; + } + + p->mediaSessionId = 0; + + if (p->messv != NULL) { + p->messv->del(p->messv); + p->messv = NULL; + } + + /* Clean up linked list */ + { + ccmes *mes, *xmes; + for (mes = p->rmes; mes != NULL; mes = xmes) { + xmes = mes->next; + ccmes_del(mes); + } + p->rmes = NULL; + } +} + +/* Shut down the connection, in such a way that we can */ +/* try and re-connect. */ +static void shutdown_ccast(ccast *p) { + ccmes mes; + + DBG((dbgo," shutdown_ccast() called\n")) + + ccmes_init(&mes); + + p->stop = 1; /* Tell the thread to exit */ + + /* Close the media channel */ + if (p->transportId != NULL && p->messv != NULL) { + mes.source_id = "sender-0"; + mes.destination_id = p->transportId; + mes.namespace = "urn:x-cast:com.google.cast.tp.connection"; + mes.binary = 0; + mes.data = (ORD8 *)"{ \"type\": \"CLOSE\" }"; + p->messv->send(p->messv, &mes); + } + + /* Stop the application */ + if (p->sessionId != NULL && p->messv != NULL) { + int reqid = ++p->requestId; + char mesbuf[1024]; + sprintf(mesbuf, "{ \"requestId\": %d, \"type\": \"STOP\", \"sessionId\": \"%s\" }", + reqid, p->sessionId); + mes.source_id = "sender-0"; + mes.destination_id = "receiver-0"; + mes.namespace = "urn:x-cast:com.google.cast.receiver"; + mes.binary = 0; + mes.data = (ORD8 *)mesbuf; + p->messv->send(p->messv, &mes); + } + + /* Close the platform channel */ + if (p->messv != NULL) { + mes.source_id = "sender-0"; + mes.destination_id = "receiver-0"; + mes.namespace = "urn:x-cast:com.google.cast.receiver"; + mes.binary = 0; + mes.data = (ORD8 *)"{ \"type\": \"CLOSE\" }"; + p->messv->send(p->messv, &mes); + } + + cleanup_ccast(p); +} + +static void del_ccast(ccast *p) { + if (p != NULL) { + + shutdown_ccast(p); + + amutex_del(p->rlock); + acond_del(p->rcond); + + free(p); + } +} + +static int load_ccast(ccast *p, char *url, unsigned char *ibuf, size_t ilen, + double bg[3], double x, double y, double w, double h); +void ccast_install_signal_handlers(ccast *p); + +/* Startup a ChromCast session */ +/* Return nz on error */ +static int start_ccast(ccast *p) { + ccpacket *pk = NULL; + ccpacket_err perr; + ccmessv_err merr; + ccmes mes, rmes; + char mesbuf[1024]; + int reqid, tries, maxtries = START_TRIES; + char *connection_chan = "urn:x-cast:com.google.cast.tp.connection"; + char *heartbeat_chan = "urn:x-cast:com.google.cast.tp.heartbeat"; + char *receiver_chan = "urn:x-cast:com.google.cast.receiver"; + + /* Try this a few times if we fail in some way */ + for (tries = 0; tries < maxtries; tries++) { + int app = 0, naps = 2; + + /* Use the default receiver rather than pattern generator */ + if (p->forcedef || getenv("ARGYLL_CCAST_DEFAULT_RECEIVER") != NULL) + app = 1; + + ccmes_init(&mes); + ccmes_init(&rmes); + + p->stop = 0; + p->stopped = 0; +// p->requestId = 0; + + amutex_init(p->rlock); + acond_init(p->rcond); + + /* Hmm. Could put creation of pk inside new_ccmessv() ? */ + if ((pk = new_ccpacket()) == NULL) { + DBG((dbgo,"start_ccast: new_ccpacket() failed\n")) + goto retry; + } + + if ((perr = pk->connect(pk, p->id.ip, 8009)) != ccpacket_OK) { + DBG((dbgo,"start_ccast: ccpacket connect failed with '%s'\n",ccpacket_emes(perr))) + goto retry; + } + + DBG((dbgo,"Got TLS connection to '%s\n'",p->id.name)) + + if ((p->messv = new_ccmessv(pk)) == NULL) { + DBG((dbgo,"start_ccast: new_ccmessv() failed\n")) + goto retry; + } + pk = NULL; /* Will get deleted with messv now */ + + /* Attempt a connection */ + mes.source_id = "sender-0"; + mes.destination_id = "receiver-0"; + mes.namespace = connection_chan; + mes.binary = 0; + mes.data = (ORD8 *)"{ \"type\": \"CONNECT\" }"; + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"start_ccast: CONNECT failed with '%s'\n",ccmessv_emes(merr))) + goto retry; + } + + /* Start the thread. */ + /* We don't want to start this until the TLS negotiations and */ + /* the synchronous ssl_readi()'s it uses are complete. */ + if ((p->rmesth = new_athread(cc_rec_thread, (void *)p)) == NULL) { + DBG((dbgo,"start_ccast: creating message thread failed\n")) + goto retry; + } + +#ifdef NEVER + /* Send a ping */ + mes.namespace = heartbeat_chan; + mes.data = (ORD8 *)"{ \"type\": \"PING\" }"; + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"start_ccast: PING failed with '%s'\n",ccmessv_emes(merr))) + return 1; + } + + /* Wait for a PONG */ +// get_a_reply(p->messv, NULL); +#endif + + /* Try and find an app we can work with */ + for (; app < naps; app++) { + char *appid; + + reqid = ++p->requestId; + + if (app == 0) { + appid = "B5C2CBFC"; /* Pattern generator reciever */ + p->patgenrcv = 1; + p->load_delay = 350.0; + } else { + appid = "CC1AD845"; /* Default Receiver */ + p->patgenrcv = 0; + p->load_delay = 1500.0; /* Actually about 600msec, but fade produces a soft */ + /* transition that instrument meas_delay() doesn't cope with accurately. */ + } + + /* Attempt to launch the Default receiver */ + sprintf(mesbuf, "{ \"requestId\": %d, \"type\": \"LAUNCH\", \"appId\": \"%s\" }", + reqid, appid); + + DBG((dbgo,"start_ccast: about to do LAUNCH\n")) + /* Launch the default application */ + /* (Presumably we would use the com.google.cast.receiver channel */ + /* for monitoring and controlling the reciever) */ + mes.namespace = receiver_chan; + mes.data = (ORD8 *)mesbuf; + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"start_ccast: LAUNCH failed with '%s'\n",ccmessv_emes(merr))) + goto retry; + } + + /* Receive the RECEIVER_STATUS status messages until it is ready to cast */ + /* and get the sessionId and transportId */ + /* We get periodic notification messages (requestId=0) as well as */ + /* a response messages to our requestId */ + + /* Wait for a reply to the LAUNCH (15 sec) */ + if (get_a_reply_id(p, receiver_chan, reqid, &rmes, 15000) != 0) { + DBG((dbgo,"start_ccast: LAUNCH failed to get a reply\n")) + goto retry; + } + + if (rmes.mtype != NULL + && strcmp(rmes.mtype, "RECEIVER_STATUS") == 0 + && rmes.tnode != NULL) { + break; /* Launched OK */ + } + + if (rmes.mtype == NULL + || strcmp(rmes.mtype, "LAUNCH_ERROR") != 0 + || rmes.tnode == NULL + || (app+1) >= naps) { + DBG((dbgo,"start_ccast: LAUNCH failed to get a RECEIVER_STATUS or LAUNCH ERROR reply\n")) + ccmes_empty(&rmes); + goto retry; + } + + /* Try the next application */ + } + + DBG((dbgo,"start_ccast: LAUNCH soceeded, load delay = %d msec\n",p->load_delay)) + { + yajl_val idn, tpn; + if ((idn = yajl_tree_get_first(rmes.tnode, "sessionId", yajl_t_string)) == NULL + || (tpn = yajl_tree_get_first(rmes.tnode, "transportId", yajl_t_string)) == NULL) { + DBG((dbgo,"start_ccast: LAUNCH failed to get sessionId & transportId\n")) + ccmes_empty(&rmes); + goto retry; + } + p->sessionId = strdup(YAJL_GET_STRING(idn)); + p->transportId = strdup(YAJL_GET_STRING(tpn)); + if (p->sessionId == NULL || p->transportId == NULL) { + DBG((dbgo,"start_ccast: strdup failed\n")) + ccmes_empty(&rmes); + goto retry; + } + } + ccmes_empty(&rmes); + + DBG((dbgo,"### Got sessionId = '%s', transportId = '%s'\n",p->sessionId, p->transportId)) + + /* Connect up to the reciever media channels */ + mes.destination_id = p->transportId; + mes.namespace = connection_chan; + mes.data = (ORD8 *)"{ \"type\": \"CONNECT\" }"; + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"messv->send CONNECT failed with '%s'\n",ccmessv_emes(merr))) + goto retry; + } + + // Hmm. Should we wait for a RECEIVER_STATUS message with id 0 here ? + + /* If we get here, we assume that we've suceeded */ + break; + + retry:; + + /* If we get here, we're going around again, so start again from scratch */ + if (pk != NULL) { + pk->del(pk); + pk = NULL; + } + + cleanup_ccast(p); + + } + + if (tries >= maxtries) { + DBG((dbgo,"Failed to start ChromeCast\n")) + return 1; + } + + DBG((dbgo,"Succeeded in starting ChromeCast\n")) + + ccast_install_signal_handlers(p); + + return 0; +} + + + +/* Get the extra load delay */ +static int get_load_delay(ccast *p) { + return p->load_delay; +} + +/* Return nz if we can send PNG directly as base64 + bg RGB, */ +/* else have to setup webserver and send URL */ +static int get_direct_send(ccast *p) { + return p->patgenrcv; +} + +/* Create a new ChromeCast */ +/* Return NULL on error */ +ccast *new_ccast(ccast_id *id, +int forcedef) { + ccast *p = NULL; + + if ((p = (ccast *)calloc(1, sizeof(ccast))) == NULL) { + DBG((dbgo, "new_ccast: calloc failed\n")) + return NULL; + } + + /* Init method pointers */ + p->del = del_ccast; + p->load = load_ccast; + p->shutdown = shutdown_ccast; + p->get_load_delay = get_load_delay; + p->get_direct_send = get_direct_send; + + p->forcedef = forcedef; + + ccast_id_copy(&p->id, id); + + /* Establish communications */ + if (start_ccast(p)) { + del_ccast(p); + return NULL; + } + + return p; +} + +/* Load up a URL */ +/* Returns nz on error: */ +/* 1 send error */ +/* 2 receieve error */ +/* 3 invalid player state/load failed/load cancelled */ +static int load_ccast( + ccast *p, + char *url, /* URL to load, NULL if sent in-line */ + unsigned char *ibuf, size_t ilen,/* PNG image to load, NULL if url */ + double bg[3], /* Background color RGB */ + double x, double y, /* Window location and size as prop. of display */ + double w, double h /* Size as multiplier of default 10% width */ +) { + ccmessv_err merr; + int reqid, firstid, lastid; + ccmes mes; + char *media_chan = "urn:x-cast:com.google.cast.media"; + char *direct_chan = "urn:x-cast:net.hoech.cast.patterngenerator"; + char *receiver_chan = "urn:x-cast:com.google.cast.receiver"; +// char *player_message_chan = "urn:x-cast:com.google.cast.player.message"; + int dchan = 0; /* Using direct channel */ + int i, maxtries = LOAD_TRIES, rv = 0; + + ccmes_init(&mes); + + /* Retry loop */ + for (i = 0; ; i++) { + unsigned char *iibuf = ibuf; + size_t iilen = ilen; + firstid = lastid = reqid = ++p->requestId; + + DBG((dbgo,"##### load_ccast try %d/%d\n",i+1,maxtries)) + + if (p->messv == NULL) { + DBG((dbgo,"mes->send LOAD failed due to lost connection\n")) + + } else { + + if (url == NULL && !p->patgenrcv) { + DBG((dbgo,"mes->send not given URL\n")) + return 1; + } + + /* Send the LOAD URL command */ + if (url != NULL) { + char *xl, mesbuf[1024]; + + xl = strrchr(url, '.'); // Get extension location + + if (xl != NULL && stricmp(xl, ".webm") == 0) { + sprintf(mesbuf, "{ \"requestId\": %d, \"type\": \"LOAD\", \"media\": " + "{ \"contentId\": \"%s\",", reqid, url); + strcat(mesbuf, + "\"contentType\": \"video/webm\" }," + "\"autplay\": \"true\" }"); + + } else if (xl != NULL && stricmp(xl, ".mp4") == 0) { + sprintf(mesbuf, "{ \"requestId\": %d, \"type\": \"LOAD\", \"media\": " + "{ \"contentId\": \"%s\",", reqid, url); + strcat(mesbuf, + "\"contentType\": \"video/mp4\" }," + "\"autplay\": \"true\" }"); + + } else { /* Assume PNG */ + sprintf(mesbuf, "{ \"requestId\": %d, \"type\": \"LOAD\", \"media\": " + "{ \"contentId\": \"%s\",", reqid, url); + strcat(mesbuf, + "\"contentType\": \"image/png\" } }"); + } + + mes.source_id = "sender-0"; + mes.destination_id = p->transportId; + mes.namespace = media_chan; + mes.binary = 0; + mes.data = (ORD8 *)mesbuf; +#ifdef CHECK_JSON + check_json((char *)mes.data); +#endif + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"mes->send LOAD failed with '%s'\n",ccmessv_emes(merr))) + rv = 1; + goto retry; /* Failed */ + } + +#ifdef NEVER + /* Send base64 PNG image & background color definition in one message */ + /* This will fail if the message is > 64K */ + } else if (iibuf != NULL) { + char *mesbuf, *cp; + int dlen; + + if ((mesbuf = malloc(1024 + EBASE64LEN(iilen))) == NULL) { + DBG((dbgo,"mes->send malloc failed\n")) + return 1; + } + + cp = mesbuf; + cp += sprintf(cp, "{ \"requestId\": %d, \"type\": \"LOAD\", \"media\": " + "{ \"contentId\": \"data:image/png;base64,",reqid); + ebase64(&dlen, cp, iibuf, iilen); + cp += dlen; + DBG((dbgo,"base64 encoded PNG = %d bytes\n",dlen)) + sprintf(cp, "|rgb(%d, %d, %d)|%f|%f|%f|%f\"," + "\"streamType\": \"LIVE\",\"contentType\": \"text/plain\" } }", + (int)(bg[0] * 255.0 + 0.5), (int)(bg[1] * 255.0 + 0.5), (int)(bg[2] * 255.0 + 0.5), + x, y, w, h); + + mes.source_id = "sender-0"; + mes.destination_id = p->transportId; + mes.namespace = media_chan; + mes.binary = 0; + mes.data = (ORD8 *)mesbuf; +#ifdef CHECK_JSON + check_json((char *)mes.data); +#endif + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"mes->send LOAD failed with '%s'\n",ccmessv_emes(merr))) + free(mesbuf); + rv = 1; + goto retry; /* Failed */ + } + free(mesbuf); + +#else /* !NEVER */ + /* Send base64 PNG image & background color definition in multiple packets */ + } else if (iibuf != NULL) { + char *mesbuf, *cp; + size_t maxlen, meslen; + size_t enclen, senclen = 0; + int dlen; /* Encoded length to send */ + + if ((mesbuf = malloc(61 * 1024)) == NULL) { + DBG((dbgo,"mes->send malloc failed\n")) + return 1; + } + + dchan = 1; + + enclen = EBASE64LEN(ilen); /* Encoded length of whole image */ + maxlen = DBASE64LEN(60 * 1024); /* Maximum image bytes to send per message */ + meslen = iilen; + if (meslen > maxlen) + meslen = maxlen; + + cp = mesbuf; + cp += sprintf(cp, "{ "); + cp += sprintf(cp, "\"requestId\": %d,",reqid); + cp += sprintf(cp, "\"foreground\": { "); + cp += sprintf(cp, "\"contentType\": \"image/png\","); + cp += sprintf(cp, "\"encoding\": \"base64\","); + cp += sprintf(cp, "\"data\": \""); + ebase64(&dlen, cp, iibuf, meslen); + DBG((dbgo,"part base64 encoded PNG = %d bytes\n",dlen)) + iibuf += meslen; + iilen -= meslen; + senclen += dlen; + cp += dlen; + cp += sprintf(cp, "\","); + cp += sprintf(cp, "\"size\": %lu",(unsigned long)EBASE64LEN(ilen)); + cp += sprintf(cp, " },"); + + cp += sprintf(cp, "\"background\": \"rgb(%d, %d, %d)\",", + (int)(bg[0] * 255.0 + 0.5), + (int)(bg[1] * 255.0 + 0.5), + (int)(bg[2] * 255.0 + 0.5)); + + cp += sprintf(cp, "\"offset\": [%f, %f],",x,y); + cp += sprintf(cp, "\"scale\": [%f, %f]",w,h); + cp += sprintf(cp, " }"); + + mes.source_id = "sender-0"; + mes.destination_id = p->transportId; + mes.namespace = direct_chan; + mes.binary = 0; + mes.data = (ORD8 *)mesbuf; +#ifdef CHECK_JSON + check_json((char *)mes.data); +#endif + + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"mes->send LOAD failed with '%s'\n",ccmessv_emes(merr))) + free(mesbuf); + rv = 1; + goto retry; /* Failed */ + } + + /* If we didn't send the whole image in one go */ + while (iilen > 0) { + + /* Send the next chunk */ + meslen = iilen; + if (meslen > maxlen) + meslen = maxlen; + + DBG((dbgo,"Sending %d bytes of %d remaining in image\n",meslen,iilen)) + + lastid = reqid = ++p->requestId; + + cp = mesbuf; + cp += sprintf(cp, "{ "); + cp += sprintf(cp, "\"requestId\": %d,",reqid); + cp += sprintf(cp, "\"foreground\": \""); + ebase64(&dlen, cp, iibuf, meslen); + DBG((dbgo,"part base64 encoded PNG = %d bytes\n",dlen)) + iibuf += meslen; + iilen -= meslen; + senclen += dlen; + + + cp += dlen; + cp += sprintf(cp, "\" }"); + + mes.source_id = "sender-0"; + mes.destination_id = p->transportId; + mes.namespace = direct_chan; + mes.binary = 0; + mes.data = (ORD8 *)mesbuf; +#ifdef CHECK_JSON + check_json((char *)mes.data); +#endif + if ((merr = p->messv->send(p->messv, &mes)) != ccmessv_OK) { + DBG((dbgo,"mes->send LOAD failed with '%s'\n",ccmessv_emes(merr))) + free(mesbuf); + rv = 1; + goto retry; /* Failed */ + } + + if ((lastid - firstid) > 0) { + + /* Wait for an ACK, to make sure the channel doesn't get choked up */ + if (get_a_reply_id(p, direct_chan, firstid, &mes, 10000) != 0) { + DBG((dbgo,"load_ccast: failed to get reply\n")) + rv = 2; + goto retry; /* Failed */ + } + if (mes.mtype == NULL) { + DBG((dbgo,"load_ccast: mtype == NULL\n")) + rv = 2; + goto retry; /* Failed */ + + } else if (dchan && strcmp(mes.mtype, "ACK") == 0) { + DBG((dbgo,"load_ccast: got ACK\n")) + + } else if (dchan && strcmp(mes.mtype, "NACK") == 0) { + /* Failed. Get error status */ + yajl_val errors; + if ((errors = yajl_tree_get_first(mes.tnode, "errors", yajl_t_array)) != NULL) { + DBG((dbgo,"NACK returned errors:\n")) + if (YAJL_IS_ARRAY(errors)) { + for (i = 0; i < errors->u.array.len; i++) { + yajl_val error = errors->u.array.values[i]; + DBG((dbgo,"%s\n",error->u.string)) + } + + } else { + DBG((dbgo,"NACK errors is not an array!\n")) + } + } else { + DBG((dbgo,"NACK failed to return errors\n")) + } + rv = 2; + goto retry; /* Failed */ + + } else { + rv = 3; + DBG((dbgo,"load_ccast: got mtype '%s'\n",mes.mtype)) + goto retry; /* Failed */ + } + ccmes_empty(&mes); + firstid++; + } + }; + free(mesbuf); + + /* This would be bad... */ + if (iilen == 0 && senclen != enclen) + fprintf(stderr,"ccast load finished but senclen %lu != enclen %lu\n", + (unsigned long)senclen,(unsigned long)enclen); +#endif /* !NEVER */ + + } else { + DBG((dbgo,"mes->send not given URL or png data\n")) + return 1; + } + } + + /* Wait for a reply to each load message */ + for (reqid = firstid; reqid <= lastid; reqid++) { + + if (get_a_reply_id(p, dchan ? direct_chan : media_chan, reqid, &mes, 5000) != 0) { + DBG((dbgo,"load_ccast: failed to get reply\n")) + rv = 2; + goto retry; /* Failed */ + } + /* Reply could be: + MEDIA_STATUS + INVALID_PLAYER_STATE, + LOAD_FAILED, + LOAD_CANCELLED + + For net.hoech.cast.patterngenerator + ACK, + NACK + */ + if (mes.mtype == NULL) { + DBG((dbgo,"load_ccast: mtype == NULL\n")) + rv = 2; + goto retry; /* Failed */ + + } else if (dchan && strcmp(mes.mtype, "ACK") == 0) { + DBG((dbgo,"load_ccast: got ACK\n")) + + } else if (dchan && strcmp(mes.mtype, "NACK") == 0) { + /* Failed. Get error status */ + yajl_val errors; + if ((errors = yajl_tree_get_first(mes.tnode, "errors", yajl_t_array)) != NULL) { + DBG((dbgo,"NACK returned errors:\n")) + if (YAJL_IS_ARRAY(errors)) { + for (i = 0; i < errors->u.array.len; i++) { + yajl_val error = errors->u.array.values[i]; + DBG((dbgo,"%s\n",error->u.string)) + } + + } else { + DBG((dbgo,"NACK errors is not an array!\n")) + } + } else { + DBG((dbgo,"NACK failed to return errors\n")) + } + rv = 2; + goto retry; /* Failed */ + + } else if (strcmp(mes.mtype, "MEDIA_STATUS") == 0) { + yajl_val node, i; + if ((i = yajl_tree_get_first(mes.tnode, "mediaSessionId", yajl_t_number)) != NULL) { + p->mediaSessionId = YAJL_GET_INTEGER(i); + DBG((dbgo,"MEDIA_STATUS returned mediaSessionId %d\n",p->mediaSessionId)) + } else { + DBG((dbgo,"MEDIA_STATUS failed to return mediaSessionId\n")) + } + /* Suceeded */ + + } else { + rv = 3; + DBG((dbgo,"load_ccast: got mtype '%s'\n",mes.mtype)) + goto retry; /* Failed */ + } + ccmes_empty(&mes); + } + /* If we got here, we succeeded */ + break; + + retry:; + +// if (!p->loaded1 || (i+1) >= maxtries) + if ((i+1) >= maxtries) { + return rv; /* Too many tries - give up */ + } + + DBG((dbgo,"load_ccast: failed on try %d/%d - re-connecting to chrome cast\n",i+1,maxtries)) + shutdown_ccast(p); /* Tear connection down */ + if (start_ccast(p)) { /* Set it up again */ + DBG((dbgo,"load_ccast: re-connecting failed\n")) + return 1; + } + /* And retry */ + rv = 0; + } /* Retry loop */ + + /* Success */ + p->loaded1 = 1; /* Loaded at least once */ + + /* Currently there is a 1.5 second fade up delay imposed by */ + /* the base ChromeCast software or default receiver. */ + if (p->load_delay > 0.0) + msec_sleep(p->load_delay); + + return rv; +} + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +/* Static list so that all open ChromCast connections can be closed on a SIGKILL */ +static ccast *ccast_list = NULL; + +/* Clean up any open ChromeCast connections */ +static void ccast_cleanup() { + ccast *pp, *np; + + for (pp = ccast_list; pp != NULL; pp = np) { + np = pp->next; + a1logd(g_log, 2, "ccast_cleanup: closing 0x%x\n",pp); + pp->shutdown(pp); + } +} + +#ifdef NT +static void (__cdecl *ccast_int)(int sig) = SIG_DFL; +static void (__cdecl *ccast_term)(int sig) = SIG_DFL; +#endif +#ifdef UNIX +static void (*ccast_hup)(int sig) = SIG_DFL; +static void (*ccast_int)(int sig) = SIG_DFL; +static void (*ccast_term)(int sig) = SIG_DFL; +#endif + +/* On something killing our process, deal with USB cleanup */ +static void ccast_sighandler(int arg) { + static amutex_static(lock); + + a1logd(g_log, 2, "ccast_sighandler: invoked with arg = %d\n",arg); + + /* Make sure we don't re-enter */ + if (amutex_trylock(lock)) { + return; + } + + ccast_cleanup(); + a1logd(g_log, 2, "ccast_sighandler: done ccast_sighandler()\n"); + + /* Call the existing handlers */ +#ifdef UNIX + if (arg == SIGHUP && ccast_hup != SIG_DFL && ccast_hup != SIG_IGN) + ccast_hup(arg); +#endif /* UNIX */ + if (arg == SIGINT && ccast_int != SIG_DFL && ccast_int != SIG_IGN) + ccast_int(arg); + if (arg == SIGTERM && ccast_term != SIG_DFL && ccast_term != SIG_IGN) + ccast_term(arg); + + a1logd(g_log, 2, "ccast_sighandler: calling exit()\n"); + + amutex_unlock(lock); + exit(0); +} + + +/* Install the cleanup signal handlers */ +void ccast_install_signal_handlers(ccast *p) { + + if (ccast_list == NULL) { + a1logd(g_log, 2, "ccast_install_signal_handlers: called\n"); +#if defined(UNIX) + ccast_hup = signal(SIGHUP, ccast_sighandler); +#endif /* UNIX */ + ccast_int = signal(SIGINT, ccast_sighandler); + ccast_term = signal(SIGTERM, ccast_sighandler); + } + + /* Add it to our static list, to allow automatic cleanup on signal */ + p->next = ccast_list; + ccast_list = p; + a1logd(g_log, 6, "ccast_install_signal_handlers: done\n"); +} + +/* Delete an ccast from our static signal cleanup list */ +void ccast_delete_from_cleanup_list(ccast *p) { + + /* Find it and delete it from our static cleanup list */ + if (ccast_list != NULL) { + a1logd(g_log, 6, "ccast_install_signal_handlers: called\n"); + if (ccast_list == p) { + ccast_list = p->next; + if (ccast_list == NULL) { +#if defined(UNIX) + signal(SIGHUP, ccast_hup); +#endif /* UNIX */ + signal(SIGINT, ccast_int); + signal(SIGTERM, ccast_term); + } + } else { + ccast *pp; + for (pp = ccast_list; pp != NULL; pp = pp->next) { + if (pp->next == p) { + pp->next = p->next; + break; + } + } + } + a1logd(g_log, 6, "ccast_install_signal_handlers: done\n"); + } +} + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* Quantization model of ChromCast and TV */ +/* ctx is a placeholder, and can be NULL */ + +#define QUANT(xx, qq) (floor((xx) * (qq) + 0.5)/(qq)) + +/* Input & output RGB 0.0 - 1.0 levels */ +void ccastQuant(void *ctx, double out[3], double in[3]) { + double r = in[0], g = in[1], b = in[2]; + double Y, Cb, Cr; + +// printf("ccastQuant: %f %f %f",r,g,b); + + /* Scale RGB to 8 bit then quantize, since that's the limit of the frame buffer */ + r = floor(r * 255.0 + 0.5); + g = floor(g * 255.0 + 0.5); + b = floor(b * 255.0 + 0.5); + + /* ChromCast hardware seems to use 9 bit coeficients */ + Y = r * QUANT((235.0 - 16.0) * 0.2126/255.0, 512.0) + + g * QUANT((235.0 - 16.0) * 0.7152/255.0, 512.0) + + b * QUANT((235.0 - 16.0) * 0.0722/255.0, 512.0); + + Cb = r * -QUANT((240.0 - 16.0) * 0.2126 /(255.0 * 1.8556), 512.0) + + g * -QUANT((240.0 - 16.0) * 0.7152 /(255.0 * 1.8556), 512.0) + + b * QUANT((240.0 - 16.0) * (1.0 - 0.0722)/(255.0 * 1.8556), 512.0); + + Cr = r * QUANT((240.0 - 16.0) * (1.0 - 0.2126)/(255.0 * 1.5748), 512.0) + + g * -QUANT((240.0 - 16.0) * 0.7152 /(255.0 * 1.5748), 512.0) + + b * -QUANT((240.0 - 16.0) * 0.0722 /(255.0 * 1.5748), 512.0); + + /* (Don't bother with offsets, since they don't affect quantization) */ + + /* Quantize YCbCr to 8 bit, since that's what ChromCast HDMI delivers */ + Y = floor(Y + 0.5); + Cb = floor(Cb + 0.5); + Cr = floor(Cr + 0.5); + + /* We simply assume that the TV decoding is perfect, */ + /* but is limited to 8 bit full range RGB output */ + r = Y * 255.0/(235.0 - 16.0) + + Cr * (1.574800000 * 255.0)/(240.0 - 16.0); + + g = Y * 255.0/(235.0 - 16.0) + + Cb * (-0.187324273 * 255.0)/(240.0 - 16.0) + + Cr * (-0.468124273 * 255.0)/(240.0 - 16.0); + + b = Y * 255.0/(235.0 - 16.0) + + Cb * (1.855600000 * 255.0)/(240.0 - 16.0); + + /* Clip */ + if (r > 255.0) + r = 255.0; + else if (r < 0.0) + r = 0.0; + if (g > 255.0) + g = 255.0; + else if (g < 0.0) + g = 0.0; + if (b > 255.0) + b = 255.0; + else if (b < 0.0) + b = 0.0; + + /* We assume that the TV is 8 bit, so */ + /* quantize to 8 bits and return to 0.0 - 1.0 range. */ + r = floor(r + 0.5)/255.0; + g = floor(g + 0.5)/255.0; + b = floor(b + 0.5)/255.0; + + out[0] = r; + out[1] = g; + out[2] = b; + +// printf(" -> %f %f %f\n",r,g,b); +} + +/* Input RGB 8 bit 0 - 255 levels, output YCbCr 8 bit 16 - 235 levels */ +/* Non-clipping, non-8 bit quantizing */ +void ccast2YCbCr_nq(void *ctx, double out[3], double in[3]) { + double r = in[0], g = in[1], b = in[2]; + double Y, Cb, Cr; + +// printf("ccast2YCbCr_nq: %f %f %f",r,g,b); + + /* ChromCast hardware seems to use 9 bit coeficients */ + Y = r * QUANT((235.0 - 16.0) * 0.2126/255.0, 512.0) + + g * QUANT((235.0 - 16.0) * 0.7152/255.0, 512.0) + + b * QUANT((235.0 - 16.0) * 0.0722/255.0, 512.0) + + 16.0; + + Cb = r * -QUANT((240.0 - 16.0) * 0.2126 /(255.0 * 1.8556), 512.0) + + g * -QUANT((240.0 - 16.0) * 0.7152 /(255.0 * 1.8556), 512.0) + + b * QUANT((240.0 - 16.0) * (1.0 - 0.0722)/(255.0 * 1.8556), 512.0) + + 16.0; + + Cr = r * QUANT((240.0 - 16.0) * (1.0 - 0.2126)/(255.0 * 1.5748), 512.0) + + g * -QUANT((240.0 - 16.0) * 0.7152 /(255.0 * 1.5748), 512.0) + + b * -QUANT((240.0 - 16.0) * 0.0722 /(255.0 * 1.5748), 512.0) + + 16.0; + + out[0] = Y; + out[1] = Cb; + out[2] = Cr; + +// printf(" -> %f %f %f\n",out[0],out[1],out[2]); +} + +/* Input RGB 8 bit 0 - 255 levels, output YCbCr 8 bit 16 - 235 levels */ +void ccast2YCbCr(void *ctx, double out[3], double in[3]) { + double rgb[3]; + +// printf("ccast2YCbCr: %f %f %f",r,g,b); + + /* Quantize RGB to 8 bit, since that's the limit of the frame buffer */ + rgb[0] = floor(in[0] + 0.5); + rgb[1] = floor(in[1] + 0.5); + rgb[2] = floor(in[2] + 0.5); + + ccast2YCbCr_nq(ctx, out, rgb); + + /* Quantize YCbCr to 8 bit, since that's what ChromCast HDMI delivers */ + out[0] = floor(out[0] + 0.5); + out[1] = floor(out[1] + 0.5); + out[2] = floor(out[2] + 0.5); +// printf(" -> %f %f %f\n",out[0],out[1],out[2]); +} + +/* Input YCbCr 8 bit 16 - 235 levels output RGB 8 bit 0 - 255 levels. */ +/* Non-clipping, non-8 bit quantizing */ +void YCbCr2ccast_nq(void *ctx, double out[3], double in[3]) { + double Y = in[0], Cb = in[1], Cr = in[2]; + double r, g, b; + +// printf("YCbCr2ccast_nq: %f %f %f",Y,Cb,Cr); + + Y -= 16.0; + Cb -= 16.0; + Cr -= 16.0; + + /* We simply assume that the TV decoding is perfect, */ + /* but is limited to 8 bit full range RGB output */ + r = Y * 255.0/(235.0 - 16.0) + + Cr * (1.574800000 * 255.0)/(240.0 - 16.0); + + g = Y * 255.0/(235.0 - 16.0) + + Cb * (-0.187324273 * 255.0)/(240.0 - 16.0) + + Cr * (-0.468124273 * 255.0)/(240.0 - 16.0); + + b = Y * 255.0/(235.0 - 16.0) + + Cb * (1.855600000 * 255.0)/(240.0 - 16.0); + + out[0] = r; + out[1] = g; + out[2] = b; + +// printf(" -> %f %f %f\n",out[0],out[1],out[2]); +} + +/* Input YCbCr 8 bit 16 - 235 levels output RGB 8 bit 0 - 255 levels. */ +/* Quantize input, and quantize and clip output. */ +/* Return nz if the output was clipped */ +int YCbCr2ccast(void *ctx, double out[3], double in[3]) { + double YCbCr[3]; + int k, rv = 0; + +// printf("YCbCr2ccast: %f %f %f",Y,Cb,Cr); + + /* Quantize YCbCr to 8 bit, since that's what ChromCast HDMI delivers */ + YCbCr[0] = floor(in[0] + 0.5); + YCbCr[1] = floor(in[1] + 0.5); + YCbCr[2] = floor(in[2] + 0.5); + + YCbCr2ccast_nq(ctx, out, YCbCr); + + /* Quantize and clip to RGB, since we assume the TV does this */ + for (k = 0; k < 3; k++) { + out[k] = floor(out[k] + 0.5); + + if (out[k] > 255.0) { + out[k] = 255.0; + rv = 1; + } else if (out[k] < 0.0) { + out[k] = 0.0; + rv = 1; + } + } + +// printf(" -> %f %f %f, clip %d\n",out[0],out[1],out[2],rv); + + return rv; +} + |