
/* 
 * 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 "conv.h"
#include "base64.h"
#include "yajl.h"
#include "ccpacket.h"
#include "ccmes.h"
#include "ccast.h"

#undef DEBUG				/* [und] */
#undef USE_DEF_RECIEVER		/* [und] */
#undef CHECK_JSON			/* [und] */

#ifdef DEBUG
# define DBG(xxx) a1logd xxx ;
#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(g_log,0,"yajl_tree_parse of send message failed with '%s'\n",errbuf);
		fprintf(g_log,0,"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 */
/* and if they are numbered, keep then in a sorted list. */
/* (Could save last known anonomous message if they prove useful) */

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((g_log,0,"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((g_log,0,"ccthread: got receive timeout (OK)\n"))
				msec_sleep(100);
			} else {
				DBG((g_log,0,"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((g_log,0,"ccthread: reconnect after error failed\n"))
					rv = 1;
					break;
				}
#endif
				if (errc++ > 20) {		/* Too many failures */
					DBG((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"ccthread: waiting for ns '%s' and mes->ns '%s'\n",
				                                 p->w_rqns, mes.namespace))
				DBG((g_log,0,"ccthread: waiting for id %d and mes->id %d\n",
				                                 p->w_rqid, mes.rqid))
			} else {
				DBG((g_log,0,"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((g_log,0,"ccthread: calloc failed\n"))
				} else {
					ccmes_transfer(nmes, &mes);
	
					DBG((g_log,0,"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((g_log,0,"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((g_log,0, "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((g_log,0,"ccthread: client was waiting for message - abort it\n"))
		acond_signal(p->rcond);
	}
	amutex_unlock(p->rlock);

	DBG((g_log,0,"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((g_log,0," 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((g_log,0," 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((g_log,0," 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((g_log,0," 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((g_log,0," get_a_reply_id failed because thread is stopped or stopping\n"))
		ccmes_init(rmes);
		return 1;
	}

	if (rv != 0) {
		DBG((g_log,0," get_a_reply_id returning error %d\n",rv))
	} else {
		ccmes_transfer(rmes, fmes);
		DBG((g_log,0," 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((g_log,0," 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((g_log,0," 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((g_log,0,"start_ccast: new_ccpacket() failed\n"))
			goto retry;
		}
		
		if ((perr = pk->connect(pk, p->id.ip, 8009)) != ccpacket_OK) {
			DBG((g_log,0,"start_ccast: ccpacket connect failed with '%s'\n",ccpacket_emes(perr)))
			goto retry;
		} 

		DBG((g_log,0,"Got TLS connection to '%s\n'",p->id.name))

		if ((p->messv = new_ccmessv(pk)) == NULL) {
			DBG((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"start_ccast: LAUNCH failed to get a RECEIVER_STATUS or LAUNCH ERROR reply\n"))
				ccmes_empty(&rmes);
				goto retry;
			}

			/* Try the next application */
		}

		DBG((g_log,0,"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((g_log,0,"start_ccast: LAUNCH failed to get sessionId & transportId\n"))
				ccmes_empty(&rmes);
				goto retry;
			}
			p->sessionId = YAJL_GET_STRINGDUP(idn);
			p->transportId = YAJL_GET_STRINGDUP(tpn);
			if (p->sessionId == NULL || p->transportId == NULL) {
				DBG((g_log,0,"start_ccast: strdup failed\n"))
				ccmes_empty(&rmes);
				goto retry;
			}
		}
		ccmes_empty(&rmes);

		DBG((g_log,0,"### 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((g_log,0,"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 succeeded */
		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((g_log,0,"Failed to start ChromeCast\n"))
		return 1;
	}

	DBG((g_log,0,"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) {				// Force default reciever
	ccast *p = NULL;

	if ((p = (ccast *)calloc(1, sizeof(ccast))) == NULL) {
		DBG((g_log,0, "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;

#ifdef USE_DEF_RECIEVER
	forcedef = 1;
#endif
	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((g_log,0,"##### load_ccast try %d/%d\n",i+1,maxtries))

		if (p->messv == NULL) {
			DBG((g_log,0,"mes->send LOAD failed due to lost connection\n"))

		} else {

			if (url == NULL && !p->patgenrcv) {
				DBG((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"load_ccast: failed to get reply\n"))
							rv = 2;
							goto retry;		/* Failed */
						}
						if (mes.mtype == NULL) {
							DBG((g_log,0,"load_ccast: mtype == NULL\n"))
							rv = 2;
							goto retry;		/* Failed */
					
						} else if (dchan && strcmp(mes.mtype, "ACK") == 0) {
							DBG((g_log,0,"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((g_log,0,"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((g_log,0,"%s\n",error->u.string))
									}
					
								} else {
									DBG((g_log,0,"NACK errors is not an array!\n"))
								}
							} else {
								DBG((g_log,0,"NACK failed to return errors\n"))
							}
							rv = 2;
							goto retry;		/* Failed */
			
						} else {
							rv = 3;
							DBG((g_log,0,"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((g_log,0,"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((g_log,0,"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((g_log,0,"load_ccast: mtype == NULL\n"))
				rv = 2;
				goto retry;		/* Failed */
		
			} else if (dchan && strcmp(mes.mtype, "ACK") == 0) {
				DBG((g_log,0,"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((g_log,0,"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((g_log,0,"%s\n",error->u.string))
						}
		
					} else {
						DBG((g_log,0,"NACK errors is not an array!\n"))
					}
				} else {
					DBG((g_log,0,"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((g_log,0,"MEDIA_STATUS returned mediaSessionId %d\n",p->mediaSessionId))
				} else {
					DBG((g_log,0,"MEDIA_STATUS failed to return mediaSessionId\n"))
				}
				/* Succeeded */

			} else {
				rv = 3;
				DBG((g_log,0,"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((g_log,0,"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((g_log,0,"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;

double or = r, og = g, ob = b;
//	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;

if (fabs(or - r) > 3.0
 || fabs(og - g) > 3.0
 || fabs(ob - b) > 3.0) {
	printf("%f %f %f -> %f %f %f\n",or,og,ob,r,g,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;
}

