diff --git a/components/lightsnapcast/component.mk b/components/lightsnapcast/component.mk new file mode 100644 index 0000000..3272a3c --- /dev/null +++ b/components/lightsnapcast/component.mk @@ -0,0 +1,2 @@ +COMPONENT_SRCDIRS := . +# CFLAGS += diff --git a/components/lightsnapcast/include/snapcast.h b/components/lightsnapcast/include/snapcast.h new file mode 100644 index 0000000..8acc293 --- /dev/null +++ b/components/lightsnapcast/include/snapcast.h @@ -0,0 +1,108 @@ +#ifndef __SNAPCAST_H__ +#define __SNAPCAST_H__ + +#include +#include +#include + +enum message_type { + SNAPCAST_MESSAGE_BASE = 0, + SNAPCAST_MESSAGE_CODEC_HEADER = 1, + SNAPCAST_MESSAGE_WIRE_CHUNK = 2, + SNAPCAST_MESSAGE_SERVER_SETTINGS = 3, + SNAPCAST_MESSAGE_TIME = 4, + SNAPCAST_MESSAGE_HELLO = 5, + SNAPCAST_MESSAGE_STREAM_TAGS = 6, + + SNAPCAST_MESSAGE_FIRST = SNAPCAST_MESSAGE_BASE, + SNAPCAST_MESSAGE_LAST = SNAPCAST_MESSAGE_STREAM_TAGS +}; + +typedef struct tv { + int32_t sec; + int32_t usec; +} tv_t; + +typedef struct base_message { + uint16_t type; + uint16_t id; + uint16_t refersTo; + tv_t sent; + tv_t received; + uint32_t size; +} base_message_t; + +extern const int BASE_MESSAGE_SIZE; +extern const int TIME_MESSAGE_SIZE; + +int base_message_serialize(base_message_t *msg, char *data, uint32_t size); + +int base_message_deserialize(base_message_t *msg, const char *data, uint32_t size); + +/* Sample Hello message +{ + "Arch": "x86_64", + "ClientName": "Snapclient", + "HostName": "my_hostname", + "ID": "00:11:22:33:44:55", + "Instance": 1, + "MAC": "00:11:22:33:44:55", + "OS": "Arch Linux", + "SnapStreamProtocolVersion": 2, + "Version": "0.17.1" +} +*/ + +typedef struct hello_message { + char *mac; + char *hostname; + char *version; + char *client_name; + char *os; + char *arch; + int instance; + char *id; + int protocol_version; +} hello_message_t; + +char* hello_message_serialize(hello_message_t* msg, size_t *size); + +typedef struct server_settings_message { + int32_t buffer_ms; + int32_t latency; + uint32_t volume; + bool muted; +} server_settings_message_t; + +int server_settings_message_deserialize(server_settings_message_t *msg, const char *json_str); + +typedef struct codec_header_message { + char *codec; + uint32_t size; + char *payload; +} codec_header_message_t; + +int codec_header_message_deserialize(codec_header_message_t *msg, const char *data, uint32_t size); +void codec_header_message_free(codec_header_message_t *msg); + +typedef struct wire_chunk_message { + tv_t timestamp; + uint32_t size; + char *payload; +} wire_chunk_message_t; + +// TODO currently copies, could be made to not copy probably +int wire_chunk_message_deserialize(wire_chunk_message_t *msg, const char *data, uint32_t size); +void wire_chunk_message_free(wire_chunk_message_t *msg); + +typedef struct time_message { + tv_t latency; +} time_message_t; + +int time_message_serialize(time_message_t *msg, char *data, uint32_t size); +int time_message_deserialize(time_message_t *msg, const char *data, uint32_t size); + + + + +#endif // __SNAPCAST_H__ diff --git a/components/lightsnapcast/snapcast.c b/components/lightsnapcast/snapcast.c new file mode 100644 index 0000000..a32346d --- /dev/null +++ b/components/lightsnapcast/snapcast.c @@ -0,0 +1,309 @@ +#include "snapcast.h" + +#ifdef ESP_PLATFORM +// The ESP-IDF changes the include directory for cJSON +#include +#else +#include +#endif + +#include +#include +#include +#include +#include + +const int BASE_MESSAGE_SIZE = 26; +const int TIME_MESSAGE_SIZE = 8; + +int base_message_serialize(base_message_t *msg, char *data, uint32_t size) { + write_buffer_t buffer; + int result = 0; + + buffer_write_init(&buffer, data, size); + + result |= buffer_write_uint16(&buffer, msg->type); + result |= buffer_write_uint16(&buffer, msg->id); + result |= buffer_write_uint16(&buffer, msg->refersTo); + result |= buffer_write_int32(&buffer, msg->sent.sec); + result |= buffer_write_int32(&buffer, msg->sent.usec); + result |= buffer_write_int32(&buffer, msg->received.sec); + result |= buffer_write_int32(&buffer, msg->received.usec); + result |= buffer_write_uint32(&buffer, msg->size); + + return result; +} + +int base_message_deserialize(base_message_t *msg, const char *data, uint32_t size) { + read_buffer_t buffer; + int result = 0; + + buffer_read_init(&buffer, data, size); + + result |= buffer_read_uint16(&buffer, &(msg->type)); + result |= buffer_read_uint16(&buffer, &(msg->id)); + result |= buffer_read_uint16(&buffer, &(msg->refersTo)); + result |= buffer_read_int32(&buffer, &(msg->sent.sec)); + result |= buffer_read_int32(&buffer, &(msg->sent.usec)); + result |= buffer_read_int32(&buffer, &(msg->received.sec)); + result |= buffer_read_int32(&buffer, &(msg->received.usec)); + result |= buffer_read_uint32(&buffer, &(msg->size)); + + return result; +} + +static cJSON* hello_message_to_json(hello_message_t *msg) { + cJSON *mac; + cJSON *hostname; + cJSON *version; + cJSON *client_name; + cJSON *os; + cJSON *arch; + cJSON *instance; + cJSON *id; + cJSON *protocol_version; + cJSON *json = NULL; + + json = cJSON_CreateObject(); + if (!json) { + goto error; + } + + mac = cJSON_CreateString(msg->mac); + if (!mac) { + goto error; + } + cJSON_AddItemToObject(json, "MAC", mac); + + hostname = cJSON_CreateString(msg->hostname); + if (!hostname) { + goto error; + } + cJSON_AddItemToObject(json, "HostName", hostname); + + version = cJSON_CreateString(msg->version); + if (!version) { + goto error; + } + cJSON_AddItemToObject(json, "Version", version); + + client_name = cJSON_CreateString(msg->client_name); + if (!client_name) { + goto error; + } + cJSON_AddItemToObject(json, "ClientName", client_name); + + os = cJSON_CreateString(msg->os); + if (!os) { + goto error; + } + cJSON_AddItemToObject(json, "OS", os); + + arch = cJSON_CreateString(msg->arch); + if (!arch) { + goto error; + } + cJSON_AddItemToObject(json, "Arch", arch); + + instance = cJSON_CreateNumber(msg->instance); + if (!instance) { + goto error; + } + cJSON_AddItemToObject(json, "Instance", instance); + + id = cJSON_CreateString(msg->id); + if (!id) { + goto error; + } + cJSON_AddItemToObject(json, "ID", id); + + protocol_version = cJSON_CreateNumber(msg->protocol_version); + if (!protocol_version) { + goto error; + } + cJSON_AddItemToObject(json, "SnapStreamProtocolVersion", protocol_version); + + goto end; +error: + cJSON_Delete(json); + +end: + return json; +} + +char* hello_message_serialize(hello_message_t* msg, size_t *size) { + int str_length, prefixed_length; + cJSON *json; + char *str = NULL; + char *prefixed_str = NULL; + + json = hello_message_to_json(msg); + if (!json) { + return NULL; + } + + str = cJSON_PrintUnformatted(json); + if (!str) { + return NULL; + } + cJSON_Delete(json); + + str_length = strlen(str); + prefixed_length = str_length + 4; + prefixed_str = malloc(prefixed_length); + if (!prefixed_str) { + return NULL; + } + + prefixed_str[0] = str_length & 0xff; + prefixed_str[1] = (str_length >> 8) & 0xff; + prefixed_str[2] = (str_length >> 16) & 0xff; + prefixed_str[3] = (str_length >> 24) & 0xff; + memcpy(&(prefixed_str[4]), str, str_length); + free(str); + *size = prefixed_length; + + return prefixed_str; +} + +int server_settings_message_deserialize(server_settings_message_t *msg, const char *json_str) { + int status = 1; + cJSON *value = NULL; + cJSON *json = cJSON_Parse(json_str); + if (!json) { + const char *error_ptr = cJSON_GetErrorPtr(); + if (error_ptr) { + // TODO change to a macro that can be diabled + fprintf(stderr, "Error before: %s\n", error_ptr); + goto end; + } + } + + if (msg == NULL) { + status = 2; + goto end; + } + + value = cJSON_GetObjectItemCaseSensitive(json, "bufferMs"); + if (cJSON_IsNumber(value)) { + msg->buffer_ms = value->valueint; + } + + value = cJSON_GetObjectItemCaseSensitive(json, "latency"); + if (cJSON_IsNumber(value)) { + msg->latency = value->valueint; + } + + value = cJSON_GetObjectItemCaseSensitive(json, "volume"); + if (cJSON_IsNumber(value)) { + msg->volume = value->valueint; + } + + value = cJSON_GetObjectItemCaseSensitive(json, "muted"); + msg->muted = cJSON_IsTrue(value); + status = 0; +end: + cJSON_Delete(json); + return status; +} + +int codec_header_message_deserialize(codec_header_message_t *msg, const char *data, uint32_t size) { + read_buffer_t buffer; + uint32_t string_size; + int result = 0; + + buffer_read_init(&buffer, data, size); + + result |= buffer_read_uint32(&buffer, &string_size); + if (result) { + // Can't allocate the proper size string if we didn't read the size, so fail early + return 1; + } + + msg->codec = malloc(string_size + 1); + if (!msg->codec) { + return 2; + } + + result |= buffer_read_buffer(&buffer, msg->codec, string_size); + // Make sure the codec is a proper C string by terminating it with a null character + msg->codec[string_size] = '\0'; + + result |= buffer_read_uint32(&buffer, &(msg->size)); + if (result) { + // Can't allocate the proper size string if we didn't read the size, so fail early + return 1; + } + + msg->payload = malloc(msg->size); + if (!msg->payload) { + return 2; + } + + result |= buffer_read_buffer(&buffer, msg->payload, msg->size); + return result; +} + +int wire_chunk_message_deserialize(wire_chunk_message_t *msg, const char *data, uint32_t size) { + read_buffer_t buffer; + int result = 0; + + buffer_read_init(&buffer, data, size); + + result |= buffer_read_int32(&buffer, &(msg->timestamp.sec)); + result |= buffer_read_int32(&buffer, &(msg->timestamp.usec)); + result |= buffer_read_uint32(&buffer, &(msg->size)); + + // If there's been an error already (especially for the size bit) return early + if (result) { + return result; + } + + // TODO maybe should check to see if need to free memory? + msg->payload = malloc(msg->size * sizeof(char)); + // Failed to allocate the memory + if (!msg->payload) { + return 2; + } + + result |= buffer_read_buffer(&buffer, msg->payload, msg->size); + return result; +} + +void codec_header_message_free(codec_header_message_t *msg) { + free(msg->codec); + msg->codec = NULL; + free(msg->payload); + msg->payload = NULL; +} + +void wire_chunk_message_free(wire_chunk_message_t *msg) { + if (msg->payload) { + free(msg->payload); + msg->payload = NULL; + } +} + +int time_message_serialize(time_message_t *msg, char *data, uint32_t size) { + write_buffer_t buffer; + int result = 0; + + buffer_write_init(&buffer, data, size); + + result |= buffer_write_int32(&buffer, msg->latency.sec); + result |= buffer_write_int32(&buffer, msg->latency.usec); + + return result; +} + +int time_message_deserialize(time_message_t *msg, const char *data, uint32_t size) { + read_buffer_t buffer; + int result = 0; + + buffer_read_init(&buffer, data, size); + + result |= buffer_read_int32(&buffer, &(msg->latency.sec)); + result |= buffer_read_int32(&buffer, &(msg->latency.usec)); + + return result; +} \ No newline at end of file