1386 lines
43 KiB
C
1386 lines
43 KiB
C
|
|
/*
|
|
* Argyll Color Correction System
|
|
* ChromCast support
|
|
*
|
|
* Author: Graeme W. Gill
|
|
* Date: 28/8/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.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* This class provides simple access to the Google ChromeCast
|
|
* for the purposes of generating Video Test patches.
|
|
*/
|
|
|
|
/*
|
|
TTBD: Ideally we should use the system mDNS resource if it is available,
|
|
and fall back to our mDNS code if not.
|
|
|
|
ie. Linux may have nothing, be using avahi-daemon,
|
|
possibly Zeroconf (== Bonjour ?)
|
|
Avahi has Bonjour compatibility library ??
|
|
|
|
ie. OS X is using mDNSRespo (Bonjour ?)
|
|
|
|
MSWin may have Bonjour installed on it ?
|
|
Has dns-sd command - what's that using (SSDP/UPnP ?)
|
|
|
|
From <http://stackoverflow.com/questions/8659638/how-does-windows-know-how-to-resolve-mdns-queries>
|
|
|
|
Bonjour for Windows allows any software using the standard name resolution APIs
|
|
to resolve mDNS names; it does so by registering a DLL (mdnsnsp.dll) as a
|
|
namespace provider using WSCInstallNameSpace.
|
|
|
|
The corresponding code is included in the mDNSResponder source (in particular,
|
|
look at the mdnsNSP and NSPTool components).
|
|
|
|
To test on Linux:
|
|
|
|
To check what's listening: sudo netstat -anp | grep 5353
|
|
|
|
/etc/init.d/avahi-daemon stop
|
|
/etc/init.d/avahi-daemon start
|
|
|
|
To test on OS X:
|
|
|
|
To check what's listening: sudo netstat -an | grep 5353
|
|
|
|
launchctl unload -w /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist
|
|
launchctl load -w /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist
|
|
*/
|
|
|
|
|
|
#include <time.h>
|
|
#include <stdlib.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include <math.h>
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <limits.h>
|
|
#include "copyright.h"
|
|
#include "aconfig.h"
|
|
#include <sys/types.h>
|
|
#ifndef SALONEINSTLIB
|
|
#include "numlib.h"
|
|
#else
|
|
#include "numsup.h"
|
|
#endif
|
|
#include "conv.h"
|
|
#include "ccmdns.h"
|
|
|
|
#undef DEBUG /* [und] */
|
|
|
|
#if defined(NT) // Windows specific
|
|
# if _WIN32_WINNT < 0x0400
|
|
# undef _WIN32_WINNT
|
|
# define _WIN32_WINNT 0x0400 // To make it link in VS2005
|
|
# endif
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <winsock2.h>
|
|
# include <windows.h>
|
|
# include <ws2tcpip.h>
|
|
|
|
# include <process.h>
|
|
# include <direct.h>
|
|
# include <io.h>
|
|
|
|
# define ERRNO GetLastError()
|
|
# define UDP_SOCKET_TIMEOUT WSAETIMEDOUT
|
|
|
|
#else /* !NT = Unix, OS X */
|
|
|
|
# include <errno.h>
|
|
# include <sys/types.h>
|
|
# include <sys/wait.h>
|
|
# include <sys/socket.h>
|
|
# include <sys/select.h>
|
|
# include <netinet/in.h>
|
|
# include <arpa/inet.h>
|
|
# include <sys/time.h>
|
|
# include <stdint.h>
|
|
# include <inttypes.h>
|
|
# include <netdb.h>
|
|
|
|
# include <pwd.h>
|
|
# include <unistd.h>
|
|
# include <dirent.h>
|
|
|
|
typedef int SOCKET;
|
|
# define ERRNO errno
|
|
|
|
# define INVALID_SOCKET -1
|
|
# define SOCKET_ERROR -1
|
|
# define UDP_SOCKET_TIMEOUT EAGAIN
|
|
|
|
#define closesocket(xxx) close(xxx);
|
|
|
|
#endif
|
|
|
|
#ifdef DEBUG
|
|
# pragma message("######### ccmdns.c DEBUG enabled !!!!! ########")
|
|
# define DBG(xxx) a1logd xxx ;
|
|
# define DBG2(xxx) a1logd xxx ;
|
|
# define DLEV 0
|
|
# define DLEVP1 0
|
|
#else
|
|
# define DBG(xxx) ;
|
|
# define DBG2(xxx) a1logd xxx ;
|
|
# define DLEV 2
|
|
# define DLEVP1 3
|
|
#endif /* DEBUG */
|
|
|
|
/* ================================================================ */
|
|
|
|
/* mDNS details */
|
|
#define DESTINATION_MCAST "224.0.0.251"
|
|
#define DESTINATION_MCASTV6 "FF02::FB"
|
|
#define SOURCE_PORT 5353
|
|
#define DESTINATION_PORT 5353
|
|
|
|
#define BUFSIZE 2048
|
|
|
|
/* Various DNS defines */
|
|
#define DNS_CLASS_IN 0x0001
|
|
|
|
/* DNS query type */
|
|
#define DNS_TYPE_A 1 /* Host IP address record */
|
|
#define DNS_TYPE_PTR 12 /* Canonical domain name */
|
|
#define DNS_TYPE_TXT 16 /* Text String */
|
|
#define DNS_TYPE_AAAA 28 /* Host IPV6 address record */
|
|
#define DNS_TYPE_SRV 33 /* Server selection (SRV) record */
|
|
#define DNS_TYPE_NSEC 47 /* DNS Next Secure Name */
|
|
|
|
|
|
char *cctype2str(cctype typ) {
|
|
switch (typ) {
|
|
case cctyp_unkn:
|
|
return "Unknown";
|
|
case cctyp_1:
|
|
return "One";
|
|
case cctyp_2:
|
|
return "Two";
|
|
case cctyp_Audio:
|
|
return "Audio";
|
|
case cctyp_Ultra:
|
|
return "Ultra";
|
|
case cctyp_Other:
|
|
return "Other";
|
|
default:
|
|
return "Unexpected";
|
|
}
|
|
}
|
|
|
|
#ifdef NEVER
|
|
/* Print out a V6 address in zero compresed form */
|
|
/* (It would be nice to add zero compression) */
|
|
static char *print_IPV6_Address(ORD8 *buf) {
|
|
static char sbuf[40], *p = sbuf;
|
|
int i;
|
|
for (i = 0; i < 8; i++) {
|
|
p += sprintf(p, "%x", buf[i * 2 + 1] * 256 + buf[i * 2 + 0]);
|
|
if (i < 7)
|
|
*p++ = ':';
|
|
}
|
|
*p++ = '\000';
|
|
return sbuf;
|
|
}
|
|
#endif
|
|
|
|
/* Write a DNS string to a buffer. String has max length of 63 */
|
|
/* (because values higher than that are used for a compression scheme.) */
|
|
/* Return the offset of the next byte after the string */
|
|
static int write_string(ORD8 *buf, int off, char *s) {
|
|
int len = strlen(s);
|
|
if (len > 63)
|
|
len = 63; // Hmm. Or what ?
|
|
buf[off] = len;
|
|
off++;
|
|
memcpy(buf + off, s, len);
|
|
off += len;
|
|
return off;
|
|
}
|
|
|
|
/* Return NZ on error */
|
|
static int init_mDNS() {
|
|
|
|
#ifdef NT
|
|
WSADATA data;
|
|
if (WSAStartup(MAKEWORD(2,2), &data)) {
|
|
DBG((g_log,0,"WSAStartup failed\n"))
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Setup the send and recieve socket */
|
|
/* Return nz on error */
|
|
static int init_socket_mDNS(SOCKET *psock) {
|
|
int nRet, nOptVal;
|
|
int off;
|
|
SOCKET sock;
|
|
struct sockaddr_in stSourceAddr;
|
|
struct ip_mreq stIpMreq;
|
|
|
|
DBG((g_log,0,"init_socket_mDNS() called\n"))
|
|
|
|
/* get a datagram (UDP) socket */
|
|
sock = socket(PF_INET, SOCK_DGRAM, 0);
|
|
if (sock == INVALID_SOCKET) {
|
|
DBG((g_log,0,"opening UDP socked failed with %d\n",ERRNO))
|
|
return 1;
|
|
}
|
|
|
|
/*----------------------- to send --------------------------- */
|
|
|
|
/* Theoretically, you do not need any special preparation to
|
|
* send to a multicast address. However, you may want a few
|
|
* things to overcome the limits of the default behavior
|
|
*/
|
|
|
|
// If we're doing a one-shot, we shouldn't transmit from port 5353, */
|
|
// but ChromCast won't see the packet if we don't. */
|
|
|
|
/* We can't send from port 5353 if someone else is using it, */
|
|
/* so set the SO_REUSEADDR option (which is enough for MSWin), */
|
|
/* and SO_REUSEPORT for OS X and Linux */
|
|
{
|
|
int on = 1;
|
|
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on))) {
|
|
DBG((g_log,0,"setsockopt(SO_REUSEADDR) failed with %d\n",ERRNO))
|
|
closesocket(sock);
|
|
return 1;
|
|
}
|
|
|
|
/* Need this to be able to open port on Unix like systems */
|
|
#ifndef NT
|
|
# ifndef SO_REUSEPORT
|
|
# ifdef __APPLE__
|
|
# define SO_REUSEPORT 0x0200
|
|
# else /* Linux */
|
|
# define SO_REUSEPORT 15
|
|
# endif
|
|
# endif
|
|
if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char *)&on, sizeof(on))) {
|
|
DBG((g_log,0,"setsockopt(SO_REUSEPORT) failed with %d\n",ERRNO))
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* init source address structure */
|
|
stSourceAddr.sin_family = PF_INET;
|
|
stSourceAddr.sin_port = htons(SOURCE_PORT);
|
|
stSourceAddr.sin_addr.s_addr = INADDR_ANY;
|
|
|
|
/*
|
|
* Calling bind() is not required, but some implementations need it
|
|
* before you can reference any multicast socket options
|
|
* and in this case we must be on port 5353 for ChromeCast to see the packet
|
|
*/
|
|
nRet = bind(sock, (struct sockaddr *)&stSourceAddr,
|
|
sizeof(struct sockaddr));
|
|
if (nRet == SOCKET_ERROR) {
|
|
DBG((g_log,0,"bind failed with %d\n",ERRNO))
|
|
closesocket(sock);
|
|
return 1;
|
|
}
|
|
|
|
/* - - - - - - - */
|
|
/* Send setup */
|
|
|
|
/* disable loopback of multicast datagrams we send, since the
|
|
* default--according to Steve Deering--is to loopback all
|
|
* datagrams sent on any interface which is a member of the
|
|
* destination group address of that datagram.
|
|
*/
|
|
nOptVal = 0;
|
|
nRet = setsockopt (sock, IPPROTO_IP, IP_MULTICAST_LOOP,
|
|
(char *)&nOptVal, sizeof(int));
|
|
if (nRet == SOCKET_ERROR) {
|
|
/* rather than notifying the user, we make note that this option
|
|
* failed. Some WinSocks don't support this option, and default
|
|
* with loopback disabled), so this failure is of no consequence.
|
|
* However, if we *do* get loop-backed data, we'll know why
|
|
*/
|
|
DBG((g_log,0,"[disabling loopback failed with %d]\n",ERRNO))
|
|
}
|
|
|
|
/* Is this desirable ? */
|
|
|
|
/* increase the IP TTL from the default of one to 64, so our
|
|
* multicast datagrams can get off of the local network
|
|
*/
|
|
nOptVal = 64;
|
|
nRet = setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,
|
|
(char *)&nOptVal, sizeof(int));
|
|
if (nRet == SOCKET_ERROR) {
|
|
DBG((g_log,0,"increasing TTL failed with %d\n",ERRNO))
|
|
closesocket(sock);
|
|
return 1;
|
|
}
|
|
|
|
if (psock != NULL)
|
|
*psock = sock;
|
|
|
|
/* - - - - - - - */
|
|
/* Recieve setup */
|
|
|
|
/* join the multicast group we want to receive datagrams from */
|
|
stIpMreq.imr_multiaddr.s_addr = inet_addr(DESTINATION_MCAST); /* group addr */
|
|
stIpMreq.imr_interface.s_addr = INADDR_ANY; /* use default */
|
|
nRet = setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
|
|
(char *)&stIpMreq, sizeof (struct ip_mreq));
|
|
|
|
if (nRet == SOCKET_ERROR) {
|
|
DBG((g_log,0,"registering for read events failed with %d\n",ERRNO))
|
|
closesocket(sock);
|
|
return 1;
|
|
}
|
|
|
|
/* Make this timeout after 100 msec second */
|
|
#ifdef NT
|
|
{
|
|
DWORD tv;
|
|
tv = 100;
|
|
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) < 0) {
|
|
DBG((g_log,0,"setsockopt timeout failed with %d\n",ERRNO))
|
|
closesocket(sock);
|
|
return 1;
|
|
}
|
|
}
|
|
#else
|
|
{
|
|
struct timeval tv;
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 100 * 1000;
|
|
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) < 0) {
|
|
DBG((g_log,0,"setsockopt timeout failed with %d\n",ERRNO))
|
|
closesocket(sock);
|
|
return 1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (psock != NULL)
|
|
*psock = sock;
|
|
|
|
DBG((g_log,0,"init mDNS socket succeed\n",ERRNO))
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Send an mDNS query */
|
|
/* On some platforms if we try to transmit & recieve at the same time (what ?) */
|
|
/* Return nz on error */
|
|
static int send_mDNS(SOCKET sock) {
|
|
int nRet;
|
|
int off;
|
|
ORD8 achOutBuf[BUFSIZE];
|
|
struct sockaddr_in stDestAddr;
|
|
|
|
DBG((g_log,0,"send_mDNS() called\n"))
|
|
|
|
/* Initialize the Destination Address structure and send */
|
|
stDestAddr.sin_family = PF_INET;
|
|
stDestAddr.sin_addr.s_addr = inet_addr(DESTINATION_MCAST);
|
|
stDestAddr.sin_port = htons(DESTINATION_PORT);
|
|
|
|
/* Setup the FQDN data packet to send */
|
|
write_ORD16_be(achOutBuf + 0, 0); /* ID */
|
|
write_ORD16_be(achOutBuf + 2, 0x8000); /* Flags - query */
|
|
write_ORD16_be(achOutBuf + 4, 1); /* QDCOUNT - one question */
|
|
write_ORD16_be(achOutBuf + 6, 0); /* ANCOUNT */
|
|
write_ORD16_be(achOutBuf + 8, 0); /* NSCOUNT */
|
|
write_ORD16_be(achOutBuf + 10, 0); /* ARCOUNT */
|
|
off = 12;
|
|
off = write_string(achOutBuf, off, "_googlecast");
|
|
off = write_string(achOutBuf, off, "_tcp");
|
|
off = write_string(achOutBuf, off, "local");
|
|
write_ORD8(achOutBuf + off, 0); /* null string */
|
|
off += 1;
|
|
write_ORD16_be(achOutBuf + off, DNS_TYPE_PTR); /* QTYPE */
|
|
off += 2;
|
|
/* Set top bit to get a unicast response (not for ChromeCast though) */
|
|
write_ORD16_be(achOutBuf + off, 0x0001); /* QCLASS */
|
|
off += 2;
|
|
|
|
nRet = sendto(sock, (char *)achOutBuf, off,
|
|
0,
|
|
(struct sockaddr *) &stDestAddr,
|
|
sizeof(struct sockaddr));
|
|
if (nRet == SOCKET_ERROR) {
|
|
DBG((g_log,0,"sending mDNS query failed with %d\n",ERRNO))
|
|
return 1;
|
|
}
|
|
|
|
DBG((g_log,0,"sending mDNS query succeed\n",ERRNO))
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int parse_dns(char **name, char **ip, cctype *ptyp, int *pcaflags, ORD8 *buf, int size);
|
|
|
|
/* Free up what get_ccids returned */
|
|
void free_ccids(ccast_id **ids) {
|
|
if (ids != NULL) {
|
|
int i;
|
|
|
|
for (i = 0; ids[i] != NULL; i++) {
|
|
free(ids[i]->name);
|
|
free(ids[i]->ip);
|
|
free(ids[i]);
|
|
}
|
|
free(ids);
|
|
}
|
|
}
|
|
|
|
/* Spend the given time waiting for replies. */
|
|
/* (Note than on MSWin this will be a minimum of 500msec) */
|
|
/* Add any ChromeCast replies to the list. */
|
|
/* return nz on error & free *ids */
|
|
static int receive_mDNS(SOCKET sock, ccast_id ***ids, int *nids, int emsec) {
|
|
unsigned int smsec;
|
|
unsigned int nSize;
|
|
int off, size;
|
|
ORD8 achInBuf[BUFSIZE];
|
|
struct sockaddr_in stSourceAddr;
|
|
|
|
DBG((g_log,0,"receive_mDNS() called with %d ids\n",*nids))
|
|
|
|
/* While there is still wait time */
|
|
for (smsec = msec_time(), emsec += smsec;msec_time() <= emsec;) {
|
|
int i;
|
|
char *name, *ip;
|
|
cctype typ;
|
|
int caflags; /* (We're not currently saving this) */
|
|
struct sockaddr stSockAddr;
|
|
|
|
/* Recv the available data */
|
|
nSize = sizeof(struct sockaddr);
|
|
size = recvfrom(sock, (char *)achInBuf,
|
|
BUFSIZE, 0, (struct sockaddr *) &stSockAddr, (socklen_t *)&nSize);
|
|
if (size == SOCKET_ERROR) {
|
|
if (ERRNO == UDP_SOCKET_TIMEOUT)
|
|
continue; /* Timeout */
|
|
DBG2((g_log,DLEV,"recvfrom failed with %d\n",ERRNO))
|
|
free_ccids(*ids);
|
|
*ids = NULL;
|
|
return 1;
|
|
}
|
|
|
|
DBG2((g_log,DLEVP1,"Got mDNS message length %d bytes\n",size))
|
|
#ifdef DEBUG
|
|
adump_bytes(g_log, " ", achInBuf, 0, size);
|
|
#endif
|
|
|
|
if (parse_dns(&name, &ip, &typ, &caflags, achInBuf, size) != 0) {
|
|
DBG((g_log,0,"Failed to parse the reply\n"))
|
|
} else {
|
|
DBG((g_log,0,"Parsed reply OK\n"))
|
|
|
|
/* If we found an entry */
|
|
if (name != NULL && ip != NULL) {
|
|
DBG((g_log,0,"Got a name '%s', IP '%s', type %s\n",name,ip, cctype2str(typ)))
|
|
|
|
/* Check if it is a Chromecast-Audio or Other */
|
|
if (typ == cctyp_Audio
|
|
|| typ == cctyp_Other) {
|
|
DBG((g_log,0,"Ignoring Chromecast-Audio/Other\n"))
|
|
free(name);
|
|
free(ip);
|
|
|
|
} else {
|
|
|
|
/* Check if it is a duplcate */
|
|
for (i = 0; i < *nids; i++) {
|
|
if (strcmp((*ids)[i]->name, name) == 0
|
|
&& strcmp((*ids)[i]->ip, ip) == 0)
|
|
break; /* yes */
|
|
}
|
|
if (i < *nids) {
|
|
DBG((g_log,0,"Duplicate\n"))
|
|
free(name);
|
|
free(ip);
|
|
} else {
|
|
ccast_id **tids;
|
|
if ((tids = realloc(*ids, (*nids + 2) * sizeof(ccast_id *))) == NULL
|
|
|| (*ids = tids, (*ids)[*nids] = malloc(sizeof(ccast_id)), (*ids)[*nids]) == NULL) {
|
|
DBG((g_log,0,"realloc/malloc fail\n"))
|
|
free(name);
|
|
free(ip);
|
|
free_ccids(*ids);
|
|
*ids = NULL;
|
|
return 1;
|
|
} else {
|
|
DBG((g_log,0,"Adding entry\n"))
|
|
(*ids)[*nids]->name = name;
|
|
(*ids)[*nids]->ip = ip;
|
|
(*ids)[*nids]->typ = typ;
|
|
(*nids)++;
|
|
(*ids)[*nids] = NULL; /* End marker */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DBG2((g_log,DLEVP1,"receive_mDNS() returning %d in list\n",*nids))
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
|
|
/* Get a list of Video output capable Chromecasts. Return NULL on error */
|
|
/* Last pointer in array is NULL */
|
|
/* Takes 1.5 second to return */
|
|
ccast_id **get_ccids(
|
|
) {
|
|
ccast_id **ids = NULL;
|
|
int nids = 0;
|
|
int i, j, k;
|
|
unsigned int smsec;
|
|
int waittime = 200;
|
|
SOCKET sock;
|
|
|
|
DBG2((g_log,DLEV,"get_ccids: called\n"))
|
|
|
|
if (init_mDNS()) {
|
|
DBG2((g_log,0,"get_ccids: init_mDNS() failed\n"))
|
|
return NULL;
|
|
}
|
|
|
|
if (init_socket_mDNS(&sock)) {
|
|
DBG2((g_log,0,"get_ccids: init_socket_mDNS() failed\n"))
|
|
return NULL;
|
|
}
|
|
|
|
|
|
smsec = msec_time();
|
|
|
|
/* Try a few times, with increasing response wait time */
|
|
for (k = 1;
|
|
((msec_time() - smsec) < 700)
|
|
|| (nids == 0 && (msec_time() - smsec) < 1600);
|
|
k++) {
|
|
|
|
DBG2((g_log,DLEV,"get_ccids: Sending mDNS query #%d:\n",k))
|
|
if (send_mDNS(sock)) {
|
|
DBG2((g_log,0,"get_ccids: send_mDNS() #1 failed\n"))
|
|
closesocket(sock);
|
|
return NULL;
|
|
}
|
|
|
|
DBG2((g_log,DLEV,"get_ccids: Waiting for mDNS reply #%d:\n",k))
|
|
if (receive_mDNS(sock, &ids, &nids, waittime)) {
|
|
DBG2((g_log,0,"get_ccids: receive_mDNS() #%d failed\n",k))
|
|
closesocket(sock);
|
|
return NULL;
|
|
}
|
|
DBG2((g_log,DLEV,"get_ccids: have %d %s\n",nids, nids == 1 ? "reply" : "replies"))
|
|
|
|
if (waittime < 500)
|
|
waittime = 500;
|
|
}
|
|
closesocket(sock);
|
|
|
|
/* If no ChromCasts found, return an empty list */
|
|
if (ids == NULL) {
|
|
DBG2((g_log,DLEV,"get_ccids: no devices found\n"))
|
|
if ((ids = calloc(sizeof(ccast_id *), 1)) == NULL) {
|
|
DBG2((g_log,0,"get_ccids: calloc fail\n"))
|
|
return NULL;
|
|
}
|
|
}
|
|
/* Sort the results so that it is stable */
|
|
for (i = 0; ids[i] != NULL && ids[i+1] != NULL; i++) {
|
|
for (j = i+1; ids[j] != NULL; j++) {
|
|
if (strcmp(ids[i]->name, ids[j]->name) > 0) {
|
|
ccast_id *tmp;
|
|
tmp = ids[i];
|
|
ids[i] = ids[j];
|
|
ids[j] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; ids[i] != NULL; i++) {
|
|
DBG2((g_log,DLEV," Entry %d:\n",i))
|
|
DBG2((g_log,DLEV," Name: %s\n",ids[i]->name))
|
|
DBG2((g_log,DLEV," IP: %s\n",ids[i]->ip))
|
|
DBG2((g_log,DLEV," Type: %s\n",cctype2str(ids[i]->typ)))
|
|
}
|
|
DBG2((g_log,DLEV,"get_ccids: Returning %d devices\n",i))
|
|
|
|
return ids;
|
|
}
|
|
|
|
void ccast_id_copy(ccast_id *dst, ccast_id *src) {
|
|
dst->name = strdup(src->name);
|
|
dst->ip = strdup(src->ip);
|
|
}
|
|
|
|
ccast_id *ccast_id_clone(ccast_id *src) {
|
|
ccast_id *dst;
|
|
|
|
if ((dst = calloc(sizeof(ccast_id), 1)) == NULL)
|
|
return dst;
|
|
|
|
if (src->name != NULL && (dst->name = strdup(src->name)) == NULL) {
|
|
free(dst);
|
|
return NULL;
|
|
}
|
|
if (src->ip != NULL && (dst->ip = strdup(src->ip)) == NULL) {
|
|
free(dst->name);
|
|
free(dst);
|
|
return NULL;
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
void ccast_id_del(ccast_id *id) {
|
|
free(id->name);
|
|
free(id->ip);
|
|
free(id);
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/* Read a string with a trailing '.' added. */
|
|
/* Return the next offset or -1 and *rv = NULL on error */
|
|
static int read_string_imp(char **rv, int *slen, ORD8 *buf, int off, int size, int rec) {
|
|
int len;
|
|
int d1 = 0;
|
|
|
|
DBG((g_log,8,"read_string_imp called for off %d (0x%x) rec %d\n",off,off,rec));
|
|
|
|
if (rec > 10) { /* Too many recursions */
|
|
DBG((g_log,8,"read_string_imp too many recursions\n"));
|
|
return -1; /* Error */
|
|
}
|
|
|
|
if (off >= size) {
|
|
DBG((g_log,8,"read_string_imp off %d >= size %d\n",off,size));
|
|
return -1; /* Error */
|
|
}
|
|
|
|
for (;;) {
|
|
DBG((g_log,8,"top of loop at off %d\n",off));
|
|
len = buf[off];
|
|
if ((len & 0xc0) == 0xc0) { /* Is it an offset marker */
|
|
int poff;
|
|
|
|
DBG((g_log,8,"got pointer\n"));
|
|
if ((size - off) < 2) {
|
|
DBG((g_log,8,"read_string_imp size %d - off %d < 2\n",size,off));
|
|
return -1;
|
|
}
|
|
|
|
poff = read_ORD16_be(buf + off); off += 2;
|
|
poff -= 0xc000;
|
|
|
|
if (poff < 0 || poff >= size) {
|
|
return -1;
|
|
}
|
|
|
|
DBG((g_log,8,"read_string_imp recursing\n"));
|
|
read_string_imp(rv, slen, buf, poff, size, rec+1);
|
|
|
|
if (slen != NULL) {
|
|
DBG((g_log,8,"after recurse, slen = %d, off = %d (0x%x)\n",*slen,off,off));
|
|
}
|
|
break; /* we're done */
|
|
|
|
} else {
|
|
DBG((g_log,8,"got string length %d\n",len));
|
|
|
|
off++;
|
|
if ((off + len) >= size) {
|
|
DBG((g_log,8,"read_string_imp size off %d + len %d >= size %d\n",off,len,size));
|
|
return -1;
|
|
}
|
|
|
|
if (len == 0) {
|
|
DBG((g_log,8,"read_string_imp len == 0 - done\n"));
|
|
break; /* we're done */
|
|
}
|
|
|
|
if (rv != NULL) {
|
|
memcpy(*rv, buf + off, len);
|
|
|
|
#ifdef DEBUG
|
|
(*rv)[len] = '\000';
|
|
DBG((g_log,8,"Copied string %p = '%s'\n",*rv,*rv));
|
|
#endif
|
|
|
|
*rv += len;
|
|
}
|
|
off += len;
|
|
|
|
if (slen != NULL) {
|
|
(*slen) += len;
|
|
DBG((g_log,8,"slen now = %d\n",*slen));
|
|
}
|
|
|
|
}
|
|
d1 = 1;
|
|
|
|
if (slen != NULL) {
|
|
(*slen)++;
|
|
}
|
|
|
|
if (rv != NULL) {
|
|
(*rv)[0] = '.';
|
|
(*rv)++;
|
|
}
|
|
}
|
|
if (slen != NULL) {
|
|
DBG((g_log,8,"returning slen = %d\n",*slen));
|
|
}
|
|
|
|
return off;
|
|
}
|
|
|
|
/* Read a string */
|
|
/* Return the next offset or -1 and *rv = NULL on error */
|
|
static int read_string(char **rv, ORD8 *buf, int off, int size) {
|
|
int len = 0, toff = off;
|
|
char *trv;
|
|
|
|
DBG((g_log,7,"read_string called for off 0x%x\n",off));
|
|
|
|
/* See how long it will be */
|
|
if ((toff = read_string_imp(NULL, &len, buf, off, size, 0)) < 0) {
|
|
DBG((g_log,7,"read_string_imp length returned error\n"));
|
|
return toff;
|
|
}
|
|
DBG((g_log,7,"read_string_imp got length %d\n",len));
|
|
|
|
if (len == 0) {
|
|
DBG((g_log,7,"Got zero length string\n"));
|
|
len++; /* Room for null string */
|
|
}
|
|
if ((*rv = trv = malloc(len)) == NULL) {
|
|
DBG((g_log,7,"malloc for string failed\n"));
|
|
return -1;
|
|
}
|
|
DBG((g_log,7,"loced %p\n",*rv));
|
|
off = read_string_imp(&trv, NULL, buf, off, size, 0);
|
|
if (off >= 0) {
|
|
(*rv)[len-1] = '\000';
|
|
DBG((g_log,7,"read string ok: %p = '%s'\n",*rv, *rv));
|
|
} else {
|
|
DBG((g_log,7,"reading string failed\n"));
|
|
}
|
|
return off;
|
|
}
|
|
|
|
/* Parse an mDNS query */
|
|
/* Return updated off value or -1 on error */
|
|
int parse_query(ORD8 *buf, int off, int size) {
|
|
char *sv;
|
|
int qtype, qclass;
|
|
|
|
DBG((g_log,0,"Parsing query at 0x%x:\n",off))
|
|
if ((off = read_string(&sv, buf, off, size)) < 0)
|
|
return -1;
|
|
DBG((g_log,0," Got string '%s'\n",sv))
|
|
free(sv);
|
|
|
|
if ((size - off) < 2)
|
|
return -1;
|
|
qtype = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," QTYPE = 0x%04x\n",qtype))
|
|
|
|
if ((size - off) < 2)
|
|
return -1;
|
|
qclass = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," QCLASS = 0x%04x\n",qtype))
|
|
|
|
return off;
|
|
}
|
|
|
|
/* Parse an mDNS reply, and set Friendly name (if known) + formal name + IP */
|
|
/* Return updated off value or -1 on error */
|
|
int parse_reply(int rrix, char **pname, char **pip, cctype *ptyp, int *pcaflags, ORD8 *buf, int off, int size) {
|
|
char *sv;
|
|
int rtype, rclass, rdlength;
|
|
unsigned int ttl;
|
|
|
|
DBG((g_log,0,"Parsing reply rrix %s at 0x%x:\n", rrix == 0 ? "AN" : rrix == 1 ? "NS" : "AR", off))
|
|
if ((off = read_string(&sv, buf, off, size)) < 0)
|
|
return -1;
|
|
|
|
DBG((g_log,0," Got string '%s', now off = 0x%x\n",sv,off))
|
|
|
|
if ((size - off) < 2) {
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
rtype = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," RTYPE = %d, now off = 0x%x\n",rtype,off))
|
|
|
|
if ((size - off) < 2) {
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
rclass = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," RCLASS = 0x%04x, now off = 0x%x\n",rclass,off))
|
|
/* rclass top bit is cache flush bit */
|
|
|
|
if ((rclass & 0x7fff) != DNS_CLASS_IN) {
|
|
DBG((g_log,0," response has RCLASS != 0x%04x\n",DNS_CLASS_IN))
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
|
|
if ((size - off) < 4)
|
|
return -1;
|
|
ttl = read_ORD32_be(buf + off); off += 4;
|
|
DBG((g_log,0," TTL = %u, now off = 0x%x\n",ttl,off))
|
|
|
|
if ((size - off) < 2)
|
|
return -1;
|
|
rdlength = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," RDLENGTH = %d, now off = 0x%x\n",rdlength,off))
|
|
|
|
if ((off + rdlength) > size) {
|
|
DBG((g_log,0," response RDLENGTH is longer than remaining buffer (%d)\n",size - off))
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
|
|
/* Just decode the replies we need */
|
|
if (rtype == DNS_TYPE_TXT) { /* Check it's a ChromeCast & get its name */
|
|
char *cp, *fn = NULL;
|
|
int rdsize = off + rdlength;
|
|
int caflags = 0;
|
|
|
|
if ((cp = strchr(sv, '.')) == NULL) {
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
*cp = '\000';
|
|
if (strcmp(cp+1, "_googlecast._tcp.local") != 0) {
|
|
DBG((g_log,0,"Not a chromecast (got '%s')\n",cp+1))
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
|
|
#ifdef NEVER
|
|
DBG((g_log,0," TXT data:\n"))
|
|
adump_bytes(g_log, " ", buf + off, 0, rdlength);
|
|
#endif
|
|
|
|
/* Chromecast TXT DATA format:
|
|
|
|
Buffer is series of strings, each beginning with a byte length.
|
|
(This is from <https://datatracker.ietf.org/doc/html/rfc6763>
|
|
Each string is id=value, with the following known values:
|
|
|
|
id=c17a8e82ee7187d5013e2d12c61bbd40 uuidNoHyphens
|
|
rm=669B9448366A01CB
|
|
ve=05 SW version ????
|
|
md=Chromecast Model ?
|
|
i.e. Chromecast-Audio
|
|
i.e. Chromecast-Ultra
|
|
i.e. Group
|
|
ic=/setup/icon.png Icon file ??
|
|
fn=Chromecast6892 Friendly name
|
|
ca=4101 Capabilities bits ???
|
|
st=1 Application running ??
|
|
bs=FA8FCA566645 Application ID ??
|
|
rs=Pattern generator ready Application state ??
|
|
|
|
*/
|
|
|
|
/* Parse the Chromacast TX Data */
|
|
for (; off < rdsize; ) {
|
|
int slen;
|
|
char *ss;
|
|
|
|
/* Read the string length */
|
|
if ((rdsize - off) < 1)
|
|
goto done_tx;
|
|
|
|
slen = read_ORD8(buf + off); off += 1;
|
|
|
|
if ((rdsize - off) < slen)
|
|
goto done_tx;
|
|
|
|
if ((ss = malloc(slen + 1)) == NULL) {
|
|
DBG((g_log,0,"malloc for sub-string failed\n"));
|
|
return -1;
|
|
}
|
|
|
|
memcpy(ss, buf + off, slen);
|
|
ss[slen] = '\000';
|
|
off += slen;
|
|
|
|
DBG((g_log,0," TX Sub-string '%s'\n", ss))
|
|
|
|
/* Record info we want: */
|
|
if (strncmp(ss, "fn=", 3) == 0) { /* Friendly name */
|
|
if ((fn = malloc(slen -3 +1)) == NULL) {
|
|
DBG((g_log,0,"malloc for fn-string failed\n"));
|
|
return -1;
|
|
}
|
|
strcpy(fn, ss + 3);
|
|
}
|
|
|
|
if (strncmp(ss, "ca=", 3) == 0) { /* Capability bits ? */
|
|
caflags = atoi(ss + 3);
|
|
}
|
|
free(ss);
|
|
}
|
|
|
|
done_tx:
|
|
|
|
if (fn != NULL) {
|
|
DBG((g_log,0," Chromacast '%s', fn '%s'\n", sv, fn))
|
|
} else {
|
|
DBG((g_log,0," Chromacast '%s'\n", sv))
|
|
}
|
|
|
|
DBG2((g_log,DLEV,"ca bits 0x%x\n", caflags))
|
|
|
|
if (strncmp(sv, "Chromecast-Ultra", 16) == 0) {
|
|
*ptyp = cctyp_Ultra;
|
|
} else if (strncmp(sv, "Chromecast-Audio", 16) == 0) {
|
|
*ptyp = cctyp_Audio;
|
|
} else {
|
|
// Hmm. A CC1 or non-Google device.
|
|
|
|
/* We're guessing that bit 0 of the ca bits == Video capable */
|
|
/* and bit 1 of the ca bits == Audio capable */
|
|
if (caflags & 1)
|
|
*ptyp = cctyp_1;
|
|
else if (caflags & 4)
|
|
*ptyp = cctyp_Audio;
|
|
else
|
|
*ptyp = cctyp_Other;
|
|
}
|
|
|
|
if (fn != NULL) {
|
|
*pname = fn;
|
|
} else {
|
|
free(fn);
|
|
if ((*pname = strdup(sv)) == NULL) {
|
|
DBG((g_log,0,"strdup failed\n"))
|
|
free(sv);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
off = rdsize;
|
|
|
|
} else if (rtype == DNS_TYPE_A) {
|
|
/* Should we check name matches ? */
|
|
if ((*pip = malloc(3 * 4 + 3 + 1)) == NULL) {
|
|
DBG((g_log,0,"malloc failed\n"))
|
|
free(*pname);
|
|
free(sv);
|
|
}
|
|
sprintf(*pip, "%d.%d.%d.%d", buf[off], buf[off+1], buf[off+2], buf[off+3]);
|
|
DBG((g_log,0," V4 address = %s\n",*pip))
|
|
|
|
off += rdlength;
|
|
} else if (rtype == DNS_TYPE_AAAA) { /* The IPV6 address */
|
|
/* Should we check name matches ? */
|
|
if ((*pip = malloc(8 * 4 + 7 + 1)) == NULL) {
|
|
DBG((g_log,0,"malloc failed\n"))
|
|
free(*pname);
|
|
free(sv);
|
|
}
|
|
sprintf(*pip, "%x:%x:%x:%x:%x:%x:%x:%x",
|
|
buf[off+0] * 256 + buf[off+1],
|
|
buf[off+2] * 256 + buf[off+3],
|
|
buf[off+4] * 256 + buf[off+5],
|
|
buf[off+6] * 256 + buf[off+7],
|
|
buf[off+8] * 256 + buf[off+9],
|
|
buf[off+10] * 256 + buf[off+11],
|
|
buf[off+12] * 256 + buf[off+13],
|
|
buf[off+14] * 256 + buf[off+15]);
|
|
DBG((g_log,0," V6 address = %s\n",*pip))
|
|
|
|
off += rdlength;
|
|
} else {
|
|
DBG((g_log,0," Skipping reply at 0x%x\n",off))
|
|
off += rdlength;
|
|
}
|
|
free(sv);
|
|
|
|
return off;
|
|
}
|
|
|
|
/* Parse an mDNS reply into a ChromCast name & IP address */
|
|
/* Allocate and return name and IP on finding ChromeCast reply, NULL otherwise */
|
|
/* Return nz on failure */
|
|
static int parse_dns(char **pname, char **pip, cctype *ptyp, int *pcaflags, ORD8 *buf, int size) {
|
|
int i, off = 0;
|
|
int id, flags, qdcount, ancount, nscount, arcount;
|
|
|
|
DBG((g_log,0,"Parsing mDNS reply:\n"))
|
|
|
|
*pname = NULL;
|
|
*pip = NULL;
|
|
|
|
// Parse reply header
|
|
if ((size - off) < 2) return 1;
|
|
id = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," ID = %d\n",id))
|
|
|
|
if ((size - off) < 2) return 1;
|
|
flags = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," FLAGS = 0x%04x\n",flags))
|
|
|
|
if ((size - off) < 2) return 1;
|
|
qdcount = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," QDCOUNT = %d\n",qdcount))
|
|
|
|
if ((size - off) < 2) return 1;
|
|
ancount = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," ANCOUNT = %d\n",ancount))
|
|
|
|
if ((size - off) < 2) return 1;
|
|
nscount = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," NSCOUNT = %d\n",nscount))
|
|
|
|
if ((size - off) < 2) return 1;
|
|
arcount = read_ORD16_be(buf + off); off += 2;
|
|
DBG((g_log,0," ARCOUNT = %d\n",arcount))
|
|
|
|
//printf("~1 after strings, off = 0x%x\n",off);
|
|
|
|
// Parse all the queries (QDCOUNT)
|
|
for (i = 0; i < qdcount; i++) {
|
|
DBG((g_log,0,"QD %d\n",i))
|
|
if ((off = parse_query(buf, off, size)) < 0) {
|
|
DBG((g_log,0," ### Parsing query failed ###\n"))
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Parse all the answers (ANCOUNT)
|
|
for (i = 0; i < ancount; i++) {
|
|
if ((off = parse_reply(0, pname, pip, ptyp, pcaflags, buf, off, size)) < 0) {
|
|
DBG((g_log,0," ### Parsing answer failed ###\n"))
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Parse all the NS records (NSCOUNT)
|
|
for (i = 0; i < nscount; i++) {
|
|
if ((off = parse_reply(1, pname, pip, ptyp, pcaflags, buf, off, size)) < 0) {
|
|
DBG((g_log,0," ### Parsing NS record failed ###\n"))
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Parse all the addition RR answers (ARCOUNT)
|
|
for (i = 0; i < arcount; i++) {
|
|
if ((off = parse_reply(2, pname, pip, ptyp, pcaflags, buf, off, size)) < 0) {
|
|
DBG((g_log,0," ### Parsing additional records failed ###\n"))
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Analysis of ChromeCast reply
|
|
|
|
0000: 00 00 84 00 00 00 00 01 00 00 00 05 0b 5f 67 6f ............._go
|
|
|
|
ID 0x0000 // ID 0
|
|
Flags 0x8400 // Standard query response, No error
|
|
QDCOUNT 0x0000 // No queries
|
|
ANCOUNT 0x0001 // 1 Answer
|
|
NSCOUNT 0x0000
|
|
ARCOUNT 0x0005 // 5 Additional records
|
|
|
|
// Answer
|
|
0b 5f 67 6f _go
|
|
0010: 6f 67 6c 65 63 61 73 74 oglecast
|
|
|
|
0010: 04 5f 74 63 70 _tcp
|
|
|
|
0010: 05 6c 6f lo
|
|
0020: 63 61 6c 00 cal.
|
|
|
|
"_googlecast" "_tcp" "cal" "" //TLD
|
|
|
|
0020: 00 0c 00 01 00 00 11 94 00 11 0e 43 ...........C
|
|
|
|
RTYPE 0x000c
|
|
RCLASS 0x0001
|
|
TTL 0x00001194
|
|
RDLENGTH 0x0011
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
*/
|
|
|
|
/*
|
|
.ca values:
|
|
|
|
Original CC 5 = 0x0005
|
|
Latest CC1 4101 = 0x1005
|
|
Latest CC2 4101 = 0x1005
|
|
Latest CC3 4101 = 0x1005
|
|
Vizio TV 2053 = 0x0805
|
|
Toshiba TV 4101 = 0x1005
|
|
Audio CC 4100 = 0x1004
|
|
Sound bar 2052 = 0x0804
|
|
|
|
Google documents the following Capability strings:
|
|
|
|
VIDEO_OUT The receiver supports video output.
|
|
VIDEO_IN The receiver supports video input (camera).
|
|
AUDIO_OUT The receiver supports audio output.
|
|
AUDIO_IN The receiver supports audio input (microphone).
|
|
MULTIZONE_GROUP The receiver represents a multi-zone group.
|
|
|
|
Which may correspond to ca bits 0, 1, 2, 3 respectively ?
|
|
|
|
*/
|
|
|
|
/*
|
|
Original CC 1 dump ?
|
|
|
|
0000: 00 00 84 00 00 00 00 01 00 00 00 05 0b 5f 67 6f ............._go
|
|
0010: 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f oglecast._tcp.lo
|
|
0020: 63 61 6c 00 00 0c 00 01 00 00 11 94 00 11 0e 43 cal............C
|
|
0030: 68 72 6f 6d 65 63 61 73 74 36 38 39 32 c0 0c c0 hromecast6892...
|
|
0040: 2e 00 10 80 01 00 00 11 94 00 67 23 69 64 3d 63 ..........g#id=c
|
|
0050: 31 37 61 65 38 38 32 65 37 65 31 38 37 35 64 30 17ae882e7e1875d0
|
|
0060: 31 33 65 64 32 31 32 63 31 36 62 62 64 34 30 05 13ed212c16bbd40.
|
|
0070: 76 65 3d 30 32 0d 6d 64 3d 43 68 72 6f 6d 65 63 ve=02.md=Chromec
|
|
0080: 61 73 74 12 69 63 3d 2f 73 65 74 75 70 2f 69 63 ast.ic=/setup/ic
|
|
0090: 6f 6e 2e 70 6e 67 11 66 6e 3d 43 68 72 6f 6d 65 on.png.fn=Chrome
|
|
00a0: 63 61 73 74 36 38 39 32 04 63 61 3d 35 04 73 74 cast6892.ca=5.st
|
|
00b0: 3d 30 c0 2e 00 21 80 01 00 00 00 78 00 17 00 00 =0...!.....x....
|
|
00c0: 00 00 1f 49 0e 43 68 72 6f 6d 65 63 61 73 74 36 ...I.Chromecast6
|
|
00d0: 38 39 32 c0 1d c0 c4 00 01 80 01 00 00 00 78 00 892...........x.
|
|
00e0: 04 0a 00 00 80 c0 2e 00 2f 80 01 00 00 11 94 00 ......../.......
|
|
00f0: 09 c0 2e 00 05 00 00 80 00 40 c0 c4 00 2f 80 01 .........@.../..
|
|
0100: 00 00 00 78 00 05 c0 c4 00 01 40 ...x......@
|
|
|
|
*/
|
|
|
|
/*
|
|
|
|
ChromeCast 1 dump
|
|
|
|
0000: 00 00 84 00 00 00 00 01 00 00 00 03 0b 5f 67 6f ............._go
|
|
0010: 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f oglecast._tcp.lo
|
|
0020: 63 61 6c 00 00 0c 00 01 00 00 00 78 00 44 2b 43 cal........x.D+C
|
|
0030: 68 72 6f 6d 65 63 61 73 74 2d 63 31 37 61 65 38 hromecast-c17ae8
|
|
0040: 38 32 65 37 65 31 38 37 35 64 30 31 33 65 64 32 82e7e1875d013ed2
|
|
0050: 31 32 63 31 36 62 62 64 34 30 0b 5f 67 6f 6f 67 12c16bbd40._goog
|
|
0060: 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f 63 61 lecast._tcp.loca
|
|
0070: 6c 00 2b 43 68 72 6f 6d 65 63 61 73 74 2d 63 31 l.+Chromecast-c1
|
|
0080: 37 61 65 38 38 32 65 37 65 31 38 37 35 64 30 31 7ae882e7e1875d01
|
|
0090: 33 65 64 32 31 32 63 31 36 62 62 64 34 30 0b 5f 3ed212c16bbd40._
|
|
00a0: 67 6f 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 googlecast._tcp.
|
|
00b0: 6c 6f 63 61 6c 00 00 10 80 01 00 00 11 94 00 82 local...........
|
|
00c0: 23 69 64 3d 63 31 37 61 65 38 38 32 65 37 65 31 #id=c17ae882e7e1
|
|
00d0: 38 37 35 64 30 31 33 65 64 32 31 32 63 31 36 62 875d013ed212c16b
|
|
00e0: 62 64 34 30 03 72 6d 3d 05 76 65 3d 30 35 0d 6d bd40.rm=.ve=05.m
|
|
00f0: 64 3d 43 68 72 6f 6d 65 63 61 73 74 12 69 63 3d d=Chromecast.ic=
|
|
0100: 2f 73 65 74 75 70 2f 69 63 6f 6e 2e 70 6e 67 11 /setup/icon.png.
|
|
0110: 66 6e 3d 43 68 72 6f 6d 65 63 61 73 74 36 38 39 fn=Chromecast689
|
|
0120: 32 07 63 61 3d 34 31 30 31 04 73 74 3d 30 0f 62 2.ca=4101.st=0.b
|
|
0130: 73 3d 46 41 38 46 43 41 35 36 36 36 34 35 03 72 s=FA8FCA566645.r
|
|
0140: 73 3d 2b 43 68 72 6f 6d 65 63 61 73 74 2d 63 31 s=+Chromecast-c1
|
|
0150: 37 61 65 38 38 32 65 37 65 31 38 37 35 64 30 31 7ae882e7e1875d01
|
|
0160: 33 65 64 32 31 32 63 31 36 62 62 64 34 30 0b 5f 3ed212c16bbd40._
|
|
0170: 67 6f 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 googlecast._tcp.
|
|
0180: 6c 6f 63 61 6c 00 00 21 80 01 00 00 00 78 00 32 local..!.....x.2
|
|
0190: 00 00 00 00 1f 49 24 63 31 37 61 65 38 38 32 2d .....I$c17ae882-
|
|
01a0: 65 37 65 31 2d 38 37 35 64 2d 30 31 33 65 2d 64 e7e1-875d-013e-d
|
|
01b0: 32 31 32 63 31 36 62 62 64 34 30 05 6c 6f 63 61 212c16bbd40.loca
|
|
01c0: 6c 00 24 63 31 37 61 65 38 38 32 2d 65 37 65 31 l.$c17ae882-e7e1
|
|
01d0: 2d 38 37 35 64 2d 30 31 33 65 2d 64 32 31 32 63 -875d-013e-d212c
|
|
01e0: 31 36 62 62 64 34 30 05 6c 6f 63 61 6c 00 00 01 16bbd40.local...
|
|
01f0: 80 01 00 00 00 78 00 04 c0 a8 01 68 .....x.....h
|
|
|
|
|
|
ChromeCast 2 dump:
|
|
|
|
0000: 00 00 84 00 00 00 00 01 00 00 00 03 0b 5f 67 6f ............._go
|
|
0010: 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f oglecast._tcp.lo
|
|
0020: 63 61 6c 00 00 0c 00 01 00 00 00 78 00 44 2b 43 cal........x.D+C
|
|
0030: 68 72 6f 6d 65 63 61 73 74 2d 38 32 38 30 30 66 hromecast-82800f
|
|
0040: 65 63 33 37 37 36 35 39 38 66 35 38 64 34 61 61 ec3776598f58d4aa
|
|
0050: 63 39 65 36 35 64 30 30 30 61 0b 5f 67 6f 6f 67 c9e65d000a._goog
|
|
0060: 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f 63 61 lecast._tcp.loca
|
|
0070: 6c 00 2b 43 68 72 6f 6d 65 63 61 73 74 2d 38 32 l.+Chromecast-82
|
|
0080: 38 30 30 66 65 63 33 37 37 36 35 39 38 66 35 38 800fec3776598f58
|
|
0090: 64 34 61 61 63 39 65 36 35 64 30 30 30 61 0b 5f d4aac9e65d000a._
|
|
00a0: 67 6f 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 googlecast._tcp.
|
|
00b0: 6c 6f 63 61 6c 00 00 10 80 01 00 00 11 94 00 a9 local...........
|
|
00c0: 23 69 64 3d 38 32 38 30 30 66 65 63 33 37 37 36 #id=82800fec3776
|
|
00d0: 35 39 38 66 35 38 64 34 61 61 63 39 65 36 35 64 598f58d4aac9e65d
|
|
00e0: 30 30 30 61 13 72 6d 3d 36 36 39 42 39 34 34 38 000a.rm=669B9448
|
|
00f0: 36 33 36 30 41 31 43 42 05 76 65 3d 30 35 0d 6d 6360A1CB.ve=05.m
|
|
0100: 64 3d 43 68 72 6f 6d 65 63 61 73 74 12 69 63 3d d=Chromecast.ic=
|
|
0110: 2f 73 65 74 75 70 2f 69 63 6f 6e 2e 70 6e 67 11 /setup/icon.png.
|
|
0120: 66 6e 3d 43 68 72 6f 6d 65 63 61 73 74 36 35 30 fn=Chromecast650
|
|
0130: 32 07 63 61 3d 34 31 30 31 04 73 74 3d 31 0f 62 2.ca=4101.st=1.b
|
|
0140: 73 3d 46 41 38 46 43 41 39 45 33 45 30 31 1a 72 s=FA8FCA9E3E01.r
|
|
0150: 73 3d 50 61 74 74 65 72 6e 20 67 65 6e 65 72 61 s=Pattern genera
|
|
0160: 74 6f 72 20 72 65 61 64 79 2b 43 68 72 6f 6d 65 tor ready+Chrome
|
|
0170: 63 61 73 74 2d 38 32 38 30 30 66 65 63 33 37 37 cast-82800fec377
|
|
0180: 36 35 39 38 66 35 38 64 34 61 61 63 39 65 36 35 6598f58d4aac9e65
|
|
0190: 64 30 30 30 61 0b 5f 67 6f 6f 67 6c 65 63 61 73 d000a._googlecas
|
|
01a0: 74 04 5f 74 63 70 05 6c 6f 63 61 6c 00 00 21 80 t._tcp.local..!.
|
|
01b0: 01 00 00 00 78 00 32 00 00 00 00 1f 49 24 38 32 ....x.2.....I$82
|
|
01c0: 38 30 30 66 65 63 2d 33 37 37 36 2d 35 39 38 66 800fec-3776-598f
|
|
01d0: 2d 35 38 64 34 2d 61 61 63 39 65 36 35 64 30 30 -58d4-aac9e65d00
|
|
01e0: 30 61 05 6c 6f 63 61 6c 00 24 38 32 38 30 30 66 0a.local.$82800f
|
|
01f0: 65 63 2d 33 37 37 36 2d 35 39 38 66 2d 35 38 64 ec-3776-598f-58d
|
|
0200: 34 2d 61 61 63 39 65 36 35 64 30 30 30 61 05 6c 4-aac9e65d000a.l
|
|
0210: 6f 63 61 6c 00 00 01 80 01 00 00 00 78 00 04 0a ocal........x...
|
|
0220: 00 00 80 ...
|
|
|
|
ChromeCast 3 (Ultra)
|
|
|
|
0000: 00 00 84 00 00 00 00 01 00 00 00 03 0b 5f 67 6f ............._go
|
|
0010: 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f oglecast._tcp.lo
|
|
0020: 63 61 6c 00 00 0c 00 01 00 00 00 78 00 34 31 43 cal........x.41C
|
|
0030: 68 72 6f 6d 65 63 61 73 74 2d 55 6c 74 72 61 2d hromecast-Ultra-
|
|
0040: 32 38 36 65 36 63 63 65 38 34 65 32 38 65 61 33 286e6cce84e28ea3
|
|
0050: 36 63 37 63 33 31 39 32 66 33 35 62 65 30 33 65 6c7c3192f35be03e
|
|
0060: c0 0c c0 2e 00 10 80 01 00 00 11 94 00 8d 23 69 ..............#i
|
|
0070: 64 3d 32 38 36 65 36 63 63 65 38 34 65 32 38 65 d=286e6cce84e28e
|
|
0080: 61 33 36 63 37 63 33 31 39 32 66 33 35 62 65 30 a36c7c3192f35be0
|
|
0090: 33 65 03 72 6d 3d 05 76 65 3d 30 35 13 6d 64 3d 3e.rm=.ve=05.md=
|
|
00a0: 43 68 72 6f 6d 65 63 61 73 74 20 55 6c 74 72 61 Chromecast Ultra
|
|
00b0: 12 69 63 3d 2f 73 65 74 75 70 2f 69 63 6f 6e 2e .ic=/setup/icon.
|
|
00c0: 70 6e 67 16 66 6e 3d 43 68 72 6f 6d 65 63 61 73 png.fn=Chromecas
|
|
00d0: 74 55 6c 74 72 61 36 32 35 30 07 63 61 3d 34 31 tUltra6250.ca=41
|
|
00e0: 30 31 04 73 74 3d 30 0f 62 73 3d 46 41 38 46 43 01.st=0.bs=FA8FC
|
|
00f0: 41 37 30 38 35 42 35 03 72 73 3d c0 2e 00 21 80 A7085B5.rs=...!.
|
|
0100: 01 00 00 00 78 00 2d 00 00 00 00 1f 49 24 32 38 ....x.-.....I$28
|
|
0110: 36 65 36 63 63 65 2d 38 34 65 32 2d 38 65 61 33 6e6cce-84e2-8ea3
|
|
0120: 2d 36 63 37 63 2d 33 31 39 32 66 33 35 62 65 30 -6c7c-3192f35be0
|
|
0130: 33 65 c0 1d c1 0d 00 01 80 01 00 00 00 78 00 04 3e...........x..
|
|
0140: 0a 00 01 1d ....
|
|
|
|
*/
|
|
|
|
|
|
/* Other non-Google devices:
|
|
|
|
Vizio M60-D1 :- Smart TV
|
|
|
|
00000000 00 00 84 00 00 00 00 01 00 00 00 03 0b 5f 67 6f |............._go|
|
|
00000010 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f |oglecast._tcp.lo|
|
|
00000020 63 61 6c 00 00 0c 00 01 00 00 00 78 00 40 27 4d |cal........x.@'M|
|
|
00000030 36 30 2d 44 31 2d 37 63 35 35 36 37 39 31 35 38 |60-D1-7c55679158|
|
|
00000040 38 64 63 64 33 37 63 37 37 32 33 31 61 30 64 64 |8dcd37c77231a0dd|
|
|
00000050 31 35 61 35 30 36 0b 5f 67 6f 6f 67 6c 65 63 61 |15a506._googleca|
|
|
00000060 73 74 04 5f 74 63 70 05 6c 6f 63 61 6c 00 27 4d |st._tcp.local.'M|
|
|
00000070 36 30 2d 44 31 2d 37 63 35 35 36 37 39 31 35 38 |60-D1-7c55679158|
|
|
00000080 38 64 63 64 33 37 63 37 37 32 33 31 61 30 64 64 |8dcd37c77231a0dd|
|
|
00000090 31 35 61 35 30 36 0b 5f 67 6f 6f 67 6c 65 63 61 |15a506._googleca|
|
|
000000a0 73 74 04 5f 74 63 70 05 6c 6f 63 61 6c 00 00 10 |st._tcp.local...|
|
|
000000b0 80 01 00 00 11 94 00 80 23 69 64 3d 37 63 35 35 |........#id=7c55|
|
|
000000c0 36 37 39 31 35 38 38 64 63 64 33 37 63 37 37 32 |6791588dcd37c772|
|
|
000000d0 33 31 61 30 64 64 31 35 61 35 30 36 13 72 6d 3d |31a0dd15a506.rm=|
|
|
000000e0 31 33 39 46 33 32 34 34 38 34 44 46 41 39 32 34 |139F324484DFA924|
|
|
000000f0 05 76 65 3d 30 35 09 6d 64 3d 4d 36 30 2d 44 31 |.ve=05.md=M60-D1|
|
|
00000100 12 69 63 3d 2f 73 65 74 75 70 2f 69 63 6f 6e 2e |.ic=/setup/icon.|
|
|
00000110 70 6e 67 0f 66 6e 3d 56 69 7a 69 6f 20 4d 36 30 |png.fn=Vizio M60|
|
|
00000120 2d 44 31 07 63 61 3d 32 30 35 33 04 73 74 3d 30 |-D1.ca=2053.st=0|
|
|
00000130 03 62 73 3d 03 72 73 3d 27 4d 36 30 2d 44 31 2d |.bs=.rs='M60-D1-|
|
|
00000140 37 63 35 35 36 37 39 31 35 38 38 64 63 64 33 37 |7c556791588dcd37|
|
|
00000150 63 37 37 32 33 31 61 30 64 64 31 35 61 35 30 36 |c77231a0dd15a506|
|
|
00000160 0b 5f 67 6f 6f 67 6c 65 63 61 73 74 04 5f 74 63 |._googlecast._tc|
|
|
00000170 70 05 6c 6f 63 61 6c 00 00 21 80 01 00 00 00 78 |p.local..!.....x|
|
|
00000180 00 32 00 00 00 00 1f 49 24 37 63 35 35 36 37 39 |.2.....I$7c55679|
|
|
00000190 31 2d 35 38 38 64 2d 63 64 33 37 2d 63 37 37 32 |1-588d-cd37-c772|
|
|
000001a0 2d 33 31 61 30 64 64 31 35 61 35 30 36 05 6c 6f |-31a0dd15a506.lo|
|
|
000001b0 63 61 6c 00 24 37 63 35 35 36 37 39 31 2d 35 38 |cal.$7c556791-58|
|
|
000001c0 38 64 2d 63 64 33 37 2d 63 37 37 32 2d 33 31 61 |8d-cd37-c772-31a|
|
|
000001d0 30 64 64 31 35 61 35 30 36 05 6c 6f 63 61 6c 00 |0dd15a506.local.|
|
|
000001e0 00 01 80 01 00 00 00 78 00 04 c0 a8 00 3e |.......x.....>|
|
|
000001ee
|
|
|
|
SmartCast Sound Bar 3851-D0 :- Sound bar
|
|
|
|
00000000 00 00 84 00 00 00 00 01 00 00 00 03 0b 5f 67 6f |............._go|
|
|
00000010 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f |oglecast._tcp.lo|
|
|
00000020 63 61 6c 00 00 0c 00 01 00 00 00 78 00 22 09 53 |cal........x.".S|
|
|
00000030 42 33 38 35 31 2d 44 30 0b 5f 67 6f 6f 67 6c 65 |B3851-D0._google|
|
|
00000040 63 61 73 74 04 5f 74 63 70 05 6c 6f 63 61 6c 00 |cast._tcp.local.|
|
|
00000050 09 53 42 33 38 35 31 2d 44 30 0b 5f 67 6f 6f 67 |.SB3851-D0._goog|
|
|
00000060 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f 63 61 |lecast._tcp.loca|
|
|
00000070 6c 00 00 10 80 01 00 00 11 94 00 8a 23 69 64 3d |l...........#id=|
|
|
00000080 64 35 36 63 33 66 30 39 36 66 30 31 30 31 36 66 |d56c3f096f01016f|
|
|
00000090 33 64 37 30 65 62 62 63 36 32 36 34 65 66 33 31 |3d70ebbc6264ef31|
|
|
000000a0 05 76 65 3d 30 34 1e 6d 64 3d 53 6d 61 72 74 43 |.ve=04.md=SmartC|
|
|
000000b0 61 73 74 20 53 6f 75 6e 64 20 42 61 72 20 33 38 |ast Sound Bar 38|
|
|
000000c0 35 31 2d 44 30 12 69 63 3d 2f 73 65 74 75 70 2f |51-D0.ic=/setup/|
|
|
000000d0 69 63 6f 6e 2e 70 6e 67 0c 66 6e 3d 53 42 33 38 |icon.png.fn=SB38|
|
|
000000e0 35 31 2d 44 30 07 63 61 3d 32 30 35 32 04 73 74 |51-D0.ca=2052.st|
|
|
000000f0 3d 30 0f 62 73 3d 46 46 46 46 46 46 46 46 46 46 |=0.bs=FFFFFFFFFF|
|
|
00000100 46 46 03 72 73 3d 09 53 42 33 38 35 31 2d 44 30 |FF.rs=.SB3851-D0|
|
|
00000110 0b 5f 67 6f 6f 67 6c 65 63 61 73 74 04 5f 74 63 |._googlecast._tc|
|
|
00000120 70 05 6c 6f 63 61 6c 00 00 21 80 01 00 00 00 78 |p.local..!.....x|
|
|
00000130 00 17 00 00 00 00 1f 49 09 53 42 33 38 35 31 2d |.......I.SB3851-|
|
|
00000140 44 30 05 6c 6f 63 61 6c 00 09 53 42 33 38 35 31 |D0.local..SB3851|
|
|
00000150 2d 44 30 05 6c 6f 63 61 6c 00 00 01 80 01 00 00 |-D0.local.......|
|
|
00000160 00 78 00 04 c0 a8 00 26 |.x.....&|
|
|
00000168
|
|
|
|
ZChromecast Toshiba :- TV ?
|
|
|
|
00000000 00 00 84 00 00 00 00 01 00 00 00 03 0b 5f 67 6f |............._go|
|
|
00000010 6f 67 6c 65 63 61 73 74 04 5f 74 63 70 05 6c 6f |oglecast._tcp.lo|
|
|
00000020 63 61 6c 00 00 0c 00 01 00 00 00 78 00 2e 2b 43 |cal........x..+C|
|
|
00000030 68 72 6f 6d 65 63 61 73 74 2d 64 61 30 33 34 62 |hromecast-da034b|
|
|
00000040 31 39 30 30 34 34 62 63 38 65 35 33 30 31 34 37 |190044bc8e530147|
|
|
00000050 63 37 61 65 33 32 63 35 61 33 c0 0c c0 2e 00 10 |c7ae32c5a3......|
|
|
00000060 80 01 00 00 11 94 00 87 23 69 64 3d 64 61 30 33 |........#id=da03|
|
|
00000070 34 62 31 39 30 30 34 34 62 63 38 65 35 33 30 31 |4b190044bc8e5301|
|
|
00000080 34 37 63 37 61 65 33 32 63 35 61 33 03 72 6d 3d |47c7ae32c5a3.rm=|
|
|
00000090 05 76 65 3d 30 35 0d 6d 64 3d 43 68 72 6f 6d 65 |.ve=05.md=Chrome|
|
|
000000a0 63 61 73 74 12 69 63 3d 2f 73 65 74 75 70 2f 69 |cast.ic=/setup/i|
|
|
000000b0 63 6f 6e 2e 70 6e 67 16 66 6e 3d 5a 43 68 72 6f |con.png.fn=ZChro|
|
|
000000c0 6d 65 63 61 73 74 20 54 6f 73 68 69 62 61 07 63 |mecast Toshiba.c|
|
|
000000d0 61 3d 34 31 30 31 04 73 74 3d 30 0f 62 73 3d 46 |a=4101.st=0.bs=F|
|
|
000000e0 41 38 46 43 41 37 30 32 30 36 32 03 72 73 3d c0 |A8FCA702062.rs=.|
|
|
000000f0 2e 00 21 80 01 00 00 00 78 00 2d 00 00 00 00 1f |..!.....x.-.....|
|
|
00000100 49 24 64 61 30 33 34 62 31 39 2d 30 30 34 34 2d |I$da034b19-0044-|
|
|
00000110 62 63 38 65 2d 35 33 30 31 2d 34 37 63 37 61 65 |bc8e-5301-47c7ae|
|
|
00000120 33 32 63 35 61 33 c0 1d c1 01 00 01 80 01 00 00 |32c5a3..........|
|
|
00000130 00 78 00 04 c0 a8 00 1b |.x......|
|
|
00000138
|
|
|
|
*/
|
|
|
|
/*
|
|
|
|
Can get info from Chromecast http server at http://XX.XX.XX.XX:8008/ssdp/device-desc.xml
|
|
|
|
Get info about the app running: http://XX.XX.XX.XX:8008/apps/ChromeCast
|
|
|
|
*/
|
|
|
|
|